diff --git a/database.go b/database.go index 5547fe6..d676650 100644 --- a/database.go +++ b/database.go @@ -39,7 +39,7 @@ func NewDatabase(path string) (Database, error) { line := scanner.Text() nodes, err := parser.Parse(line) if err != nil { - panic(err) + return Database{}, fmt.Errorf("failed to parse database file: %w", err) } for _, node := range nodes { @@ -68,7 +68,7 @@ func NewDatabase(path string) (Database, error) { } if err := scanner.Err(); err != nil { - panic(err) + return Database{}, fmt.Errorf("failed to scan database file: %w", err) } return database, nil @@ -90,7 +90,7 @@ func (d Database) GetNodeAttr(name, attr string) (string, bool) { func (d Database) Query(query string) ([]Node, error) { tokens, err := internal.Tokenize(query) if err != nil { - panic(err) + return nil, err } var nodes []Node diff --git a/database_test.go b/database_test.go index 9aedbc6..d41576f 100644 --- a/database_test.go +++ b/database_test.go @@ -13,24 +13,23 @@ import ( ) func testDatabase(t *testing.T, context spec.G, it spec.S) { - var ( - Expect = NewWithT(t).Expect - path string - ) + var Expect = NewWithT(t).Expect - it.Before(func() { - file, err := os.CreateTemp("", "genders") - Expect(err).NotTo(HaveOccurred()) - defer file.Close() + context("NewDatabase", func() { + var path string - path = file.Name() - }) + it.Before(func() { + file, err := os.CreateTemp("", "genders") + Expect(err).NotTo(HaveOccurred()) + defer file.Close() - it.After(func() { - Expect(os.Remove(path)).To(Succeed()) - }) + path = file.Name() + }) + + it.After(func() { + Expect(os.Remove(path)).To(Succeed()) + }) - context("NewDatabase", func() { it("loads the database from a path on disk", func() { _, err := libgenders.NewDatabase(path) Expect(err).NotTo(HaveOccurred()) @@ -43,6 +42,17 @@ func testDatabase(t *testing.T, context spec.G, it spec.S) { Expect(err).To(MatchError(ContainSubstring("no-such-file: no such file or directory"))) }) }) + + context("when the file cannot be parsed", func() { + it.Before(func() { + Expect(os.WriteFile(path, []byte("node[%%-%%] attr=val\n"), 0600)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := libgenders.NewDatabase(path) + Expect(err).To(MatchError(ContainSubstring("failed to parse database file"))) + }) + }) }) }) @@ -815,5 +825,14 @@ func testDatabase(t *testing.T, context spec.G, it spec.S) { }) } }) + + context("failure cases", func() { + context("when the query cannot be tokenized", func() { + it("returns an error", func() { + _, err := database.Query(") mismatched parentheses (") + Expect(err).To(MatchError(ContainSubstring("failed to tokenize query"))) + }) + }) + }) }) } diff --git a/internal/init_test.go b/internal/init_test.go index a129352..68e7bea 100644 --- a/internal/init_test.go +++ b/internal/init_test.go @@ -11,9 +11,9 @@ func TestInternal(t *testing.T) { suite := spec.New(" libgenders/internal", spec.Report(report.Terminal{})) suite("Parser", testParser) suite("Query", testQuery) - suite.Pend("Scanner", testScanner) - suite.Pend("Stack", testStack) + suite("Scanner", testScanner) suite("Set", testSet) + suite("Stack", testStack) suite("Token", testToken) suite.Run(t) } diff --git a/internal/parser.go b/internal/parser.go index ee3b4c8..7799843 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -1,6 +1,7 @@ package internal import ( + "fmt" "maps" "strconv" "strings" @@ -21,7 +22,11 @@ func (p Parser) Parse(line string) ([]Node, error) { } fields := strings.Fields(line) - names := p.parseNames(fields[0]) + names, err := p.parseNames(fields[0]) + if err != nil { + return nil, err + } + var attributes map[string]string if len(fields) > 1 { attributes = p.parseAttrs(fields[1]) @@ -65,7 +70,7 @@ func (p Parser) copyAttrs(attributes map[string]string, name string) map[string] return attrs } -func (p Parser) parseNames(field string) []string { +func (p Parser) parseNames(field string) ([]string, error) { var ( name string inRange bool @@ -97,16 +102,21 @@ func (p Parser) parseNames(field string) []string { var names []string for _, f := range fields { - names = append(names, p.parseName(f)...) + fieldNames, err := p.parseName(f) + if err != nil { + return nil, err + } + + names = append(names, fieldNames...) } - return names + return names, nil } -func (p Parser) parseName(field string) []string { +func (p Parser) parseName(field string) ([]string, error) { parts := strings.FieldsFunc(field, func(c rune) bool { return c == '[' || c == ']' }) if len(parts) < 2 { - return parts + return parts, nil } prefix := parts[0] @@ -117,22 +127,27 @@ func (p Parser) parseName(field string) []string { suffix = parts[2] } + indices, err := p.parseRange(strings.Split(rng, ",")...) + if err != nil { + return nil, fmt.Errorf("failed to parse name %q: %w", field, err) + } + var names []string - for _, index := range p.parseRange(strings.Split(rng, ",")...) { + for _, index := range indices { names = append(names, prefix+index+suffix) } - return names + return names, nil } -func (p Parser) parseRange(ranges ...string) []string { +func (p Parser) parseRange(ranges ...string) ([]string, error) { var elems []string for _, rng := range ranges { start, end, _ := strings.Cut(rng, "-") first, err := strconv.Atoi(start) if err != nil { - panic(err) + return nil, fmt.Errorf("failed to parse range %q: %w", rng, err) } if len(end) == 0 { @@ -142,7 +157,7 @@ func (p Parser) parseRange(ranges ...string) []string { last, err := strconv.Atoi(end) if err != nil { - panic(err) + return nil, fmt.Errorf("failed to parse range %q: %w", rng, err) } for i := first; i <= last; i++ { @@ -150,5 +165,5 @@ func (p Parser) parseRange(ranges ...string) []string { } } - return elems + return elems, nil } diff --git a/internal/parser_test.go b/internal/parser_test.go index 5c01bec..32e58eb 100644 --- a/internal/parser_test.go +++ b/internal/parser_test.go @@ -152,6 +152,22 @@ func testParser(t *testing.T, context spec.G, it spec.S) { })) }) }) + + context("failure cases", func() { + context("when the first range value is non-numeric", func() { + it("returns an error", func() { + _, err := parser.Parse("node[banana-25] attr1,attr2=val2") + Expect(err).To(MatchError(ContainSubstring("failed to parse name \"node[banana-25]\": failed to parse range \"banana-25\""))) + }) + }) + + context("when the last range value is non-numeric", func() { + it("returns an error", func() { + _, err := parser.Parse("node[1-banana] attr1,attr2=val2") + Expect(err).To(MatchError(ContainSubstring("failed to parse name \"node[1-banana]\": failed to parse range \"1-banana\""))) + }) + }) + }) }) context("when the line only specifies a node name", func() { diff --git a/internal/scanner_test.go b/internal/scanner_test.go index f750a59..c0425ef 100644 --- a/internal/scanner_test.go +++ b/internal/scanner_test.go @@ -3,9 +3,38 @@ package internal_test import ( "testing" + "github.com/ryanmoran/libgenders/internal" "github.com/sclevine/spec" + + . "github.com/onsi/gomega" ) func testScanner(t *testing.T, context spec.G, it spec.S) { - // TODO + var Expect = NewWithT(t).Expect + + context("Next", func() { + it("splits a given query into tokenizable characters", func() { + var parts []string + scanner := internal.NewScanner("some query") + for scanner.Len() > 0 { + part, err := scanner.Next() + Expect(err).NotTo(HaveOccurred()) + parts = append(parts, part) + } + + Expect(parts).To(Equal([]string{"s", "o", "m", "e", " ", "q", "u", "e", "r", "y"})) + }) + + it("treats set operators specially", func() { + var parts []string + scanner := internal.NewScanner("w && x || y -- z") + for scanner.Len() > 0 { + part, err := scanner.Next() + Expect(err).NotTo(HaveOccurred()) + parts = append(parts, part) + } + + Expect(parts).To(Equal([]string{"w", " ", "&&", " ", "x", " ", "||", " ", "y", " ", "--", " ", "z"})) + }) + }) } diff --git a/internal/stack_test.go b/internal/stack_test.go index 625b8b8..9d25378 100644 --- a/internal/stack_test.go +++ b/internal/stack_test.go @@ -3,9 +3,24 @@ package internal_test import ( "testing" + "github.com/ryanmoran/libgenders/internal" "github.com/sclevine/spec" + + . "github.com/onsi/gomega" ) -func testStack(t *testing.T, context spec.G, it spec.S) { - // TODO +func testStack(t *testing.T, _ spec.G, it spec.S) { + var Expect = NewWithT(t).Expect + + it("acts like a stack for Tokens", func() { + stack := internal.Stack{} + stack.Push(internal.NewToken(internal.ValueTokenKind, "some-value")) + stack.Push(internal.NewToken(internal.ValueTokenKind, "other-value")) + Expect(stack.IsEmpty()).To(BeFalse()) + + Expect(stack.Top()).To(Equal(internal.NewToken(internal.ValueTokenKind, "other-value"))) + Expect(stack.Pop()).To(Equal(internal.NewToken(internal.ValueTokenKind, "other-value"))) + Expect(stack.Pop()).To(Equal(internal.NewToken(internal.ValueTokenKind, "some-value"))) + Expect(stack.IsEmpty()).To(BeTrue()) + }) } diff --git a/internal/token.go b/internal/token.go index 653c4c6..200a4a3 100644 --- a/internal/token.go +++ b/internal/token.go @@ -1,6 +1,7 @@ package internal import ( + "fmt" "slices" "strings" ) @@ -40,7 +41,7 @@ func Tokenize(query string) ([]Token, error) { for scanner.Len() > 0 { s, err := scanner.Next() if err != nil { - panic(err) + return nil, err } var token Token @@ -115,11 +116,7 @@ func Tokenize(query string) ([]Token, error) { } if operators.IsEmpty() { - panic("invalid") - } - - if operators.Top().Kind != LeftParenTokenKind { - panic("invalid") + return nil, fmt.Errorf("failed to tokenize query %q: mismatched parentheses", query) } operators.Pop() @@ -132,7 +129,7 @@ func Tokenize(query string) ([]Token, error) { for !operators.IsEmpty() { if operators.Top().Kind == LeftParenTokenKind { - panic("invalid") + return nil, fmt.Errorf("failed to tokenize query %q: mismatched parentheses", query) } output = append(output, operators.Pop()) diff --git a/internal/token_test.go b/internal/token_test.go index 9eacaff..a0cf162 100644 --- a/internal/token_test.go +++ b/internal/token_test.go @@ -84,5 +84,21 @@ func testToken(t *testing.T, context spec.G, it spec.S) { {Kind: internal.ValueTokenKind, Text: "attr1"}, })) }) + + context("failure cases", func() { + context("when the left parentheses in the query are mismatched", func() { + it("returns an error", func() { + _, err := internal.Tokenize("((attr1 && ~attr3) || attr1 -- attr5)) && attr7") + Expect(err).To(MatchError("failed to tokenize query \"((attr1 && ~attr3) || attr1 -- attr5)) && attr7\": mismatched parentheses")) + }) + }) + + context("when the right parentheses in the query are mismatched", func() { + it("returns an error", func() { + _, err := internal.Tokenize("((attr1 && ~attr3) || (attr1 -- attr5) && attr7") + Expect(err).To(MatchError("failed to tokenize query \"((attr1 && ~attr3) || (attr1 -- attr5) && attr7\": mismatched parentheses")) + }) + }) + }) }) }