package fejkdata import ( "fmt" "regexp" "strings" "testing" ) // swedishPlaces is a two-variant sibling whose variants pair a locality with the // postal-code prefix that really belongs to it. const swedishPlaces = `{"format":"%s","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}` // agree reports whether a rendered "postcode locality" pair is a real pairing. var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`) func places(format string) string { return strings.Replace(swedishPlaces, "%s", format, 1) } // varies renders src until two renders of it satisfy differ, failing after 400 tries. func varies(t *testing.T, f *Generator, src string, differ func(got string) bool) { t.Helper() for i := 0; i < 400; i++ { if differ(mustRender(t, f, src)) { return } } t.Fatalf("400 renders of %s never drew apart, want each {…} a pick of its own", src) } func TestEveryTokenDrawsAfresh(t *testing.T) { f := engine(3) pair := func(got string) bool { p := strings.Split(got, "|"); return p[0] != p[1] } varies(t, f, `{"format":"{w}|{w}","w":["a","b","c","d","e"]}`, pair) varies(t, f, `{"format":"{p.first}|{p.first}","p":{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}}`, pair) varies(t, f, `{"format":"{w}|{uppercase(w)}","w":["a","b","c","d","e"]}`, func(got string) bool { p := strings.Split(got, "|") return strings.ToUpper(p[0]) != p[1] }) varies(t, f, `{"format":"{n}|{calc(n * 1)}","n":["1","2","3","4"]}`, pair) varies(t, f, places("{place.postal-code} {place.locality}"), func(got string) bool { return !agree.MatchString(got) }) } func TestEveryReferenceDrawsAfresh(t *testing.T) { f := newGenerator(t, writeData(t, map[string]string{ "person": `{"format":"{first} {last}","first":["Ada","Bo","Cy","Di"],"last":["Byron","Ek","Lind","Ros"]}`, }), WithSeed(4)) for _, src := range []string{ `"{/person.first}|{/person.first}"`, `"{/person}|{/person.first} {/person.last}"`, `{"format":"{a}|{b}","a":"{/person.last}","b":"{/person.last}"}`, } { differ := false for i := 0; i < 400 && !differ; i++ { p := strings.Split(fakeTemplate(t, f, src), "|") differ = p[0] != p[1] } if !differ { t.Errorf("400 renders of %s never drew apart, want each reference a pick of its own", src) } } } func TestANamedFieldCorrelatesItsReads(t *testing.T) { f := engine(1) seen := map[string]bool{} for i := 0; i < 500; i++ { got := mustRender(t, f, places("{place as p}{p.postal-code} {p.locality}")) if !agree.MatchString(got) { t.Fatalf("draw %d = %q, want a postcode and locality that pair", i, got) } seen[got[len(got)-4:]] = true } if len(seen) < 2 { t.Fatalf("500 draws produced only %v, want both localities", seen) } } func TestANamedFieldReadsItsVariantsWhole(t *testing.T) { // One variant holds addr as a plain template where another holds a choice; the name // keeps the variant drawn at every level its reads pass. f := engine(13) tmpl := `{"format":"{p as q}{q.addr.city}/{q.addr.zip}","p":[ {"format":"{addr}","addr":{"format":"{city}","city":"Kiruna","zip":"98100"}}, {"format":"{addr}","addr":[ {"format":"{city}","city":"Malmö","zip":"21100"}, {"format":"{city}","city":"Lund","zip":"22100"}]}]}` for i := 0; i < 400; i++ { got := mustRender(t, f, tmpl) if got != "Kiruna/98100" && got != "Malmö/21100" && got != "Lund/22100" { t.Fatalf("draw %d = %q, want a city with its own zip", i, got) } } } func TestANamedFieldInARepeatPicksEachLine(t *testing.T) { f := engine(5) tmpl := `{"format":"{lines}","lines":` + strings.Replace(places("{place as p}{p.postal-code} {p.locality}"), `"format":`, `"repeat":6,"separator":"\n","format":`, 1) + `}` varied := false for i := 0; i < 40; i++ { lines := strings.Split(mustRender(t, f, tmpl), "\n") if len(lines) != 6 { t.Fatalf("repeat 6 produced %d lines", len(lines)) } for _, line := range lines { if !agree.MatchString(line) { t.Fatalf("line %q does not pair", line) } } varied = varied || lines[0] != lines[1] || lines[1] != lines[2] } if !varied { t.Fatal("40 renders of repeat 6 never varied within a render, want a pick per line") } } func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) { for src, want := range map[string]string{ `{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`, `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `write {place.x} where it is read`, `{"format":"{place as p}{p}","place":["x","y"]}`: `write {place} where it is read`, `{"format":"{n}|{x}","x":{"format":"{a as n}{a}","a":["1","2"]}}`: `write {x.a} where it is read`, `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`, `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, } { if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s = %v, want it refused naming %s", src, err, want) } } } func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) { // A path token renders what it lands on, not the level it started from, so the // cycle walk has to follow it there. _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"static","x":"{/b}"}}`, "b": `"{/a}"`, }))) if err == nil || !strings.Contains(err.Error(), "reference cycle") { t.Fatalf("New = %v, want the cycle through {p.x} rejected", err) } } func TestACycleInALaterVariantIsRejected(t *testing.T) { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}]}`, "hop": `"{/cat}"`, }))) if err == nil || !strings.Contains(err.Error(), "reference cycle") { t.Fatalf("New = %v, want the cycle in the later variant rejected", err) } } func TestCycleThroughAPathTokenIsRejected(t *testing.T) { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/b}"}}`, "b": `"{/a}"`, }))) if err == nil || !strings.Contains(err.Error(), "reference cycle") { t.Fatalf("New = %v, want the cycle through {p.x} rejected", err) } } func TestADeepDiamondChainLoads(t *testing.T) { // A diamond chain is 2^n routes through n nodes, so every load walk must visit each // node once, not once per route. files := map[string]string{"l0": `"x"`} for i := 1; i <= 30; i++ { files[fmt.Sprintf("l%d", i)] = fmt.Sprintf( `{"format":"{a}{b}","a":"{/l%d}","b":"{/l%d}"}`, i-1, i-1) } files["thing"] = `{"format":"{p.first} {/l30}","p":{"format":"x","first":["A","B"]}}` if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil { t.Fatalf("New = %v, want a deep diamond chain to load", err) } } func TestNestedChoiceDrawsOneVariant(t *testing.T) { // A choice item may itself be a choice, so a path unwraps until it reaches a value. f := engine(21) seen := map[string]bool{} for i := 0; i < 200; i++ { seen[mustRender(t, f, `{"format":"{p.x}","p":[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]}`)] = true } if !seen["1"] || !seen["2"] || len(seen) != 2 { t.Fatalf("200 draws produced %v, want 1 and 2", seen) } } func TestRepeatingLevelIsNamedInTheError(t *testing.T) { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":"z"}}}`, }))) if err == nil || !strings.Contains(err.Error(), `"p.a"`) { t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err) } } func TestPathIntoARepeatingLevelIsRejected(t *testing.T) { // A path reads one level's draw, so it can never apply that level's repeat, directly or // behind a choice. for _, cat := range []string{ `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":"z"}}`, `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":"x"},{"format":"{a}","a":"y"}]}`, } { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat}))) if err == nil || !strings.Contains(err.Error(), "repeat") { t.Errorf("New(%s) = %v, want a path into a repeating level rejected", cat, err) } } } func TestPathIntoAPlainTemplateNamesTheMissingField(t *testing.T) { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{a.nope}","a":{"format":"x","b":"1"}}`, }))) if err == nil || !strings.Contains(err.Error(), `no field "nope"`) { t.Fatalf("New = %v, want it to name the missing field", err) } } func TestADrawIsSeedStable(t *testing.T) { tmpl := places("{place as p}{p.postal-code} {p.locality}") if a, b := mustRender(t, engine(42), tmpl), mustRender(t, engine(42), tmpl); a != b { t.Fatalf("same seed gave %q and %q, want an identical draw", a, b) } } func TestDottedTokenErrors(t *testing.T) { rejected := map[string]struct { file string want string }{ "unknown head": { `{"format":"{nope.x}","place":{"format":"{v}","v":"A"}}`, `no field "nope"`, }, "unknown tail": { `{"format":"{place.nope}","place":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}]}`, `"nope"`, }, "tail only some variants carry": { `{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},"x"]}`, `locality`, }, "path into a literal": { `{"format":"{x.y}","x":"plain"}`, `"y"`, }, "dotted field key": { `{"format":"[{a.b}]","a.b":"V"}`, `contains "."`, }, } for name, c := range rejected { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file}))) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want) } } } func TestEmptyPathSegmentIsRejected(t *testing.T) { for name, file := range map[string]string{ "trailing dot": `{"format":"[{a.}]","a":"x"}`, "triple dot": `{"format":"[{a...b}]","a":"x"}`, } { _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) if err == nil || !strings.Contains(err.Error(), "empty segment") { t.Errorf("%s: New = %v, want it to name the empty segment", name, err) } } } func TestASubFieldIsReachableByFake(t *testing.T) { dir := writeData(t, map[string]string{"address": places("{place as p}{p.postal-code} {p.locality}")}) f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(9)) if err != nil { t.Fatalf("New = %v", err) } for _, path := range []string{"address", "address.place", "address.place.locality", "address.place.postal-code"} { if _, err := f.Fake(path); err != nil { t.Errorf("Fake(%q) = %v, want it to render", path, err) } } } func TestAFieldBoundWholeIsItsPickAsAColumn(t *testing.T) { f := newGenerator(t, writeData(t, map[string]string{ "row": `{"format":"{place as p}{p.locality}","place":[{"format":"{locality}","locality":"Stockholm","zip":"1"},{"format":"{locality}","locality":"Tranås","zip":"5"}],"zip":"{p.zip}"}`, }), WithSeed(1)) zips := map[string]string{"Stockholm": "1", "Tranås": "5"} seen := map[string]bool{} for i := 0; i < 100; i++ { r, err := f.FakeRecord("row") if err != nil { t.Fatal(err) } c := r.Columns() if zips[c[0].Value] != c[1].Value { t.Fatalf("%s: want the place column and the zip read through p one place", r.JSON()) } seen[c[0].Value] = true } if len(seen) != 2 { t.Errorf("place drew only %v in 100 records, want both", seen) } } func TestAColumnReadingABoundColumnIsThatColumn(t *testing.T) { f := newGenerator(t, writeData(t, map[string]string{ "row": `{"format":"{place as p}","place":[null,{"format":"{int(1,9)}","datatype":"integer"}],"a":"{p}","z":"{p}"}`, }), WithSeed(1)) nulls := 0 for i := 0; i < 100; i++ { r, err := f.FakeRecord("row") if err != nil { t.Fatal(err) } c := r.Columns() for _, col := range c { if col.DataType != DataTypeInteger || col.Null != c[0].Null || col.Value != c[0].Value { t.Fatalf("%s: want a, place and z one integer pick, null alike", r.JSON()) } } if c[0].Null { nulls++ } } if nulls == 0 || nulls == 100 { t.Errorf("the pick was null %d times in 100 records, want both outcomes", nulls) } } func TestAFreshReadInsideANamedPickSharesNoValueWithIt(t *testing.T) { f := engine(5) src := `{"format":"{w as p}{p.a}|{p.a.b}","w":{"format":"{a}","a":{"format":"{c}|{b}","b":["1","2","3"],"c":{"format":"{b}","b":["x","y","z"]}}}}` want := regexp.MustCompile(`^[xyz]\|([123])\|([123])$`) for i := 0; i < 100; i++ { got := mustRender(t, f, src) if m := want.FindStringSubmatch(got); m == nil || m[1] != m[2] { t.Fatalf("draw %d = %q, want c's b from x, y or z, and p.a's b equal to {p.a.b}", i, got) } } } func TestEveryReadKeysEachLevelFromItsHeadToItsLeaf(t *testing.T) { root := compiled(t, `{"format":"{place.geo as g}{g.town.zip} {g.town} {x.y.z}","place":{"format":"{geo}","geo":{"format":"{town}","town":{"format":"{zip}","zip":"1"}}},"x":{"format":"{y}","y":{"format":"{z}","z":"2"}}}`).(*template) want := map[string]string{ "g.town.zip": "[ geo geo.town geo.town.zip]", "g.town": "[ geo geo.town]", "x.y.z": "[x x.y x.y.z]", } for _, o := range root.compiled.ops { for _, a := range o.arms { if got := fmt.Sprint(a.levels); got != want[a.spelling] { t.Errorf("{%s} keys its levels %s, want %s", a.spelling, got, want[a.spelling]) } } } }