diff --git a/name_test.go b/name_test.go new file mode 100644 index 0000000..5107422 --- /dev/null +++ b/name_test.go @@ -0,0 +1,212 @@ +package fejkdata + +import ( + "strings" + "testing" +) + +// nameTables is a region table linked to by a municipality table. +func nameTables() map[string]string { + return map[string]string{ + "region.json": `{"format":"{name}","rows":"region.tsv","key":"code"}`, + "region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n", + "municipality.json": `{"format":"{name}","rows":"municipality.tsv","key":"code","parent":"region"}`, + "municipality.tsv": "code\tname\tregion\n0180\tStockholm\t01\n0184\tSolna\t01\n1280\tMalmö\t12\n1281\tLund\t12\n", + } +} + +var municipalityCode = map[string]string{"Stockholm": "0180", "Solna": "0184", "Malmö": "1280", "Lund": "1281"} + +var municipalityRegion = map[string]string{"Stockholm": "Stockholms län", "Solna": "Stockholms län", "Malmö": "Skåne län", "Lund": "Skåne län"} + +func TestANameIsOnePickOfEverythingUnderIt(t *testing.T) { + files := nameTables() + files["card.json"] = `"{/region as r}{r.name}|{r.municipality.name}|{r.municipality.code}|{r}"` + f := newGenerator(t, writeFiles(t, files), WithSeed(3)) + for i := 0; i < 200; i++ { + got := strings.Split(fake(t, f, "card"), "|") + region, municipality, code, whole := got[0], got[1], got[2], got[3] + if municipalityCode[municipality] != code || municipalityRegion[municipality] != region || whole != region { + t.Fatalf("card = %q, want one region, and one municipality inside it", got) + } + } +} + +func TestANameReadWholeAgreesWithItsPaths(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `{"format":"{first} {last}","first":["Ada","Bo","Cy"],"last":["Byron","Ek","Lind"]}`, + "card": `"{/person as p}{p}|{p.last}|{p.first}"`, + }) + f := newGenerator(t, dir, WithSeed(5)) + for i := 0; i < 200; i++ { + got := strings.Split(fake(t, f, "card"), "|") + if got[0] != got[2]+" "+got[1] { + t.Fatalf("card = %q, want the whole person to match its parts", got) + } + } +} + +func TestANameDrawsEachFieldUnderItOnce(t *testing.T) { + dir := writeData(t, map[string]string{ + "word": `{"format":"{w}-{w}","w":["a","b","c","d","e","f","g","h"]}`, + "card": `"{/word as w}{w}"`, + }) + f := newGenerator(t, dir, WithSeed(7)) + for i := 0; i < 100; i++ { + if got := fake(t, f, "card"); got[0] != got[2] { + t.Fatalf("card = %q, want one draw of w under the name", got) + } + } +} + +func TestABindingPrintsNothing(t *testing.T) { + dir := writeData(t, map[string]string{ + "word": `["a","b"]`, + "card": `"<{/word as w}>{w}"`, + }) + f := newGenerator(t, dir, WithSeed(1)) + if got := fake(t, f, "card"); got != "<>a" && got != "<>b" { + t.Fatalf("card = %q, want the binding to print nothing", got) + } +} + +func TestSiblingFieldsAndColumnsReadOneName(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `{"format":"{first} {last}","first":["Ada","Bo","Cy"],"last":["Byron","Ek","Lind"]}`, + "row": `{"format":"{first} {last}","first":"{/person as p}{p.first}","last":"{p.last}","whole":"{p}"}`, + }) + f := newGenerator(t, dir, WithSeed(9)) + for i := 0; i < 100; i++ { + r, err := f.FakeRecord("row") + if err != nil { + t.Fatal(err) + } + c := r.Columns() + if c[2].Value != c[0].Value+" "+c[1].Value { + t.Fatalf("columns = %v, want one person across them", c) + } + if got := strings.Fields(fake(t, f, "row")); len(got) != 2 { + t.Fatalf("row = %q, want a first and a last name", got) + } + } +} + +func TestAFieldReadsTheNameItsCategoryBinds(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `{"format":"{first} {last}","first":"Ada","last":"Byron"}`, + "row": `{"format":"{first}","first":"{/person as p}{p.first}","last":"{p.last}"}`, + }) + f := newGenerator(t, dir, WithSeed(1)) + if got := fake(t, f, "row.last"); got != "Byron" { + t.Fatalf("row.last = %q, want Byron", got) + } +} + +func TestANameInsideARepeatPicksEachIteration(t *testing.T) { + dir := writeData(t, map[string]string{ + "word": `["a","b","c","d","e","f","g","h"]`, + "line": `{"format":"{each}","each":{"format":"{/word as w}{w}{w}","repeat":40,"separator":" "}}`, + }) + f := newGenerator(t, dir, WithSeed(11)) + seen := map[string]bool{} + for _, pair := range strings.Fields(fake(t, f, "line")) { + if pair[0] != pair[1] { + t.Fatalf("pair %q, want one pick per iteration", pair) + } + seen[pair] = true + } + if len(seen) < 2 { + t.Fatalf("pairs %v, want each iteration to pick anew", seen) + } +} + +func TestANameOutsideARepeatKeepsItsPick(t *testing.T) { + dir := writeData(t, map[string]string{ + "word": `["a","b","c","d","e","f","g","h"]`, + "line": `{"format":"{/word as w}{each}","each":{"format":"{w}","repeat":40,"separator":" "}}`, + }) + f := newGenerator(t, dir, WithSeed(13)) + got := strings.Fields(fake(t, f, "line")) + for _, w := range got { + if w != got[0] { + t.Fatalf("line = %q, want one pick on every line", got) + } + } +} + +func TestTwoRendersPickTwice(t *testing.T) { + dir := writeData(t, map[string]string{ + "word": `["a","b","c","d","e","f","g","h"]`, + "one": `"{/word as w}{w}"`, + "two": `"{/one}{/one}{/one}{/one}{/one}{/one}"`, + }) + f := newGenerator(t, dir, WithSeed(17)) + got := fake(t, f, "two") + if strings.Count(got, got[:1]) == len(got) { + t.Fatalf("two = %q, want each bare reference to its own pick", got) + } +} + +func TestANameInAnInlineTemplate(t *testing.T) { + dir := writeData(t, map[string]string{ + "person": `{"format":"{first} {last}","first":["Ada","Bo","Cy"],"last":["Byron","Ek","Lind"]}`, + }) + f := newGenerator(t, dir, WithSeed(19)) + for i := 0; i < 50; i++ { + got := strings.Split(fakeTemplate(t, f, "{/person as p}{p}|{p.first} {p.last}"), "|") + if got[0] != got[1] { + t.Fatalf("template = %q, want one person", got) + } + } +} + +func TestNameErrors(t *testing.T) { + cases := []struct { + name string + files map[string]string + want string + }{ + {"bound twice", map[string]string{"word": `["a","b"]`, "card": `{"format":"{x}{y}","x":"{/word as w}{w}","y":"{/word as w}"}`}, + `name "w" is bound twice in one scope`}, + {"bound outside the repeat too", map[string]string{"word": `["a","b"]`, "card": `{"format":"{/word as w}{x}","x":{"format":"{/word as w}{w}","repeat":2}}`}, + `name "w" is bound outside this repeat already`}, + {"a field too", map[string]string{"word": `["a","b"]`, "card": `{"format":"{/word as w}{w}","w":"x"}`}, + `name "w" is a field too`}, + {"an option", map[string]string{"word": `["a","b"]`, "card": `"{/word as format}{format}"`}, + `"format" is an option and can never be a name`}, + {"no reference", map[string]string{"card": `{"format":"{word as w}{w}","word":["a","b"]}`}, + `"word" is no reference`}, + {"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`}, + `nothing reads name "w"`}, + {"unbound", map[string]string{"word": `["a","b"]`, "card": `{"format":"{x}{w}","x":{"format":"{/word as w}{w}","repeat":2}}`}, + `no field "w"`}, + {"padded", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}{w}"`}, + `write {/word as w}`}, + {"selector after a name", map[string]string{"region.json": nameTables()["region.json"], "card": `"{/region as r}{r.municipality[0180].name}"`}, + `a path through name "r" selects no row`}, + {"in a table's format", map[string]string{"word": `["a","b"]`, "t.json": `{"format":"{/word as w}{w}","rows":"t.tsv","key":"k"}`, "t.tsv": "k\nx\ny\n"}, + "a table binds no name"}, + {"in a field's name", map[string]string{"card": `{"format":"{a as b}","a as b":"x"}`}, + `contains " as "`}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + files := map[string]string{} + for name, body := range c.files { + if !strings.Contains(name, ".") { + name += ".json" + } + files[name] = body + } + if c.name == "selector after a name" { + for k, v := range nameTables() { + files[k] = v + } + } + _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))) + if err == nil || !strings.Contains(err.Error(), c.want) { + t.Fatalf("New() = %v, want an error holding %q", err, c.want) + } + }) + } +}