diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index ab5bf51..f4dc0af 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -12,11 +12,48 @@ import ( "testing" ) -const ( - svSE = "../../data/sv_SE/sv_SE" - enUS = "../../data/en_US/en_US" - misc = "../../data/misc/misc" -) +// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test +// layers its categories unprefixed with -d. +var svSE, enUS, misc string + +func TestMain(m *testing.M) { + tmp, err := os.MkdirTemp("", "fejkdata-cli-test") + if err != nil { + panic(err) + } + svSE, enUS, misc = bareCopy(tmp, "svse"), bareCopy(tmp, "enus"), bareCopy(tmp, "misc") + code := m.Run() + os.RemoveAll(tmp) + os.Exit(code) +} + +// bareCopy copies the category and rows files of the shipped module in data/ into a +// folder under tmp, and returns that folder. +func bareCopy(tmp, module string) string { + src, dst := filepath.Join("..", "..", "data", module), filepath.Join(tmp, module) + if err := os.Mkdir(dst, 0o755); err != nil { + panic(err) + } + for _, pattern := range []string{"*.json", "*.tsv"} { + files, err := filepath.Glob(filepath.Join(src, pattern)) + if err != nil { + panic(err) + } + for _, f := range files { + if strings.HasPrefix(filepath.Base(f), ".") { + continue + } + b, err := os.ReadFile(f) + if err == nil { + err = os.WriteFile(filepath.Join(dst, filepath.Base(f)), b, 0o644) + } + if err != nil { + panic(err) + } + } + } + return dst +} func runOut(stdin string, args ...string) (int, string, string) { var out, errb bytes.Buffer diff --git a/data/geo/SE/fs_test.go b/data/enus/fs_test.go similarity index 63% rename from data/geo/SE/fs_test.go rename to data/enus/fs_test.go index 1940ed3..4475144 100644 --- a/data/geo/SE/fs_test.go +++ b/data/enus/fs_test.go @@ -1,18 +1,18 @@ -package SE_test +package enus_test import ( "io/fs" "testing" "github.com/larvit/fejkdata" - "github.com/larvit/fejkdata/data/geo/SE" + "github.com/larvit/fejkdata/data/enus" ) func TestFSLoadsByItsManifest(t *testing.T) { - if _, err := fs.Stat(SE.FS, ".fejkdata.json"); err != nil { + if _, err := fs.Stat(enus.FS, ".fejkdata.json"); err != nil { t.Fatalf("FS holds no manifest: %v", err) } - f, err := fejkdata.New(fejkdata.WithDataFS(SE.FS)) + f, err := fejkdata.New(fejkdata.WithDataFS(enus.FS)) if err != nil { t.Fatalf("New(WithDataFS(FS)) = %v", err) } diff --git a/data/en_US/fs_test.go b/data/geose/fs_test.go similarity index 63% rename from data/en_US/fs_test.go rename to data/geose/fs_test.go index 6b8c4e7..187af70 100644 --- a/data/en_US/fs_test.go +++ b/data/geose/fs_test.go @@ -1,18 +1,18 @@ -package en_US_test +package geose_test import ( "io/fs" "testing" "github.com/larvit/fejkdata" - "github.com/larvit/fejkdata/data/en_US" + "github.com/larvit/fejkdata/data/geose" ) func TestFSLoadsByItsManifest(t *testing.T) { - if _, err := fs.Stat(en_US.FS, ".fejkdata.json"); err != nil { + if _, err := fs.Stat(geose.FS, ".fejkdata.json"); err != nil { t.Fatalf("FS holds no manifest: %v", err) } - f, err := fejkdata.New(fejkdata.WithDataFS(en_US.FS)) + f, err := fejkdata.New(fejkdata.WithDataFS(geose.FS)) if err != nil { t.Fatalf("New(WithDataFS(FS)) = %v", err) } diff --git a/data/sv_SE/fs_test.go b/data/geous/fs_test.go similarity index 63% rename from data/sv_SE/fs_test.go rename to data/geous/fs_test.go index 39011b0..afcfe57 100644 --- a/data/sv_SE/fs_test.go +++ b/data/geous/fs_test.go @@ -1,18 +1,18 @@ -package sv_SE_test +package geous_test import ( "io/fs" "testing" "github.com/larvit/fejkdata" - "github.com/larvit/fejkdata/data/sv_SE" + "github.com/larvit/fejkdata/data/geous" ) func TestFSLoadsByItsManifest(t *testing.T) { - if _, err := fs.Stat(sv_SE.FS, ".fejkdata.json"); err != nil { + if _, err := fs.Stat(geous.FS, ".fejkdata.json"); err != nil { t.Fatalf("FS holds no manifest: %v", err) } - f, err := fejkdata.New(fejkdata.WithDataFS(sv_SE.FS)) + f, err := fejkdata.New(fejkdata.WithDataFS(geous.FS)) if err != nil { t.Fatalf("New(WithDataFS(FS)) = %v", err) } diff --git a/data/geo/US/fs_test.go b/data/svse/fs_test.go similarity index 63% rename from data/geo/US/fs_test.go rename to data/svse/fs_test.go index ca1236e..81673ec 100644 --- a/data/geo/US/fs_test.go +++ b/data/svse/fs_test.go @@ -1,18 +1,18 @@ -package US_test +package svse_test import ( "io/fs" "testing" "github.com/larvit/fejkdata" - "github.com/larvit/fejkdata/data/geo/US" + "github.com/larvit/fejkdata/data/svse" ) func TestFSLoadsByItsManifest(t *testing.T) { - if _, err := fs.Stat(US.FS, ".fejkdata.json"); err != nil { + if _, err := fs.Stat(svse.FS, ".fejkdata.json"); err != nil { t.Fatalf("FS holds no manifest: %v", err) } - f, err := fejkdata.New(fejkdata.WithDataFS(US.FS)) + f, err := fejkdata.New(fejkdata.WithDataFS(svse.FS)) if err != nil { t.Fatalf("New(WithDataFS(FS)) = %v", err) } diff --git a/data_test.go b/data_test.go index c9e61ba..c21ac6e 100644 --- a/data_test.go +++ b/data_test.go @@ -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/misc", WithSeed(1)) + f := newBareModule(t, "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/misc", WithSeed(1)) + f := newBareModule(t, "misc", WithSeed(1)) re := map[string]*regexp.Regexp{ "currency": regexp.MustCompile(`^[A-Z]{3}$`), "currency.name": regexp.MustCompile(`\p{L}`), @@ -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 + "/geo/" + country + "/" + table + ".tsv") + b, err := os.ReadFile("data/geo" + strings.ToLower(country) + "/" + table + ".tsv") if err != nil { t.Fatal(err) } @@ -468,7 +468,7 @@ func TestShippedTableCellsAreTrimmed(t *testing.T) { } // TestShippedNamespacedTree loads every shipped module and reaches each locale through its -// folder segment: data/sv_SE/sv_SE/person.json -> sv_SE.person. +// namespace: data/svse/person.json, mounted under sv_SE, is sv_SE.person. func TestShippedNamespacedTree(t *testing.T) { f := shipped(t, WithSeed(1)) for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} { diff --git a/fejkdata_test.go b/fejkdata_test.go index b680b35..3c2751a 100644 --- a/fejkdata_test.go +++ b/fejkdata_test.go @@ -3,6 +3,8 @@ package fejkdata import ( "errors" "io/fs" + "os" + "path/filepath" "strings" "testing" "testing/fstest" @@ -35,6 +37,17 @@ func newGeneratorN(t *testing.T, dirs []string, opts ...Option) *Generator { return f } +// newBareModule loads the shipped module in data/ without its manifest, so its +// categories sit at the root, failing on error. +func newBareModule(t *testing.T, module string, opts ...Option) *Generator { + t.Helper() + f, err := New(append([]Option{WithDataFS(withoutManifest{os.DirFS(filepath.Join("data", module))})}, opts...)...) + if err != nil { + t.Fatalf("New(%s without its manifest): %v", module, err) + } + return f +} + // fake generates a value, failing the test on error. func fake(t *testing.T, f *Generator, path string) string { t.Helper() diff --git a/imports_test.go b/imports_test.go index 1842f26..a43043b 100644 --- a/imports_test.go +++ b/imports_test.go @@ -31,12 +31,12 @@ func TestPackageImports(t *testing.T) { allowed := map[string][]string{ ".": {"internal/builtinfunc", "internal/datafiles", "internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/jsonvalue", "internal/proven", "internal/rows"}, "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": {"data/enus", "data/geose", "data/geous", "data/misc", "data/svse"}, + "data/enus": nil, + "data/geose": nil, + "data/geous": nil, "data/misc": nil, - "data/sv_SE": nil, + "data/svse": nil, "internal/builtinfunc": {"internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/proven"}, "internal/datafiles": {"internal/grammar", "internal/jsonvalue"}, "internal/datatype": nil, @@ -48,13 +48,13 @@ func TestPackageImports(t *testing.T) { "internal/rows": {"internal/drawstate", "internal/grammar", "internal/invariant"}, } testsAlso := map[string][]string{ - ".": {"data", "data/en_US", "data/geo/SE", "data/geo/US", "data/misc", "data/sv_SE"}, + ".": {"data", "data/enus", "data/geose", "data/geous", "data/misc", "data/svse"}, "cmd/fejkdata": {"internal/grammar"}, - "data/en_US": {"."}, - "data/geo/SE": {"."}, - "data/geo/US": {"."}, + "data/enus": {"."}, + "data/geose": {"."}, + "data/geous": {"."}, "data/misc": {"."}, - "data/sv_SE": {"."}, + "data/svse": {"."}, } seen := map[string]bool{} eachPackage(t, func(dir string, pkg *build.Package) { diff --git a/reference_test.go b/reference_test.go index 8a323d0..7b04e3e 100644 --- a/reference_test.go +++ b/reference_test.go @@ -206,7 +206,7 @@ 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", and no loaded module names a module it reads by default`, + `fejkdata: b: reference {/nope}: no entry "nope", and no loaded manifest names a module to add`, }, } for _, c := range cases { diff --git a/shipped_test.go b/shipped_test.go index 7ab659b..37b3a59 100644 --- a/shipped_test.go +++ b/shipped_test.go @@ -18,11 +18,11 @@ import ( "testing/fstest" "github.com/larvit/fejkdata/data" - "github.com/larvit/fejkdata/data/en_US" - "github.com/larvit/fejkdata/data/geo/SE" - "github.com/larvit/fejkdata/data/geo/US" + "github.com/larvit/fejkdata/data/enus" + "github.com/larvit/fejkdata/data/geose" + "github.com/larvit/fejkdata/data/geous" "github.com/larvit/fejkdata/data/misc" - "github.com/larvit/fejkdata/data/sv_SE" + "github.com/larvit/fejkdata/data/svse" "github.com/larvit/fejkdata/internal/datafiles" ) @@ -36,24 +36,42 @@ 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. +// shippedModule is a module data.Modules lists, its folder under data/, and the namespace its +// manifest mounts it under. type shippedModule struct { - fsys fs.FS - dir string + fsys fs.FS + dir string + namespace 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, "/", ".")+".") +// holds reports whether the category at path is the module's. +func (m shippedModule) holds(path string) bool { return strings.HasPrefix(path, m.namespace+".") } + +// whole is the module's files with a manifest naming only its namespace, so it loads whole in New. +func (m shippedModule) whole(t testing.TB) fs.FS { + t.Helper() + out := fstest.MapFS{datafiles.ManifestFile: {Data: []byte(`{"namespace": "` + m.namespace + `"}`)}} + err := fs.WalkDir(m.fsys, ".", func(p string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || p == datafiles.ManifestFile { + return err + } + b, err := fs.ReadFile(m.fsys, p) + out[p] = &fstest.MapFile{Data: b} + return err + }) + if err != nil { + t.Fatal(err) + } + return out } // shippedModules is every module data.Modules lists, each beside its folder under data/. func shippedModules(t testing.TB) []shippedModule { t.Helper() - out := []shippedModule{{en_US.FS, "en_US"}, {SE.FS, "geo/SE"}, {US.FS, "geo/US"}, {misc.FS, "misc"}, {sv_SE.FS, "sv_SE"}} + out := []shippedModule{{enus.FS, "enus", "en_US"}, {geose.FS, "geose", "geo.SE"}, {geous.FS, "geous", "geo.US"}, {misc.FS, "misc", "misc"}, {svse.FS, "svse", "sv_SE"}} var listed []fs.FS for _, m := range out { listed = append(listed, m.fsys) @@ -70,7 +88,7 @@ func newShippedWhole(t testing.TB, opts ...Option) *Generator { t.Helper() var modules []fs.FS for _, m := range shippedModules(t) { - modules = append(modules, withoutManifest{m.fsys}) + modules = append(modules, m.whole(t)) } f, err := New(append([]Option{WithDataFS(modules...)}, opts...)...) if err != nil { @@ -141,7 +159,8 @@ func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, erro out := make([][]byte, len(modules)) for i := range modules { var b bytes.Buffer - b.WriteString("{\n\t\"index\": {" + strings.Join(index[i], ",") + "\n\t}") + namespace, _ := json.Marshal(modules[i].namespace) + b.WriteString("{\n\t\"index\": {" + strings.Join(index[i], ",") + "\n\t},\n\t\"namespace\": " + string(namespace)) if reads := readsOf(i, direct, modules); len(reads) > 0 { line, _ := json.Marshal(reads) b.WriteString(",\n\t\"reads\": " + string(line)) @@ -211,11 +230,11 @@ func TestEachShippedModuleRendersBesideTheModulesItReads(t *testing.T) { } func TestAReadNoModuleProvidesNamesTheReadersModules(t *testing.T) { - f, err := New(WithDataFS(sv_SE.FS)) + f, err := New(WithDataFS(svse.FS)) if err != nil { - t.Fatalf("New(WithDataFS(sv_SE.FS)) = %v", err) + t.Fatalf("New(WithDataFS(svse.FS)) = %v", err) } - if _, err := f.Fake("sv_SE.address"); err == nil || !strings.Contains(err.Error(), "the loaded modules read by default github.com/larvit/fejkdata/data/geo/SE") { + if _, err := f.Fake("sv_SE.address"); err == nil || !strings.Contains(err.Error(), "is a module missing? The loaded modules read github.com/larvit/fejkdata/data/geose") { t.Errorf(`Fake("sv_SE.address") = %v, want the module it reads named`, err) } for name, opt := range map[string]Option{ @@ -226,7 +245,7 @@ func TestAReadNoModuleProvidesNamesTheReadersModules(t *testing.T) { if err == nil { _, err = f.Fake("x") } - if err == nil || !strings.Contains(err.Error(), "no loaded module names a module it reads by default") { + if err == nil || !strings.Contains(err.Error(), "no loaded manifest names a module to add") { t.Errorf("%s: x = %v, want it said that no loaded module names one", name, err) } } @@ -237,7 +256,7 @@ func TestADataPathManifestNamesWhatItReads(t *testing.T) { ".fejkdata.json": `{"reads": ["example.com/places"]}`, "x.json": `"{/place}"`, }) - if _, err := New(WithDataPath(dir)); err == nil || !strings.Contains(err.Error(), "the loaded modules read by default example.com/places") { + if _, err := New(WithDataPath(dir)); err == nil || !strings.Contains(err.Error(), "is a module missing? The loaded modules read example.com/places") { t.Errorf("New = %v, want the module x reads named", err) } } @@ -250,8 +269,43 @@ func TestAnIndexedModuleNamesWhatItReads(t *testing.T) { if err != nil { t.Fatalf("New = %v", err) } - if _, err := f.Fake("x"); err == nil || !strings.Contains(err.Error(), "the loaded modules read by default example.com/places") { - t.Errorf(`Fake("x") = %v, want the module x reads named`, err) + const note = "is a module missing? The loaded modules read example.com/places" + for name, reach := range map[string]func() error{ + "Fake": func() error { _, err := f.Fake("x"); return err }, + "FakeTemplate": func() error { _, err := f.FakeTemplate("{/x}"); return err }, + "FakeStruct": func() error { + return f.FakeStruct(&struct { + A string `fake:"/x"` + }{}) + }, + } { + if err := reach(); err == nil || strings.Count(err.Error(), note) != 1 { + t.Errorf("%s reaching x = %v, want the module x reads named once", name, err) + } + } + if _, err := f.FakeTemplate("{/nope}"); err == nil || strings.Contains(err.Error(), "module") { + t.Errorf("FakeTemplate({/nope}) = %v, want no module named: no load failed", err) + } +} + +func TestANamespaceMountsAModuleUnderIt(t *testing.T) { + for name, opt := range map[string]Option{ + "a data path": WithDataPath(writeFiles(t, map[string]string{ + ".fejkdata.json": `{"namespace": "my.data"}`, + "word.json": `"hej"`, + })), + "an indexed source": WithDataFS(fstest.MapFS{ + ".fejkdata.json": {Data: []byte(`{"index": {"my.data.word": {"paths": [""]}}, "namespace": "my.data"}`)}, + "word.json": {Data: []byte(`"hej"`)}, + }), + } { + f, err := New(opt) + if err != nil { + t.Fatalf("New(%s) = %v", name, err) + } + if got := fake(t, f, "my.data.word"); got != "hej" { + t.Errorf("%s: my.data.word = %q, want hej", name, got) + } } } @@ -427,6 +481,9 @@ func TestABrokenManifestFailsNewNamingEveryMistake(t *testing.T) { `{"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"}, `{"reads": ["a", "a"]}`: {`reads item 2 repeats "a"`}, + `{"namespace": 5}`: {"namespace must be a string, not a number"}, + `{"namespace": "a..b"}`: {`namespace "a..b"`}, + `{"index": {"b.x": {"paths": [""]}}, "namespace": "a"}`: {`index entry "b.x" lies outside the namespace "a"`}, } { _, err := New(WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(manifest)}})) for _, w := range want { diff --git a/table_test.go b/table_test.go index c704704..8be4f86 100644 --- a/table_test.go +++ b/table_test.go @@ -778,7 +778,7 @@ func TestTableOverridesByLayering(t *testing.T) { } func TestShippedTables(t *testing.T) { - f := newGenerator(t, "data/misc/misc", WithSeed(1)) + f := newBareModule(t, "misc", WithSeed(1)) for path, want := range map[string]string{ "territory[SE]": "Sweden", "territory[Sweden].alpha3": "SWE", @@ -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", "misc", file)) + data, err := os.ReadFile(filepath.Join("data", "misc", file)) if err != nil { t.Fatal(err) }