Rename the exported type to Generator and retire the faker wording #2
@@ -13,15 +13,15 @@ lacked the locale coverage and format control we needed.
|
||||
(`sv_SE`), but the engine treats folders as plain namespaces — name yours
|
||||
anything.
|
||||
- **Data lives in JSON** — all source data is recursive JSON on disk, read when
|
||||
you create a faker and then served from memory. Add or change data without
|
||||
you create a generator and then served from memory. Add or change data without
|
||||
touching the library. Behavior belongs in data too: the engine grows a
|
||||
built-in function only for what data can't express (a checksum, a time-based
|
||||
id), never for what character classes and choices already do.
|
||||
- **Composable** — templates nest without limit: weighted choices, character
|
||||
classes and sub-templates combine to model any format.
|
||||
- **Reproducible** — seed a faker and it emits the same sequence every time, for a
|
||||
given version of the data: changing how a value is composed shifts the stream for
|
||||
that value and for everything drawn after it in the same faker.
|
||||
- **Reproducible** — seed a generator and it emits the same sequence every time,
|
||||
for a given version of the data: changing how a value is composed shifts the
|
||||
stream for that value and for everything drawn after it in the same generator.
|
||||
Every built-in draws only from that seed — no wall-clock, no `crypto/rand` —
|
||||
so determinism holds end to end.
|
||||
- **Zero dependencies** — standard library only.
|
||||
@@ -133,7 +133,7 @@ phone 072-402 91 67
|
||||
address.locality Linköping
|
||||
```
|
||||
|
||||
Seed a faker for reproducible output — same seed + locale yields an identical
|
||||
Seed a generator for reproducible output — same seed + locale yields an identical
|
||||
sequence, handy for stable tests:
|
||||
|
||||
```go
|
||||
@@ -148,7 +148,7 @@ av == bv // true
|
||||
dotted fields and folder segments (what the CLI's `-list` prints). Every path it
|
||||
lists renders.
|
||||
|
||||
A `*Fejkdata` is **not** safe for concurrent use — create one per goroutine.
|
||||
A `*Generator` is **not** safe for concurrent use — create one per goroutine.
|
||||
|
||||
## Data
|
||||
|
||||
@@ -219,7 +219,7 @@ within a choice:
|
||||
```
|
||||
|
||||
Only template (object) nodes carry `weight` — a bare string or nested array in a
|
||||
choice always counts as `1`. Weights are checked when you create the faker: a
|
||||
choice always counts as `1`. Weights are checked when you create the generator: a
|
||||
negative, non-numeric, or all-zero set is rejected at `New`, so a typo fails
|
||||
fast instead of silently skewing output.
|
||||
|
||||
@@ -264,14 +264,14 @@ form prefixes the century outside the checksummed core:
|
||||
"core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ … ] } }
|
||||
```
|
||||
|
||||
A function must be deterministic (no wall-clock), so a seeded faker stays
|
||||
A function must be deterministic (no wall-clock), so a seeded generator stays
|
||||
reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from
|
||||
the rng, not the clock — the result is a valid, reproducible value, not a real
|
||||
point in time.
|
||||
|
||||
There are four kinds. **Derivations** read the digits emitted so far, so put them
|
||||
after their payload; **generators** read only the rng, so they stand alone; one
|
||||
**session counter** (`seq`) advances state held on the faker; and one
|
||||
after their payload; **samples** read only the rng, so they stand alone; one
|
||||
**session counter** (`seq`) advances state held on the generator; and one
|
||||
**computation** (`calc`) evaluates arithmetic over sibling fields. Arguments are
|
||||
validated at `New` (a bad count, range, country, or expression fails fast); a
|
||||
length, count or decimal place beyond a sane maximum is rejected there too, so a
|
||||
@@ -282,25 +282,25 @@ fat-fingered `hex(2000000000)` can't try to allocate gigabytes at render.
|
||||
| `{luhn()}` | derivation | Luhn check digit (mod-10) over preceding digits |
|
||||
| `{mod11()}` | derivation | weighted mod-11 check char (weights 2–7 from the right); `X` when it would be 10 |
|
||||
| `{ean()}` | derivation | EAN-13 / UPC-A / ISBN-13 / GTIN check digit |
|
||||
| `{uuid()}` | generator | UUID v7 (v4 ships as data — see [Data](#data)) |
|
||||
| `{ulid()}` | generator | ULID, 26-char Crockford base32 |
|
||||
| `{nanoid(n)}` | generator | URL-safe Nano ID, `n` chars |
|
||||
| `{hex(n)}` | generator | `n` lowercase hex digits |
|
||||
| `{base64(n)}` | generator | `n` random bytes, base64 |
|
||||
| `{int(min,max)}` | generator | uniform integer in `[min, max]` |
|
||||
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals |
|
||||
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
||||
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this faker's sequence; `name` selects an independent counter |
|
||||
| `{uuid()}` | sample | UUID v7 (v4 ships as data — see [Data](#data)) |
|
||||
| `{ulid()}` | sample | ULID, 26-char Crockford base32 |
|
||||
| `{nanoid(n)}` | sample | URL-safe Nano ID, `n` chars |
|
||||
| `{hex(n)}` | sample | `n` lowercase hex digits |
|
||||
| `{base64(n)}` | sample | `n` random bytes, base64 |
|
||||
| `{int(min,max)}` | sample | uniform integer in `[min, max]` |
|
||||
| `{float(min,max,dp)}` | sample | number in `[min, max]` with `dp` decimals |
|
||||
| `{iban(CC)}` | sample | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
||||
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this generator's sequence; `name` selects an independent counter |
|
||||
| `{calc(expr)}`, `{calc(expr,dp)}` | computation | value of an arithmetic expression over number literals and sibling fields; `dp` rounds |
|
||||
|
||||
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
||||
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation:
|
||||
prefix in data and call `{ean()}`). `{iban()}` is a sample, not a derivation:
|
||||
an IBAN's check digits sit *before* the account number, which a left-to-right
|
||||
reader can't reach, so it emits the whole value (a generic numeric BBAN — valid
|
||||
length and checksum, not real bank routing).
|
||||
|
||||
`{seq()}`'s counter lives on the faker, so it spans `Fake` calls (and `repeat`)
|
||||
and resets when you build a new faker — `seq` is reproducible by being ordered,
|
||||
`{seq()}`'s counter lives on the generator, so it spans `Fake` calls (and `repeat`)
|
||||
and resets when you build a new generator — `seq` is reproducible by being ordered,
|
||||
not random. It's the natural fit for a primary-key column in the SQL example above.
|
||||
|
||||
**Computation.** `{calc(expr)}` evaluates an arithmetic expression — `+ - * /`,
|
||||
@@ -332,7 +332,7 @@ data dirs:
|
||||
{ "format": "Hej, {..en_US.person}!" }
|
||||
```
|
||||
|
||||
renders e.g. `Hej, Pat Smith!`. References are bound when you create the faker, so
|
||||
renders e.g. `Hej, Pat Smith!`. References are bound when you create the generator, so
|
||||
a path that is unknown, names a folder, or steps through a multi-variant choice
|
||||
fails at `New`. A reference that leads back to its own value (directly, mutually,
|
||||
or through a chain) is a cycle that would never finish rendering, so it too is
|
||||
@@ -516,7 +516,7 @@ docker compose run --rm test # latest
|
||||
## Layout
|
||||
|
||||
```
|
||||
fejkdata.go Fejkdata, New, List, options, seeding
|
||||
fejkdata.go Generator, New, List, options, seeding
|
||||
node.go the node model and JSON -> node compilation
|
||||
render.go Fake and the recursive renderer (choices, format strings, paths, bound draws)
|
||||
template.go the {token} grammar: scanning, function and path tokens, validation
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func benchFaker(b *testing.B, dir string) *Fejkdata {
|
||||
func benchGenerator(b *testing.B, dir string) *Generator {
|
||||
b.Helper()
|
||||
f, err := New([]string{dir}, WithSeed(1))
|
||||
if err != nil {
|
||||
@@ -16,7 +16,7 @@ func benchFaker(b *testing.B, dir string) *Fejkdata {
|
||||
}
|
||||
|
||||
func benchPath(b *testing.B, dir, path string) {
|
||||
f := benchFaker(b, dir)
|
||||
f := benchGenerator(b, dir)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
+11
-11
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxLen caps generator output lengths (hex, nanoid, base64) and maxDecimals caps
|
||||
// maxLen caps sample output lengths (hex, nanoid, base64) and maxDecimals caps
|
||||
// float/calc decimal places, so a fat-fingered or overflowing argument fails at
|
||||
// New instead of trying to allocate gigabytes — or panicking — at render.
|
||||
const (
|
||||
@@ -16,10 +16,10 @@ const (
|
||||
maxDecimals = 1024
|
||||
)
|
||||
|
||||
// builtins is the registry of {name(args)} functions. Two kinds: derivations read
|
||||
// the digits emitted so far in the current expansion (luhn, mod11, ean — place
|
||||
// them after their payload); generators read only the rng (uuid, ulid, ...). All
|
||||
// must stay pure over (rng, emitted, args) so a seeded faker is reproducible — a
|
||||
// builtins is the registry of {name(args)} functions. Derivations read the
|
||||
// digits emitted so far in the current expansion (luhn, mod11, ean — place them
|
||||
// after their payload); samples read only the rng (uuid, ulid, ...). All
|
||||
// must stay pure over (rng, emitted, args) so seeded output is reproducible — a
|
||||
// time-based id (uuid v7, ulid) draws its timestamp from the rng, not the wall
|
||||
// clock. Add a builtin only for what data can't express: a random v4 UUID and a
|
||||
// 24-hex ObjectID both ship as data, so the uuid builtin is v7.
|
||||
@@ -27,8 +27,8 @@ var builtins = map[string]builtin{
|
||||
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
||||
"mod11": {arity: 0, prep: derive(mod11Check)},
|
||||
"ean": {arity: 0, prep: derive(eanCheck)},
|
||||
"uuid": {arity: 0, prep: generate(uuidV7)},
|
||||
"ulid": {arity: 0, prep: generate(ulid)},
|
||||
"uuid": {arity: 0, prep: sample(uuidV7)},
|
||||
"ulid": {arity: 0, prep: sample(ulid)},
|
||||
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||
n := atoi(a[0])
|
||||
return func(s *session, _ string, _ []string) string { return nanoid(s, n) }
|
||||
@@ -62,7 +62,7 @@ var builtins = map[string]builtin{
|
||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
||||
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
||||
// each call. An optional name selects an independent counter; no name uses the
|
||||
// default one. Deterministic by construction, so a seeded faker stays stable.
|
||||
// default one. Deterministic by construction, so seeded output stays stable.
|
||||
"seq": {arity: -1, check: seqArg, prep: func(a []string) callFn {
|
||||
key := ""
|
||||
if len(a) == 1 {
|
||||
@@ -74,8 +74,8 @@ var builtins = map[string]builtin{
|
||||
}},
|
||||
}
|
||||
|
||||
// derive and generate are the two argument-free builtin shapes the README names: a
|
||||
// derivation reads the output emitted so far, a generator reads only the rng. Each
|
||||
// derive and sample are the two argument-free builtin shapes the README names: a
|
||||
// derivation reads the output emitted so far, a sample reads only the rng. Each
|
||||
// lifts that one function into the prep every registry entry supplies.
|
||||
func derive(f func(emitted string) string) func([]string) callFn {
|
||||
return func([]string) callFn {
|
||||
@@ -83,7 +83,7 @@ func derive(f func(emitted string) string) func([]string) callFn {
|
||||
}
|
||||
}
|
||||
|
||||
func generate(f func(rng) string) func([]string) callFn {
|
||||
func sample(f func(rng) string) func([]string) callFn {
|
||||
return func([]string) callFn {
|
||||
return func(s *session, _ string, _ []string) string { return f(s) }
|
||||
}
|
||||
|
||||
+6
-6
@@ -34,9 +34,9 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinGeneratorsReproducible pins the determinism guardrail: every
|
||||
// generator draws only from the seeded rng, so same seed -> same value.
|
||||
func TestBuiltinGeneratorsReproducible(t *testing.T) {
|
||||
// TestBuiltinSamplesReproducible pins the determinism guardrail: every
|
||||
// sample draws only from the seeded rng, so same seed -> same value.
|
||||
func TestBuiltinSamplesReproducible(t *testing.T) {
|
||||
for _, tmpl := range []string{
|
||||
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
|
||||
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
|
||||
@@ -96,8 +96,8 @@ func TestBuiltinChecksums(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on
|
||||
// each call, named counters independent, and the whole thing scoped to one faker
|
||||
// (session) so a fresh faker restarts at 1.
|
||||
// each call, named counters independent, and the whole thing scoped to one
|
||||
// session so a fresh Generator restarts at 1.
|
||||
func TestBuiltinSeqPerSession(t *testing.T) {
|
||||
f := engine(1)
|
||||
for i := 1; i <= 5; i++ {
|
||||
@@ -163,7 +163,7 @@ func TestBuiltinIBAN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ibanValid checks the IBAN mod-97 rule independently of the generator: move the
|
||||
// ibanValid checks the IBAN mod-97 rule independently of the builtin: move the
|
||||
// first four chars to the end, map letters A-Z to 10-35, the number mod 97 == 1.
|
||||
func ibanValid(s string) bool {
|
||||
r := s[4:] + s[:4]
|
||||
|
||||
+5
-5
@@ -126,7 +126,7 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(3))
|
||||
f := newGenerator(t, dir, WithSeed(3))
|
||||
for i := 0; i < 300; i++ {
|
||||
got := fake(t, f, "inv")
|
||||
var net, qty, want float64
|
||||
@@ -145,7 +145,7 @@ func TestCalcOperandSharesOneDraw(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(5))
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fake(t, f, "same")
|
||||
if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] {
|
||||
@@ -158,7 +158,7 @@ func TestCalcOperandSharesOneDraw(t *testing.T) {
|
||||
// held, so an ordinary {w} {w} still draws twice.
|
||||
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
|
||||
f := newFejkdata(t, dir, WithSeed(5))
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 200; i++ {
|
||||
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
|
||||
return
|
||||
@@ -173,7 +173,7 @@ func TestCalcHoldIsPerExpansion(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(11))
|
||||
f := newGenerator(t, dir, WithSeed(11))
|
||||
varied := false
|
||||
for i := 0; i < 50; i++ {
|
||||
got := fake(t, f, "rep")
|
||||
@@ -199,7 +199,7 @@ func TestCalcHoldIsPerTemplate(t *testing.T) {
|
||||
"nest": `{"format":"{v}={calc(v * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
|
||||
"inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(13))
|
||||
f := newGenerator(t, dir, WithSeed(13))
|
||||
differed := false
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fake(t, f, "nest")
|
||||
|
||||
+12
-12
@@ -51,8 +51,8 @@ func TestShippedDataCategories(t *testing.T) {
|
||||
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
|
||||
}
|
||||
|
||||
en := newFejkdata(t, "data/en_US", WithSeed(1))
|
||||
sv := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||
en := newGenerator(t, "data/en_US", WithSeed(1))
|
||||
sv := newGenerator(t, "data/sv_SE", WithSeed(1))
|
||||
for _, c := range cases {
|
||||
for i := 0; i < 200; i++ {
|
||||
if v := fake(t, en, c.path); !c.en.MatchString(v) {
|
||||
@@ -69,7 +69,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 := newFejkdata(t, "data/misc", WithSeed(1))
|
||||
f := newGenerator(t, "data/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}$`)
|
||||
@@ -93,7 +93,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 := newFejkdata(t, "data/misc", WithSeed(1))
|
||||
f := newGenerator(t, "data/misc", WithSeed(1))
|
||||
re := map[string]*regexp.Regexp{
|
||||
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
|
||||
"currency.name": regexp.MustCompile(`\p{L}`),
|
||||
@@ -134,7 +134,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 := newFejkdata(t, "data/sv_SE", WithSeed(11))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(11))
|
||||
for _, path := range []string{"person", "person.last"} {
|
||||
for i := 0; i < 20000; i++ {
|
||||
name := fake(t, f, path)
|
||||
@@ -152,7 +152,7 @@ func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
||||
// is a real calendar date (so month-length variants never emit e.g. Apr 31 or
|
||||
// Feb 30) and the trailing digit is a valid Luhn checksum over the other nine.
|
||||
func TestSwedishPersonnummer(t *testing.T) {
|
||||
sv := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||
sv := newGenerator(t, "data/sv_SE", WithSeed(1))
|
||||
sawLongMonthEnd := false
|
||||
for i := 0; i < 2000; i++ {
|
||||
v := fake(t, sv, "ssn")
|
||||
@@ -176,7 +176,7 @@ func TestSwedishPersonnummer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShippedSwedishPhone(t *testing.T) {
|
||||
f := newFejkdata(t, "data/sv_SE", WithSeed(11))
|
||||
f := newGenerator(t, "data/sv_SE", 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, "phone"); !re.MatchString(n) {
|
||||
@@ -186,7 +186,7 @@ func TestShippedSwedishPhone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShippedSwedishAddress(t *testing.T) {
|
||||
f := newFejkdata(t, "data/sv_SE", WithSeed(3))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(3))
|
||||
digit := regexp.MustCompile(`\d`)
|
||||
for i := 0; i < 30; i++ {
|
||||
a := fake(t, f, "address")
|
||||
@@ -205,7 +205,7 @@ func TestShippedSwedishAddress(t *testing.T) {
|
||||
|
||||
func TestShippedPersonHasParts(t *testing.T) {
|
||||
for _, dir := range []string{"data/sv_SE", "data/en_US"} {
|
||||
f := newFejkdata(t, dir, WithSeed(7))
|
||||
f := newGenerator(t, dir, WithSeed(7))
|
||||
for i := 0; i < 30; i++ {
|
||||
if name := fake(t, f, "person"); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
|
||||
t.Fatalf("%s person %q lacks first and last name", dir, name)
|
||||
@@ -215,7 +215,7 @@ func TestShippedPersonHasParts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShippedUSPhone(t *testing.T) {
|
||||
f := newFejkdata(t, "data/en_US", WithSeed(11))
|
||||
f := newGenerator(t, "data/en_US", WithSeed(11))
|
||||
re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`)
|
||||
for i := 0; i < 50; i++ {
|
||||
if n := fake(t, f, "phone"); !re.MatchString(n) {
|
||||
@@ -227,7 +227,7 @@ func TestShippedUSPhone(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 := newFejkdata(t, "data", WithSeed(1))
|
||||
f := newGenerator(t, "data", 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)
|
||||
@@ -249,7 +249,7 @@ func digitsOnly(s string) string {
|
||||
}
|
||||
|
||||
// luhnValid verifies a full number (payload + trailing check digit). It doubles
|
||||
// from the second-from-right, independent of the generator's own Luhn code.
|
||||
// from the second-from-right, independent of the builtin's own Luhn code.
|
||||
func luhnValid(s string) bool {
|
||||
sum, double := 0, false
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
|
||||
+8
-8
@@ -240,7 +240,7 @@ func TestPathThroughChoice(t *testing.T) {
|
||||
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
||||
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
for i := 0; i < 200; i++ {
|
||||
if got := fake(t, f, "every.f"); got != "1" && got != "2" {
|
||||
t.Fatalf("every.f = %q, want 1 or 2", got)
|
||||
@@ -277,7 +277,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
|
||||
t.Fatalf("New = %v, want the dotted field name rejected", err)
|
||||
}
|
||||
// The same data without the dotted key is fine, and the path resolves.
|
||||
f := newFejkdata(t, writeData(t, map[string]string{
|
||||
f := newGenerator(t, writeData(t, map[string]string{
|
||||
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
|
||||
}), WithSeed(1))
|
||||
if !slices.Contains(f.List(), "cat.a.b") {
|
||||
@@ -292,7 +292,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
|
||||
// single-variant choice always picks 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 := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(1))
|
||||
_, err := f.Fake("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)
|
||||
@@ -306,7 +306,7 @@ func TestCategoryRootShapes(t *testing.T) {
|
||||
"obj": `{"format":"00"}`, // object root
|
||||
"lit": `"hello"`, // bare-string root
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
|
||||
t.Errorf("object-root category = %q, want two digits", got)
|
||||
}
|
||||
@@ -327,7 +327,7 @@ func TestLongStringList(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := newFejkdata(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1))
|
||||
f := newGenerator(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1))
|
||||
|
||||
valid := map[string]bool{}
|
||||
for _, v := range names {
|
||||
@@ -355,7 +355,7 @@ var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
|
||||
|
||||
func TestShippedStreetComposition(t *testing.T) {
|
||||
// street is a choice of composed {first}{last} templates and literal names.
|
||||
f := newFejkdata(t, "data/sv_SE", WithSeed(5))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(5))
|
||||
for i := 0; i < 300; i++ {
|
||||
if s := fake(t, f, "address.street"); !swedishName.MatchString(s) {
|
||||
t.Fatalf("street %q is not a Swedish street name", s)
|
||||
@@ -366,7 +366,7 @@ func TestShippedStreetComposition(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 := newFejkdata(t, "data/sv_SE", WithSeed(6))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(6))
|
||||
for i := 0; i < 300; i++ {
|
||||
if s := fake(t, f, "person.last"); !swedishName.MatchString(s) {
|
||||
t.Fatalf("last name %q is not a Swedish surname", s)
|
||||
@@ -376,7 +376,7 @@ func TestShippedLastNameComposition(t *testing.T) {
|
||||
|
||||
func TestShippedStreetNumberFormats(t *testing.T) {
|
||||
// Reachable via a hyphenated path; covers all five weighted number variants.
|
||||
f := newFejkdata(t, "data/sv_SE", WithSeed(8))
|
||||
f := newGenerator(t, "data/sv_SE", WithSeed(8))
|
||||
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
|
||||
for i := 0; i < 300; i++ {
|
||||
if n := fake(t, f, "address.street-number"); !re.MatchString(n) {
|
||||
|
||||
+12
-12
@@ -13,7 +13,7 @@
|
||||
// the last one wins on a name clash, so you can layer custom data over the
|
||||
// built-ins. The JSON template format is documented in the README.
|
||||
//
|
||||
// A [Fejkdata] is not safe for concurrent use; create one per goroutine.
|
||||
// A [Generator] is not safe for concurrent use; create one per goroutine.
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
@@ -24,15 +24,15 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Fejkdata generates fake data from a loaded namespace tree. Create one with [New].
|
||||
type Fejkdata struct {
|
||||
// Generator generates fake data from a loaded namespace tree. Create one with [New].
|
||||
type Generator struct {
|
||||
rand *session
|
||||
categories map[string]node // root namespace: name -> compiled node tree
|
||||
}
|
||||
|
||||
// session is one faker's mutable render state: the seeded rng plus the {seq()}
|
||||
// counters. Scoping it to the Fejkdata means sequences (and randomness) belong to
|
||||
// that faker and reset when you create a new one. Embedding *rand.Rand makes a
|
||||
// session is one generator's mutable render state: the seeded rng plus the {seq()}
|
||||
// counters. Scoping it to the Generator means sequences (and randomness) belong to
|
||||
// that generator and reset when you create a new one. Embedding *rand.Rand makes a
|
||||
// *session satisfy the rng interface the renderer draws from.
|
||||
type session struct {
|
||||
*rand.Rand
|
||||
@@ -50,21 +50,21 @@ type config struct {
|
||||
seeded bool
|
||||
}
|
||||
|
||||
// Option configures a [Fejkdata].
|
||||
// Option configures a [Generator].
|
||||
type Option func(*config)
|
||||
|
||||
// WithSeed makes output reproducible: two fakers with the same seed and locale
|
||||
// WithSeed makes output reproducible: two generators with the same seed and locale
|
||||
// emit identical sequences.
|
||||
func WithSeed(seed uint64) Option {
|
||||
return func(c *config) { c.seed, c.seeded = seed, true }
|
||||
}
|
||||
|
||||
// New builds a faker from one or more data directories (e.g. "./data/sv_SE").
|
||||
// New builds a generator from one or more data directories (e.g. "./data/sv_SE").
|
||||
// Each JSON file becomes a category named after the file (address.json ->
|
||||
// "address") and each subdirectory a namespace segment; directories are merged
|
||||
// in order, the last winning a name clash. It errors on a missing directory,
|
||||
// invalid JSON, or no data found.
|
||||
func New(paths []string, opts ...Option) (*Fejkdata, error) {
|
||||
func New(paths []string, opts ...Option) (*Generator, error) {
|
||||
cats, err := loadData(paths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
@@ -73,7 +73,7 @@ func New(paths []string, opts ...Option) (*Fejkdata, error) {
|
||||
for _, opt := range opts {
|
||||
opt(&c)
|
||||
}
|
||||
return &Fejkdata{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
||||
return &Generator{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
||||
}
|
||||
|
||||
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
||||
@@ -81,7 +81,7 @@ func New(paths []string, opts ...Option) (*Fejkdata, error) {
|
||||
// single-variant choices the way a reference does. A choice consumes no segment, so
|
||||
// a path continues through a multi-variant one only where every variant carries it,
|
||||
// which is the rule Fake applies too: List is the set of paths Fake accepts.
|
||||
func (f *Fejkdata) List() []string {
|
||||
func (f *Generator) List() []string {
|
||||
var out []string
|
||||
for _, name := range sortedNames(f.categories) {
|
||||
for _, p := range paths(f.categories[name]) {
|
||||
|
||||
+9
-8
@@ -5,14 +5,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newFejkdata creates a faker over a single data directory, failing on error. Most
|
||||
// tests load one dir; newFejkdataN loads several (last-loaded wins on conflicts).
|
||||
func newFejkdata(t *testing.T, dir string, opts ...Option) *Fejkdata {
|
||||
// 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).
|
||||
func newGenerator(t *testing.T, dir string, opts ...Option) *Generator {
|
||||
t.Helper()
|
||||
return newFejkdataN(t, []string{dir}, opts...)
|
||||
return newGeneratorN(t, []string{dir}, opts...)
|
||||
}
|
||||
|
||||
func newFejkdataN(t *testing.T, dirs []string, opts ...Option) *Fejkdata {
|
||||
func newGeneratorN(t *testing.T, dirs []string, opts ...Option) *Generator {
|
||||
t.Helper()
|
||||
f, err := New(dirs, opts...)
|
||||
if err != nil {
|
||||
@@ -22,7 +23,7 @@ func newFejkdataN(t *testing.T, dirs []string, opts ...Option) *Fejkdata {
|
||||
}
|
||||
|
||||
// fake generates a value, failing the test on error.
|
||||
func fake(t *testing.T, f *Fejkdata, path string) string {
|
||||
func fake(t *testing.T, f *Generator, path string) string {
|
||||
t.Helper()
|
||||
s, err := f.Fake(path)
|
||||
if err != nil {
|
||||
@@ -39,7 +40,7 @@ func TestNewMissingDirectory(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWithSeedIsDeterministic(t *testing.T) {
|
||||
a, b := newFejkdata(t, "data/sv_SE", WithSeed(42)), newFejkdata(t, "data/sv_SE", WithSeed(42))
|
||||
a, b := newGenerator(t, "data/sv_SE", WithSeed(42)), newGenerator(t, "data/sv_SE", WithSeed(42))
|
||||
for i := 0; i < 50; i++ {
|
||||
if x, y := fake(t, a, "person"), fake(t, b, "person"); x != y {
|
||||
t.Fatalf("same seed diverged at %d: %q != %q", i, x, y)
|
||||
@@ -48,7 +49,7 @@ func TestWithSeedIsDeterministic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDifferentSeedsDiffer(t *testing.T) {
|
||||
a, b := newFejkdata(t, "data/en_US", WithSeed(1)), newFejkdata(t, "data/en_US", WithSeed(2))
|
||||
a, b := newGenerator(t, "data/en_US", WithSeed(1)), newGenerator(t, "data/en_US", WithSeed(2))
|
||||
for i := 0; i < 50; i++ {
|
||||
if fake(t, a, "person") != fake(t, b, "person") {
|
||||
return
|
||||
|
||||
+9
-9
@@ -21,7 +21,7 @@ func TestList(t *testing.T) {
|
||||
// Only the fields every variant carries are addressable, so "extra" is not.
|
||||
"coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`,
|
||||
})
|
||||
got := newFejkdata(t, dir, WithSeed(1)).List()
|
||||
got := newGenerator(t, dir, WithSeed(1)).List()
|
||||
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("List() = %v, want %v", got, want)
|
||||
@@ -50,7 +50,7 @@ func writeData(t *testing.T, files map[string]string) string {
|
||||
func TestNewLoadsAnyDirName(t *testing.T) {
|
||||
// No locale tag required: a directory named anything loads fine.
|
||||
dir := writeData(t, map[string]string{"greeting": `["hej", "hallå"]`})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "greeting"); got != "hej" && got != "hallå" {
|
||||
t.Fatalf("greeting = %q, want hej or hallå", got)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func TestFoldersBecomeDotPaths(t *testing.T) {
|
||||
"sv_SE/greeting": `["hej"]`,
|
||||
"en_US/greeting": `["hi"]`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
||||
t.Fatalf("sv_SE.greeting = %q, want hej", got)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
||||
if got := fake(t, f, "a.b.c.thing.x.y.z"); got != "leaf" {
|
||||
t.Fatalf("a.b.c.thing.x.y.z = %q, want leaf", got)
|
||||
@@ -97,7 +97,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
|
||||
|
||||
func TestRenderingAFolderErrors(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if _, err := f.Fake("sv_SE"); err == nil {
|
||||
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestRenderingAFolderErrors(t *testing.T) {
|
||||
func TestMultiPathLastWins(t *testing.T) {
|
||||
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
|
||||
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
|
||||
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||
if got := fake(t, f, "greeting"); got != "from-b" {
|
||||
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func TestMultiPathLastWins(t *testing.T) {
|
||||
func TestMultiPathMergesFolders(t *testing.T) {
|
||||
a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`})
|
||||
b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`})
|
||||
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
||||
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
|
||||
}
|
||||
@@ -142,7 +142,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 := newFejkdata(t, "data", WithSeed(4))
|
||||
f := newGenerator(t, "data", WithSeed(4))
|
||||
paths := f.List()
|
||||
for _, p := range paths {
|
||||
for i := 0; i < 20; i++ {
|
||||
@@ -171,7 +171,7 @@ func TestHiddenEntriesAreSkipped(t *testing.T) {
|
||||
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "cat"); got != "V" {
|
||||
t.Fatalf("cat = %q, want V", got)
|
||||
}
|
||||
|
||||
+7
-7
@@ -9,7 +9,7 @@ func TestRootReferenceAcrossFolders(t *testing.T) {
|
||||
"en_US/person": `["Pat Smith"]`,
|
||||
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
||||
t.Fatalf("greeting = %q, want \"Hej, Pat Smith!\"", got)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func TestReferenceIntoAField(t *testing.T) {
|
||||
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
|
||||
"card": `{"format":"signed {..who.last}"}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "card"); got != "signed Byron" {
|
||||
t.Fatalf("card = %q, want \"signed Byron\"", got)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func TestReferenceInAlternation(t *testing.T) {
|
||||
"far": `["X"]`,
|
||||
"near": `{"format":"{here|..far}","here":["H"]}`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(2))
|
||||
f := newGenerator(t, dir, WithSeed(2))
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 100; i++ {
|
||||
seen[fake(t, f, "near")] = true
|
||||
@@ -50,7 +50,7 @@ func TestReferenceInAlternation(t *testing.T) {
|
||||
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
||||
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
||||
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
||||
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||
f := newGeneratorN(t, []string{a, b}, WithSeed(1))
|
||||
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
||||
t.Fatalf("slug = %q, want the-river", got)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func TestReferenceChain(t *testing.T) {
|
||||
"b": `{"format":"{..c}"}`,
|
||||
"c": `["deep"]`,
|
||||
})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "a"); got != "deep" {
|
||||
t.Fatalf("a = %q, want deep", got)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestReferenceErrors(t *testing.T) {
|
||||
// starting with the reference prefix is covered by that same rule, since ".."
|
||||
// starts with "." — it is skipped, not rejected.
|
||||
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
||||
f := newFejkdata(t, writeData(t, map[string]string{
|
||||
f := newGenerator(t, writeData(t, map[string]string{
|
||||
"sv_SE/ok": `["fine"]`,
|
||||
"sv_SE/..bad": `{"format":"{..nope}"}`,
|
||||
"sv_SE/..y/ct": `{"format":"{..nope}"}`,
|
||||
@@ -134,7 +134,7 @@ func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
||||
// category, which terminates, and stays renderable by path.
|
||||
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
||||
f := newFejkdata(t, dir, WithSeed(1))
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
if got := fake(t, f, "cat"); got != "hi" {
|
||||
t.Fatalf("cat = %q, want hi", got)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type rng interface {
|
||||
// names and the category (JSON file) come first, then named fields within it,
|
||||
// e.g. "sv_SE.address" or "sv_SE.address.street". Choices along the way are
|
||||
// resolved at random. A path naming a folder (no value of its own) is an error.
|
||||
func (f *Fejkdata) Fake(path string) (string, error) {
|
||||
func (f *Generator) Fake(path string) (string, error) {
|
||||
n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, "."))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||
|
||||
@@ -2,7 +2,7 @@ package fejkdata
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestSeededOutputIsStable pins the exact bytes a seeded faker emits for each
|
||||
// TestSeededOutputIsStable pins the exact bytes a seeded generator emits for each
|
||||
// piece of the token grammar, so a change to how a format is scanned or rendered
|
||||
// cannot quietly shift the rng stream that reproducibility depends on.
|
||||
func TestSeededOutputIsStable(t *testing.T) {
|
||||
@@ -31,7 +31,7 @@ func TestSeededOutputIsStable(t *testing.T) {
|
||||
"weights": {"big", "big", "big", "big"},
|
||||
}
|
||||
for path := range want {
|
||||
f := newFejkdata(t, dir, WithSeed(42))
|
||||
f := newGenerator(t, dir, WithSeed(42))
|
||||
for i, expect := range want[path] {
|
||||
if got := fake(t, f, path); got != expect {
|
||||
t.Errorf("%s draw %d = %q, want %q", path, i, got, expect)
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// engine builds a seeded faker with no loaded categories, for rendering tests.
|
||||
func engine(seed uint64) *Fejkdata { return &Fejkdata{rand: newRand(seed, true)} }
|
||||
// engine builds a seeded generator with no loaded categories, for rendering tests.
|
||||
func engine(seed uint64) *Generator { return &Generator{rand: newRand(seed, true)} }
|
||||
|
||||
// parse unmarshals a JSON template fragment into its dynamic form.
|
||||
func parse(t *testing.T, s string) any {
|
||||
@@ -30,7 +30,7 @@ func compiled(t *testing.T, s string) node {
|
||||
return n
|
||||
}
|
||||
|
||||
func mustRender(t *testing.T, f *Fejkdata, s string) string {
|
||||
func mustRender(t *testing.T, f *Generator, s string) string {
|
||||
t.Helper()
|
||||
return render(f.rand, compiled(t, s))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user