Rename the project to fejkdata and record the fork source #1
@@ -16,12 +16,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
# `docker build` streams the context to the daemon. A compose bind-mount of
|
||||||
|
# `.` mounts an empty host dir instead, because the job is itself a container.
|
||||||
- name: Latest supported Go
|
- name: Latest supported Go
|
||||||
run: docker compose run --rm ci
|
run: docker build .
|
||||||
# Runs even when the step above failed, so a red build says whether the
|
# Runs even when the step above failed, so a red build says whether the
|
||||||
# failure is version-specific.
|
# failure is version-specific.
|
||||||
- name: Lowest supported Go
|
- name: Lowest supported Go
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
env:
|
run: docker build --build-arg GO_VERSION=1.22.12 .
|
||||||
GO_VERSION: 1.22.12
|
|
||||||
run: docker compose run --rm ci
|
|
||||||
|
|||||||
+8
-5
@@ -1,7 +1,7 @@
|
|||||||
# Reproducible test/build image. `docker build .` fails if vet or tests fail,
|
# The merge gate: `docker build .` fails if vet, formatting or tests fail, and CI
|
||||||
# so it doubles as CI. Day-to-day, prefer `docker compose run` (bind-mounts
|
# runs exactly this. Day-to-day, prefer `docker compose run` (bind-mounts source,
|
||||||
# source, no rebuilds). GO_VERSION defaults to the latest stable Go; override it
|
# no rebuilds). GO_VERSION defaults to the latest stable Go; override it to test
|
||||||
# to test the lowest supported version: docker build --build-arg GO_VERSION=1.22 .
|
# the lowest supported version: docker build --build-arg GO_VERSION=1.22.12 .
|
||||||
ARG GO_VERSION=1.26.4
|
ARG GO_VERSION=1.26.4
|
||||||
FROM golang:${GO_VERSION}
|
FROM golang:${GO_VERSION}
|
||||||
|
|
||||||
@@ -12,4 +12,7 @@ COPY go.mod ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go vet ./... && go test ./...
|
RUN go vet ./... && \
|
||||||
|
{ unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
||||||
|
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }; } && \
|
||||||
|
go test ./...
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# fakes
|
# fejkdata
|
||||||
|
|
||||||
|
Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes).
|
||||||
|
|
||||||
A Go library and CLI for generating fake data, built for
|
A Go library and CLI for generating fake data, built for
|
||||||
**internationalization**. It exists because the existing Go fake libraries
|
**internationalization**. It exists because the existing Go fake libraries
|
||||||
@@ -26,21 +28,21 @@ lacked the locale coverage and format control we needed.
|
|||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Install the `fakes` command, then give it one or more `-data-path` directories
|
Install the `fejkdata` command, then give it one or more `-data-path` directories
|
||||||
and a path — it prints one value to stdout. Each dot segment descends one level:
|
and a path — it prints one value to stdout. Each dot segment descends one level:
|
||||||
folders, then the category (a JSON file), then fields inside it.
|
folders, then the category (a JSON file), then fields inside it.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install github.com/Timewave-AB/fakes/cmd/fakes@latest
|
go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest
|
||||||
|
|
||||||
fakes -data-path ./data/sv_SE person # Sara Eriksson
|
fejkdata -data-path ./data/sv_SE person # Sara Eriksson
|
||||||
fakes -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category)
|
fejkdata -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category)
|
||||||
fakes -data-path ./data sv_SE.person # point at the tree; the folder is a segment
|
fejkdata -data-path ./data sv_SE.person # point at the tree; the folder is a segment
|
||||||
fakes -data-path ./data/sv_SE -data-path ./mydata word # layer dirs; the last wins a name clash
|
fejkdata -data-path ./data/sv_SE -data-path ./mydata word # layer dirs; the last wins a name clash
|
||||||
fakes -seed 42 -data-path ./data/sv_SE address
|
fejkdata -seed 42 -data-path ./data/sv_SE address
|
||||||
fakes -repeat 3 -data-path ./data/sv_SE person # three values, one per line
|
fejkdata -repeat 3 -data-path ./data/sv_SE person # three values, one per line
|
||||||
fakes -repeat 3 -separator ', ' -data-path ./data/sv_SE word # nät, barn, sol
|
fejkdata -repeat 3 -separator ', ' -data-path ./data/sv_SE word # nät, barn, sol
|
||||||
fakes -data-path ./data/sv_SE -list # every path this data offers
|
fejkdata -data-path ./data/sv_SE -list # every path this data offers
|
||||||
```
|
```
|
||||||
|
|
||||||
`-data-path` is repeatable (last wins a name clash) and the path comes last —
|
`-data-path` is repeatable (last wins a name clash) and the path comes last —
|
||||||
@@ -49,7 +51,7 @@ independent draw — joined by `-separator` (default a newline, so values land o
|
|||||||
per line). Not sure what a data set offers? `-list` prints every path you can ask
|
per line). Not sure what a data set offers? `-list` prints every path you can ask
|
||||||
for; `-version` prints the build version.
|
for; `-version` prints the build version.
|
||||||
|
|
||||||
Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit
|
Without installing, run it from a checkout with `go run ./cmd/fejkdata …`. Exit
|
||||||
codes: `0` success (including `-list`, `-version`, `-h`), `1` runtime error
|
codes: `0` success (including `-list`, `-version`, `-h`), `1` runtime error
|
||||||
(missing dir, unknown path), `2` misuse.
|
(missing dir, unknown path), `2` misuse.
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ the `),(` separator; the outer `V#ALUES(…)` wraps that into one valid row list
|
|||||||
letter token — see [Data format](#data-format).)
|
letter token — see [Data format](#data-format).)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fakes -seed 1 -data-path ./data/sv_SE sql
|
fejkdata -seed 1 -data-path ./data/sv_SE sql
|
||||||
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
|
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,13 +86,13 @@ Raise the template's `repeat` for more rows per statement; use the CLI's
|
|||||||
`-repeat` for more statements — together they build a whole seed file:
|
`-repeat` for more statements — together they build a whole seed file:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fakes -repeat 100 -data-path ./data/sv_SE sql > seed.sql
|
fejkdata -repeat 100 -data-path ./data/sv_SE sql > seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Library
|
## Library
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get github.com/Timewave-AB/fakes # requires Go 1.22+ (for math/rand/v2)
|
go get gitea.larvit.se/larvit/fejkdata # requires Go 1.22+ (for math/rand/v2)
|
||||||
```
|
```
|
||||||
|
|
||||||
Point `New` at one or more data directories, then generate values by path with
|
Point `New` at one or more data directories, then generate values by path with
|
||||||
@@ -104,11 +106,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/Timewave-AB/fakes"
|
"gitea.larvit.se/larvit/fejkdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
f, err := fakes.New([]string{"./data/sv_SE"})
|
f, err := fejkdata.New([]string{"./data/sv_SE"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -135,8 +137,8 @@ Seed a faker for reproducible output — same seed + locale yields an identical
|
|||||||
sequence, handy for stable tests:
|
sequence, handy for stable tests:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
a, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
|
a, _ := fejkdata.New([]string{"./data/sv_SE"}, fejkdata.WithSeed(42))
|
||||||
b, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
|
b, _ := fejkdata.New([]string{"./data/sv_SE"}, fejkdata.WithSeed(42))
|
||||||
av, _ := a.Fake("person")
|
av, _ := a.Fake("person")
|
||||||
bv, _ := b.Fake("person")
|
bv, _ := b.Fake("person")
|
||||||
av == bv // true
|
av == bv // true
|
||||||
@@ -146,7 +148,7 @@ av == bv // true
|
|||||||
dotted fields and folder segments (what the CLI's `-list` prints). Every path it
|
dotted fields and folder segments (what the CLI's `-list` prints). Every path it
|
||||||
lists renders.
|
lists renders.
|
||||||
|
|
||||||
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
A `*Fejkdata` is **not** safe for concurrent use — create one per goroutine.
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
@@ -167,7 +169,7 @@ by their children, and any other clash is won by the last directory loaded. That
|
|||||||
lets you layer your own data over the built-ins without copying them:
|
lets you layer your own data over the built-ins without copying them:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
fakes.New([]string{"./data/sv_SE", "./mydata"}) // mydata overrides on a clash
|
fejkdata.New([]string{"./data/sv_SE", "./mydata"}) // mydata overrides on a clash
|
||||||
```
|
```
|
||||||
|
|
||||||
Each shipped locale carries these categories, formatted per locale (e.g. `date`
|
Each shipped locale carries these categories, formatted per locale (e.g. `date`
|
||||||
@@ -480,7 +482,6 @@ Source is bind-mounted; build caches persist in the `gocache` volume.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose run --rm test # run tests
|
docker compose run --rm test # run tests
|
||||||
docker compose run --rm ci # vet + format check + tests
|
|
||||||
docker compose run --rm cover # tests with coverage
|
docker compose run --rm cover # tests with coverage
|
||||||
docker compose run --rm bench # benchmarks
|
docker compose run --rm bench # benchmarks
|
||||||
docker compose run --rm build # compile the library
|
docker compose run --rm build # compile the library
|
||||||
@@ -495,9 +496,14 @@ docker compose run --rm --user "$(id -u):$(id -g)" fmt # gofmt -w .
|
|||||||
docker compose run --rm --user "$(id -u):$(id -g)" tidy # go mod tidy
|
docker compose run --rm --user "$(id -u):$(id -g)" tidy # go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
Every pull request runs `docker compose run --rm ci` against both the latest and
|
Every pull request runs `docker build .` against both the latest and the lowest
|
||||||
the lowest supported Go, and must pass before it can be merged. `docker build .`
|
supported Go, and must pass before it can be merged. That build is the whole
|
||||||
runs the same vet and tests locally.
|
gate — vet, format check and tests — so run it locally before pushing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build . # latest
|
||||||
|
docker build --build-arg GO_VERSION=1.22.12 . # lowest supported
|
||||||
|
```
|
||||||
|
|
||||||
Tests run against the latest Go by default. Set `GO_VERSION` to check the lowest
|
Tests run against the latest Go by default. Set `GO_VERSION` to check the lowest
|
||||||
supported version too:
|
supported version too:
|
||||||
@@ -510,7 +516,7 @@ docker compose run --rm test # latest
|
|||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
fakes.go Fakes, New, List, options, seeding
|
fejkdata.go Fejkdata, New, List, options, seeding
|
||||||
node.go the node model and JSON -> node compilation
|
node.go the node model and JSON -> node compilation
|
||||||
render.go Fake and the recursive renderer (choices, format strings, paths, bound draws)
|
render.go Fake and the recursive renderer (choices, format strings, paths, bound draws)
|
||||||
template.go the {token} grammar: scanning, function and path tokens, validation
|
template.go the {token} grammar: scanning, function and path tokens, validation
|
||||||
@@ -518,7 +524,7 @@ reference.go {..path} binding across the tree, the render graph, and the walk
|
|||||||
builtins.go the {name()} function registry and its implementations
|
builtins.go the {name()} function registry and its implementations
|
||||||
calc.go the {calc()} arithmetic evaluator: parser, eval, validation
|
calc.go the {calc()} arithmetic evaluator: parser, eval, validation
|
||||||
data.go data loading: folders/files -> namespace tree, multi-path merge
|
data.go data loading: folders/files -> namespace tree, multi-path merge
|
||||||
cmd/fakes/ the `fakes` CLI (New + Fake/List over stdout)
|
cmd/fejkdata/ the `fejkdata` CLI (New + Fake/List over stdout)
|
||||||
data/ shipped data (JSON): locale folders + a misc folder
|
data/ shipped data (JSON): locale folders + a misc folder
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func benchFaker(b *testing.B, dir string) *Fakes {
|
func benchFaker(b *testing.B, dir string) *Fejkdata {
|
||||||
b.Helper()
|
b.Helper()
|
||||||
f, err := New([]string{dir}, WithSeed(1))
|
f, err := New([]string{dir}, WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -97,7 +97,7 @@ const hexDigits = "0123456789abcdef"
|
|||||||
func atoi(s string) int {
|
func atoi(s string) int {
|
||||||
n, err := strconv.Atoi(s)
|
n, err := strconv.Atoi(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("fakes: builtin arg %q reached prep unvalidated: %v", s, err))
|
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", s, err))
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ func atoi(s string) int {
|
|||||||
func atof(s string) float64 {
|
func atof(s string) float64 {
|
||||||
f, err := strconv.ParseFloat(s, 64)
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("fakes: builtin arg %q reached prep unvalidated: %v", s, err))
|
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", s, err))
|
||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -48,7 +48,7 @@ func (n calcIdx) eval(operands []string) float64 {
|
|||||||
// than returning NaN, so a node kind indexVars forgets is a stack trace and not a
|
// than returning NaN, so a node kind indexVars forgets is a stack trace and not a
|
||||||
// silently wrong number.
|
// silently wrong number.
|
||||||
func (n calcVar) eval([]string) float64 {
|
func (n calcVar) eval([]string) float64 {
|
||||||
panic(fmt.Sprintf("fakes: calc operand %q was never placed", string(n)))
|
panic(fmt.Sprintf("fejkdata: calc operand %q was never placed", string(n)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
||||||
@@ -98,7 +98,7 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
func calcPrep(args []string) callFn {
|
func calcPrep(args []string) callFn {
|
||||||
expr, err := parseCalc(args[0])
|
expr, err := parseCalc(args[0])
|
||||||
if err != nil { // a nil AST would be a nil dereference per render, with no message
|
if err != nil { // a nil AST would be a nil dereference per render, with no message
|
||||||
panic(fmt.Sprintf("fakes: calc(%q) reached prep unparsed: %v", args[0], err))
|
panic(fmt.Sprintf("fejkdata: calc(%q) reached prep unparsed: %v", args[0], err))
|
||||||
}
|
}
|
||||||
at := make(map[string]int)
|
at := make(map[string]int)
|
||||||
for i, name := range calcVars(expr) {
|
for i, name := range calcVars(expr) {
|
||||||
@@ -121,7 +121,7 @@ func indexVars(n calcNode, at map[string]int) calcNode {
|
|||||||
case calcVar:
|
case calcVar:
|
||||||
i, placed := at[string(n)]
|
i, placed := at[string(n)]
|
||||||
if !placed { // calcVars named every operand, so a miss means the two disagree
|
if !placed { // calcVars named every operand, so a miss means the two disagree
|
||||||
panic(fmt.Sprintf("fakes: calc operand %q is not among the names read for it", string(n)))
|
panic(fmt.Sprintf("fejkdata: calc operand %q is not among the names read for it", string(n)))
|
||||||
}
|
}
|
||||||
return calcIdx(i)
|
return calcIdx(i)
|
||||||
case calcNeg:
|
case calcNeg:
|
||||||
|
|||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -126,7 +126,7 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
|||||||
dir := writeData(t, map[string]string{
|
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"]}`,
|
"inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(3))
|
f := newFejkdata(t, dir, WithSeed(3))
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
got := fake(t, f, "inv")
|
got := fake(t, f, "inv")
|
||||||
var net, qty, want float64
|
var net, qty, want float64
|
||||||
@@ -145,7 +145,7 @@ func TestCalcOperandSharesOneDraw(t *testing.T) {
|
|||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(5))
|
f := newFejkdata(t, dir, WithSeed(5))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
got := fake(t, f, "same")
|
got := fake(t, f, "same")
|
||||||
if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] {
|
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.
|
// held, so an ordinary {w} {w} still draws twice.
|
||||||
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
|
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
|
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
|
||||||
f := newFakes(t, dir, WithSeed(5))
|
f := newFejkdata(t, dir, WithSeed(5))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
|
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
|
||||||
return
|
return
|
||||||
@@ -173,7 +173,7 @@ func TestCalcHoldIsPerExpansion(t *testing.T) {
|
|||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(11))
|
f := newFejkdata(t, dir, WithSeed(11))
|
||||||
varied := false
|
varied := false
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
got := fake(t, f, "rep")
|
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"],
|
"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"]}}`,
|
"inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(13))
|
f := newFejkdata(t, dir, WithSeed(13))
|
||||||
differed := false
|
differed := false
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
got := fake(t, f, "nest")
|
got := fake(t, f, "nest")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Command fakes prints one fake value from one or more data directories.
|
// Command fejkdata prints one fake value from one or more data directories.
|
||||||
//
|
//
|
||||||
// fakes -data-path ./data/sv_SE person # a full person
|
// fejkdata -data-path ./data/sv_SE person # a full person
|
||||||
// fakes -data-path ./data/sv_SE person.last # just the surname (dotted path)
|
// fejkdata -data-path ./data/sv_SE person.last # just the surname (dotted path)
|
||||||
// fakes -data-path ./data sv_SE.person # point at the tree, address by folder
|
// fejkdata -data-path ./data sv_SE.person # point at the tree, address by folder
|
||||||
// fakes -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins
|
// fejkdata -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins
|
||||||
// fakes -seed 42 -data-path ./data/sv_SE address
|
// fejkdata -seed 42 -data-path ./data/sv_SE address
|
||||||
//
|
//
|
||||||
// It is a thin CLI over the fakes library: New(dirs) then Fake(path).
|
// It is a thin CLI over the fejkdata library: New(dirs) then Fake(path).
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -18,10 +18,10 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Timewave-AB/fakes"
|
"gitea.larvit.se/larvit/fejkdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usage = `Usage: fakes -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path>
|
const usage = `Usage: fejkdata -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path>
|
||||||
|
|
||||||
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
|
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
|
||||||
<path> a category, or a dotted path into one (person, person.last)
|
<path> a category, or a dotted path into one (person, person.last)
|
||||||
@@ -45,7 +45,7 @@ func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
|||||||
// run is main's testable core: it returns the process exit code (0 ok, 1
|
// run is main's testable core: it returns the process exit code (0 ok, 1
|
||||||
// runtime error, 2 misuse) and writes only to the given streams.
|
// runtime error, 2 misuse) and writes only to the given streams.
|
||||||
func run(args []string, stdout, stderr io.Writer) int {
|
func run(args []string, stdout, stderr io.Writer) int {
|
||||||
fs := flag.NewFlagSet("fakes", flag.ContinueOnError)
|
fs := flag.NewFlagSet("fejkdata", flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
fs.Usage = func() { fmt.Fprintln(stderr, usage) }
|
fs.Usage = func() { fmt.Fprintln(stderr, usage) }
|
||||||
seed := fs.Uint64("seed", 0, "seed for reproducible output")
|
seed := fs.Uint64("seed", 0, "seed for reproducible output")
|
||||||
@@ -62,7 +62,7 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Fprintln(stdout, "fakes "+buildVersion())
|
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if len(dirs) == 0 {
|
if len(dirs) == 0 {
|
||||||
@@ -70,15 +70,15 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts []fakes.Option
|
var opts []fejkdata.Option
|
||||||
fs.Visit(func(fl *flag.Flag) {
|
fs.Visit(func(fl *flag.Flag) {
|
||||||
if fl.Name == "seed" {
|
if fl.Name == "seed" {
|
||||||
opts = append(opts, fakes.WithSeed(*seed))
|
opts = append(opts, fejkdata.WithSeed(*seed))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if *list {
|
if *list {
|
||||||
f, err := fakes.New(dirs, opts...)
|
f, err := fejkdata.New(dirs, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -98,7 +98,7 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
f, err := fakes.New(dirs, opts...)
|
f, err := fejkdata.New(dirs, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -176,8 +176,9 @@ func TestRunVersion(t *testing.T) {
|
|||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("-version = %d, stderr=%q", code, errb)
|
t.Fatalf("-version = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(out) == "" {
|
version, ok := strings.CutPrefix(out, "fejkdata ")
|
||||||
t.Error("-version: want a version on stdout")
|
if !ok || strings.TrimSpace(version) == "" {
|
||||||
|
t.Errorf("-version = %q, want the command name and a version on stdout", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+3
-10
@@ -1,10 +1,12 @@
|
|||||||
# Docker-based dev workflow — no local tooling beyond Docker is required.
|
# Docker-based dev workflow — no local tooling beyond Docker is required.
|
||||||
#
|
#
|
||||||
# docker compose run --rm test
|
# docker compose run --rm test
|
||||||
# docker compose run --rm ci
|
|
||||||
# docker compose run --rm vet
|
# docker compose run --rm vet
|
||||||
# docker compose run --rm build
|
# docker compose run --rm build
|
||||||
#
|
#
|
||||||
|
# The full gate — vet, formatting and tests — is `docker build .`, which is what
|
||||||
|
# CI runs.
|
||||||
|
#
|
||||||
# Build caches persist in the `gocache` volume, so re-runs are fast and the
|
# Build caches persist in the `gocache` volume, so re-runs are fast and the
|
||||||
# host tree stays clean. Commands that rewrite source (fmt, tidy) keep your
|
# host tree stays clean. Commands that rewrite source (fmt, tidy) keep your
|
||||||
# ownership when run with `--user "$(id -u):$(id -g)"`.
|
# ownership when run with `--user "$(id -u):$(id -g)"`.
|
||||||
@@ -52,15 +54,6 @@ services:
|
|||||||
<<: *go
|
<<: *go
|
||||||
command: go mod tidy
|
command: go mod tidy
|
||||||
|
|
||||||
# vet + format check + tests, in one container.
|
|
||||||
ci:
|
|
||||||
<<: *go
|
|
||||||
command: >
|
|
||||||
sh -c 'go vet ./... &&
|
|
||||||
{ unformatted=$$(gofmt -l .); test -z "$$unformatted" ||
|
|
||||||
{ echo "unformatted files:"; echo "$$unformatted"; exit 1; }; } &&
|
|
||||||
go test ./...'
|
|
||||||
|
|
||||||
# Interactive shell for ad-hoc work: docker compose run --rm dev
|
# Interactive shell for ad-hoc work: docker compose run --rm dev
|
||||||
dev:
|
dev:
|
||||||
<<: *go
|
<<: *go
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
+12
-12
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -51,8 +51,8 @@ func TestShippedDataCategories(t *testing.T) {
|
|||||||
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
|
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
|
||||||
}
|
}
|
||||||
|
|
||||||
en := newFakes(t, "data/en_US", WithSeed(1))
|
en := newFejkdata(t, "data/en_US", WithSeed(1))
|
||||||
sv := newFakes(t, "data/sv_SE", WithSeed(1))
|
sv := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if v := fake(t, en, c.path); !c.en.MatchString(v) {
|
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
|
// v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
|
||||||
// whose trailing {luhn()} check passes.
|
// whose trailing {luhn()} check passes.
|
||||||
func TestShippedMiscCategories(t *testing.T) {
|
func TestShippedMiscCategories(t *testing.T) {
|
||||||
f := newFakes(t, "data/misc", WithSeed(1))
|
f := newFejkdata(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}$`)
|
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}$`)
|
mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
||||||
objectid := regexp.MustCompile(`^[0-9a-f]{24}$`)
|
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
|
// codes follow their standard shape, dotted sub-paths resolve, emoji is a real
|
||||||
// glyph, and a coordinate parses to a point within geographic bounds.
|
// glyph, and a coordinate parses to a point within geographic bounds.
|
||||||
func TestShippedMiscReferenceData(t *testing.T) {
|
func TestShippedMiscReferenceData(t *testing.T) {
|
||||||
f := newFakes(t, "data/misc", WithSeed(1))
|
f := newFejkdata(t, "data/misc", WithSeed(1))
|
||||||
re := map[string]*regexp.Regexp{
|
re := map[string]*regexp.Regexp{
|
||||||
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
|
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
|
||||||
"currency.name": regexp.MustCompile(`\p{L}`),
|
"currency.name": regexp.MustCompile(`\p{L}`),
|
||||||
@@ -134,7 +134,7 @@ func TestShippedMiscReferenceData(t *testing.T) {
|
|||||||
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
|
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
|
||||||
// regexes miss: no generated name repeats a character three times over.
|
// regexes miss: no generated name repeats a character three times over.
|
||||||
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(11))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(11))
|
||||||
for _, path := range []string{"person", "person.last"} {
|
for _, path := range []string{"person", "person.last"} {
|
||||||
for i := 0; i < 20000; i++ {
|
for i := 0; i < 20000; i++ {
|
||||||
name := fake(t, f, path)
|
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
|
// 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.
|
// Feb 30) and the trailing digit is a valid Luhn checksum over the other nine.
|
||||||
func TestSwedishPersonnummer(t *testing.T) {
|
func TestSwedishPersonnummer(t *testing.T) {
|
||||||
sv := newFakes(t, "data/sv_SE", WithSeed(1))
|
sv := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||||
sawLongMonthEnd := false
|
sawLongMonthEnd := false
|
||||||
for i := 0; i < 2000; i++ {
|
for i := 0; i < 2000; i++ {
|
||||||
v := fake(t, sv, "ssn")
|
v := fake(t, sv, "ssn")
|
||||||
@@ -176,7 +176,7 @@ func TestSwedishPersonnummer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShippedSwedishPhone(t *testing.T) {
|
func TestShippedSwedishPhone(t *testing.T) {
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(11))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(11))
|
||||||
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
|
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
if n := fake(t, f, "phone"); !re.MatchString(n) {
|
if n := fake(t, f, "phone"); !re.MatchString(n) {
|
||||||
@@ -186,7 +186,7 @@ func TestShippedSwedishPhone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShippedSwedishAddress(t *testing.T) {
|
func TestShippedSwedishAddress(t *testing.T) {
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(3))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(3))
|
||||||
digit := regexp.MustCompile(`\d`)
|
digit := regexp.MustCompile(`\d`)
|
||||||
for i := 0; i < 30; i++ {
|
for i := 0; i < 30; i++ {
|
||||||
a := fake(t, f, "address")
|
a := fake(t, f, "address")
|
||||||
@@ -205,7 +205,7 @@ func TestShippedSwedishAddress(t *testing.T) {
|
|||||||
|
|
||||||
func TestShippedPersonHasParts(t *testing.T) {
|
func TestShippedPersonHasParts(t *testing.T) {
|
||||||
for _, dir := range []string{"data/sv_SE", "data/en_US"} {
|
for _, dir := range []string{"data/sv_SE", "data/en_US"} {
|
||||||
f := newFakes(t, dir, WithSeed(7))
|
f := newFejkdata(t, dir, WithSeed(7))
|
||||||
for i := 0; i < 30; i++ {
|
for i := 0; i < 30; i++ {
|
||||||
if name := fake(t, f, "person"); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
|
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)
|
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) {
|
func TestShippedUSPhone(t *testing.T) {
|
||||||
f := newFakes(t, "data/en_US", WithSeed(11))
|
f := newFejkdata(t, "data/en_US", WithSeed(11))
|
||||||
re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`)
|
re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`)
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
if n := fake(t, f, "phone"); !re.MatchString(n) {
|
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
|
// 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.
|
// reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person.
|
||||||
func TestShippedNamespacedTree(t *testing.T) {
|
func TestShippedNamespacedTree(t *testing.T) {
|
||||||
f := newFakes(t, "data", WithSeed(1))
|
f := newFejkdata(t, "data", WithSeed(1))
|
||||||
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
|
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
|
||||||
if got := fake(t, f, path); got == "" {
|
if got := fake(t, f, path); got == "" {
|
||||||
t.Fatalf("Fake(%q) returned empty", path)
|
t.Fatalf("Fake(%q) returned empty", path)
|
||||||
|
|||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -240,7 +240,7 @@ func TestPathThroughChoice(t *testing.T) {
|
|||||||
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
||||||
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if got := fake(t, f, "every.f"); got != "1" && got != "2" {
|
if got := fake(t, f, "every.f"); got != "1" && got != "2" {
|
||||||
t.Fatalf("every.f = %q, want 1 or 2", got)
|
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)
|
t.Fatalf("New = %v, want the dotted field name rejected", err)
|
||||||
}
|
}
|
||||||
// The same data without the dotted key is fine, and the path resolves.
|
// The same data without the dotted key is fine, and the path resolves.
|
||||||
f := newFakes(t, writeData(t, map[string]string{
|
f := newFejkdata(t, writeData(t, map[string]string{
|
||||||
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
|
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
|
||||||
}), WithSeed(1))
|
}), WithSeed(1))
|
||||||
if !slices.Contains(f.List(), "cat.a.b") {
|
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
|
// 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.
|
// guard and the error can name the field that is missing.
|
||||||
func TestMissingFieldNamesItself(t *testing.T) {
|
func TestMissingFieldNamesItself(t *testing.T) {
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(1))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(1))
|
||||||
_, err := f.Fake("person.typo")
|
_, err := f.Fake("person.typo")
|
||||||
if err == nil || !strings.Contains(err.Error(), `no field "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)
|
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
|
"obj": `{"format":"00"}`, // object root
|
||||||
"lit": `"hello"`, // bare-string root
|
"lit": `"hello"`, // bare-string root
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
|
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
|
||||||
t.Errorf("object-root category = %q, want two digits", got)
|
t.Errorf("object-root category = %q, want two digits", got)
|
||||||
}
|
}
|
||||||
@@ -327,7 +327,7 @@ func TestLongStringList(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
f := newFakes(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1))
|
f := newFejkdata(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1))
|
||||||
|
|
||||||
valid := map[string]bool{}
|
valid := map[string]bool{}
|
||||||
for _, v := range names {
|
for _, v := range names {
|
||||||
@@ -355,7 +355,7 @@ var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
|
|||||||
|
|
||||||
func TestShippedStreetComposition(t *testing.T) {
|
func TestShippedStreetComposition(t *testing.T) {
|
||||||
// street is a choice of composed {first}{last} templates and literal names.
|
// street is a choice of composed {first}{last} templates and literal names.
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(5))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(5))
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if s := fake(t, f, "address.street"); !swedishName.MatchString(s) {
|
if s := fake(t, f, "address.street"); !swedishName.MatchString(s) {
|
||||||
t.Fatalf("street %q is not a Swedish street name", 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) {
|
func TestShippedLastNameComposition(t *testing.T) {
|
||||||
// last is a choice of patronymic {first}sson templates, compound
|
// last is a choice of patronymic {first}sson templates, compound
|
||||||
// {first}{last} templates and literal surnames.
|
// {first}{last} templates and literal surnames.
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(6))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(6))
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if s := fake(t, f, "person.last"); !swedishName.MatchString(s) {
|
if s := fake(t, f, "person.last"); !swedishName.MatchString(s) {
|
||||||
t.Fatalf("last name %q is not a Swedish surname", 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) {
|
func TestShippedStreetNumberFormats(t *testing.T) {
|
||||||
// Reachable via a hyphenated path; covers all five weighted number variants.
|
// Reachable via a hyphenated path; covers all five weighted number variants.
|
||||||
f := newFakes(t, "data/sv_SE", WithSeed(8))
|
f := newFejkdata(t, "data/sv_SE", WithSeed(8))
|
||||||
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
|
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if n := fake(t, f, "address.street-number"); !re.MatchString(n) {
|
if n := fake(t, f, "address.street-number"); !re.MatchString(n) {
|
||||||
|
|||||||
+12
-12
@@ -1,10 +1,10 @@
|
|||||||
// Package fakes generates fake data from recursive JSON templates.
|
// Package fejkdata generates fake data from recursive JSON templates.
|
||||||
//
|
//
|
||||||
// Data lives in JSON on disk, not in Go. Point [New] at one or more data
|
// Data lives in JSON on disk, not in Go. Point [New] at one or more data
|
||||||
// directories; folders and files become a dot-path namespace, then generate
|
// directories; folders and files become a dot-path namespace, then generate
|
||||||
// values by path:
|
// values by path:
|
||||||
//
|
//
|
||||||
// f, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
|
// f, _ := fejkdata.New([]string{"./data/sv_SE"}, fejkdata.WithSeed(42))
|
||||||
// f.Fake("address") // "Storgatan 12\n234 56 Göteborg"
|
// f.Fake("address") // "Storgatan 12\n234 56 Göteborg"
|
||||||
// f.Fake("address.locality") // "Göteborg"
|
// f.Fake("address.locality") // "Göteborg"
|
||||||
//
|
//
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
// the last one wins on a name clash, so you can layer custom data over the
|
// 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.
|
// built-ins. The JSON template format is documented in the README.
|
||||||
//
|
//
|
||||||
// A [Fakes] is not safe for concurrent use; create one per goroutine.
|
// A [Fejkdata] is not safe for concurrent use; create one per goroutine.
|
||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
@@ -24,14 +24,14 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
// Fejkdata generates fake data from a loaded namespace tree. Create one with [New].
|
||||||
type Fakes struct {
|
type Fejkdata struct {
|
||||||
rand *session
|
rand *session
|
||||||
categories map[string]node // root namespace: name -> compiled node tree
|
categories map[string]node // root namespace: name -> compiled node tree
|
||||||
}
|
}
|
||||||
|
|
||||||
// session is one faker's mutable render state: the seeded rng plus the {seq()}
|
// session is one faker's mutable render state: the seeded rng plus the {seq()}
|
||||||
// counters. Scoping it to the Fakes means sequences (and randomness) belong to
|
// 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
|
// that faker and reset when you create a new one. Embedding *rand.Rand makes a
|
||||||
// *session satisfy the rng interface the renderer draws from.
|
// *session satisfy the rng interface the renderer draws from.
|
||||||
type session struct {
|
type session struct {
|
||||||
@@ -50,7 +50,7 @@ type config struct {
|
|||||||
seeded bool
|
seeded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option configures a [Fakes].
|
// Option configures a [Fejkdata].
|
||||||
type Option func(*config)
|
type Option func(*config)
|
||||||
|
|
||||||
// WithSeed makes output reproducible: two fakers with the same seed and locale
|
// WithSeed makes output reproducible: two fakers with the same seed and locale
|
||||||
@@ -64,16 +64,16 @@ func WithSeed(seed uint64) Option {
|
|||||||
// "address") and each subdirectory a namespace segment; directories are merged
|
// "address") and each subdirectory a namespace segment; directories are merged
|
||||||
// in order, the last winning a name clash. It errors on a missing directory,
|
// in order, the last winning a name clash. It errors on a missing directory,
|
||||||
// invalid JSON, or no data found.
|
// invalid JSON, or no data found.
|
||||||
func New(paths []string, opts ...Option) (*Fakes, error) {
|
func New(paths []string, opts ...Option) (*Fejkdata, error) {
|
||||||
cats, err := loadData(paths)
|
cats, err := loadData(paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fakes: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
var c config
|
var c config
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&c)
|
opt(&c)
|
||||||
}
|
}
|
||||||
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
return &Fejkdata{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
||||||
@@ -81,7 +81,7 @@ func New(paths []string, opts ...Option) (*Fakes, error) {
|
|||||||
// single-variant choices the way a reference does. A choice consumes no segment, so
|
// 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,
|
// 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.
|
// which is the rule Fake applies too: List is the set of paths Fake accepts.
|
||||||
func (f *Fakes) List() []string {
|
func (f *Fejkdata) List() []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, name := range sortedNames(f.categories) {
|
for _, name := range sortedNames(f.categories) {
|
||||||
for _, p := range paths(f.categories[name]) {
|
for _, p := range paths(f.categories[name]) {
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newFakes creates a faker over a single data directory, failing on error. Most
|
// newFejkdata creates a faker over a single data directory, failing on error. Most
|
||||||
// tests load one dir; newFakesN loads several (last-loaded wins on conflicts).
|
// tests load one dir; newFejkdataN loads several (last-loaded wins on conflicts).
|
||||||
func newFakes(t *testing.T, dir string, opts ...Option) *Fakes {
|
func newFejkdata(t *testing.T, dir string, opts ...Option) *Fejkdata {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return newFakesN(t, []string{dir}, opts...)
|
return newFejkdataN(t, []string{dir}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFakesN(t *testing.T, dirs []string, opts ...Option) *Fakes {
|
func newFejkdataN(t *testing.T, dirs []string, opts ...Option) *Fejkdata {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
f, err := New(dirs, opts...)
|
f, err := New(dirs, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -22,7 +22,7 @@ func newFakesN(t *testing.T, dirs []string, opts ...Option) *Fakes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fake generates a value, failing the test on error.
|
// fake generates a value, failing the test on error.
|
||||||
func fake(t *testing.T, f *Fakes, path string) string {
|
func fake(t *testing.T, f *Fejkdata, path string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
s, err := f.Fake(path)
|
s, err := f.Fake(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,7 +39,7 @@ func TestNewMissingDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWithSeedIsDeterministic(t *testing.T) {
|
func TestWithSeedIsDeterministic(t *testing.T) {
|
||||||
a, b := newFakes(t, "data/sv_SE", WithSeed(42)), newFakes(t, "data/sv_SE", WithSeed(42))
|
a, b := newFejkdata(t, "data/sv_SE", WithSeed(42)), newFejkdata(t, "data/sv_SE", WithSeed(42))
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
if x, y := fake(t, a, "person"), fake(t, b, "person"); x != y {
|
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)
|
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) {
|
func TestDifferentSeedsDiffer(t *testing.T) {
|
||||||
a, b := newFakes(t, "data/en_US", WithSeed(1)), newFakes(t, "data/en_US", WithSeed(2))
|
a, b := newFejkdata(t, "data/en_US", WithSeed(1)), newFejkdata(t, "data/en_US", WithSeed(2))
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
if fake(t, a, "person") != fake(t, b, "person") {
|
if fake(t, a, "person") != fake(t, b, "person") {
|
||||||
return
|
return
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
module github.com/Timewave-AB/fakes
|
module gitea.larvit.se/larvit/fejkdata
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -21,7 +21,7 @@ func TestList(t *testing.T) {
|
|||||||
// Only the fields every variant carries are addressable, so "extra" is not.
|
// 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"]}]`,
|
"coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`,
|
||||||
})
|
})
|
||||||
got := newFakes(t, dir, WithSeed(1)).List()
|
got := newFejkdata(t, dir, WithSeed(1)).List()
|
||||||
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("List() = %v, want %v", 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) {
|
func TestNewLoadsAnyDirName(t *testing.T) {
|
||||||
// No locale tag required: a directory named anything loads fine.
|
// No locale tag required: a directory named anything loads fine.
|
||||||
dir := writeData(t, map[string]string{"greeting": `["hej", "hallå"]`})
|
dir := writeData(t, map[string]string{"greeting": `["hej", "hallå"]`})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "greeting"); got != "hej" && got != "hallå" {
|
if got := fake(t, f, "greeting"); got != "hej" && got != "hallå" {
|
||||||
t.Fatalf("greeting = %q, want hej or hallå", got)
|
t.Fatalf("greeting = %q, want hej or hallå", got)
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestFoldersBecomeDotPaths(t *testing.T) {
|
|||||||
"sv_SE/greeting": `["hej"]`,
|
"sv_SE/greeting": `["hej"]`,
|
||||||
"en_US/greeting": `["hi"]`,
|
"en_US/greeting": `["hi"]`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
|
||||||
t.Fatalf("sv_SE.greeting = %q, want hej", got)
|
t.Fatalf("sv_SE.greeting = %q, want hej", got)
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
|
|||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
|
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
|
// 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" {
|
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)
|
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) {
|
func TestRenderingAFolderErrors(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
|
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if _, err := f.Fake("sv_SE"); err == nil {
|
if _, err := f.Fake("sv_SE"); err == nil {
|
||||||
t.Fatal("Fake(folder) = nil error, want a not-a-value error")
|
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) {
|
func TestMultiPathLastWins(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
|
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
|
||||||
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
|
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
|
||||||
f := newFakesN(t, []string{a, b}, WithSeed(1))
|
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "greeting"); got != "from-b" {
|
if got := fake(t, f, "greeting"); got != "from-b" {
|
||||||
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
|
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) {
|
func TestMultiPathMergesFolders(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`})
|
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"]`})
|
b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`})
|
||||||
f := newFakesN(t, []string{a, b}, WithSeed(1))
|
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.person"); got != "from-a" {
|
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)
|
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
|
// tree: everything List advertises renders, every time, and the sub-fields the
|
||||||
// README advertises are discoverable.
|
// README advertises are discoverable.
|
||||||
func TestListedPathsAllRender(t *testing.T) {
|
func TestListedPathsAllRender(t *testing.T) {
|
||||||
f := newFakes(t, "data", WithSeed(4))
|
f := newFejkdata(t, "data", WithSeed(4))
|
||||||
paths := f.List()
|
paths := f.List()
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
for i := 0; i < 20; i++ {
|
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 {
|
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "cat"); got != "V" {
|
if got := fake(t, f, "cat"); got != "V" {
|
||||||
t.Fatalf("cat = %q, want V", got)
|
t.Fatalf("cat = %q, want V", got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ func TestRootReferenceAcrossFolders(t *testing.T) {
|
|||||||
"en_US/person": `["Pat Smith"]`,
|
"en_US/person": `["Pat Smith"]`,
|
||||||
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
|
||||||
t.Fatalf("greeting = %q, want \"Hej, Pat Smith!\"", got)
|
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"]}]`,
|
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
|
||||||
"card": `{"format":"signed {..who.last}"}`,
|
"card": `{"format":"signed {..who.last}"}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "card"); got != "signed Byron" {
|
if got := fake(t, f, "card"); got != "signed Byron" {
|
||||||
t.Fatalf("card = %q, want \"signed Byron\"", got)
|
t.Fatalf("card = %q, want \"signed Byron\"", got)
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ func TestReferenceInAlternation(t *testing.T) {
|
|||||||
"far": `["X"]`,
|
"far": `["X"]`,
|
||||||
"near": `{"format":"{here|..far}","here":["H"]}`,
|
"near": `{"format":"{here|..far}","here":["H"]}`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(2))
|
f := newFejkdata(t, dir, WithSeed(2))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
seen[fake(t, f, "near")] = true
|
seen[fake(t, f, "near")] = true
|
||||||
@@ -50,7 +50,7 @@ func TestReferenceInAlternation(t *testing.T) {
|
|||||||
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
func TestReferenceCombinesLoadedPaths(t *testing.T) {
|
||||||
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
a := writeData(t, map[string]string{"en_US/word": `["river"]`})
|
||||||
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
|
||||||
f := newFakesN(t, []string{a, b}, WithSeed(1))
|
f := newFejkdataN(t, []string{a, b}, WithSeed(1))
|
||||||
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
if got := fake(t, f, "mine.slug"); got != "the-river" {
|
||||||
t.Fatalf("slug = %q, want the-river", got)
|
t.Fatalf("slug = %q, want the-river", got)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func TestReferenceChain(t *testing.T) {
|
|||||||
"b": `{"format":"{..c}"}`,
|
"b": `{"format":"{..c}"}`,
|
||||||
"c": `["deep"]`,
|
"c": `["deep"]`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "a"); got != "deep" {
|
if got := fake(t, f, "a"); got != "deep" {
|
||||||
t.Fatalf("a = %q, want deep", got)
|
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 ".."
|
// starting with the reference prefix is covered by that same rule, since ".."
|
||||||
// starts with "." — it is skipped, not rejected.
|
// starts with "." — it is skipped, not rejected.
|
||||||
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
|
||||||
f := newFakes(t, writeData(t, map[string]string{
|
f := newFejkdata(t, writeData(t, map[string]string{
|
||||||
"sv_SE/ok": `["fine"]`,
|
"sv_SE/ok": `["fine"]`,
|
||||||
"sv_SE/..bad": `{"format":"{..nope}"}`,
|
"sv_SE/..bad": `{"format":"{..nope}"}`,
|
||||||
"sv_SE/..y/ct": `{"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.
|
// category, which terminates, and stays renderable by path.
|
||||||
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFejkdata(t, dir, WithSeed(1))
|
||||||
if got := fake(t, f, "cat"); got != "hi" {
|
if got := fake(t, f, "cat"); got != "hi" {
|
||||||
t.Fatalf("cat = %q, want hi", got)
|
t.Fatalf("cat = %q, want hi", got)
|
||||||
}
|
}
|
||||||
@@ -189,12 +189,12 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"cycle inside a choice arm",
|
"cycle inside a choice arm",
|
||||||
map[string]string{"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`},
|
map[string]string{"cat": `[{"format":"hi","x":{"format":"{..cat.x}"}}]`},
|
||||||
"fakes: reference cycle: cat.x -> ..cat.x",
|
"fejkdata: reference cycle: cat.x -> ..cat.x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bad reference reached through another reference",
|
"bad reference reached through another reference",
|
||||||
map[string]string{"a": `{"format":"{..b}"}`, "b": `{"format":"{..nope}"}`},
|
map[string]string{"a": `{"format":"{..b}"}`, "b": `{"format":"{..nope}"}`},
|
||||||
`fakes: b: reference {..nope}: no entry "nope"`,
|
`fejkdata: b: reference {..nope}: no entry "nope"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,13 +17,13 @@ type rng interface {
|
|||||||
// names and the category (JSON file) come first, then named fields within it,
|
// 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
|
// 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.
|
// resolved at random. A path naming a folder (no value of its own) is an error.
|
||||||
func (f *Fakes) Fake(path string) (string, error) {
|
func (f *Fejkdata) Fake(path string) (string, error) {
|
||||||
n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, "."))
|
n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, "."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("fakes: %s: %w", path, err)
|
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||||
}
|
}
|
||||||
if _, ok := n.(*group); ok {
|
if _, ok := n.(*group); ok {
|
||||||
return "", fmt.Errorf("fakes: %s names a folder, not a value", path)
|
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
||||||
}
|
}
|
||||||
return render(f.rand, n), nil
|
return render(f.rand, n), nil
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func render(s *session, n node) string {
|
|||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("fakes: uncompiled node %T", n))
|
panic(fmt.Sprintf("fejkdata: uncompiled node %T", n))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +225,11 @@ func readField(s *session, t *template, held *draws, a arm) string {
|
|||||||
func child(n node, seg string) node {
|
func child(n node, seg string) node {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("fakes: %q under %T, which carries no fields", seg, n))
|
panic(fmt.Sprintf("fejkdata: %q under %T, which carries no fields", seg, n))
|
||||||
}
|
}
|
||||||
c, ok := t.fields[seg]
|
c, ok := t.fields[seg]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("fakes: no field %q under a drawn level", seg))
|
panic(fmt.Sprintf("fejkdata: no field %q under a drawn level", seg))
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ func TestSeededOutputIsStable(t *testing.T) {
|
|||||||
"weights": {"big", "big", "big", "big"},
|
"weights": {"big", "big", "big", "big"},
|
||||||
}
|
}
|
||||||
for path := range want {
|
for path := range want {
|
||||||
f := newFakes(t, dir, WithSeed(42))
|
f := newFejkdata(t, dir, WithSeed(42))
|
||||||
for i, expect := range want[path] {
|
for i, expect := range want[path] {
|
||||||
if got := fake(t, f, path); got != expect {
|
if got := fake(t, f, path); got != expect {
|
||||||
t.Errorf("%s draw %d = %q, want %q", path, i, got, expect)
|
t.Errorf("%s draw %d = %q, want %q", path, i, got, expect)
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package fakes
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// engine builds a seeded faker with no loaded categories, for rendering tests.
|
// engine builds a seeded faker with no loaded categories, for rendering tests.
|
||||||
func engine(seed uint64) *Fakes { return &Fakes{rand: newRand(seed, true)} }
|
func engine(seed uint64) *Fejkdata { return &Fejkdata{rand: newRand(seed, true)} }
|
||||||
|
|
||||||
// parse unmarshals a JSON template fragment into its dynamic form.
|
// parse unmarshals a JSON template fragment into its dynamic form.
|
||||||
func parse(t *testing.T, s string) any {
|
func parse(t *testing.T, s string) any {
|
||||||
@@ -30,7 +30,7 @@ func compiled(t *testing.T, s string) node {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustRender(t *testing.T, f *Fakes, s string) string {
|
func mustRender(t *testing.T, f *Fejkdata, s string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return render(f.rand, compiled(t, s))
|
return render(f.rand, compiled(t, s))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user