From 41292f9d895129bf01c4f4ae0d07a963deedd76d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 12:06:43 +0200 Subject: [PATCH] Test internal/grammar, and allow the root to import it --- builtins_test.go | 4 ++- calc_test.go | 25 +++++++--------- format_test.go | 16 ---------- imports_test.go | 3 +- internal/grammar/calc_test.go | 31 ++++++++++++++++++++ internal/grammar/format_test.go | 47 ++++++++++++++++++++++++++++++ internal/grammar/name_test.go | 16 ++++++++++ internal/grammar/path_test.go | 40 +++++++++++++++++++++++++ internal/grammar/reference_test.go | 21 +++++++++++++ 9 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 internal/grammar/calc_test.go create mode 100644 internal/grammar/format_test.go create mode 100644 internal/grammar/name_test.go create mode 100644 internal/grammar/path_test.go create mode 100644 internal/grammar/reference_test.go diff --git a/builtins_test.go b/builtins_test.go index a195b05..4ca623f 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" "time" + + "github.com/larvit/fejkdata/internal/grammar" ) func TestBuiltinIDGenerators(t *testing.T) { @@ -454,7 +456,7 @@ func TestNoRefOperandsMatchesTheOperandGrammar(t *testing.T) { if b.operands == nil { continue } - if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), isRef); b.noRefOperands == takesRef { + if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), grammar.IsRef); b.noRefOperands == takesRef { t.Errorf("%s: noRefOperands = %v, but its operands func reads a reference: %v", name, b.noRefOperands, takesRef) } } diff --git a/calc_test.go b/calc_test.go index d01ea2d..e535177 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" "testing" + + "github.com/larvit/fejkdata/internal/grammar" ) // TestCalcArithmetic pins the operators, precedence, parentheses and unary minus @@ -99,26 +101,19 @@ func TestCalcReproducible(t *testing.T) { // call that is not a calc, and for one whose expression does not parse: none, either way. func TestParsedCallReadsOnlyAnOperandBuiltin(t *testing.T) { for _, format := range []string{"{luhn()}", "{calc()}", "{calc(1 +)}", "{calc(()}"} { - if toks, err := parseFormat(format); err != nil || len(toks) != 1 || toks[0].names != nil { - t.Errorf("parseFormat(%q) = %+v, %v, want one call reading no operand", format, toks, err) + if toks, err := grammar.ParseFormat(format, builtinOperands); err != nil || len(toks) != 1 || toks[0].Names != nil { + t.Errorf("ParseFormat(%q) = %+v, %v, want one call reading no operand", format, toks, err) } } - if toks, err := parseFormat("{calc(net * qty)}"); err != nil || len(toks) != 1 || len(toks[0].names) != 2 { - t.Errorf("parseFormat({calc(net * qty)}) = %+v, %v, want both operands", toks, err) + if toks, err := grammar.ParseFormat("{calc(net * qty)}", builtinOperands); err != nil || len(toks) != 1 || len(toks[0].Names) != 2 { + t.Errorf("ParseFormat({calc(net * qty)}) = %+v, %v, want both operands", toks, err) } } -// TestCalcGuardsPanic pins the guards on calc's own invariants: checkCalc parsed -// the expression before prep sees it, and indexVars places every name calcVars -// read, so a break in either is reported rather than computed around. -func TestCalcGuardsPanic(t *testing.T) { - for name, call := range map[string]func(){ - "prep on an expression that does not parse": func() { calcPrep([]string{"1 +"}) }, - "an operand name never placed": func() { calcVar("n").eval(nil) }, - "indexVars on a name it did not read": func() { indexVars(calcVar("n"), map[string]int{}) }, - } { - mustPanic(t, name, call) - } +// TestCalcPrepPanicsOnAnUncheckedExpression pins the guard on calc's own invariant: +// checkCalc parsed the expression before prep sees it. +func TestCalcPrepPanicsOnAnUncheckedExpression(t *testing.T) { + mustPanic(t, "prep on an expression that does not parse", func() { calcPrep([]string{"1 +"}) }) } // --- one draw, one value: a calc operand reads the expansion's draw --- diff --git a/format_test.go b/format_test.go index eae7414..de444e6 100644 --- a/format_test.go +++ b/format_test.go @@ -1,7 +1,6 @@ package fejkdata import ( - "reflect" "regexp" "strings" "testing" @@ -131,18 +130,3 @@ func TestFormatCompileErrors(t *testing.T) { } } } - -// TestSplitArgsQuotesOutsideSelectors pins the arg grammar: a comma splits outside a -// selector and outside a quoted layout, and a quote inside a selector is a name's text. -func TestSplitArgsQuotesOutsideSelectors(t *testing.T) { - for in, want := range map[string][]string{ - "a, b": {"a", "b"}, - "1990-01-01,1990-12-31,'January 2, 2006'": {"1990-01-01", "1990-12-31", "'January 2, 2006'"}, - "/geo.US.locality[O'Fallon].name, 2": {"/geo.US.locality[O'Fallon].name", "2"}, - "'[a,b]'": {"'[a,b]'"}, - } { - if got := splitArgs(in); !reflect.DeepEqual(got, want) { - t.Errorf("splitArgs(%q) = %q, want %q", in, got, want) - } - } -} diff --git a/imports_test.go b/imports_test.go index 35807ba..6b6bea8 100644 --- a/imports_test.go +++ b/imports_test.go @@ -29,9 +29,10 @@ func TestPackageImports(t *testing.T) { } checked := 0 allowed := map[string][]string{ - ".": {"internal/drawstate"}, + ".": {"internal/drawstate", "internal/grammar"}, "cmd/fejkdata": {"."}, "internal/drawstate": nil, + "internal/grammar": nil, } seen := map[string]bool{} err = filepath.WalkDir(".", func(dir string, d fs.DirEntry, err error) error { diff --git a/internal/grammar/calc_test.go b/internal/grammar/calc_test.go new file mode 100644 index 0000000..191a20c --- /dev/null +++ b/internal/grammar/calc_test.go @@ -0,0 +1,31 @@ +package grammar + +import ( + "reflect" + "testing" +) + +func TestParseCalcPlacesEachOperandAtItsFirstReading(t *testing.T) { + n, err := ParseCalc("b * (a - b) / -2") + if err != nil { + t.Fatal(err) + } + want := CalcBin{'/', CalcBin{'*', CalcVar{"b", 0}, CalcBin{'-', CalcVar{"a", 1}, CalcVar{"b", 0}}}, CalcNeg{CalcNum(2)}} + if !reflect.DeepEqual(n, want) { + t.Errorf("ParseCalc = %#v, want %#v", n, want) + } + if got := CalcVars(n); !reflect.DeepEqual(got, []string{"b", "a"}) { + t.Errorf("CalcVars = %q, want [b a]", got) + } + if got := CalcText(n); got != "((b * (a - b)) / -2)" { + t.Errorf("CalcText = %q", got) + } +} + +func TestParseCalcRefusesAnUnfinishedExpression(t *testing.T) { + for _, expr := range []string{"", " ", "1 +", "(1", "1 2", "a.b", "postal-code *"} { + if _, err := ParseCalc(expr); err == nil { + t.Errorf("ParseCalc(%q) = nil error, want one", expr) + } + } +} diff --git a/internal/grammar/format_test.go b/internal/grammar/format_test.go new file mode 100644 index 0000000..4223797 --- /dev/null +++ b/internal/grammar/format_test.go @@ -0,0 +1,47 @@ +package grammar + +import ( + "reflect" + "testing" +) + +// TestSplitArgsQuotesOutsideSelectors pins the arg grammar: a comma splits outside a +// selector and outside a quoted layout, and a quote inside a selector is a name's text. +func TestSplitArgsQuotesOutsideSelectors(t *testing.T) { + for in, want := range map[string][]string{ + "a, b": {"a", "b"}, + "1990-01-01,1990-12-31,'January 2, 2006'": {"1990-01-01", "1990-12-31", "'January 2, 2006'"}, + "/geo.US.locality[O'Fallon].name, 2": {"/geo.US.locality[O'Fallon].name", "2"}, + "'[a,b]'": {"'[a,b]'"}, + } { + if got := SplitArgs(in); !reflect.DeepEqual(got, want) { + t.Errorf("SplitArgs(%q) = %q, want %q", in, got, want) + } + } +} + +func TestParseFormatReadsEachTokenKind(t *testing.T) { + toks, err := ParseFormat("a{{{x|/y.z[a.b]}{/p as q}{f(1, n)}", func(fn string, args []string) []string { + return args[1:] + }) + if err != nil { + t.Fatal(err) + } + want := []Token{ + {Kind: LiteralRun, Lit: "a{"}, + {Kind: NameRead, Body: "x|/y.z[a.b]", Names: []string{"x", "/y.z[a.b]"}}, + {Kind: NameBind, Body: "/p as q", BoundRef: "/p", Bound: "q"}, + {Kind: BuiltinCall, Body: "f(1, n)", Fn: "f", Args: []string{"1", "n"}, Names: []string{"n"}}, + } + if !reflect.DeepEqual(toks, want) { + t.Errorf("ParseFormat = %+v, want %+v", toks, want) + } +} + +func TestParseFormatRefusesUnbalancedBraces(t *testing.T) { + for _, format := range []string{"{a", "a}", "{a{b}}", "{f(}"} { + if _, err := ParseFormat(format, nil); err == nil { + t.Errorf("ParseFormat(%q) = nil error, want one", format) + } + } +} diff --git a/internal/grammar/name_test.go b/internal/grammar/name_test.go new file mode 100644 index 0000000..5b648ff --- /dev/null +++ b/internal/grammar/name_test.go @@ -0,0 +1,16 @@ +package grammar + +import "testing" + +func TestCheckNameRefusesWhatAGrammarReserves(t *testing.T) { + for _, name := range []string{"", "-", "a as b", "a.b", "a|b", "a(b", "a{b", "a}b", "a/b", "a[b", "a]b", `a"b`} { + if CheckName(name) == nil { + t.Errorf("CheckName(%q) = nil, want an error", name) + } + } + for _, name := range []string{"a", "postal-code", "é", "a_b"} { + if err := CheckName(name); err != nil { + t.Errorf("CheckName(%q) = %v, want nil", name, err) + } + } +} diff --git a/internal/grammar/path_test.go b/internal/grammar/path_test.go new file mode 100644 index 0000000..5ea21ae --- /dev/null +++ b/internal/grammar/path_test.go @@ -0,0 +1,40 @@ +package grammar + +import ( + "reflect" + "testing" +) + +func TestSplitPathKeepsSelectorsAndStepsUpWhole(t *testing.T) { + for path, want := range map[string][]string{ + "a": {"a"}, + "a.b": {"a", "b"}, + "misc.territory[SE].name": {"misc", "territory", "[SE]", "name"}, + "x[a.b]": {"x", "[a.b]"}, + "city[Oslo]..country": {"city", "[Oslo]", "..", "country"}, + "a..b": {"a", "..", "b"}, + } { + segs, err := SplitPath(path) + if err != nil || !reflect.DeepEqual(segs, want) { + t.Errorf("SplitPath(%q) = %q, %v, want %q", path, segs, err, want) + continue + } + if got := JoinSegments(segs); got != path { + t.Errorf("JoinSegments(%q) = %q, want %q", segs, got, path) + } + } +} + +func TestSplitPathRefusesAStraySelectorBracket(t *testing.T) { + for _, path := range []string{"[a]", "a.[b]", "a[b", "a]", "a[]", "a[b]c", "a[b[c]]"} { + if _, err := SplitPath(path); err == nil { + t.Errorf("SplitPath(%q) = nil error, want one", path) + } + } +} + +func TestNameSegmentsDropSelectorsAndStepsUp(t *testing.T) { + if got := NameSegments([]string{"a", "[b]", "..", "c"}); !reflect.DeepEqual(got, []string{"a", "c"}) { + t.Errorf("NameSegments = %q, want [a c]", got) + } +} diff --git a/internal/grammar/reference_test.go b/internal/grammar/reference_test.go new file mode 100644 index 0000000..4f32ea0 --- /dev/null +++ b/internal/grammar/reference_test.go @@ -0,0 +1,21 @@ +package grammar + +import "testing" + +func TestRefShapeSplitsTheSigil(t *testing.T) { + for ref, want := range map[string][2]string{ + "/a.b": {"/", "a.b"}, + ".a": {".", "a"}, + "..a.b": {"..", "a.b"}, + } { + sigil, rest, err := RefShape(ref) + if err != nil || sigil != want[0] || rest != want[1] { + t.Errorf("RefShape(%q) = %q, %q, %v, want %q", ref, sigil, rest, err, want) + } + } + for _, ref := range []string{"/", "//a", ".../a", "/a..b.", "/a.[b]"} { + if _, _, err := RefShape(ref); err == nil { + t.Errorf("RefShape(%q) = nil error, want one", ref) + } + } +}