diff options
| -rw-r--r-- | cmd/atlas.go | 2 | ||||
| -rw-r--r-- | pkg/index/filters.go | 40 | ||||
| -rw-r--r-- | pkg/index/filters_test.go | 30 | ||||
| -rw-r--r-- | pkg/index/index.go | 32 | ||||
| -rw-r--r-- | pkg/index/index_test.go | 53 | ||||
| -rw-r--r-- | pkg/query/lexer_test.go | 41 | ||||
| -rw-r--r-- | pkg/query/outputs.go | 4 | ||||
| -rw-r--r-- | pkg/query/outputs_test.go | 26 | ||||
| -rw-r--r-- | pkg/query/parser.go | 4 | ||||
| -rw-r--r-- | pkg/query/parser_test.go | 282 |
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) } }) } |
