From 91cbae2a4496a391b51d647702efa54730d999a6 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 23:03:53 +0200 Subject: [PATCH] Tests for the shipped data layer, New options and concurrent Fake --- Dockerfile | 2 +- bench_test.go | 5 +- bound_test.go | 76 ++++++++++++------------- cmd/fejkdata/main_test.go | 31 ++++++++++- compose.yaml | 2 +- edge_test.go | 10 ++-- fejkdata_test.go | 8 ++- loading_test.go | 2 +- reference_test.go | 6 +- shipped_data_test.go | 114 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 201 insertions(+), 55 deletions(-) create mode 100644 shipped_data_test.go diff --git a/Dockerfile b/Dockerfile index 63be7a8..227d6da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ COPY . . RUN go vet ./... && \ { unformatted="$(gofmt -l .)"; test -z "$unformatted" || \ { echo "unformatted files:"; echo "$unformatted"; exit 1; }; } && \ - go test ./... + go test -race ./... diff --git a/bench_test.go b/bench_test.go index 9c69865..6022f31 100644 --- a/bench_test.go +++ b/bench_test.go @@ -8,7 +8,7 @@ import ( func benchGenerator(b *testing.B, dir string) *Generator { b.Helper() - f, err := New([]string{dir}, WithSeed(1)) + f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(1)) if err != nil { b.Fatal(err) } @@ -91,11 +91,10 @@ func BenchmarkBoundWide(b *testing.B) { benchPath(b, dir, "row") } -// BenchmarkNew measures load+compile+validate of the whole shipped tree. func BenchmarkNew(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := New([]string{"data"}, WithSeed(1)); err != nil { + if _, err := New(WithSeed(1)); err != nil { b.Fatal(err) } } diff --git a/bound_test.go b/bound_test.go index b6eeac0..36ea30c 100644 --- a/bound_test.go +++ b/bound_test.go @@ -54,7 +54,7 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) { "either order": `{"format":"{p.first} + {p}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`, } for name, file := range rejected { - _, err := New([]string{writeData(t, map[string]string{"cat": file})}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) if err == nil { t.Errorf("%s: New = nil error, want the overlapping tokens rejected", name) continue @@ -64,9 +64,9 @@ func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) { } } // The same path twice is one spelling, so it stays legal. - if _, err := New([]string{writeData(t, map[string]string{ + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.first} + {p.first}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`, - })}); err != nil { + }))); err != nil { t.Errorf("New = %v, want one path read twice accepted", err) } } @@ -75,17 +75,17 @@ func TestCalcOperandNamingABoundLevelIsRejected(t *testing.T) { // A calc operand renders its field, so naming a bound level in one is the same // overlap as a bare token: {calc(item * 1)} renders what {item.price} reads a // path into, and the two disagree. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{item.price}|{calc(item * 1)}","item":[` + `{"format":"{price}","price":"10"},{"format":"{price}","price":"20"}]}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "reads a path into") { t.Fatalf("New = %v, want the calc operand rejected as an overlap", err) } // Arithmetic over fields no path names is untouched. - if _, err := New([]string{writeData(t, map[string]string{ + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`, - })}); err != nil { + }))); err != nil { t.Errorf("New = %v, want plain arithmetic accepted", err) } } @@ -104,16 +104,16 @@ func TestReferenceNamingABoundLevelIsRejected(t *testing.T) { `"inner":{"format":"{..cat.p}"}}`, } for name, file := range rejected { - _, err := New([]string{writeData(t, map[string]string{"cat": file})}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) if err == nil || !strings.Contains(err.Error(), "reads a path into") { t.Errorf("%s: New = %v, want the reference rejected as an overlap", name, err) } } // A reference to anything this format does not bind is untouched. - if _, err := New([]string{writeData(t, map[string]string{ + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.first} {..surname}","p":[{"format":"{first}","first":["Anna","Bo"]}]}`, "surname": `["Eriksson","Lindqvist"]`, - })}); err != nil { + }))); err != nil { t.Errorf("New = %v, want a reference outside the bound level accepted", err) } } @@ -122,9 +122,9 @@ 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. A head whose own format names nothing // would otherwise hide the cycle until render, where it is fatal. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"static","x":{"format":"{..a}"}}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "reference cycle") { t.Fatalf("New = %v, want the cycle through {p.x} rejected", err) } @@ -133,10 +133,10 @@ func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) { func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) { // {p.a} renders q, so it is a route to the level {q.x} holds — even though p's // own format names nothing. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":{"format":"{..thing.q}"}},` + `"q":{"format":"{x}","x":["1","2"]}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "reads a path into") { t.Fatalf("New = %v, want the second route to q rejected", err) } @@ -212,7 +212,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) { }, } for name, c := range rejected { - _, err := New([]string{writeData(t, c.files)}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) if err == nil || !strings.Contains(err.Error(), "a {calc()} also reads") { t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err) continue @@ -259,7 +259,7 @@ func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) { }, } for name, files := range accepted { - if _, err := New([]string{writeData(t, files)}); err != nil { + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) } } @@ -282,7 +282,7 @@ func TestAPathReachesEveryVariantItMightDraw(t *testing.T) { }, } for name, c := range rejected { - _, err := New([]string{writeData(t, map[string]string{"cat": c.file})}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file}))) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want) } @@ -300,7 +300,7 @@ func TestALevelAPathNeverRendersIsAccepted(t *testing.T) { `"p":{"format":"{first}","first":["A","B"]},"q":{"format":"{a} {..thing.p}","a":["1","2"]}}`, } for name, file := range accepted { - if _, err := New([]string{writeData(t, map[string]string{"thing": file})}); err != nil { + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"thing": file}))); err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) } } @@ -315,7 +315,7 @@ func TestADeepDiamondChainLoads(t *testing.T) { `{"format":"{a}{b}","a":{"format":"{..l%d}"},"b":{"format":"{..l%d}"}}`, i-1, i-1) } files["thing"] = `{"format":"{p.first} {..l30}","p":{"format":"x","first":["A","B"]}}` - if _, err := New([]string{writeData(t, files)}); err != nil { + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil { t.Fatalf("New = %v, want a deep diamond chain to load", err) } } @@ -324,11 +324,11 @@ func TestASharedNodeIsWalkedOnce(t *testing.T) { // Two fields reaching one node make the render graph a diamond, not a tree. // The search past a bound level must take that in its stride rather than walk // the shared node once per route. - f, err := New([]string{writeData(t, map[string]string{ + f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.first}|{q}","p":[{"format":"{first}","first":["Anna","Bo"]}],` + `"q":{"format":"{a}{b}","a":{"format":"{..shared}"},"b":{"format":"{..shared}"}}}`, "shared": `["x"]`, - })}, WithSeed(1)) + })), WithSeed(1)) if err != nil { t.Fatalf("New = %v, want a shared node accepted", err) } @@ -341,9 +341,9 @@ func TestTheEarlierReaderIsNamed(t *testing.T) { // A token and a calc operand can name one level. The one the format writes // first is the one reported, so the error points at the same place a reader // looking at the format would start. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":[{"format":"{first}","first":["1","2"]}]}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) { t.Fatalf("New = %v, want the calc operand named, being written first", err) } @@ -353,10 +353,10 @@ func TestReferenceToAMatchingStringIsAccepted(t *testing.T) { // A literal renders one fixed string, so no draw of it can disagree with a // held one. Two unrelated literals that merely spell the same text must not // read as the same node — the trap when comparing a value type. - if _, err := New([]string{writeData(t, map[string]string{ + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{p.city} {..other.tag}","p":{"format":"{city}","city":"Stockholm"}}`, "other": `{"format":"x","tag":"Stockholm"}`, - })}); err != nil { + }))); err != nil { t.Fatalf("New = %v, want a reference to a matching string accepted", err) } } @@ -378,9 +378,9 @@ func TestNestedChoiceDrawsOneVariant(t *testing.T) { func TestRepeatingLevelIsNamedInTheError(t *testing.T) { // The level carrying the repeat is the one to fix, so the error names it // rather than the head the path started from. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":["z"]}}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), `"p.a"`) { t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err) } @@ -390,9 +390,9 @@ func TestPathIntoARepeatingLevelBehindAChoiceIsRejected(t *testing.T) { // A choice of rows is the shape this feature is for, so the repeat rule has to // reach inside one — otherwise the direct spelling is a load error and the same // mistake behind a choice silently drops the repeat. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":["x"]},{"format":"{a}","a":["y"]}]}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "repeat") { t.Fatalf("New = %v, want the repeat behind a choice rejected", err) } @@ -401,9 +401,9 @@ func TestPathIntoARepeatingLevelBehindAChoiceIsRejected(t *testing.T) { func TestPathIntoARepeatingLevelIsRejected(t *testing.T) { // A path reads one level's draw, so it can never apply that level's repeat. // The engine rejects options that cannot take effect, and this is one. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":["z"]}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "repeat") { t.Fatalf("New = %v, want a path into a repeating level rejected", err) } @@ -412,9 +412,9 @@ func TestPathIntoARepeatingLevelIsRejected(t *testing.T) { func TestPathIntoAPlainTemplateNamesTheMissingField(t *testing.T) { // A head that is one template, not a choice, reports the missing segment // directly — there are no variants to compare. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "cat": `{"format":"{a.nope}","a":{"format":"x","b":"1"}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), `no field "nope"`) { t.Fatalf("New = %v, want it to name the missing field", err) } @@ -532,7 +532,7 @@ func TestDottedTokenErrors(t *testing.T) { }, } for name, c := range rejected { - _, err := New([]string{writeData(t, map[string]string{"cat": c.file})}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file}))) if err == nil { t.Errorf("%s: New = nil error, want it rejected at load", name) continue @@ -665,7 +665,7 @@ func TestEmptyPathSegmentIsRejected(t *testing.T) { "double dot": `{"format":"[{a..b}]","a":{"format":"x"}}`, } for name, file := range rejected { - _, err := New([]string{writeData(t, map[string]string{"cat": file})}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file}))) if err == nil { t.Errorf("%s: New = nil error, want the unfinished path rejected", name) continue @@ -680,9 +680,9 @@ func TestCycleThroughAPathTokenIsRejected(t *testing.T) { // A path token is a render edge like any other, so a cycle routed through one // must be caught at New. Reaching render would be fatal: the recursion never // terminates, and a stack overflow cannot be recovered. - _, err := New([]string{writeData(t, map[string]string{ + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ "a": `{"format":"{p.x}","p":{"format":"{x}","x":{"format":"{..a}"}}}`, - })}) + }))) if err == nil || !strings.Contains(err.Error(), "reference cycle") { t.Fatalf("New = %v, want the cycle through {p.x} rejected", err) } @@ -692,7 +692,7 @@ func TestBoundPathIsReachableByFake(t *testing.T) { // Binding changes how a format reads a sibling, not what List and Fake offer: // the sub-fields stay addressable on their own. dir := writeData(t, map[string]string{"address": places("{place.postal-code} {place.locality}")}) - f, err := New([]string{dir}, WithSeed(9)) + f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(9)) if err != nil { t.Fatalf("New = %v", err) } diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index 79eaa18..56cb624 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -162,7 +162,7 @@ func TestRunRepeatAdvancesRNG(t *testing.T) { } func TestRunMisuse(t *testing.T) { - for _, args := range [][]string{{}, {"--data-path", svSE}, {"person"}, {"-d", svSE, "person", "word"}, {"-d", svSE, "--list", "person"}} { + for _, args := range [][]string{{}, {"--data-path", svSE}, {"-d", svSE, "person", "word"}, {"-d", svSE, "--list", "person"}, {"--no-shipped-data", "sv_SE.person"}} { code, out, errb := runOut(args...) if code != 2 { t.Errorf("run(%v) = %d, want 2", args, code) @@ -240,3 +240,32 @@ func TestRunMissingDirFails(t *testing.T) { t.Error("want an error message on stderr") } } + +func TestRunShippedDataByDefault(t *testing.T) { + code, out, errb := runOut("--seed", "1", "sv_SE.person") + if code != 0 || strings.TrimSpace(out) == "" { + t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb) + } + code, list, _ := runOut("--list") + if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") { + t.Errorf("--list without --data-path = %d, %q", code, list) + } + code, _, errb = runOut("person") + if code != 1 || !strings.Contains(errb, "person") { + t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb) + } +} + +func TestRunNoShippedData(t *testing.T) { + code, list, errb := runOut("--no-shipped-data", "-d", svSE, "--list") + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + if strings.Contains(list, "en_US") || !strings.Contains(list, "person\n") { + t.Errorf("--no-shipped-data --list = %q, want only the given dir", list) + } + code, out, _ := runOut("--no-shipped-data", "-d", svSE, "-s", "3", "person") + if code != 0 || strings.TrimSpace(out) == "" { + t.Errorf("run = %d, out=%q", code, out) + } +} diff --git a/compose.yaml b/compose.yaml index 18f706c..fda93dd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,7 +28,7 @@ x-go: &go services: test: <<: *go - command: go test ./... + command: go test -race ./... cover: <<: *go diff --git a/edge_test.go b/edge_test.go index 61f4bb6..223e31b 100644 --- a/edge_test.go +++ b/edge_test.go @@ -56,11 +56,11 @@ func TestNewErrors(t *testing.T) { if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil { t.Fatal(err) } - if _, err := New([]string{file}); err == nil { + if _, err := New(WithoutShippedData(), WithDataPath(file)); err == nil { t.Error("New(file) = nil error, want not-a-directory error") } // Invalid JSON in a category file fails. - if _, err := New([]string{writeData(t, map[string]string{"broken": `{ not json`})}); err == nil { + if _, err := New(WithoutShippedData(), 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 @@ -168,7 +168,7 @@ func TestNewErrors(t *testing.T) { }, } for name, c := range rejected { - _, err := New([]string{writeData(t, c.files)}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) if err == nil { t.Errorf("%s: New = nil error, want it rejected at load", name) continue @@ -195,7 +195,7 @@ func TestNewErrors(t *testing.T) { "one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":["1"]}`}, "a", "11"}, } for name, c := range accepted { - f, err := New([]string{writeData(t, c.files)}) + f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) if err != nil { t.Errorf("%s: New = %v, want it accepted", name, err) continue @@ -272,7 +272,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([]string{dir}) + _, err := New(WithoutShippedData(), WithDataPath(dir)) if err == nil || !strings.Contains(err.Error(), `field "a.b" contains "."`) { t.Fatalf("New = %v, want the dotted field name rejected", err) } diff --git a/fejkdata_test.go b/fejkdata_test.go index ff5f564..d85401f 100644 --- a/fejkdata_test.go +++ b/fejkdata_test.go @@ -15,7 +15,11 @@ func newGenerator(t *testing.T, dir string, opts ...Option) *Generator { func newGeneratorN(t *testing.T, dirs []string, opts ...Option) *Generator { t.Helper() - f, err := New(dirs, opts...) + all := []Option{WithoutShippedData()} + for _, dir := range dirs { + all = append(all, WithDataPath(dir)) + } + f, err := New(append(all, opts...)...) if err != nil { t.Fatalf("New(%q): %v", dirs, err) } @@ -33,7 +37,7 @@ func fake(t *testing.T, f *Generator, path string) string { } func TestNewMissingDirectory(t *testing.T) { - _, err := New([]string{"data/de_DE"}) + _, err := New(WithoutShippedData(), WithDataPath("data/de_DE")) if err == nil || !strings.Contains(err.Error(), "de_DE") { t.Fatalf("New(missing) error = %v, want it to name the path", err) } diff --git a/loading_test.go b/loading_test.go index 5727bde..d8890ca 100644 --- a/loading_test.go +++ b/loading_test.go @@ -57,7 +57,7 @@ func TestNewLoadsAnyDirName(t *testing.T) { } func TestNewEmptyDirErrors(t *testing.T) { - if _, err := New([]string{writeData(t, nil)}); err == nil { + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, nil))); err == nil { t.Fatal("New(empty dir) = nil error") } } diff --git a/reference_test.go b/reference_test.go index 62ceada..3ed55ef 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":{"format":"{..nope}"}}`}, } for name, files := range cases { - if _, err := New([]string{writeData(t, files)}); err == nil { + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err == nil { t.Errorf("%s: New = nil error, want a reference error", name) } } @@ -161,7 +161,7 @@ func TestNewErrorIsDeterministic(t *testing.T) { dir := writeData(t, files) var first string for i := 0; i < 50; i++ { - _, err := New([]string{dir}) + _, err := New(WithoutShippedData(), WithDataPath(dir)) if err == nil { t.Fatalf("%s: New = nil error, want a load error", name) } @@ -198,7 +198,7 @@ func TestNewErrorPathIsCanonical(t *testing.T) { }, } for _, c := range cases { - _, err := New([]string{writeData(t, c.files)}) + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files))) if err == nil { t.Errorf("%s: New = nil error", c.name) continue diff --git a/shipped_data_test.go b/shipped_data_test.go new file mode 100644 index 0000000..660fccd --- /dev/null +++ b/shipped_data_test.go @@ -0,0 +1,114 @@ +package fejkdata + +import ( + "reflect" + "slices" + "strings" + "sync" + "testing" + "testing/fstest" +) + +func TestNewDefaultsToShippedData(t *testing.T) { + f, err := New(WithSeed(1)) + if err != nil { + t.Fatalf("New() = %v", err) + } + paths := f.List() + for _, p := range []string{"sv_SE.person", "en_US.address", "misc.uuid"} { + if !slices.Contains(paths, p) { + t.Errorf("List() omits shipped %q", p) + } + } + if got := fake(t, f, "sv_SE.person"); got == "" { + t.Error("sv_SE.person rendered empty") + } +} + +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)) + if err != nil { + t.Fatalf("New = %v", err) + } + if got := fake(t, f, "sv_SE.word"); got != "only-mine" { + t.Errorf("sv_SE.word = %q, want the layered file to win", got) + } + if got := fake(t, f, "sv_SE.person"); got == "" { + t.Error("sv_SE.person should still come from the shipped data") + } + if got := fake(t, f, "greeting"); got != "hej" { + t.Errorf("greeting = %q, want hej", got) + } +} + +func TestUserDataMayReferenceShipped(t *testing.T) { + dir := writeData(t, map[string]string{"greeting": `{"format":"Hej {..sv_SE.person}!"}`}) + f, err := New(WithDataPath(dir), WithSeed(1)) + if err != nil { + t.Fatalf("New = %v", err) + } + if got := fake(t, f, "greeting"); !strings.HasPrefix(got, "Hej ") || !strings.HasSuffix(got, "!") { + t.Errorf("greeting = %q", got) + } +} + +func TestWithoutShippedDataNeedsASource(t *testing.T) { + _, err := New(WithoutShippedData()) + if err == nil || !strings.Contains(err.Error(), "WithDataPath") { + t.Fatalf("New(WithoutShippedData()) = %v, want an error naming WithDataPath", err) + } +} + +func TestWithoutShippedDataListsOnlyOwn(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"}) { + t.Errorf("List() = %v, want only the loaded dir", got) + } +} + +func TestWithDataFS(t *testing.T) { + fsys := fstest.MapFS{ + "greeting.json": {Data: []byte(`["hej"]`)}, + "nested/x.json": {Data: []byte(`{"format":"{y}","y":["z"]}`)}, + ".hidden.json": {Data: []byte(`["ignored"]`)}, + "broken/no.json": {Data: []byte(`["x"]`)}, + } + f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1)) + if err != nil { + t.Fatalf("New(WithDataFS) = %v", err) + } + if got := fake(t, f, "greeting"); got != "hej" { + t.Errorf("greeting = %q, want hej", got) + } + if got := fake(t, f, "nested.x.y"); got != "z" { + 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") { + t.Errorf("New(bad fs) = %v, want an error naming the file", err) + } +} + +func TestFakeIsSafeForConcurrentUse(t *testing.T) { + f, err := New(WithSeed(1)) + if err != nil { + t.Fatal(err) + } + var wg sync.WaitGroup + for g := 0; g < 8; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 200; i++ { + if _, err := f.Fake("sv_SE.person"); err != nil { + t.Error(err) + return + } + f.List() + } + }() + } + wg.Wait() +}