From 3368f085fdf1d1d2eec6f075839854a61b257d9e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 10 Oct 2026 13:08:01 +0200 Subject: [PATCH] Test that the shipped data loads from a Go package per module, that a bare New fails naming WithDataFS, and that a read nothing provides names the modules its reader's manifest reads --- bench_test.go | 38 +++---- calc_test.go | 6 +- cmd/fejkdata/main_test.go | 6 +- data/en_US/fs_test.go | 22 ++++ data/geo/SE/fs_test.go | 22 ++++ data/geo/US/fs_test.go | 22 ++++ data/misc/fs_test.go | 22 ++++ data/sv_SE/fs_test.go | 22 ++++ data_test.go | 38 +++---- datatype_test.go | 8 +- doc_test.go | 2 +- fejkdata_test.go | 15 ++- imports_test.go | 14 ++- inline_test.go | 2 +- loading_test.go | 14 +-- name_test.go | 4 +- node_test.go | 2 +- path_test.go | 4 +- perf_test.go | 30 ++--- pick_test.go | 20 ++-- readme_test.go | 10 +- record_test.go | 2 +- reference_test.go | 22 ++-- shipped_data_test.go | 40 +++++-- shipped_test.go | 232 ++++++++++++++++++++++++++++++-------- table_test.go | 40 +++---- 26 files changed, 471 insertions(+), 188 deletions(-) create mode 100644 data/en_US/fs_test.go create mode 100644 data/geo/SE/fs_test.go create mode 100644 data/geo/US/fs_test.go create mode 100644 data/misc/fs_test.go create mode 100644 data/sv_SE/fs_test.go diff --git a/bench_test.go b/bench_test.go index 8d94278..91d94d0 100644 --- a/bench_test.go +++ b/bench_test.go @@ -6,17 +6,17 @@ import ( "testing" ) -func benchGenerator(b *testing.B, dir string) *Generator { +func benchGenerator(b *testing.B, data Option) *Generator { b.Helper() - f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(1)) + f, err := New(data, WithSeed(1)) if err != nil { b.Fatal(err) } return f } -func benchPath(b *testing.B, dir, path string) { - f := benchGenerator(b, dir) +func benchPath(b *testing.B, data Option, path string) { + f := benchGenerator(b, data) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -26,12 +26,12 @@ func benchPath(b *testing.B, dir, path string) { } } -func BenchmarkPerson(b *testing.B) { benchPath(b, "data", "sv_SE.person") } -func BenchmarkAddress(b *testing.B) { benchPath(b, "data", "sv_SE.address") } -func BenchmarkWord(b *testing.B) { benchPath(b, "data", "sv_SE.word") } -func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data", "misc.creditcard") } -func BenchmarkPersonnummer(b *testing.B) { benchPath(b, "data", "sv_SE.personnummer") } -func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data", "misc.uuid") } +func BenchmarkPerson(b *testing.B) { benchPath(b, withShipped(), "sv_SE.person") } +func BenchmarkAddress(b *testing.B) { benchPath(b, withShipped(), "sv_SE.address") } +func BenchmarkWord(b *testing.B) { benchPath(b, withShipped(), "sv_SE.word") } +func BenchmarkCreditcard(b *testing.B) { benchPath(b, withShipped(), "misc.creditcard") } +func BenchmarkPersonnummer(b *testing.B) { benchPath(b, withShipped(), "sv_SE.personnummer") } +func BenchmarkUUIDv7(b *testing.B) { benchPath(b, withShipped(), "misc.uuid") } func tmpData(b *testing.B, name, body string) string { b.Helper() @@ -44,17 +44,17 @@ func tmpData(b *testing.B, name, body string) string { func BenchmarkCalc(b *testing.B) { dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`) - benchPath(b, dir, "inv") + benchPath(b, WithDataPath(dir), "inv") } func BenchmarkLongLiteral(b *testing.B) { dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) VALUES (123, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`) - benchPath(b, dir, "sql") + benchPath(b, WithDataPath(dir), "sql") } func BenchmarkRepeat(b *testing.B) { dir := tmpData(b, "many", `{"format":"{word}","repeat":20,"separator":", ","word":["alpha","beta","gamma","delta"]}`) - benchPath(b, dir, "many") + benchPath(b, WithDataPath(dir), "many") } // BenchmarkNamed measures a format that binds a field to a name and reads two paths through @@ -62,18 +62,18 @@ func BenchmarkRepeat(b *testing.B) { // independent fields. func BenchmarkNamed(b *testing.B) { dir := tmpData(b, "addr", `{"format":"{place as p}{p.postal-code} {p.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`) - benchPath(b, dir, "addr") + benchPath(b, WithDataPath(dir), "addr") } func BenchmarkUnnamed(b *testing.B) { dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`) - benchPath(b, dir, "addr") + benchPath(b, WithDataPath(dir), "addr") } // BenchmarkNamedDeep reads two paths through a name and an intermediate level. func BenchmarkNamedDeep(b *testing.B) { dir := tmpData(b, "addr", `{"format":"{p as q}{q.addr.city} {q.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`) - benchPath(b, dir, "addr") + benchPath(b, WithDataPath(dir), "addr") } // BenchmarkNamedWide reads ten paths through one name: the point where the maps a pick keeps @@ -82,13 +82,13 @@ func BenchmarkNamedWide(b *testing.B) { dir := tmpData(b, "row", `{"format":"{r as s}{s.a}{s.b}{s.c}{s.d}{s.e}{s.f}{s.g}{s.h}{s.i}{s.j}","r":[ {"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"}, {"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`) - benchPath(b, dir, "row") + benchPath(b, WithDataPath(dir), "row") } func BenchmarkFirstFake(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - f, err := New(WithSeed(1)) + f, err := New(withShipped(), WithSeed(1)) if err != nil { b.Fatal(err) } @@ -101,7 +101,7 @@ func BenchmarkFirstFake(b *testing.B) { func BenchmarkNew(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := New(WithSeed(1)); err != nil { + if _, err := New(withShipped(), WithSeed(1)); err != nil { b.Fatal(err) } } diff --git a/calc_test.go b/calc_test.go index 90ad27b..ada0eaa 100644 --- a/calc_test.go +++ b/calc_test.go @@ -303,14 +303,14 @@ func TestCalcRefusesABadOperand(t *testing.T) { "zero": `"0"`, "card": card, }) - if _, err := New(WithDataPath(dir), WithSeed(1)); err == nil || !strings.Contains(err.Error(), want) { + if _, err := New(withShipped(), WithDataPath(dir), WithSeed(1)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("New with card %s = %v, want an error containing %q", card, err, want) } } } func TestATypedColumnRefusesACalcOverNoDigits(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "row": `{"format":"","x":{"format":"{calc(d + 1)}","d":"{digits(0)}","datatype":"integer"}}`, }))) if err == nil || !strings.Contains(err.Error(), "{digits(0)} prints nothing, which is no number") { @@ -338,7 +338,7 @@ func TestATypedColumnProvesACalcOverAName(t *testing.T) { "word": `["1","x"]`, "order": `{"format":"{/word as w}{w}","double":{"format":"{calc(w * 2)}","datatype":"integer"}}`, }) - if _, err := New(WithDataPath(bad), WithSeed(1)); err == nil || !strings.Contains(err.Error(), `operand "w": "x" is not a number`) { + if _, err := New(withShipped(), WithDataPath(bad), WithSeed(1)); err == nil || !strings.Contains(err.Error(), `operand "w": "x" is not a number`) { t.Fatalf("New = %v, want the typed column refused over an operand that is not always a number", err) } } diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index 502cda9..ab5bf51 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -13,9 +13,9 @@ import ( ) const ( - svSE = "../../data/sv_SE" - enUS = "../../data/en_US" - misc = "../../data/misc" + svSE = "../../data/sv_SE/sv_SE" + enUS = "../../data/en_US/en_US" + misc = "../../data/misc/misc" ) func runOut(stdin string, args ...string) (int, string, string) { diff --git a/data/en_US/fs_test.go b/data/en_US/fs_test.go new file mode 100644 index 0000000..6b8c4e7 --- /dev/null +++ b/data/en_US/fs_test.go @@ -0,0 +1,22 @@ +package en_US_test + +import ( + "io/fs" + "testing" + + "github.com/larvit/fejkdata" + "github.com/larvit/fejkdata/data/en_US" +) + +func TestFSLoadsByItsManifest(t *testing.T) { + if _, err := fs.Stat(en_US.FS, ".fejkdata.json"); err != nil { + t.Fatalf("FS holds no manifest: %v", err) + } + f, err := fejkdata.New(fejkdata.WithDataFS(en_US.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(FS)) = %v", err) + } + if len(f.List()) == 0 { + t.Error("List() is empty, want the module's categories") + } +} diff --git a/data/geo/SE/fs_test.go b/data/geo/SE/fs_test.go new file mode 100644 index 0000000..1940ed3 --- /dev/null +++ b/data/geo/SE/fs_test.go @@ -0,0 +1,22 @@ +package SE_test + +import ( + "io/fs" + "testing" + + "github.com/larvit/fejkdata" + "github.com/larvit/fejkdata/data/geo/SE" +) + +func TestFSLoadsByItsManifest(t *testing.T) { + if _, err := fs.Stat(SE.FS, ".fejkdata.json"); err != nil { + t.Fatalf("FS holds no manifest: %v", err) + } + f, err := fejkdata.New(fejkdata.WithDataFS(SE.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(FS)) = %v", err) + } + if len(f.List()) == 0 { + t.Error("List() is empty, want the module's categories") + } +} diff --git a/data/geo/US/fs_test.go b/data/geo/US/fs_test.go new file mode 100644 index 0000000..ca1236e --- /dev/null +++ b/data/geo/US/fs_test.go @@ -0,0 +1,22 @@ +package US_test + +import ( + "io/fs" + "testing" + + "github.com/larvit/fejkdata" + "github.com/larvit/fejkdata/data/geo/US" +) + +func TestFSLoadsByItsManifest(t *testing.T) { + if _, err := fs.Stat(US.FS, ".fejkdata.json"); err != nil { + t.Fatalf("FS holds no manifest: %v", err) + } + f, err := fejkdata.New(fejkdata.WithDataFS(US.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(FS)) = %v", err) + } + if len(f.List()) == 0 { + t.Error("List() is empty, want the module's categories") + } +} diff --git a/data/misc/fs_test.go b/data/misc/fs_test.go new file mode 100644 index 0000000..50ca960 --- /dev/null +++ b/data/misc/fs_test.go @@ -0,0 +1,22 @@ +package misc_test + +import ( + "io/fs" + "testing" + + "github.com/larvit/fejkdata" + "github.com/larvit/fejkdata/data/misc" +) + +func TestFSLoadsByItsManifest(t *testing.T) { + if _, err := fs.Stat(misc.FS, ".fejkdata.json"); err != nil { + t.Fatalf("FS holds no manifest: %v", err) + } + f, err := fejkdata.New(fejkdata.WithDataFS(misc.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(FS)) = %v", err) + } + if len(f.List()) == 0 { + t.Error("List() is empty, want the module's categories") + } +} diff --git a/data/sv_SE/fs_test.go b/data/sv_SE/fs_test.go new file mode 100644 index 0000000..39011b0 --- /dev/null +++ b/data/sv_SE/fs_test.go @@ -0,0 +1,22 @@ +package sv_SE_test + +import ( + "io/fs" + "testing" + + "github.com/larvit/fejkdata" + "github.com/larvit/fejkdata/data/sv_SE" +) + +func TestFSLoadsByItsManifest(t *testing.T) { + if _, err := fs.Stat(sv_SE.FS, ".fejkdata.json"); err != nil { + t.Fatalf("FS holds no manifest: %v", err) + } + f, err := fejkdata.New(fejkdata.WithDataFS(sv_SE.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(FS)) = %v", err) + } + if len(f.List()) == 0 { + t.Error("List() is empty, want the module's categories") + } +} diff --git a/data_test.go b/data_test.go index 231cb92..c736447 100644 --- a/data_test.go +++ b/data_test.go @@ -50,7 +50,7 @@ func TestShippedDataCategories(t *testing.T) { regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)}, } - f := newGenerator(t, "data", WithSeed(1)) + f := shipped(t, WithSeed(1)) for _, c := range cases { for i := 0; i < 200; i++ { if v := fake(t, f, "en_US."+c.path); !c.en.MatchString(v) { @@ -67,7 +67,7 @@ func TestShippedDataCategories(t *testing.T) { // v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers // whose trailing {luhn()} check passes. func TestShippedMiscCategories(t *testing.T) { - f := newGenerator(t, "data/misc", WithSeed(1)) + f := newGenerator(t, "data/misc/misc", WithSeed(1)) v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`) objectid := regexp.MustCompile(`^[0-9a-f]{24}$`) @@ -95,7 +95,7 @@ func TestShippedMiscCategories(t *testing.T) { // codes follow their standard shape, dotted sub-paths resolve, emoji is a real // glyph, and a coordinate parses to a point within geographic bounds. func TestShippedMiscReferenceData(t *testing.T) { - f := newGenerator(t, "data/misc", WithSeed(1)) + f := newGenerator(t, "data/misc/misc", WithSeed(1)) re := map[string]*regexp.Regexp{ "currency": regexp.MustCompile(`^[A-Z]{3}$`), "currency.name": regexp.MustCompile(`\p{L}`), @@ -158,7 +158,7 @@ func TestShippedMiscReferenceData(t *testing.T) { // TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape // regexes miss: no generated name repeats a character three times over. func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) { - f := newGenerator(t, "data", WithSeed(11)) + f := shipped(t, WithSeed(11)) for _, path := range []string{"sv_SE.person", "sv_SE.person.last"} { for i := 0; i < 20000; i++ { name := fake(t, f, path) @@ -177,7 +177,7 @@ func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) { // male), the trailing digit is a Luhn checksum over the other nine, and a // samordningsnummer is the same with 60 added to the day. func TestSwedishPersonnummer(t *testing.T) { - sv := newGenerator(t, "data", WithSeed(1)) + sv := shipped(t, WithSeed(1)) re := regexp.MustCompile(`^\d{6}-23[89]\d$`) sawLongMonthEnd, birth := false, map[string]bool{} for i := 0; i < 2000; i++ { @@ -216,7 +216,7 @@ func TestSwedishPersonnummer(t *testing.T) { // 666, its group 01-99 and its serial 0001-9999; an ITIN is 9XX-GG-XXXX with GG // in 50-65, 70-88, 90-92 or 94-99. func TestShippedUSTaxIds(t *testing.T) { - f := newGenerator(t, "data", WithSeed(5)) + f := shipped(t, WithSeed(5)) ssn := regexp.MustCompile(`^(\d{3})-(\d{2})-(\d{4})$`) itin := regexp.MustCompile(`^9\d{2}-(5\d|6[0-5]|7\d|8[0-8]|9[0-24-9])-\d{4}$`) for i := 0; i < 2000; i++ { @@ -240,7 +240,7 @@ func TestShippedUSTaxIds(t *testing.T) { // TestShippedPersonNames pins the name tables: a sex pins its first names, a name // both sexes carry appears under both, and the record's sex agrees with its name. func TestShippedPersonNames(t *testing.T) { - f := newGenerator(t, "data", WithSeed(9)) + f := shipped(t, WithSeed(9)) for path, want := range map[string]string{ "sv_SE.sex[f]": "kvinna", "sv_SE.sex[man].code": "m", @@ -281,7 +281,7 @@ func TestShippedPersonNames(t *testing.T) { // TestShippedPersonIsOneNamedPick pins that a person's first name, sex and title come // from one pick, and that two names bound to one person category are two people. func TestShippedPersonIsOneNamedPick(t *testing.T) { - f := newGenerator(t, "data", WithSeed(9)) + f := shipped(t, WithSeed(9)) for _, locale := range []string{"sv_SE", "en_US"} { apart := false for i := 0; i < 300; i++ { @@ -302,7 +302,7 @@ func TestShippedPersonIsOneNamedPick(t *testing.T) { } func TestShippedSwedishPhone(t *testing.T) { - f := newGenerator(t, "data", WithSeed(11)) + f := shipped(t, WithSeed(11)) re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`) for i := 0; i < 50; i++ { if n := fake(t, f, "sv_SE.phone"); !re.MatchString(n) { @@ -312,7 +312,7 @@ func TestShippedSwedishPhone(t *testing.T) { } func TestShippedSwedishAddress(t *testing.T) { - f := newGenerator(t, "data", WithSeed(3)) + f := shipped(t, WithSeed(3)) digit := regexp.MustCompile(`\d`) for i := 0; i < 30; i++ { a := fake(t, f, "sv_SE.address") @@ -333,7 +333,7 @@ func TestShippedSwedishAddress(t *testing.T) { // locality and region are one place in the geo tables, read through a name in a template or // across a struct's tags, and that a reference beside the name is another address. func TestShippedAddressIsOneNamedPick(t *testing.T) { - f := newGenerator(t, "data", WithSeed(3)) + f := shipped(t, WithSeed(3)) for _, c := range []struct{ locale, country, template, largest string }{ {"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}|", "Stockholm"}, {"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}", "New York"}, @@ -379,7 +379,7 @@ func TestShippedAddressIsOneNamedPick(t *testing.T) { // one locality holds, the region empty for SE, whose address carries none. func geoPlaces(t *testing.T, country string) map[string]bool { rows := func(table string) [][]string { - b, err := os.ReadFile("data/geo/" + country + "/" + table + ".tsv") + b, err := os.ReadFile("data/geo/" + country + "/geo/" + country + "/" + table + ".tsv") if err != nil { t.Fatal(err) } @@ -417,7 +417,7 @@ func geoPlaces(t *testing.T, country string) map[string]bool { } func TestShippedPersonHasParts(t *testing.T) { - f := newGenerator(t, "data", WithSeed(7)) + f := shipped(t, WithSeed(7)) for _, path := range []string{"sv_SE.person", "en_US.person"} { for i := 0; i < 30; i++ { if name := fake(t, f, path); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) { @@ -429,7 +429,7 @@ func TestShippedPersonHasParts(t *testing.T) { // NANPA's central office code records mark N11, 555, 950, 958, 959 and 976 unassignable. func TestShippedUSPhone(t *testing.T) { - f := newGenerator(t, "data", WithSeed(11)) + f := shipped(t, WithSeed(11)) re := regexp.MustCompile(`^(?:\(\d{3}\) |\d{3}-)([2-9]\d{2})-\d{4}$`) unassignable := map[string]bool{"555": true, "950": true, "958": true, "959": true, "976": true} for i := 0; i < 2000; i++ { @@ -470,7 +470,7 @@ func TestShippedTableCellsAreTrimmed(t *testing.T) { // TestShippedNamespacedTree loads the whole data/ tree (not a single locale) and // reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person. func TestShippedNamespacedTree(t *testing.T) { - f := newGenerator(t, "data", WithSeed(1)) + f := shipped(t, WithSeed(1)) for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} { if got := fake(t, f, path); got == "" { t.Fatalf("Fake(%q) returned empty", path) @@ -514,7 +514,7 @@ func luhnValid(s string) bool { var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`) func TestShippedStreetIsARegisteredName(t *testing.T) { - f := newGenerator(t, "data", WithSeed(5)) + f := shipped(t, WithSeed(5)) street := regexp.MustCompile(`^\p{L}[\p{L}\d:.-]*([ -][\p{L}\d:.-]+)*$`) for i := 0; i < 300; i++ { if s := fake(t, f, "sv_SE.address.street"); !street.MatchString(s) { @@ -526,7 +526,7 @@ func TestShippedStreetIsARegisteredName(t *testing.T) { func TestShippedLastNameComposition(t *testing.T) { // last is a choice of patronymic {first}sson templates, compound // {first}{last} templates and literal surnames. - f := newGenerator(t, "data", WithSeed(6)) + f := shipped(t, WithSeed(6)) for i := 0; i < 300; i++ { if s := fake(t, f, "sv_SE.person.last"); !swedishName.MatchString(s) { t.Fatalf("last name %q is not a Swedish surname", s) @@ -536,7 +536,7 @@ func TestShippedLastNameComposition(t *testing.T) { func TestShippedStreetNumberFormats(t *testing.T) { // Reachable via a hyphenated path; covers all five weighted number variants. - f := newGenerator(t, "data", WithSeed(8)) + f := shipped(t, WithSeed(8)) re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`) for i := 0; i < 300; i++ { if n := fake(t, f, "sv_SE.address.street-number"); !re.MatchString(n) { @@ -548,7 +548,7 @@ func TestShippedStreetNumberFormats(t *testing.T) { // TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and // that the everyday titles are reachable. func TestShippedUSTitleAgreesWithSex(t *testing.T) { - f := newGenerator(t, "data", WithSeed(4)) + f := shipped(t, WithSeed(4)) female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true} seen := map[string]bool{} for i := 0; i < 3000; i++ { diff --git a/datatype_test.go b/datatype_test.go index 7c1a40f..c385bb9 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -88,7 +88,7 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { for name, body := range tree { files[name] = body } - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))) + _, err := New(WithDataPath(writeData(t, files))) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: New = %v, want an error containing %q", c.name, err, c.want) } @@ -102,14 +102,14 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { `[{"format":"1","datatype":"integer"},{"format":"1.5","datatype":"number"}]`: "its items hold integer and number; a column holds one datatype", } { row := `{"format":"","col":` + column + `}` - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"row": row}))); err == nil || !strings.HasSuffix(err.Error(), want) { + if _, err := New(WithDataPath(writeData(t, map[string]string{"row": row}))); err == nil || !strings.HasSuffix(err.Error(), want) { t.Errorf("items of two declared datatypes %s: New = %v, want an error ending %q", column, err, want) } if _, err := newGenerator(t, writeData(t, tree)).NewTemplate(row); err == nil || !strings.HasSuffix(err.Error(), want) { t.Errorf("items of two declared datatypes %s: NewTemplate = %v, want an error ending %q", column, err, want) } } - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "a": `{"format":"","c":["{/b.x}",{"format":"1","datatype":"integer"}]}`, "b": `{"format":"","x":["2",{"format":"1","datatype":"integer"}]}`, }))) @@ -148,7 +148,7 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { } { row := `{"format":"","col":` + column + `}` src := `{"format":"","code":["200","404"],"n":{"format":"{int(1,9)}","datatype":"integer"}}` - f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row, "src": src})), WithSeed(1)) + f, err := New(WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row, "src": src})), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want it loaded", column, err) continue diff --git a/doc_test.go b/doc_test.go index d1ffc06..9599eef 100644 --- a/doc_test.go +++ b/doc_test.go @@ -265,7 +265,7 @@ func TestGoExamplesPrintWhatTheyRender(t *testing.T) { if lines == nil { t.Errorf("%s prints no seeded Fake, so nothing holds its example to a render", name) } - f, err := New(WithSeed(42)) + f, err := New(withShipped(), WithSeed(42)) if err != nil { t.Fatal(err) } diff --git a/fejkdata_test.go b/fejkdata_test.go index 392ff90..b680b35 100644 --- a/fejkdata_test.go +++ b/fejkdata_test.go @@ -7,10 +7,13 @@ import ( "testing" "testing/fstest" + "github.com/larvit/fejkdata/data" "github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/jsonvalue" ) +func withShipped() Option { return WithDataFS(data.Modules()...) } + // newGenerator creates a generator over a single data directory, failing on // error. Most tests load one dir; newGeneratorN loads several (last-loaded wins // on conflicts). @@ -21,7 +24,7 @@ func newGenerator(t *testing.T, dir string, opts ...Option) *Generator { func newGeneratorN(t *testing.T, dirs []string, opts ...Option) *Generator { t.Helper() - all := []Option{WithoutShippedData()} + var all []Option for _, dir := range dirs { all = append(all, WithDataPath(dir)) } @@ -43,14 +46,14 @@ func fake(t *testing.T, f *Generator, path string) string { } func TestNewMissingDirectory(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath("data/de_DE")) + _, err := New(WithDataPath("data/de_DE")) if err == nil || !strings.Contains(err.Error(), "de_DE") || !errors.Is(err, fs.ErrNotExist) { t.Fatalf("New(missing) error = %v, want the real error, naming the path", err) } } func TestNewRejectsAnEmptyDataPath(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath("")) + _, err := New(WithDataPath("")) if err == nil || !strings.Contains(err.Error(), "empty") { t.Fatalf("New(WithDataPath(\"\")) = %v, want the empty path named", err) } @@ -60,14 +63,14 @@ func TestNewReportsAnEntropyFailure(t *testing.T) { saved := randomBytes randomBytes = func([]byte) (int, error) { return 0, errors.New("no entropy") } defer func() { randomBytes = saved }() - _, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"w.json": {Data: []byte(`"x"`)}})) + _, err := New(WithDataFS(fstest.MapFS{"w.json": {Data: []byte(`"x"`)}})) if err == nil || !strings.Contains(err.Error(), "no entropy") { t.Fatalf("New() without entropy = %v, want the failure reported", err) } } func TestWithSeedIsDeterministic(t *testing.T) { - a, b := newGenerator(t, "data", WithSeed(42)), newGenerator(t, "data", WithSeed(42)) + a, b := shipped(t, WithSeed(42)), shipped(t, WithSeed(42)) for i := 0; i < 50; i++ { if x, y := fake(t, a, "sv_SE.person"), fake(t, b, "sv_SE.person"); x != y { t.Fatalf("same seed diverged at %d: %q != %q", i, x, y) @@ -76,7 +79,7 @@ func TestWithSeedIsDeterministic(t *testing.T) { } func TestDifferentSeedsDiffer(t *testing.T) { - a, b := newGenerator(t, "data", WithSeed(1)), newGenerator(t, "data", WithSeed(2)) + a, b := shipped(t, WithSeed(1)), shipped(t, WithSeed(2)) for i := 0; i < 50; i++ { if fake(t, a, "en_US.person") != fake(t, b, "en_US.person") { return diff --git a/imports_test.go b/imports_test.go index b9a4c39..766a17b 100644 --- a/imports_test.go +++ b/imports_test.go @@ -30,7 +30,13 @@ func TestPackageImports(t *testing.T) { checked := 0 allowed := map[string][]string{ ".": {"internal/builtinfunc", "internal/datafiles", "internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/jsonvalue", "internal/proven", "internal/rows"}, - "cmd/fejkdata": {"."}, + "cmd/fejkdata": {".", "data"}, + "data": {"data/en_US", "data/geo/SE", "data/geo/US", "data/misc", "data/sv_SE"}, + "data/en_US": nil, + "data/geo/SE": nil, + "data/geo/US": nil, + "data/misc": nil, + "data/sv_SE": nil, "internal/builtinfunc": {"internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/proven"}, "internal/datafiles": {"internal/grammar", "internal/jsonvalue"}, "internal/datatype": nil, @@ -42,7 +48,13 @@ func TestPackageImports(t *testing.T) { "internal/rows": {"internal/drawstate", "internal/grammar", "internal/invariant"}, } testsAlso := map[string][]string{ + ".": {"data", "data/sv_SE"}, "cmd/fejkdata": {"internal/grammar"}, + "data/en_US": {"."}, + "data/geo/SE": {"."}, + "data/geo/US": {"."}, + "data/misc": {"."}, + "data/sv_SE": {"."}, } seen := map[string]bool{} eachPackage(t, func(dir string, pkg *build.Package) { diff --git a/inline_test.go b/inline_test.go index e82b21c..828d711 100644 --- a/inline_test.go +++ b/inline_test.go @@ -16,7 +16,7 @@ func tmpl(t *testing.T, f *Generator, input string) string { func shipped(t *testing.T, opts ...Option) *Generator { t.Helper() - f, err := New(append([]Option{WithSeed(1)}, opts...)...) + f, err := New(append([]Option{withShipped(), WithSeed(1)}, opts...)...) if err != nil { t.Fatal(err) } diff --git a/loading_test.go b/loading_test.go index 96b8e4f..ab6445c 100644 --- a/loading_test.go +++ b/loading_test.go @@ -139,7 +139,7 @@ func TestMultiPathMergesFolders(t *testing.T) { // tree: everything List advertises renders, every time, and the sub-fields the // README advertises are discoverable. func TestListedPathsAllRender(t *testing.T) { - f := newGenerator(t, "data", WithSeed(4)) + f := shipped(t, WithSeed(4)) paths := f.List() for _, p := range paths { for i := 0; i < 20; i++ { @@ -187,12 +187,12 @@ func TestRepeatProductAlongAPathIsCapped(t *testing.T) { "b": `{"format":"x","repeat":2048}`, }, } { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))) + _, err := New(WithDataPath(writeData(t, files))) if err == nil || !strings.Contains(err.Error(), "repeat") || !strings.Contains(err.Error(), "1048576") { t.Errorf("%s: New = %v, want the repeat product rejected naming the maximum", name, err) } } - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + if _, err := New(WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{a}{c}","repeat":1024,"a":{"format":"{b}","repeat":1024,"b":"x"},"c":{"format":"y","repeat":1024}}`, }))); err != nil { t.Errorf("New = %v, want 1024 x 1024 along one path accepted", err) @@ -205,11 +205,11 @@ func TestNewErrors(t *testing.T) { if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil { t.Fatal(err) } - if _, err := New(WithoutShippedData(), WithDataPath(file)); err == nil || !strings.Contains(err.Error(), "xx_XX is not a directory") { + if _, err := New(WithDataPath(file)); err == nil || !strings.Contains(err.Error(), "xx_XX is not a directory") { t.Errorf("New(file) = %v, want not-a-directory error", err) } // Invalid JSON in a category file fails. - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"broken": `{ not json`}))); err == nil { + if _, err := New(WithDataPath(writeData(t, map[string]string{"broken": `{ not json`}))); err == nil { t.Error("New(invalid JSON) = nil error") } // An option that cannot take effect, and a category or folder no dot path can @@ -300,7 +300,7 @@ func TestNewErrors(t *testing.T) { }, } for name, c := range rejected { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) + _, err := New(WithDataPath(writeData(t, c.files))) if err == nil { t.Errorf("%s: New = nil error, want it rejected at load", name) continue @@ -328,7 +328,7 @@ func TestNewErrors(t *testing.T) { "repeated reference arm": {map[string]string{"a": `"x"`, "b": `"{/a|/a}"`}, "b", "x"}, } for name, c := range accepted { - f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) + f, err := New(WithDataPath(writeData(t, c.files))) if err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) continue diff --git a/name_test.go b/name_test.go index c624439..dc8827d 100644 --- a/name_test.go +++ b/name_test.go @@ -253,7 +253,7 @@ func TestNameErrors(t *testing.T) { } files[name] = body } - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))) + _, err := New(WithDataPath(writeFiles(t, files))) if c.want == "" { if err != nil { t.Fatalf("New() = %v, want it to load", err) @@ -463,7 +463,7 @@ func TestARowThroughANameHintsAPathThatLoads(t *testing.T) { files := nameTables() files["card.json"] = `{"format":"{x as r}{r[01]}","x":"{/region}"}` want := `a path through name "r" may not select a row; bind the row to a name of its own` - if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { + if _, err := New(WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("New(a field-bound name selecting a row) = %v, want %s", err, want) } } diff --git a/node_test.go b/node_test.go index b37111e..c899efb 100644 --- a/node_test.go +++ b/node_test.go @@ -17,7 +17,7 @@ func TestAnOptionOfTheWrongKindIsNamedByItsJSONKind(t *testing.T) { t.Errorf("compile(%s) = %v, want %q", src, err, want) } } - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":5}`, "t.tsv": "a\nx\n"}))) + _, err := New(WithDataPath(writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":5}`, "t.tsv": "a\nx\n"}))) if err == nil || !strings.Contains(err.Error(), "key must be a string, not a number") { t.Errorf("key 5 = %v, want the kind named", err) } diff --git a/path_test.go b/path_test.go index a7bf163..7983033 100644 --- a/path_test.go +++ b/path_test.go @@ -130,7 +130,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) { dir := writeData(t, map[string]string{ "cat": `[{"format":"{a.b}","a.b":"1"},{"format":"{a}","a":{"format":"{b}","b":"2"}}]`, }) - _, err := New(WithoutShippedData(), WithDataPath(dir)) + _, err := New(WithDataPath(dir)) if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) { t.Fatalf("New = %v, want the dotted field name rejected", err) } @@ -150,7 +150,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) { // single-variant choice always draws the same item, so it needs no every-variant // guard and the error can name the field that is missing. func TestMissingFieldNamesItself(t *testing.T) { - f := newGenerator(t, "data", WithSeed(1)) + f := shipped(t, WithSeed(1)) _, err := f.Fake("sv_SE.person.typo") if err == nil || !strings.Contains(err.Error(), `no field "typo"`) { t.Errorf("Fake(person.typo) = %v, want it to name the missing field", err) diff --git a/perf_test.go b/perf_test.go index dfd95b0..ef47b62 100644 --- a/perf_test.go +++ b/perf_test.go @@ -44,7 +44,7 @@ func TestNoRenderAllocRegression(t *testing.T) { {"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`, 3}, } for _, s := range shapes { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}})) + f, err := New(WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}})) if err != nil { t.Fatalf("New(%s): %v", s.name, err) } @@ -66,7 +66,7 @@ func TestNoReferenceAllocRegression(t *testing.T) { {"a repeat of a name", `{"format":"{r}","r":{"format":"{/word as n}{n.w}{n.w}","repeat":20,"separator":", "}}`, 162, 1.10}, {"a read two levels under a pick", `{"format":"{place.geo as g}{g} {g.town.zip}","place":{"format":"{geo}","geo":{"format":"{town.zip}","town":{"format":"{zip}","zip":"1"}}}}`, 12, 1}, } { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ + f, err := New(WithDataFS(fstest.MapFS{ "word.json": {Data: []byte(word)}, "x.json": {Data: []byte(s.json)}, })) @@ -81,7 +81,7 @@ func TestNoReferenceAllocRegression(t *testing.T) { // A table read pins a row inline and reads its cells in place. func TestNoTableAllocRegression(t *testing.T) { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ + f, err := New(WithDataFS(fstest.MapFS{ "region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)}, "region.tsv": {Data: []byte("code\tname\tpopulation\n01\tStockholms län\t2400000\n12\tSkåne län\t1400000\n14\tVästra Götalands län\t1750000\n")}, "x.json": {Data: []byte(`"{/region.name}, {/region.code}"`)}, @@ -113,7 +113,7 @@ func TestNoTableAllocRegression(t *testing.T) { } deep[name+".json"], deep[name+".tsv"] = &fstest.MapFile{Data: []byte(category)}, &fstest.MapFile{Data: []byte(rows)} } - f, err = New(WithoutShippedData(), WithDataFS(deep)) + f, err = New(WithDataFS(deep)) if err != nil { t.Fatal(err) } @@ -129,7 +129,7 @@ func TestNoRecordAllocRegression(t *testing.T) { {"record 3 columns", `{"format":"","a":"x","b":"y","c":"z"}`}, {"record 50 columns", wideTokenJSON(50)}, } { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}})) + f, err := New(WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}})) if err != nil { t.Fatalf("New(%s): %v", s.name, err) } @@ -148,7 +148,7 @@ func TestNoRecordAllocRegression(t *testing.T) { } func TestNoStructAllocRegression(t *testing.T) { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(`{"format":"","a":"x","b":"y","c":"z"}`)}})) + f, err := New(WithDataFS(fstest.MapFS{"x.json": {Data: []byte(`{"format":"","a":"x","b":"y","c":"z"}`)}})) if err != nil { t.Fatal(err) } @@ -166,20 +166,24 @@ func TestNoStructAllocRegression(t *testing.T) { } } -func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") } -func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") } +func BenchmarkNestedDepth25(b *testing.B) { + benchPath(b, WithDataPath(tmpData(b, "deep", nestedJSON(25))), "deep") +} +func BenchmarkNestedDepth100(b *testing.B) { + benchPath(b, WithDataPath(tmpData(b, "deep", nestedJSON(100))), "deep") +} func BenchmarkWideTokens100(b *testing.B) { - benchPath(b, tmpData(b, "wide", wideTokenJSON(100)), "wide") + benchPath(b, WithDataPath(tmpData(b, "wide", wideTokenJSON(100))), "wide") } func BenchmarkWideTokens500(b *testing.B) { - benchPath(b, tmpData(b, "wide", wideTokenJSON(500)), "wide") + benchPath(b, WithDataPath(tmpData(b, "wide", wideTokenJSON(500))), "wide") } -// New decodes the shipped manifest and loads nothing else. +// New decodes the shipped modules' manifests and loads nothing else. func TestNoNewAllocRegression(t *testing.T) { - const base = 2449.0 + const base = 2864.0 allocs := testing.AllocsPerRun(100, func() { - if _, err := New(WithSeed(1)); err != nil { + if _, err := New(withShipped(), WithSeed(1)); err != nil { t.Fatal(err) } }) diff --git a/pick_test.go b/pick_test.go index 72aa1b1..f9f5350 100644 --- a/pick_test.go +++ b/pick_test.go @@ -152,7 +152,7 @@ func TestANameReadOnceIsWhatItBinds(t *testing.T) { 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{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"static","x":"{/b}"}}`, "b": `"{/a}"`, }))) @@ -162,7 +162,7 @@ func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) { } func TestACycleInALaterVariantIsRejected(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}]}`, "hop": `"{/cat}"`, }))) @@ -172,7 +172,7 @@ func TestACycleInALaterVariantIsRejected(t *testing.T) { } func TestCycleThroughAPathTokenIsRejected(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/b}"}}`, "b": `"{/a}"`, }))) @@ -190,7 +190,7 @@ func TestADeepDiamondChainLoads(t *testing.T) { `{"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 { + if _, err := New(WithDataPath(writeData(t, files))); err != nil { t.Fatalf("New = %v, want a deep diamond chain to load", err) } } @@ -208,7 +208,7 @@ func TestNestedChoiceDrawsOneVariant(t *testing.T) { } func TestRepeatingLevelIsNamedInTheError(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(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"`) { @@ -223,7 +223,7 @@ func TestPathIntoARepeatingLevelIsRejected(t *testing.T) { `{"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}))) + _, err := New(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) } @@ -231,7 +231,7 @@ func TestPathIntoARepeatingLevelIsRejected(t *testing.T) { } func TestPathIntoAPlainTemplateNamesTheMissingField(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(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"`) { @@ -273,7 +273,7 @@ func TestDottedTokenErrors(t *testing.T) { }, } for name, c := range rejected { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file}))) + _, err := New(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) } @@ -285,7 +285,7 @@ func TestEmptyPathSegmentIsRejected(t *testing.T) { "trailing dot": `{"format":"[{a.}]","a":"x"}`, "triple dot": `{"format":"[{a...b}]","a":"x"}`, } { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) + _, err := New(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) } @@ -294,7 +294,7 @@ func TestEmptyPathSegmentIsRejected(t *testing.T) { 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)) + f, err := New(WithDataPath(dir), WithSeed(9)) if err != nil { t.Fatalf("New = %v", err) } diff --git a/readme_test.go b/readme_test.go index 49eebbc..f7f716b 100644 --- a/readme_test.go +++ b/readme_test.go @@ -65,7 +65,7 @@ func TestReadmeExamplesLoadAndRender(t *testing.T) { if strings.Contains(body, "…") { continue } - f, err := New(WithDataPath(writeFiles(t, exampleFiles(t, src, at[0], body))), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(writeFiles(t, exampleFiles(t, src, at[0], body))), WithSeed(1)) if err != nil { t.Errorf("README example does not load: %v\n%s", err, body) continue @@ -91,7 +91,7 @@ func TestReadmeRecordExample(t *testing.T) { if block == nil { t.Fatal("README lost the Records example") } - f, err := New(WithDataPath(writeData(t, map[string]string{"users": block[1]})), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(writeData(t, map[string]string{"users": block[1]})), WithSeed(1)) if err != nil { t.Fatal(err) } @@ -119,7 +119,7 @@ func TestReadmeSQLExampleOutput(t *testing.T) { if block == nil || want == nil { t.Fatal("README lost the SQL example or its printed output") } - f, err := New(WithDataPath(writeData(t, map[string]string{"sql": block[1]})), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(writeData(t, map[string]string{"sql": block[1]})), WithSeed(1)) if err != nil { t.Fatal(err) } @@ -135,7 +135,7 @@ func TestReadmeDatatypeExample(t *testing.T) { t.Fatal("README lost the Datatype section") } block := jsonBlock.FindStringSubmatch(src[i:]) - f, err := New(WithDataPath(writeData(t, map[string]string{"order": block[1]})), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(writeData(t, map[string]string{"order": block[1]})), WithSeed(1)) if err != nil { t.Fatal(err) } @@ -179,7 +179,7 @@ func TestReadmeTableExample(t *testing.T) { t.Fatal("README Table section has no json block") } body := src[i:][at[2]:at[3]] - f, err := New(WithDataPath(writeFiles(t, exampleFiles(t, src, i+at[0], body))), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(writeFiles(t, exampleFiles(t, src, i+at[0], body))), WithSeed(1)) if err != nil { t.Fatal(err) } diff --git a/record_test.go b/record_test.go index 3ecd3c5..768850b 100644 --- a/record_test.go +++ b/record_test.go @@ -530,7 +530,7 @@ func TestRecordTemplateRejectsATopLevelRepeat(t *testing.T) { // command a fixture author types: a category of one value has no columns, and the // error names the record that gives it one. func TestRecordOfAFieldlessCategoryNamesTheWrapper(t *testing.T) { - f := newGenerator(t, "data", WithSeed(1)) + f := shipped(t, WithSeed(1)) want := `{"format":"","personnummer":"{/sv_SE.personnummer}"}` _, err := f.FakeRecord("sv_SE.personnummer") if err == nil || !strings.Contains(err.Error(), want) { diff --git a/reference_test.go b/reference_test.go index 866a8dc..30b55cb 100644 --- a/reference_test.go +++ b/reference_test.go @@ -107,7 +107,7 @@ func TestReferenceErrors(t *testing.T) { "field key using the reference prefix": {"cat": `{"format":"hi","..x":"{/nope}"}`}, } for name, files := range cases { - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil { + if _, err := New(WithDataPath(writeData(t, files))); err == nil { t.Errorf("%s: New = nil error, want a reference error", name) } } @@ -142,13 +142,13 @@ func TestAReferenceIntoItsOwnCategoryIsAFreshDraw(t *testing.T) { "the category in its format": `{"format":"{/cat}"}`, "a field reading itself": `{"format":"{x}","x":"{/cat.x}"}`, } { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) + _, err := New(WithDataPath(writeData(t, map[string]string{"cat": file}))) if err == nil || !strings.Contains(err.Error(), "cycle") { t.Errorf("%s: New = %v, want the cycle refused", name, err) } } // Reading a sibling as a path is the spelling that stays. - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + if _, err := New(WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{y.v}","y":{"format":"{v}","v":["1","2"]}}`, }))); err != nil { t.Errorf("New = %v, want the sibling path accepted", err) @@ -173,7 +173,7 @@ func TestNewErrorIsDeterministic(t *testing.T) { dir := writeData(t, files) var first string for i := 0; i < 50; i++ { - _, err := New(WithoutShippedData(), WithDataPath(dir)) + _, err := New(WithDataPath(dir)) if err == nil { t.Fatalf("%s: New = nil error, want a load error", name) } @@ -206,11 +206,11 @@ func TestNewErrorPathIsCanonical(t *testing.T) { { "bad reference reached through another reference", map[string]string{"a": `"{/b}"`, "b": `"{/nope}"`}, - `fejkdata: b: reference {/nope}: no entry "nope"`, + `fejkdata: b: reference {/nope}: no entry "nope", and the module holding b names no module it reads by default`, }, } for _, c := range cases { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) + _, err := New(WithDataPath(writeData(t, c.files))) if err == nil { t.Errorf("%s: New = nil error", c.name) continue @@ -222,7 +222,7 @@ func TestNewErrorPathIsCanonical(t *testing.T) { } func TestReferenceThroughChoiceNeedsEveryVariant(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + _, err := New(WithDataPath(writeData(t, map[string]string{ "who": `[{"format":"{f}{h}","f":"1","h":"x"},{"format":"{g}{h}","g":"2","h":"y"}]`, "card": `"{/who.f}"`, }))) @@ -255,7 +255,7 @@ func TestReferencesIntoOneLevelLoad(t *testing.T) { {"row": `{"format":"{p.first} {/hop}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`, "hop": `"{/row.p.last}"`}, {"sv_SE/person": cat, "sv_SE/mail": `"{.person} <{/sv_SE.person.p.first}>"`}, } { - f, err := New(WithoutShippedData(), WithDataPath(writeData(t, files)), WithSeed(1)) + f, err := New(WithDataPath(writeData(t, files)), WithSeed(1)) if err != nil { t.Errorf("New(%v) = %v, want each read a draw of its own", files, err) continue @@ -325,7 +325,7 @@ func TestReferenceSigilErrors(t *testing.T) { "missing sibling": {"sv_SE/a": `"{.nope}"`}, "slash in a field name": {"a": `{"format":"{x}","x":"1","a/b":"2"}`}, } { - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil { + if _, err := New(WithDataPath(writeData(t, files))); err == nil { t.Errorf("%s: New = nil error, want a reference error", name) } } @@ -336,7 +336,7 @@ func TestSlashAfterAReferenceSigilIsRejected(t *testing.T) { "after ..": {"sv_SE/person": `"Ada"`, "sv_SE/deep/a": `"{../person}"`}, "after .": {"sv_SE/person": `"Ada"`, "sv_SE/a": `"{./person}"`}, } { - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))) + _, err := New(WithDataPath(writeData(t, files))) if err == nil || !strings.Contains(err.Error(), "person}") || !strings.Contains(err.Error(), "write {") { t.Errorf("%s: New = %v, want the slash rejected naming the spelling", name, err) } @@ -344,7 +344,7 @@ func TestSlashAfterAReferenceSigilIsRejected(t *testing.T) { } func TestACycleThroughASelectedRowIsRefused(t *testing.T) { - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{ + _, err := New(WithDataPath(writeFiles(t, map[string]string{ "x.json": `{"format":"{v} {/y}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `"{/x[1]}"`, diff --git a/shipped_data_test.go b/shipped_data_test.go index 43a7e91..382e002 100644 --- a/shipped_data_test.go +++ b/shipped_data_test.go @@ -9,8 +9,16 @@ import ( "testing/fstest" ) -func TestNewDefaultsToShippedData(t *testing.T) { - f, err := New(WithSeed(1)) +func TestABareNewFailsNamingWithDataFS(t *testing.T) { + for name, opts := range map[string][]Option{"no option": nil, "only a seed": {WithSeed(1)}} { + if _, err := New(opts...); err == nil || !strings.Contains(err.Error(), "WithDataFS(data.Modules()...)") { + t.Errorf("New(%s) = %v, want it refused, naming WithDataFS(data.Modules()...)", name, err) + } + } +} + +func TestEveryShippedModuleLoads(t *testing.T) { + f, err := New(withShipped(), WithSeed(1)) if err != nil { t.Fatalf("New() = %v", err) } @@ -27,7 +35,7 @@ func TestNewDefaultsToShippedData(t *testing.T) { func TestWithDataPathLayersOverShipped(t *testing.T) { dir := writeData(t, map[string]string{"sv_SE/word": `"only-mine"`, "greeting": `"hej"`}) - f, err := New(WithDataPath(dir), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(dir), WithSeed(1)) if err != nil { t.Fatalf("New = %v", err) } @@ -44,7 +52,7 @@ func TestWithDataPathLayersOverShipped(t *testing.T) { func TestUserDataMayReferenceShipped(t *testing.T) { dir := writeData(t, map[string]string{"greeting": `"Hej {/sv_SE.person}!"`}) - f, err := New(WithDataPath(dir), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(dir), WithSeed(1)) if err != nil { t.Fatalf("New = %v", err) } @@ -61,8 +69,8 @@ func TestAGeneratorWithDataSaysNothingOfNoData(t *testing.T) { func TestAGeneratorWithNoDataRendersWhatReadsNone(t *testing.T) { for name, opts := range map[string][]Option{ - "no source": {WithoutShippedData()}, - "empty folder": {WithoutShippedData(), WithDataPath(t.TempDir())}, + "no module": {WithDataFS()}, + "empty folder": {WithDataPath(t.TempDir())}, } { f, err := New(opts...) if err != nil { @@ -83,7 +91,7 @@ func TestAGeneratorWithNoDataRendersWhatReadsNone(t *testing.T) { } } -func TestWithoutShippedDataListsOnlyOwn(t *testing.T) { +func TestADataPathAloneListsOnlyItsOwn(t *testing.T) { dir := writeData(t, map[string]string{"greeting": `"hej"`}) f := newGenerator(t, dir, WithSeed(1)) if got := f.List(); !reflect.DeepEqual(got, []string{"greeting"}) { @@ -98,7 +106,7 @@ func TestWithDataFS(t *testing.T) { ".hidden.json": {Data: []byte(`"ignored"`)}, "broken/no.json": {Data: []byte(`"x"`)}, } - f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1)) + f, err := New(WithDataFS(fsys), WithSeed(1)) if err != nil { t.Fatalf("New(WithDataFS) = %v", err) } @@ -109,13 +117,25 @@ func TestWithDataFS(t *testing.T) { t.Errorf("nested.x.y = %q, want z", got) } bad := fstest.MapFS{"broken.json": {Data: []byte(`{ not json`)}} - if _, err := New(WithoutShippedData(), WithDataFS(bad)); err == nil || !strings.Contains(err.Error(), "broken.json") { + if _, err := New(WithDataFS(bad)); err == nil || !strings.Contains(err.Error(), "broken.json") { t.Errorf("New(bad fs) = %v, want an error naming the file", err) } } +func TestWithDataFSLayersEachModuleOverTheOneBefore(t *testing.T) { + a := fstest.MapFS{"x.json": {Data: []byte(`"a"`)}, "y.json": {Data: []byte(`"a"`)}} + b := fstest.MapFS{"x.json": {Data: []byte(`"b"`)}} + f, err := New(WithDataFS(a, b)) + if err != nil { + t.Fatalf("New(WithDataFS(a, b)) = %v", err) + } + if got := fake(t, f, "x") + fake(t, f, "y"); got != "ba" { + t.Errorf("x and y = %q, want b's x and a's y", got) + } +} + func TestFakeIsSafeForConcurrentUse(t *testing.T) { - f, err := New(WithSeed(1)) + f, err := New(withShipped(), WithSeed(1)) if err != nil { t.Fatal(err) } diff --git a/shipped_test.go b/shipped_test.go index f165e15..653d625 100644 --- a/shipped_test.go +++ b/shipped_test.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io/fs" + "maps" "os" "path" "path/filepath" @@ -16,11 +18,11 @@ import ( "testing" "testing/fstest" + "github.com/larvit/fejkdata/data" + "github.com/larvit/fejkdata/data/sv_SE" "github.com/larvit/fejkdata/internal/datafiles" ) -var shippedManifestFile = path.Join("data", datafiles.ManifestFile) - // withoutManifest is fsys with its manifest hidden, so it loads whole in New. type withoutManifest struct{ fs.FS } @@ -31,15 +33,52 @@ func (w withoutManifest) Open(name string) (fs.File, error) { return w.FS.Open(name) } +// shippedModule is a module data.Modules lists, and its folder under data/, slash-separated. +type shippedModule struct { + fsys fs.FS + dir string +} + +// importPath is the Go package the module's FS is exported from. +func (m shippedModule) importPath() string { return "github.com/larvit/fejkdata/data/" + m.dir } + +// holds reports whether the category at path is the module's: its tree spells its folder. +func (m shippedModule) holds(path string) bool { + return strings.HasPrefix(path, strings.ReplaceAll(m.dir, "/", ".")+".") +} + +// shippedModules is every module data.Modules lists, each folder found by descending its +// tree while a folder holds one folder and nothing else. +func shippedModules(t testing.TB) []shippedModule { + t.Helper() + var out []shippedModule + for _, fsys := range data.Modules() { + dir := "." + for { + entries, err := fs.ReadDir(fsys, dir) + if err != nil { + t.Fatal(err) + } + entries = slices.DeleteFunc(entries, func(e fs.DirEntry) bool { return strings.HasPrefix(e.Name(), ".") }) + if len(entries) != 1 || !entries[0].IsDir() { + break + } + dir = path.Join(dir, entries[0].Name()) + } + out = append(out, shippedModule{fsys: fsys, dir: dir}) + } + return out +} + // newShippedWhole loads every shipped category in New, as a source without an index does, so // a test reads the whole shipped tree. func newShippedWhole(t testing.TB, opts ...Option) *Generator { t.Helper() - data, err := fs.Sub(shippedFS, "data") - if err != nil { - t.Fatal(err) + var modules []fs.FS + for _, m := range shippedModules(t) { + modules = append(modules, withoutManifest{m.fsys}) } - f, err := New(append([]Option{WithoutShippedData(), WithDataFS(withoutManifest{data})}, opts...)...) + f, err := New(append([]Option{WithDataFS(modules...)}, opts...)...) if err != nil { t.Fatalf("New() = %v", err) } @@ -55,57 +94,150 @@ func loadedCategories(f *Generator) []string { return out } -// REGENERATE=1 rewrites the manifest. -func TestShippedManifestIsCurrent(t *testing.T) { - got, err := shippedManifest(newShippedWhole(t)) +// REGENERATE=1 rewrites the manifests. +func TestShippedManifestsAreCurrent(t *testing.T) { + modules := shippedModules(t) + manifests, err := shippedManifests(newShippedWhole(t), modules) if err != nil { t.Fatal(err) } - if os.Getenv("REGENERATE") == "1" { - if err := os.WriteFile(shippedManifestFile, got, 0o644); err != nil { + for i, m := range modules { + file := filepath.Join("data", filepath.FromSlash(m.dir), datafiles.ManifestFile) + if os.Getenv("REGENERATE") == "1" { + if err := os.WriteFile(file, manifests[i], 0o644); err != nil { + t.Fatal(err) + } + continue + } + want, err := os.ReadFile(file) + if err != nil { t.Fatal(err) } - return - } - want, err := os.ReadFile(shippedManifestFile) - if err != nil { - t.Fatal(err) - } - if string(got) != string(want) { - t.Fatalf("%s is stale; regenerate it: docker compose run --rm --user \"$(id -u):$(id -g)\" generate", shippedManifestFile) + if string(manifests[i]) != string(want) { + t.Errorf("%s is stale; regenerate it: docker compose run --rm --user \"$(id -u):$(id -g)\" generate", file) + } } } -// shippedManifest is the manifest of whole's tree, one category a line. -func shippedManifest(whole *Generator) ([]byte, error) { - var b bytes.Buffer - b.WriteString("{\n\t\"index\": {") - for i, s := range categorySites(&whole.root) { +// shippedManifests is each module's manifest, read from whole's tree: its index, one category a +// line, and every other module it reads, directly or through another. +func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, error) { + owner := func(path string) int { + return slices.IndexFunc(modules, func(m shippedModule) bool { return m.holds(path) }) + } + index := make([][]string, len(modules)) + direct := make([][]int, len(modules)) + for _, s := range categorySites(&whole.root) { + i := owner(s.path) + if i < 0 { + return nil, fmt.Errorf("no shipped module holds %s", s.path) + } line, err := json.Marshal(indexEntry(s)) if err != nil { return nil, err } - if i > 0 { - b.WriteString(",") - } key, _ := json.Marshal(s.path) - b.WriteString("\n\t\t" + string(key) + ": " + string(line)) + index[i] = append(index[i], "\n\t\t"+string(key)+": "+string(line)) + for _, c := range needs(&whole.root, s) { + if j := owner(categoryPath(c.dir, c.name)); j != i && !slices.Contains(direct[i], j) { + direct[i] = append(direct[i], j) + } + } } - b.WriteString("\n\t}\n}\n") - return b.Bytes(), nil + out := make([][]byte, len(modules)) + for i := range modules { + var b bytes.Buffer + b.WriteString("{\n\t\"index\": {" + strings.Join(index[i], ",") + "\n\t}") + if reads := readsOf(i, direct, modules); len(reads) > 0 { + line, _ := json.Marshal(reads) + b.WriteString(",\n\t\"reads\": " + string(line)) + } + b.WriteString("\n}\n") + out[i] = b.Bytes() + } + return out, nil +} + +// readsOf is every module module i reads, directly or through another, by import path. +func readsOf(i int, direct [][]int, modules []shippedModule) []string { + seen := map[int]bool{i: true} + queue := slices.Clone(direct[i]) + var out []string + for ; len(queue) > 0; queue = queue[1:] { + j := queue[0] + if seen[j] { + continue + } + seen[j] = true + out = append(out, modules[j].importPath()) + queue = append(queue, direct[j]...) + } + sort.Strings(out) + return out } func shippedIndex(t *testing.T) map[string]datafiles.IndexEntry { t.Helper() - m, err := shippedSource.Manifest() - if err != nil { - t.Fatal(err) + index := map[string]datafiles.IndexEntry{} + for _, m := range shippedModules(t) { + manifest, err := datafiles.FS(m.fsys, "").Manifest() + if err != nil { + t.Fatal(err) + } + maps.Copy(index, manifest.Index) + } + return index +} + +func TestEachShippedModuleNamesWhatItReads(t *testing.T) { + for _, m := range shippedModules(t) { + manifest, err := datafiles.FS(m.fsys, "").Manifest() + if err != nil { + t.Fatal(err) + } + if m.dir == "sv_SE" && !slices.Contains(manifest.Reads, "github.com/larvit/fejkdata/data/geo/SE") { + t.Errorf("sv_SE reads %v, want geo/SE among them", manifest.Reads) + } + if slices.Contains(manifest.Reads, m.importPath()) { + t.Errorf("%s reads %v, want itself left out", m.dir, manifest.Reads) + } + } +} + +func TestAReadNoModuleProvidesNamesTheReadersModules(t *testing.T) { + f, err := New(WithDataFS(sv_SE.FS)) + if err != nil { + t.Fatalf("New(WithDataFS(sv_SE.FS)) = %v", err) + } + if _, err := f.Fake("sv_SE.address"); err == nil || !strings.Contains(err.Error(), "the module holding sv_SE.address reads by default github.com/larvit/fejkdata/data/geo/SE") { + t.Errorf(`Fake("sv_SE.address") = %v, want the module it reads named`, err) + } + for name, opt := range map[string]Option{ + "no manifest": WithDataPath(writeData(t, map[string]string{"x": `"{/y}"`})), + "no reads in one": WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(`{"index": {"x": {"paths": [""]}}}`)}, "x.json": {Data: []byte(`"{/y}"`)}}), + } { + f, err := New(opt) + if err == nil { + _, err = f.Fake("x") + } + if err == nil || !strings.Contains(err.Error(), "the module holding x names no module it reads by default") { + t.Errorf("%s: x = %v, want it said that x's module names none", name, err) + } + } +} + +func TestADataPathManifestNamesWhatItReads(t *testing.T) { + dir := writeFiles(t, map[string]string{ + ".fejkdata.json": `{"reads": ["example.com/places"]}`, + "x.json": `"{/place}"`, + }) + if _, err := New(WithDataPath(dir)); err == nil || !strings.Contains(err.Error(), "the module holding x reads by default example.com/places") { + t.Errorf("New = %v, want the module x reads named", err) } - return m.Index } func TestNewLoadsNoShippedCategory(t *testing.T) { - f, err := New() + f, err := New(withShipped()) if err != nil { t.Fatalf("New() = %v", err) } @@ -149,7 +281,7 @@ func TestReachingACategoryLoadsWhatItReads(t *testing.T) { absent: []string{"sv_SE.person"}, }, } { - f, err := New() + f, err := New(withShipped()) if err != nil { t.Fatalf("New() = %v", err) } @@ -171,7 +303,7 @@ func TestReachingACategoryLoadsWhatItReads(t *testing.T) { } func TestADataPathLoadsWhatItReads(t *testing.T) { - f, err := New(WithDataPath(writeData(t, map[string]string{"greeting": `"Hej {/sv_SE.person}!"`}))) + f, err := New(withShipped(), WithDataPath(writeData(t, map[string]string{"greeting": `"Hej {/sv_SE.person}!"`}))) if err != nil { t.Fatalf("New() = %v", err) } @@ -196,7 +328,7 @@ func TestAReplacementBreakingAShippedReaderFailsItsFirstReach(t *testing.T) { "sv_SE/last-name.json": {Data: []byte(`"x"`)}, }), } { - f, err := New(opt) + f, err := New(withShipped(), opt) if err != nil { t.Fatalf("New(%s) = %v", name, err) } @@ -216,7 +348,7 @@ func TestAnIndexedSourceLoadsOnFirstReachLikeTheShippedOne(t *testing.T) { "b.json": {Data: []byte(`"x"`)}, "sub/c.json": {Data: []byte(`{"format":"{d}","d":"y"}`)}, } - f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1)) + f, err := New(WithDataFS(fsys), WithSeed(1)) if err != nil { t.Fatalf("New() = %v", err) } @@ -235,7 +367,7 @@ func TestAnIndexedSourceLoadsOnFirstReachLikeTheShippedOne(t *testing.T) { } func TestAnIndexedCategoryFailsAtFirstReachAndEveryReachAfter(t *testing.T) { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ + f, err := New(WithDataFS(fstest.MapFS{ ".fejkdata.json": {Data: []byte(`{"index": {"bad": {"paths": [""]}, "good": {"paths": [""]}}}`)}, "bad.json": {Data: []byte(`"{/nope}"`)}, "good.json": {Data: []byte(`"fine"`)}, @@ -273,8 +405,10 @@ func TestABrokenManifestFailsNewNamingEveryMistake(t *testing.T) { `{"index": {"a": {"paths": ["", "x", "x"]}}}`: {`paths item 3 repeats "x"`}, `{"index": {"a": {"paths": [""], "parent": "b"}}}`: {`parent "b" names no entry`}, `{"index": {"a": {"paths": [""], "reads": ["b"]}}}`: {`unknown key "reads"`}, + `{"reads": "b"}`: {"reads must be a list, not a string"}, + `{"reads": ["", 3]}`: {"reads item 1 is empty", "reads item 2 must be a string, not a number"}, } { - _, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(manifest)}})) + _, err := New(WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(manifest)}})) for _, w := range want { if err == nil || !strings.Contains(err.Error(), w) { t.Errorf("New(manifest %s) = %v, want an error naming %s", manifest, err, w) @@ -292,7 +426,7 @@ func TestNewMatchesErrLoadOnDataThatFailsToLoad(t *testing.T) { "a broken category": WithDataPath(writeData(t, map[string]string{"x": `{ not json`})), "a broken manifest": WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(`{`)}}), } { - if _, err := New(opt); !errors.Is(err, ErrLoad) { + if _, err := New(withShipped(), opt); !errors.Is(err, ErrLoad) { t.Errorf("New(%s) = %v, want it to match ErrLoad", name, err) } } @@ -301,14 +435,14 @@ func TestNewMatchesErrLoadOnDataThatFailsToLoad(t *testing.T) { func TestOnDemandRendersAsTheWholeLoad(t *testing.T) { whole := newShippedWhole(t) paths := whole.List() - lazy, err := New() + lazy, err := New(withShipped()) if err != nil { t.Fatalf("New() = %v", err) } if got := lazy.List(); !reflect.DeepEqual(got, paths) { t.Fatalf("List() on demand = %v, want %v", got, paths) } - a, err := New(WithSeed(7)) + a, err := New(withShipped(), WithSeed(7)) if err != nil { t.Fatal(err) } @@ -326,7 +460,7 @@ func TestOnDemandRendersAsTheWholeLoad(t *testing.T) { func TestEveryShippedCategoryLoadsAlone(t *testing.T) { for _, p := range sortedNames(shippedIndex(t)) { - f, err := New() + f, err := New(withShipped()) if err != nil { t.Fatalf("New() = %v", err) } @@ -337,7 +471,7 @@ func TestEveryShippedCategoryLoadsAlone(t *testing.T) { } func TestListRunsBesideAnOnDemandLoad(t *testing.T) { - f, err := New() + f, err := New(withShipped()) if err != nil { t.Fatalf("New() = %v", err) } @@ -359,7 +493,7 @@ func TestListRunsBesideAnOnDemandLoad(t *testing.T) { } func TestATableWhoseEntryOmitsItsParentFailsAtFirstReach(t *testing.T) { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ + f, err := New(WithDataFS(fstest.MapFS{ ".fejkdata.json": {Data: []byte(`{"index": {"city": {"paths": ["", "country", "name"]}, "country": {"paths": ["", "code", "name"]}}}`)}, "country.json": {Data: []byte(`{"format":"{name}","rows":"country.tsv","key":"code"}`)}, "country.tsv": {Data: []byte("code\tname\nSE\tSweden\n")}, @@ -382,7 +516,7 @@ func TestATableWhoseEntryOmitsItsParentFailsAtFirstReach(t *testing.T) { } func TestADataPathTableUnderAShippedTableLoads(t *testing.T) { - f, err := New(WithSeed(1), WithDataPath(writeFiles(t, map[string]string{ + f, err := New(withShipped(), WithSeed(1), WithDataPath(writeFiles(t, map[string]string{ "sv_SE/nickname.json": `{"format":"{name}","rows":"nickname.tsv","key":"name","parent":"sex"}`, "sv_SE/nickname.tsv": "name\tsex\nKalle\tm\nLotta\tf\n", }))) @@ -405,7 +539,7 @@ func indexEntry(s categorySite) datafiles.IndexEntry { } func TestAnIndexedCategoryWithNoFileNamesTheManifest(t *testing.T) { - f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ + f, err := New(WithDataFS(fstest.MapFS{ ".fejkdata.json": {Data: []byte(`{"index": {"gone": {"paths": [""]}}}`)}, })) if err != nil { diff --git a/table_test.go b/table_test.go index e6ef200..c704704 100644 --- a/table_test.go +++ b/table_test.go @@ -165,7 +165,7 @@ func TestTableCellsAreStringNodes(t *testing.T) { "place.json": `{"format":"{name}","rows":"place.tsv"}`, "place.tsv": "name\tzip\nA\t{name}\nB\t2\n", }) - if _, err := New(WithoutShippedData(), WithDataPath(bad)); err == nil || !strings.Contains(err.Error(), "place.tsv") || !strings.Contains(err.Error(), `no field "name"`) { + if _, err := New(WithDataPath(bad)); err == nil || !strings.Contains(err.Error(), "place.tsv") || !strings.Contains(err.Error(), `no field "name"`) { t.Fatalf("New = %v, want a cell reading a column refused, naming the file", err) } } @@ -327,7 +327,7 @@ func TestTableStepUpRefusals(t *testing.T) { if _, err := f.Fake(path); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("Fake(%q) = %v, want an error mentioning %s", path, err, want) } - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{ + _, err := New(WithDataPath(writeFiles(t, with(geo(), map[string]string{ "x.json": `{"format":"{y}","y":"a"}`, "t.json": strconv.Quote("{/" + path + "}"), })))) @@ -335,7 +335,7 @@ func TestTableStepUpRefusals(t *testing.T) { t.Errorf("{/%s}: New = %v, want an error mentioning %s", path, err, want) } } - if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{"s.json": `{"format":"{a..b}","a":{"format":"{b}","b":"x"}}`}))); err == nil || !strings.Contains(err.Error(), "only from a table's row") { + if _, err := New(WithDataPath(writeFiles(t, map[string]string{"s.json": `{"format":"{a..b}","a":{"format":"{b}","b":"x"}}`}))); err == nil || !strings.Contains(err.Error(), "only from a table's row") { t.Errorf("{a..b} on a field: New = %v, want an error mentioning only from a table's row", err) } } @@ -464,7 +464,7 @@ func TestTableSelectorInAReference(t *testing.T) { "selector on template": {`"{/x[1].a}"`, "not a table"}, "selector on repeating template": {`"{/rep[1]}"`, "not a table"}, } { - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, map[string]string{"bad.json": c.json, "rep.json": `{"format":"x","repeat":2}`})))) + _, err := New(WithDataPath(writeFiles(t, with(files, map[string]string{"bad.json": c.json, "rep.json": `{"format":"x","repeat":2}`})))) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: New = %v, want an error mentioning %q", name, err, c.want) } @@ -484,7 +484,7 @@ func TestTableReadsBesideEachOtherLoad(t *testing.T) { `"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, `"{/region[14].name} {/locality[L4].name}"`, } { - f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) + f, err := New(WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want each read a draw of its own", json, err) continue @@ -504,7 +504,7 @@ func TestSelectedRowsAgreeAcrossSpellings(t *testing.T) { "a draw under the selection": `"{/region[12].name}|{/region[12].municipality.locality[L4].name}"`, } for name, json := range accepted { - f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) + f, err := New(WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) continue @@ -595,7 +595,7 @@ func TestTableFences(t *testing.T) { "a category referencing itself": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n{/t.a}\ny\n"}, "cycle"}, } for name, c := range rejected { - _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, c.files))) + _, err := New(WithDataPath(writeFiles(t, c.files))) if err == nil { t.Errorf("%s: New = nil error, want it rejected at load", name) continue @@ -616,7 +616,7 @@ func TestTableFences(t *testing.T) { "a cell repeating a reference": with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n{/region} {/region} {uppercase(/region)}\n"}), } for name, files := range accepted { - f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files)), WithSeed(1)) + f, err := New(WithDataPath(writeFiles(t, files)), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want each read a draw of its own", name, err) continue @@ -627,7 +627,7 @@ func TestTableFences(t *testing.T) { } } } - if _, err := New(WithoutShippedData(), WithDataFS(os.DirFS(writeFiles(t, base)))); err != nil { + if _, err := New(WithDataFS(os.DirFS(writeFiles(t, base)))); err != nil { t.Fatalf("New(WithDataFS) = %v, want a table loaded from any fs.FS", err) } inline := newGenerator(t, writeFiles(t, base)) @@ -711,7 +711,7 @@ func TestTableRowsAreAlternatives(t *testing.T) { } symbols := regexp.MustCompile(`€|kr|\$`) for name, more := range accepted { - g, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))), WithSeed(1)) + g, err := New(WithDataPath(writeFiles(t, with(files, more))), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) continue @@ -743,7 +743,7 @@ func TestTableRowsAreAlternatives(t *testing.T) { "a cell reaching another's cells": {"y.json": `{"format":"{sym}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tsym\n1\t{/cur[SEK].sym}\n2\t{/cur[USD].sym}\n", "cur.tsv": "code\tsym\nEUR\t€\nSEK\tkr\nUSD\t$\n", "country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/y[1].sym} {/cur[EUR].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"}, } for name, more := range twoRows { - g, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))), WithSeed(1)) + g, err := New(WithDataPath(writeFiles(t, with(files, more))), WithSeed(1)) if err != nil { t.Errorf("%s: New = %v, want each read a draw of its own", name, err) continue @@ -765,7 +765,7 @@ func TestTableOverridesByLayering(t *testing.T) { "misc/language.json": `{"format":"{name}","rows":"language.tsv","key":"code"}`, "misc/language.tsv": "code\tname\nxx\tNowhere\nyy\tElsewhere\n", }) - f, err := New(WithDataPath(mine), WithSeed(1)) + f, err := New(withShipped(), WithDataPath(mine), WithSeed(1)) if err != nil { t.Fatal(err) } @@ -778,7 +778,7 @@ func TestTableOverridesByLayering(t *testing.T) { } func TestShippedTables(t *testing.T) { - f := newGenerator(t, "data/misc", WithSeed(1)) + f := newGenerator(t, "data/misc/misc", WithSeed(1)) for path, want := range map[string]string{ "territory[SE]": "Sweden", "territory[Sweden].alpha3": "SWE", @@ -893,7 +893,7 @@ func TestShippedTables(t *testing.T) { Method string `fake:"{m.method}"` Safe bool `fake:"{m.safe}"` } - if err := newGenerator(t, "data", WithSeed(1)).FakeStruct(&row); err != nil { + if err := shipped(t, WithSeed(1)).FakeStruct(&row); err != nil { t.Fatalf("FakeStruct into a bool = %v, want the register's yes and no shipped as true and false", err) } want, named := map[string][2]bool{ @@ -992,7 +992,7 @@ func TestEveryTldRowSpellsItsKey(t *testing.T) { // columns asked for and that every row fills them. func shippedRows(t *testing.T, file string, want ...string) (map[string]int, [][]string) { t.Helper() - data, err := os.ReadFile(filepath.Join("data", "misc", file)) + data, err := os.ReadFile(filepath.Join("data", "misc", "misc", file)) if err != nil { t.Fatal(err) } @@ -1122,7 +1122,7 @@ func TestCellReadsLoadAndRender(t *testing.T) { "region.tsv": "code\tname\tpopulation\tnote\n01\tStockholms län\t2400000\t{/addr}\n12\tSkåne län\t1400000\t-\n14\tVästra Götalands län\t1750000\t-\n", }) for _, x := range []string{`"{/region.note} {/municipality.note}"`, `"{/region}|{/municipality}"`} { - f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(cells, map[string]string{"x.json": x}))), WithSeed(1)) + f, err := New(WithDataPath(writeFiles(t, with(cells, map[string]string{"x.json": x}))), WithSeed(1)) if err != nil { t.Fatalf("New(%s) = %v, want the cells accepted", x, err) } @@ -1136,7 +1136,7 @@ func TestDatatypeRefusesARowRead(t *testing.T) { for _, format := range []string{"{/region}", "{/region[01]}"} { files := with(geo(), map[string]string{"row.json": `{"format":"","col":{"format":"` + format + `","datatype":"integer"}}`}) want := `"{name}" renders a row of region, which is composed text` - if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { + if _, err := New(WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: New = %v, want an error containing %q", format, err, want) } } @@ -1148,12 +1148,12 @@ func TestATableErrorNamesItsPathAndACellItsLine(t *testing.T) { "g/t.tsv": "a\tb\nx\t-\ny\t{/nope}\n", } want := "g.t.b, line 3: reference {/nope}" - if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { + if _, err := New(WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("a cell's bad reference: New = %v, want it to contain %q", err, want) } files["g/t.json"], files["g/t.tsv"] = `{"format":"{a} {/nope}","rows":"t.tsv","key":"a"}`, "a\tb\nx\t-\ny\t-\n" want = "g.t: reference {/nope}" - if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { + if _, err := New(WithDataPath(writeFiles(t, files))); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("a format's bad reference: New = %v, want it to contain %q", err, want) } f := newGenerator(t, writeFiles(t, geo()), WithSeed(1)) @@ -1165,7 +1165,7 @@ func TestATableErrorNamesItsPathAndACellItsLine(t *testing.T) { t.Errorf("Fake(%s) = %v, want it to contain %q", path, err, want) } } - shipped, err := New(WithSeed(1)) + shipped, err := New(withShipped(), WithSeed(1)) if err != nil { t.Fatal(err) }