aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/atlas.go2
-rw-r--r--pkg/index/filters.go40
-rw-r--r--pkg/index/filters_test.go30
-rw-r--r--pkg/index/index.go32
-rw-r--r--pkg/index/index_test.go53
-rw-r--r--pkg/query/lexer_test.go41
-rw-r--r--pkg/query/outputs.go4
-rw-r--r--pkg/query/outputs_test.go26
-rw-r--r--pkg/query/parser.go4
-rw-r--r--pkg/query/parser_test.go282
10 files changed, 235 insertions, 279 deletions
diff --git a/cmd/atlas.go b/cmd/atlas.go
index 3eabfbb..b0cebdb 100644
--- a/cmd/atlas.go
+++ b/cmd/atlas.go
@@ -132,7 +132,7 @@ func main() {
})
indexFs.Parse(args[1:])
- case "help", "--help", "-help":
+ case "help":
printHelp()
flag.PrintDefaults()
os.Exit(0)
diff --git a/pkg/index/filters.go b/pkg/index/filters.go
index 44b12cf..a60a629 100644
--- a/pkg/index/filters.go
+++ b/pkg/index/filters.go
@@ -15,7 +15,7 @@ import (
type DocFilter struct {
Name string
- Filter func(infoPath, io.ReadSeeker) bool
+ Filter func(InfoPath, io.ReadSeeker) bool
}
const FilterHelp string = `
@@ -77,8 +77,8 @@ func ParseFilter(s string) (DocFilter, error) {
func NewExtensionFilter(ext string) DocFilter {
return DocFilter{
ext + " Filter",
- func(ip infoPath, _ io.ReadSeeker) bool {
- return filepath.Ext(ip.path) == ext
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ return filepath.Ext(ip.Path) == ext
},
}
}
@@ -86,8 +86,8 @@ func NewExtensionFilter(ext string) DocFilter {
func NewMaxFilesizeFilter(size int64) DocFilter {
return DocFilter{
fmt.Sprintf("Max Size Filter %d", size),
- func(ip infoPath, _ io.ReadSeeker) bool {
- return ip.info.Size() <= size
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ return ip.Info.Size() <= size
},
}
}
@@ -95,8 +95,8 @@ func NewMaxFilesizeFilter(size int64) DocFilter {
func NewExcludeFilenameFilter(excluded []string) DocFilter {
return DocFilter{
"Excluded Filename filter",
- func(ip infoPath, _ io.ReadSeeker) bool {
- filename := filepath.Base(ip.path)
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ filename := filepath.Base(ip.Path)
return !slices.Contains(excluded, filename)
},
}
@@ -105,8 +105,8 @@ func NewExcludeFilenameFilter(excluded []string) DocFilter {
func NewIncludeFilenameFilter(included []string) DocFilter {
return DocFilter{
"Included Filename filter",
- func(ip infoPath, _ io.ReadSeeker) bool {
- filename := filepath.Base(ip.path)
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ filename := filepath.Base(ip.Path)
return slices.Contains(included, filename)
},
}
@@ -116,8 +116,8 @@ func NewIncludeFilenameFilter(included []string) DocFilter {
func NewExcludeParentFilter(badParent string) DocFilter {
return DocFilter{
"Excluded Parent Directory filter: " + badParent,
- func(ip infoPath, _ io.ReadSeeker) bool {
- return !slices.Contains(strings.Split(ip.path, string(os.PathSeparator)), badParent)
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ return !slices.Contains(strings.Split(ip.Path, string(os.PathSeparator)), badParent)
},
}
}
@@ -130,8 +130,8 @@ func NewIncludeRegexFilter(pattern string) (DocFilter, error) {
return DocFilter{
"Included Regex Filter: " + pattern,
- func(ip infoPath, _ io.ReadSeeker) bool {
- return re.MatchString(ip.path)
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ return re.MatchString(ip.Path)
},
}, nil
}
@@ -143,23 +143,21 @@ func NewExcludeRegexFilter(pattern string) (DocFilter, error) {
return DocFilter{
"Excluded Regex Filter: " + pattern,
- func(ip infoPath, _ io.ReadSeeker) bool {
- return !re.MatchString(ip.path)
+ func(ip InfoPath, _ io.ReadSeeker) bool {
+ return !re.MatchString(ip.Path)
},
}, nil
}
var YamlHeaderFilter = DocFilter{
"YAML Header Filter",
- yamlHeaderFilterFunc,
-}
-
-func yamlHeaderFilterFunc(_ infoPath, r io.ReadSeeker) bool {
- return yamlHeaderPos(r) > 0
+ func(_ InfoPath, rs io.ReadSeeker) bool {
+ return YamlHeaderPos(rs) > 0
+ },
}
// Position of the end of a yaml header, negative
-func yamlHeaderPos(r io.ReadSeeker) int64 {
+func YamlHeaderPos(r io.ReadSeeker) int64 {
const bufSize = 4096
buf := make([]byte, bufSize)
diff --git a/pkg/index/filters_test.go b/pkg/index/filters_test.go
index f46874c..bedf5e9 100644
--- a/pkg/index/filters_test.go
+++ b/pkg/index/filters_test.go
@@ -1,10 +1,12 @@
-package index
+package index_test
import (
"bytes"
"io"
"os"
"testing"
+
+ "github.com/jpappel/atlas/pkg/index"
)
func noYamlHeader() io.ReadSeeker {
@@ -27,7 +29,7 @@ func trailingYamlHeader() io.ReadSeeker {
return bytes.NewReader(buf)
}
-func extensionless(t *testing.T) infoPath {
+func extensionless(t *testing.T) index.InfoPath {
root := t.TempDir()
path := root + "/" + "afile"
f, err := os.Create(path)
@@ -45,10 +47,10 @@ func extensionless(t *testing.T) infoPath {
t.Fatal(err)
}
- return infoPath{path, info}
+ return index.InfoPath{path, info}
}
-func markdownExtension(t *testing.T) infoPath {
+func markdownExtension(t *testing.T) index.InfoPath {
root := t.TempDir()
path := root + "/" + "a.md"
f, err := os.Create(path)
@@ -62,23 +64,23 @@ func markdownExtension(t *testing.T) infoPath {
t.Fatal(err)
}
- return infoPath{path, info}
+ return index.InfoPath{path, info}
}
-func parentDirectory(t *testing.T) infoPath {
+func parentDirectory(t *testing.T) index.InfoPath {
root := t.TempDir()
dir := root + "/parent"
path := dir + "/a"
- return infoPath{path: path}
+ return index.InfoPath{Path: path}
}
-func grandparentDirectory(t *testing.T) infoPath {
+func grandparentDirectory(t *testing.T) index.InfoPath {
root := t.TempDir()
dir := root + "/grandparent/parent"
path := dir + "/a"
- return infoPath{path: path}
+ return index.InfoPath{Path: path}
}
func TestYamlHeaderFilter(t *testing.T) {
@@ -94,7 +96,7 @@ func TestYamlHeaderFilter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := yamlHeaderFilterFunc(infoPath{}, tt.r)
+ got := index.YamlHeaderPos(tt.r) > 0
if got != tt.want {
t.Errorf("YamlHeaderFilter() = %v, want %v", got, tt.want)
}
@@ -105,7 +107,7 @@ func TestYamlHeaderFilter(t *testing.T) {
func TestExtensionFilter(t *testing.T) {
tests := []struct {
name string
- infoGen func(*testing.T) infoPath
+ infoGen func(*testing.T) index.InfoPath
ext string
want bool
}{
@@ -116,7 +118,7 @@ func TestExtensionFilter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- docFilter := NewExtensionFilter(tt.ext)
+ docFilter := index.NewExtensionFilter(tt.ext)
ip := tt.infoGen(t)
got := docFilter.Filter(ip, nil)
@@ -130,7 +132,7 @@ func TestExtensionFilter(t *testing.T) {
func TestExcludeParentFilter(t *testing.T) {
tests := []struct {
name string
- infoGen func(*testing.T) infoPath
+ infoGen func(*testing.T) index.InfoPath
parent string
want bool
}{
@@ -152,7 +154,7 @@ func TestExcludeParentFilter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- docFilter := NewExcludeParentFilter(tt.parent)
+ docFilter := index.NewExcludeParentFilter(tt.parent)
ip := tt.infoGen(t)
got := docFilter.Filter(ip, nil)
diff --git a/pkg/index/index.go b/pkg/index/index.go
index 419607c..a35b670 100644
--- a/pkg/index/index.go
+++ b/pkg/index/index.go
@@ -36,9 +36,9 @@ type ParseOpts struct {
IgnoreMetaError bool
}
-type infoPath struct {
- path string
- info os.FileInfo
+type InfoPath struct {
+ Path string
+ Info os.FileInfo
}
type Index struct {
@@ -191,12 +191,12 @@ func (doc Document) Equal(other Document) bool {
return true
}
-func visit(file infoPath, visitQueue chan<- infoPath, filterQueue chan<- infoPath, wg *sync.WaitGroup) {
+func visit(file InfoPath, visitQueue chan<- InfoPath, filterQueue chan<- InfoPath, wg *sync.WaitGroup) {
// TODO: check if symlink, and handle appropriately
// TODO: extract error out of function
- if file.info.IsDir() {
- entries, err := os.ReadDir(file.path)
+ if file.Info.IsDir() {
+ entries, err := os.ReadDir(file.Path)
if err != nil {
panic(err)
}
@@ -209,17 +209,17 @@ func visit(file infoPath, visitQueue chan<- infoPath, filterQueue chan<- infoPat
}
// PERF: prevents deadlock but introduces an additional goroutine overhead per file
go func(path string) {
- visitQueue <- infoPath{path: path, info: entryInfo}
- }(file.path + "/" + entry.Name())
+ visitQueue <- InfoPath{Path: path, Info: entryInfo}
+ }(file.Path + "/" + entry.Name())
}
- } else if file.info.Mode().IsRegular() {
+ } else if file.Info.Mode().IsRegular() {
filterQueue <- file
}
wg.Done()
}
-func workerTraverse(wg *sync.WaitGroup, visitQueue chan infoPath, filterQueue chan<- infoPath) {
+func workerTraverse(wg *sync.WaitGroup, visitQueue chan InfoPath, filterQueue chan<- InfoPath) {
for work := range visitQueue {
visit(work, visitQueue, filterQueue, wg)
}
@@ -236,8 +236,8 @@ func (idx Index) Traverse(numWorkers uint) []string {
panic(err)
}
- jobs := make(chan infoPath, numWorkers)
- filterQueue := make(chan infoPath, numWorkers)
+ jobs := make(chan InfoPath, numWorkers)
+ filterQueue := make(chan InfoPath, numWorkers)
activeJobs := &sync.WaitGroup{}
@@ -248,7 +248,7 @@ func (idx Index) Traverse(numWorkers uint) []string {
// init send
activeJobs.Add(1)
- jobs <- infoPath{path: idx.Root, info: rootInfo}
+ jobs <- InfoPath{Path: idx.Root, Info: rootInfo}
// close jobs queue
go func() {
@@ -259,7 +259,7 @@ func (idx Index) Traverse(numWorkers uint) []string {
// gather
for doc := range filterQueue {
- docs = append(docs, doc.path)
+ docs = append(docs, doc.Path)
}
return docs
@@ -278,7 +278,7 @@ func (idx Index) FilterOne(path string) bool {
defer f.Close()
for _, docFilter := range idx.Filters {
- if !docFilter.Filter(infoPath{string(path), info}, f) {
+ if !docFilter.Filter(InfoPath{string(path), info}, f) {
return false
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
@@ -340,7 +340,7 @@ func ParseDoc(path string, opts ParseOpts) (*Document, error) {
}
doc.FileTime = info.ModTime()
- pos := yamlHeaderPos(f)
+ pos := YamlHeaderPos(f)
f.Seek(0, io.SeekStart)
if pos < 0 {
return nil, fmt.Errorf("Can't find YAML header in %s", path)
diff --git a/pkg/index/index_test.go b/pkg/index/index_test.go
index 3f9b900..0a3239d 100644
--- a/pkg/index/index_test.go
+++ b/pkg/index/index_test.go
@@ -1,4 +1,4 @@
-package index
+package index_test
import (
"errors"
@@ -7,16 +7,18 @@ import (
"slices"
"testing"
"time"
+
+ "github.com/jpappel/atlas/pkg/index"
)
-var indexCases map[string]func(t *testing.T) Index
+var indexCases map[string]func(t *testing.T) index.Index
func init() {
- indexCases = make(map[string]func(t *testing.T) Index)
+ indexCases = make(map[string]func(t *testing.T) index.Index)
- indexCases["single file"] = func(t *testing.T) Index {
+ indexCases["single file"] = func(t *testing.T) index.Index {
root := t.TempDir()
- index := Index{Root: root, Filters: []DocFilter{NewExtensionFilter(".md")}}
+ index := index.Index{Root: root, Filters: []index.DocFilter{index.NewExtensionFilter(".md")}}
f, err := os.Create(root + "/a_file.md")
if err != nil {
@@ -27,16 +29,16 @@ func init() {
return index
}
- indexCases["large file"] = func(t *testing.T) Index {
+ indexCases["large file"] = func(t *testing.T) index.Index {
root := t.TempDir()
- index := Index{Root: root}
+ index := index.Index{Root: root}
return index
}
- indexCases["worker saturation"] = func(t *testing.T) Index {
+ indexCases["worker saturation"] = func(t *testing.T) index.Index {
root := t.TempDir()
- index := Index{Root: root}
+ index := index.Index{Root: root}
permission := os.FileMode(0o777)
for _, dirName := range []string{"a", "b", "c", "d", "e", "f"} {
@@ -61,7 +63,7 @@ func init() {
func TestIndex_Traverse(t *testing.T) {
tests := []struct {
name string
- indexCase func(t *testing.T) Index
+ indexCase func(t *testing.T) index.Index
numWorkers uint
want []string
}{
@@ -104,7 +106,7 @@ func TestIndex_Filter(t *testing.T) {
tests := []struct {
name string
paths []string
- indexCase func(t *testing.T) Index
+ indexCase func(t *testing.T) index.Index
numWorkers uint
want []string
}{
@@ -154,7 +156,8 @@ func TestIndex_ParseOne(t *testing.T) {
tests := []struct {
name string
pathMaker func(t *testing.T) string
- want *Document
+ parseOpts index.ParseOpts
+ want *index.Document
wantErr error
}{
{
@@ -166,7 +169,8 @@ func TestIndex_ParseOne(t *testing.T) {
f.WriteString("---\ntitle: A title\n---\n")
return path
},
- &Document{Title: "A title"},
+ index.ParseOpts{},
+ &index.Document{Title: "A title"},
nil,
},
{
@@ -184,7 +188,8 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{Tags: []string{"a", "b", "c"}},
+ index.ParseOpts{},
+ &index.Document{Tags: []string{"a", "b", "c"}},
nil,
},
{
@@ -197,7 +202,8 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{Date: time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC)},
+ index.ParseOpts{},
+ &index.Document{Date: time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC)},
nil,
},
{
@@ -210,7 +216,8 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{Authors: []string{"Rob Pike"}},
+ index.ParseOpts{},
+ &index.Document{Authors: []string{"Rob Pike"}},
nil,
},
{
@@ -223,7 +230,8 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{Authors: []string{"Robert Griesemer", "Rob Pike", "Ken Thompson"}},
+ index.ParseOpts{},
+ &index.Document{Authors: []string{"Robert Griesemer", "Rob Pike", "Ken Thompson"}},
nil,
},
{
@@ -238,7 +246,8 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{OtherMeta: "unknownKey: value\n"},
+ index.ParseOpts{ParseMeta: true},
+ &index.Document{OtherMeta: "unknownKey: value\n"},
nil,
},
{
@@ -253,8 +262,9 @@ func TestIndex_ParseOne(t *testing.T) {
return path
},
- &Document{},
- ErrHeaderParse,
+ index.ParseOpts{},
+ &index.Document{},
+ index.ErrHeaderParse,
},
}
for _, tt := range tests {
@@ -262,8 +272,7 @@ func TestIndex_ParseOne(t *testing.T) {
path := tt.pathMaker(t)
tt.want.Path = path
- // TODO: add ParseOpts as test param
- got, gotErr := ParseDoc(path, ParseOpts{ParseMeta: true})
+ got, gotErr := index.ParseDoc(path, tt.parseOpts)
if !errors.Is(gotErr, tt.wantErr) {
t.Errorf("Recieved unexpected error: want %v got %v", tt.wantErr, gotErr)
diff --git a/pkg/query/lexer_test.go b/pkg/query/lexer_test.go
index 0cfc1de..d931b96 100644
--- a/pkg/query/lexer_test.go
+++ b/pkg/query/lexer_test.go
@@ -1,14 +1,45 @@
-package query
+package query_test
import (
"testing"
+
+ "github.com/jpappel/atlas/pkg/query"
+)
+
+type Token = query.Token
+
+const (
+ TOK_UNKNOWN = query.TOK_UNKNOWN
+ TOK_CLAUSE_OR = query.TOK_CLAUSE_OR
+ TOK_CLAUSE_AND = query.TOK_CLAUSE_AND
+ TOK_CLAUSE_START = query.TOK_CLAUSE_START
+ TOK_CLAUSE_END = query.TOK_CLAUSE_END
+ TOK_OP_NEG = query.TOK_OP_NEG
+ TOK_OP_EQ = query.TOK_OP_EQ
+ TOK_OP_AP = query.TOK_OP_AP
+ TOK_OP_NE = query.TOK_OP_NE
+ TOK_OP_LT = query.TOK_OP_LT
+ TOK_OP_LE = query.TOK_OP_LE
+ TOK_OP_GE = query.TOK_OP_GE
+ TOK_OP_GT = query.TOK_OP_GT
+ TOK_OP_PIPE = query.TOK_OP_PIPE
+ TOK_OP_ARG = query.TOK_OP_ARG
+ TOK_CAT_TITLE = query.TOK_CAT_TITLE
+ TOK_CAT_AUTHOR = query.TOK_CAT_AUTHOR
+ TOK_CAT_DATE = query.TOK_CAT_DATE
+ TOK_CAT_FILETIME = query.TOK_CAT_FILETIME
+ TOK_CAT_TAGS = query.TOK_CAT_TAGS
+ TOK_CAT_LINKS = query.TOK_CAT_LINKS
+ TOK_CAT_META = query.TOK_CAT_META
+ TOK_VAL_STR = query.TOK_VAL_STR
+ TOK_VAL_DATETIME = query.TOK_VAL_DATETIME
)
func TestLex(t *testing.T) {
tests := []struct {
name string
query string
- want []Token
+ want []query.Token
}{
{"empty query", "", []Token{{Type: TOK_CLAUSE_START}, {TOK_CLAUSE_AND, "and"}, {Type: TOK_CLAUSE_END}}},
{"quoted statement", `a:"ken thompson"`, []Token{
@@ -66,7 +97,7 @@ func TestLex(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := Lex(tt.query)
+ got := query.Lex(tt.query)
gl, wl := len(got), len(tt.want)
if gl != wl {
@@ -83,8 +114,8 @@ func TestLex(t *testing.T) {
}
if t.Failed() {
- t.Log("Got\n", TokensStringify(got))
- t.Log("Want\n", TokensStringify(tt.want))
+ t.Log("Got\n", query.TokensStringify(got))
+ t.Log("Want\n", query.TokensStringify(tt.want))
}
})
}
diff --git a/pkg/query/outputs.go b/pkg/query/outputs.go
index 00b9ddf..7dac42e 100644
--- a/pkg/query/outputs.go
+++ b/pkg/query/outputs.go
@@ -102,7 +102,7 @@ func (o JsonOutput) Output(docs []*index.Document) (string, error) {
return string(b), nil
}
-func parseOutputFormat(formatStr string) ([]OutputToken, []string, error) {
+func ParseOutputFormat(formatStr string) ([]OutputToken, []string, error) {
toks := make([]OutputToken, 0, 16)
curTok := make([]rune, 0, 16)
strToks := make([]string, 0, 8)
@@ -157,7 +157,7 @@ func parseOutputFormat(formatStr string) ([]OutputToken, []string, error) {
}
func NewCustomOutput(formatStr string, datetimeFormat string) (CustomOutput, error) {
- outToks, strToks, err := parseOutputFormat(formatStr)
+ outToks, strToks, err := ParseOutputFormat(formatStr)
if err != nil {
return CustomOutput{}, err
}
diff --git a/pkg/query/outputs_test.go b/pkg/query/outputs_test.go
index b5fdba7..8a1bb29 100644
--- a/pkg/query/outputs_test.go
+++ b/pkg/query/outputs_test.go
@@ -1,44 +1,58 @@
-package query
+package query_test
import (
"errors"
"slices"
"testing"
+
+ "github.com/jpappel/atlas/pkg/query"
+)
+
+const (
+ OUT_TOK_STR = query.OUT_TOK_STR
+ OUT_TOK_PATH = query.OUT_TOK_PATH
+ OUT_TOK_TITLE = query.OUT_TOK_TITLE
+ OUT_TOK_DATE = query.OUT_TOK_DATE
+ OUT_TOK_FILETIME = query.OUT_TOK_FILETIME
+ OUT_TOK_AUTHORS = query.OUT_TOK_AUTHORS
+ OUT_TOK_TAGS = query.OUT_TOK_TAGS
+ OUT_TOK_LINKS = query.OUT_TOK_LINKS
+ OUT_TOK_META = query.OUT_TOK_META
)
func Test_parseOutputFormat(t *testing.T) {
tests := []struct {
name string
formatStr string
- wantToks []OutputToken
+ wantToks []query.OutputToken
wantStrToks []string
wantErr error
}{
{
"one big string",
"here is a string with no placeholders",
- []OutputToken{OUT_TOK_STR},
+ []query.OutputToken{OUT_TOK_STR},
[]string{"here is a string with no placeholders"},
nil,
},
{
"default format",
"%p %T %d authors:%a tags:%t",
- []OutputToken{OUT_TOK_PATH, OUT_TOK_STR, OUT_TOK_TITLE, OUT_TOK_STR, OUT_TOK_DATE, OUT_TOK_STR, OUT_TOK_AUTHORS, OUT_TOK_STR, OUT_TOK_TAGS},
+ []query.OutputToken{OUT_TOK_PATH, OUT_TOK_STR, OUT_TOK_TITLE, OUT_TOK_STR, OUT_TOK_DATE, OUT_TOK_STR, OUT_TOK_AUTHORS, OUT_TOK_STR, OUT_TOK_TAGS},
[]string{" ", " ", " authors:", " tags:"},
nil,
},
{
"literal percents",
"%%%p%%%T%%",
- []OutputToken{OUT_TOK_STR, OUT_TOK_PATH, OUT_TOK_STR, OUT_TOK_TITLE, OUT_TOK_STR},
+ []query.OutputToken{OUT_TOK_STR, OUT_TOK_PATH, OUT_TOK_STR, OUT_TOK_TITLE, OUT_TOK_STR},
[]string{"%", "%", "%"},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- gotToks, gotStrToks, gotErr := parseOutputFormat(tt.formatStr)
+ gotToks, gotStrToks, gotErr := query.ParseOutputFormat(tt.formatStr)
if !errors.Is(gotErr, tt.wantErr) {
t.Errorf("Recieved unexpected error: got %v want %v", gotErr, tt.wantErr)
diff --git a/pkg/query/parser.go b/pkg/query/parser.go
index 7ac9918..14cc227 100644
--- a/pkg/query/parser.go
+++ b/pkg/query/parser.go
@@ -290,17 +290,17 @@ func (root Clause) Order() int {
func (root *Clause) DFS() iter.Seq[*Clause] {
return func(yield func(*Clause) bool) {
- stack := make([]*Clause, 0, len(root.Clauses))
+ stack := make([]*Clause, 0, len(root.Clauses)+1)
stack = append(stack, root)
for len(stack) != 0 {
node := stack[len(stack)-1]
+ stack = stack[:len(stack)-1]
if !yield(node) {
return
}
- stack := stack[:len(stack)-1]
stack = append(stack, node.Clauses...)
}
}
diff --git a/pkg/query/parser_test.go b/pkg/query/parser_test.go
index 6ea5c10..e3ab971 100644
--- a/pkg/query/parser_test.go
+++ b/pkg/query/parser_test.go
@@ -1,222 +1,124 @@
package query_test
import (
+ "errors"
"slices"
"testing"
"github.com/jpappel/atlas/pkg/query"
)
-func TestClause_Flatten(t *testing.T) {
+const (
+ CAT_UNKNOWN = query.CAT_UNKNOWN
+ CAT_TITLE = query.CAT_TITLE
+ CAT_AUTHOR = query.CAT_AUTHOR
+ CAT_DATE = query.CAT_DATE
+ CAT_FILETIME = query.CAT_FILETIME
+ CAT_TAGS = query.CAT_TAGS
+ CAT_LINKS = query.CAT_LINKS
+ CAT_META = query.CAT_META
+
+ OP_UNKNOWN = query.OP_UNKNOWN
+ OP_EQ = query.OP_EQ
+ OP_AP = query.OP_AP
+ OP_NE = query.OP_NE
+ OP_LT = query.OP_LT
+ OP_LE = query.OP_LE
+ OP_GE = query.OP_GE
+ OP_GT = query.OP_GT
+ OP_PIPE = query.OP_PIPE
+ OP_ARG = query.OP_ARG
+)
+
+func TestParse(t *testing.T) {
tests := []struct {
- name string
- root *query.Clause
- expected query.Clause
- }{
- {
- "empty",
- &query.Clause{},
- query.Clause{},
+ name string
+ tokens []query.Token
+ want *query.Clause
+ wantErr error
+ }{{
+ "empty clause",
+ []query.Token{
+ {Type: TOK_CLAUSE_START}, {Type: TOK_CLAUSE_AND}, {Type: TOK_CLAUSE_END},
},
- {
- "empty with child",
- &query.Clause{
- Operator: query.COP_OR,
- Clauses: []*query.Clause{
- {
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- },
- },
- },
- },
- query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- },
- },
+ &query.Clause{Operator: query.COP_AND},
+ nil,
+ }, {
+ "simple clause",
+ []query.Token{
+ {Type: TOK_CLAUSE_START}, {Type: TOK_CLAUSE_AND},
+ {TOK_CAT_AUTHOR, "a"}, {TOK_OP_AP, ":"}, {TOK_VAL_STR, "ken thompson"},
+ {Type: TOK_CLAUSE_END},
},
- {
- "already flat",
- &query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- },
- },
- query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- },
+ &query.Clause{
+ Operator: query.COP_AND,
+ Statements: []query.Statement{
+ {Category: CAT_AUTHOR, Operator: OP_AP, Value: query.StringValue{"ken thompson"}},
},
},
- {
- "flatten 1 layer, multiple clauses",
- &query.Clause{
- Operator: query.COP_OR,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- },
- Clauses: []*query.Clause{
- {Operator: query.COP_OR, Statements: []query.Statement{{Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}}}},
- {Operator: query.COP_OR, Statements: []query.Statement{{Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}}}},
- },
- },
- query.Clause{
- Operator: query.COP_OR,
- Statements: []query.Statement{
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}},
- },
- },
+ nil,
+ }, {
+ "nested clause",
+ []query.Token{
+ {Type: TOK_CLAUSE_START}, {Type: TOK_CLAUSE_AND},
+ {TOK_CAT_AUTHOR, "a"}, {TOK_OP_AP, ":"}, {TOK_VAL_STR, "Alonzo Church"},
+ {Type: TOK_CLAUSE_START}, {Type: TOK_CLAUSE_OR},
+ {TOK_CAT_AUTHOR, "a"}, {TOK_OP_EQ, "="}, {TOK_VAL_STR, "Alan Turing"},
+ {Type: TOK_CLAUSE_END},
+ {Type: TOK_CLAUSE_END},
},
- {
- "flatten 2 layers",
- &query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- },
- Clauses: []*query.Clause{
- {
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}},
- },
- Clauses: []*query.Clause{
- {
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}},
- },
- },
- },
- },
- },
- },
- query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}},
- },
- },
- },
- {
- "flatten 1 child keep 1 child",
- &query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- },
- Clauses: []*query.Clause{
- {
- Operator: query.COP_OR,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}},
- },
- },
- {
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}},
- },
- },
- },
+ &query.Clause{
+ Operator: query.COP_AND,
+ Statements: []query.Statement{
+ {Category: CAT_AUTHOR, Operator: OP_AP, Value: query.StringValue{"Alonzo Church"}},
},
- query.Clause{
- Operator: query.COP_AND,
- Statements: []query.Statement{
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"foobar"}},
- {Category: query.CAT_TITLE, Operator: query.OP_AP, Value: query.StringValue{"a very interesting title"}},
- {Category: query.CAT_TAGS, Operator: query.OP_EQ, Value: query.StringValue{"barfoo"}},
- },
- Clauses: []*query.Clause{
- {
- Operator: query.COP_OR,
- Statements: []query.Statement{
- {Category: query.CAT_AUTHOR, Operator: query.OP_AP, Value: query.StringValue{"jp"}},
- {Category: query.CAT_AUTHOR, Operator: query.OP_NE, Value: query.StringValue{"pj"}},
- },
+ Clauses: []*query.Clause{
+ {
+ Operator: query.COP_OR,
+ Statements: []query.Statement{
+ {Category: CAT_AUTHOR, Operator: OP_EQ, Value: query.StringValue{"Alan Turing"}},
},
},
},
},
- }
+ nil,
+ }}
for _, tt := range tests {
- o := query.Optimizer{}
t.Run(tt.name, func(t *testing.T) {
- o.Flatten(tt.root)
-
- slices.SortFunc(tt.root.Statements, query.StatementCmp)
- slices.SortFunc(tt.expected.Statements, query.StatementCmp)
-
- stmtsEq := slices.EqualFunc(tt.root.Statements, tt.expected.Statements,
- func(a, b query.Statement) bool {
- return a.Category == b.Category && a.Operator == b.Operator && a.Negated == b.Negated && a.Value.Compare(b.Value) == 0
- },
- )
-
- if !stmtsEq {
- t.Error("Statments not equal")
- if gL, wL := len(tt.root.Statements), len(tt.expected.Statements); gL != wL {
- t.Logf("Different number of statements: got %d want %d\n", gL, wL)
- }
+ gotC, gotErr := query.Parse(tt.tokens)
+ if !errors.Is(gotErr, tt.wantErr) {
+ t.Fatalf("Different parse error than expected: got %v, want %v", gotErr, tt.wantErr)
+ } else if gotErr != nil {
+ return
}
- gotL, wantL := len(tt.root.Clauses), len(tt.expected.Clauses)
+ got := slices.Collect(gotC.DFS())
+ want := slices.Collect(tt.want.DFS())
+ gotL, wantL := len(got), len(want)
if gotL != wantL {
- t.Errorf("Incorrect number of children clauses: got %d want %d\n", gotL, wantL)
+ t.Errorf("Different number of clauses than expected: got %d, want %d", gotL, wantL)
}
- })
- }
-}
-func TestParse(t *testing.T) {
- tests := []struct {
- name string // description of this test case
- // Named input parameters for target function.
- tokens []query.Token
- want *query.Clause
- wantErr bool
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, gotErr := query.Parse(tt.tokens)
- if gotErr != nil {
- if !tt.wantErr {
- t.Errorf("Parse() failed: %v", gotErr)
+ for i := range min(gotL, wantL) {
+ gotC, wantC := got[i], want[i]
+
+ if gotC.Operator != wantC.Operator {
+ t.Error("Different clause operator than expected")
+ } else if !slices.EqualFunc(gotC.Statements, wantC.Statements,
+ func(s1, s2 query.Statement) bool {
+ return s1.Negated == s2.Negated && s1.Category == s2.Category && s1.Operator == s2.Operator && s1.Value.Compare(s2.Value) == 0
+ }) {
+ t.Error("Different statements than expected")
+ } else if len(gotC.Clauses) != len(wantC.Clauses) {
+ t.Error("Different number of child clauses than expected")
+ }
+
+ if t.Failed() {
+ t.Log("Got\n", gotC)
+ t.Log("Want\n", wantC)
+ break
}
- return
- }
- if tt.wantErr {
- t.Fatal("Parse() succeeded unexpectedly")
- }
- // TODO: update the condition below to compare got with tt.want.
- if true {
- t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}