Rename the exported type to Generator and retire the faker wording #2

Merged
lilleman merged 3 commits from generator-rename into main 2026-09-01 21:46:31 +02:00
14 changed files with 85 additions and 85 deletions
Showing only changes of commit 04b7b6a432 - Show all commits
+13 -13
View File
@@ -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
- **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 faker.
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
**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
@@ -290,7 +290,7 @@ fat-fingered `hex(2000000000)` can't try to allocate gigabytes at render.
| `{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 |
| `{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
@@ -299,8 +299,8 @@ 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
View File
@@ -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++ {
+2 -2
View File
@@ -19,7 +19,7 @@ const (
// 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
// 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.
@@ -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 {
+2 -2
View File
@@ -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++ {
+5 -5
View File
@@ -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")
+11 -11
View File
@@ -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)
+8 -8
View File
@@ -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
View File
@@ -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]) {
+8 -8
View File
@@ -5,14 +5,14 @@ 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 +22,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 +39,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 +48,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
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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))
}