Tests for the shipped data layer, New options and concurrent Fake

This commit is contained in:
2026-09-01 23:03:53 +02:00
parent cf9de21495
commit 91cbae2a44
10 changed files with 201 additions and 55 deletions
+1 -1
View File
@@ -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 ./...
+2 -3
View File
@@ -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)
}
}
+38 -38
View File
@@ -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)
}
+30 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ x-go: &go
services:
test:
<<: *go
command: go test ./...
command: go test -race ./...
cover:
<<: *go
+5 -5
View File
@@ -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)
}
+6 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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")
}
}
+3 -3
View File
@@ -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
+114
View File
@@ -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()
}