From 8fa33694b9b38b22b6f7d23235b063e1d901f9f4 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 2 Sep 2026 12:42:21 +0200 Subject: [PATCH] Tests for references held like siblings and the lowercase, uppercase and ascii transforms --- calc_test.go | 12 ++++----- reference_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++---- transform_test.go | 53 +++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 transform_test.go diff --git a/calc_test.go b/calc_test.go index b9d6072..ab38c72 100644 --- a/calc_test.go +++ b/calc_test.go @@ -88,18 +88,18 @@ func TestCalcReproducible(t *testing.T) { } } -// TestCalcTokenOperandsReadsOnlyACalc pins what the helper answers for a body that +// TestTokenOperandsReadsOnlyAnOperandBuiltin pins what the helper answers for a body that // is not a calc, and for one whose expression does not parse: nothing, either way. // checkCalc is what reports a bad expression, so the callers that run after it // never meet one — but they must not have to depend on that order to be safe. -func TestCalcTokenOperandsReadsOnlyACalc(t *testing.T) { +func TestTokenOperandsReadsOnlyAnOperandBuiltin(t *testing.T) { for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} { - if got := calcTokenOperands(body); got != nil { - t.Errorf("calcTokenOperands(%q) = %v, want none", body, got) + if got := tokenOperands(body); got != nil { + t.Errorf("tokenOperands(%q) = %v, want none", body, got) } } - if got := calcTokenOperands("calc(net * qty)"); len(got) != 2 { - t.Errorf("calcTokenOperands(calc(net * qty)) = %v, want both operands", got) + if got := tokenOperands("calc(net * qty)"); len(got) != 2 { + t.Errorf("tokenOperands(calc(net * qty)) = %v, want both operands", got) } } diff --git a/reference_test.go b/reference_test.go index 7ac7b52..90d35b6 100644 --- a/reference_test.go +++ b/reference_test.go @@ -1,6 +1,9 @@ package fejkdata -import "testing" +import ( + "strings" + "testing" +) // TestRootReferenceAcrossFolders is the headline case: a category in one folder // pulls a value from another via a {..path} reference resolved from the data root. @@ -15,8 +18,7 @@ func TestRootReferenceAcrossFolders(t *testing.T) { } } -// TestReferenceIntoAField reaches a field inside a referenced category, crossing -// a single-variant choice and then a template field (..who.last). +// TestReferenceIntoAField reaches a field inside a referenced category. func TestReferenceIntoAField(t *testing.T) { dir := writeData(t, map[string]string{ "who": `{"format":"{first} {last}","first":"Ada","last":"Byron"}`, @@ -76,8 +78,8 @@ func TestReferenceErrors(t *testing.T) { cases := map[string]map[string]string{ "missing target": {"card": `"{..nope.gone}"`}, "folder target": {"en_US/word": `"w"`, "card": `"{..en_US}"`}, - "multi-variant on the path": { - "who": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`, + "a variant on the path lacks the field": { + "who": `[{"format":"{f}","f":"1"},{"format":"{g}","g":"2"}]`, "card": `"{..who.f}"`, }, "empty reference path": {"card": `"{..}"`}, @@ -208,3 +210,58 @@ func TestNewErrorPathIsCanonical(t *testing.T) { } } } + +func TestReferencePathIsHeld(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg"}]`, + "card": `"{..person.first} {..person.last}"`, + }) + f := newGenerator(t, dir, WithSeed(3)) + seen := map[string]bool{} + for i := 0; i < 50; i++ { + got := fake(t, f, "card") + if got != "Anna Andersson" && got != "Bo Berg" { + t.Fatalf("card = %q, want one person's first and last name", got) + } + seen[got] = true + } + if len(seen) != 2 { + t.Fatalf("card only ever rendered %v", seen) + } +} + +func TestReferenceThroughChoiceNeedsEveryVariant(t *testing.T) { + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + "who": `[{"format":"{f}{h}","f":"1","h":"x"},{"format":"{g}{h}","g":"2","h":"y"}]`, + "card": `"{..who.f}"`, + }))) + if err == nil || !strings.Contains(err.Error(), "not every variant") { + t.Fatalf("New = %v, want the missing variant named", err) + } +} + +func TestBareReferenceDrawsEachTime(t *testing.T) { + dir := writeData(t, map[string]string{ + "die": `["1","2","3","4","5","6"]`, + "roll": `"{..die} {..die}"`, + }) + f := newGenerator(t, dir, WithSeed(1)) + for i := 0; i < 50; i++ { + if got := fake(t, f, "roll"); got[0] != got[2] { + return + } + } + t.Fatal("two bare references always agreed; each should be its own draw") +} + +func TestReferenceOverlapIsRejected(t *testing.T) { + for name, file := range map[string]string{ + "head beside a path": `{"format":"{..cat.p} {..cat.p.first}","p":[{"format":"{first}","first":"A"},{"format":"{first}","first":"B"}]}`, + "sibling path beside a reference path": `{"format":"{p.first} {..cat.p.last}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`, + } { + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) + if err == nil || !strings.Contains(err.Error(), "reads a path into") { + t.Errorf("%s: New = %v, want the overlap rejected", name, err) + } + } +} diff --git a/transform_test.go b/transform_test.go new file mode 100644 index 0000000..31d8279 --- /dev/null +++ b/transform_test.go @@ -0,0 +1,53 @@ +package fejkdata + +import "testing" + +func TestTransforms(t *testing.T) { + f := engine(1) + for src, want := range map[string]string{ + `{"format":"{uppercase(x)}|{lowercase(x)}|{ascii(x)}","x":"Åsa Ödegård-Nuñez"}`: "ÅSA ÖDEGÅRD-NUÑEZ|åsa ödegård-nuñez|Asa Odegard-Nunez", + `{"format":"{lowercase(ascii(x))}","x":"Åsa"}`: "asa", + `{"format":"{ascii(x)}","x":"Ærø ß 日本"}`: "AEro ss ", + } { + if got := mustRender(t, f, src); got != want { + t.Errorf("render(%s) = %q, want %q", src, got, want) + } + } +} + +func TestTransformReadsTheHeldDraw(t *testing.T) { + f := engine(2) + src := `{"format":"{p.first}={lowercase(p.first)}","p":[{"format":"{first}","first":"Anna"},{"format":"{first}","first":"Bo"}]}` + for i := 0; i < 50; i++ { + if got := mustRender(t, f, src); got != "Anna=anna" && got != "Bo=bo" { + t.Fatalf("render = %q, want the transform over the same draw", got) + } + } +} + +func TestTransformOverAReference(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `[{"format":"{first} {last}","first":"Åsa","last":"Öberg"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`, + "email": `"{..person.first} {..person.last} <{lowercase(ascii(..person.first))}.{lowercase(ascii(..person.last))}@example.com>"`, + }) + f := newGenerator(t, dir, WithSeed(5)) + for i := 0; i < 50; i++ { + if got := fake(t, f, "email"); got != "Åsa Öberg " && got != "Bo Ek " { + t.Fatalf("email = %q, want a record from one draw", got) + } + } +} + +func TestTransformArgs(t *testing.T) { + for _, bad := range []string{ + `{"format":"{lowercase()}","x":"v"}`, + `{"format":"{lowercase(nope)}","x":"v"}`, + `{"format":"{ascii(x,y)}","x":"v","y":"w"}`, + `{"format":"{lowercase(hex(2))}","x":"v"}`, + `{"format":"{lowercase(x)} {x.a}","x":{"format":"{a}","a":"1"}}`, + } { + if _, err := compile(parse(t, bad)); err == nil { + t.Errorf("compile(%s) = nil error, want it rejected", bad) + } + } +}