Files
fejkdata/builtins_test.go
T
M 39d59aec23 Pin the prep-time guards, and say why an operand's set is the narrow one
The guards report an invariant break instead of computing around it, and no
data reaches them, so they are called directly: coverage 97.4% -> 97.7%,
which also covers the empty-cover skip through a literal operand.

operandDraw's doc keeps why neither wider set works, since both were tried
here — containment reaches a sibling the operand never renders, the render
closure reaches a source two operands share — and drops the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00

220 lines
7.5 KiB
Go

package fakes
import (
"encoding/base64"
"regexp"
"strconv"
"testing"
)
func TestBuiltinIDGenerators(t *testing.T) {
cases := []struct {
name string
tmpl string
re *regexp.Regexp
}{
{"uuid v7", `{"format":"{uuid()}"}`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
{"ulid", `{"format":"{ulid()}"}`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
{"nanoid", `{"format":"{nanoid(21)}"}`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
{"hex", `{"format":"{hex(16)}"}`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
}
f := engine(1)
for _, c := range cases {
seen := map[string]bool{}
for i := 0; i < 200; i++ {
got := mustRender(t, f, c.tmpl)
if !c.re.MatchString(got) {
t.Fatalf("%s = %q, want %s", c.name, got, c.re)
}
seen[got] = true
}
if len(seen) < 190 {
t.Fatalf("%s not varied: %d uniques in 200", c.name, len(seen))
}
}
}
// TestBuiltinGeneratorsReproducible pins the determinism guardrail: every
// generator draws only from the seeded rng, so same seed -> same value.
func TestBuiltinGeneratorsReproducible(t *testing.T) {
for _, tmpl := range []string{
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
`{"format":"{float(0,1,6)}"}`, `{"format":"{base64(12)}"}`, `{"format":"{iban(SE)}"}`,
} {
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
}
}
}
func TestBuiltinIntInclusiveRange(t *testing.T) {
f := engine(1)
lo, hi := false, false
for i := 0; i < 1000; i++ {
n, err := strconv.Atoi(mustRender(t, f, `{"format":"{int(3,7)}"}`))
if err != nil || n < 3 || n > 7 {
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
}
lo, hi = lo || n == 3, hi || n == 7
}
if !lo || !hi {
t.Fatalf("int(3,7) never hit a bound: lo=%v hi=%v (bounds must be inclusive)", lo, hi)
}
}
func TestBuiltinFloat(t *testing.T) {
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
for i := 0; i < 200; i++ {
if got := mustRender(t, f, `{"format":"{float(1,2,3)}"}`); !re.MatchString(got) {
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
}
}
}
func TestBuiltinBase64(t *testing.T) {
got := mustRender(t, engine(1), `{"format":"{base64(9)}"}`)
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
}
}
// TestBuiltinChecksums fixes the payload with escapes and compares to a
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
func TestBuiltinChecksums(t *testing.T) {
cases := map[string]string{
`{"format":"#1#2#3#4#5#6#7#8{mod11()}"}`: "123456785", // weights 2..7 from the right
`{"format":"#6{mod11()}"}`: "6X", // remainder 10 -> X
`{"format":"#4#0#0#6#3#8#1#3#3#3#9#3{ean()}"}`: "4006381333931", // EAN-13 (= ISBN-13) check digit
}
f := engine(1)
for tmpl, want := range cases {
if got := mustRender(t, f, tmpl); got != want {
t.Fatalf("%s = %q, want %q", tmpl, got, want)
}
}
}
// 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.
func TestBuiltinSeqPerSession(t *testing.T) {
f := engine(1)
for i := 1; i <= 5; i++ {
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) {
t.Fatalf("seq call %d = %q, want %d", i, got, i)
}
}
if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" {
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
}
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" {
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
}
// repeat drives one counter forward across its renders...
if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" {
t.Fatalf("repeat seq = %q, want 1,2,3", got)
}
// ...and a fresh session restarts from 1.
if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" {
t.Fatalf("new session seq = %q, want 1", got)
}
}
// TestBuiltinArgLimits pins the New-time guards that keep a fat-fingered or
// overflowing argument from panicking or OOM-ing at render. Each must be rejected
// at compile, not detonate when the template is drawn.
func TestBuiltinArgLimits(t *testing.T) {
bad := []string{
`{"format":"{int(-9223372036854775808,9223372036854775807)}"}`, // span overflows int -> IntN panic
`{"format":"{hex(2000000000)}"}`, // ~2 GB string
`{"format":"{nanoid(2000000000)}"}`,
`{"format":"{base64(2000000000)}"}`,
`{"format":"{float(-1e308,1e308,2)}"}`, // span is +Inf
`{"format":"{float(0,1,100000)}"}`, // decimals -> huge string
`{"format":"{calc(1+1,100000)}"}`, // calc decimals -> huge string
`{"format":"{x}","x":["1"],"repeat":2000000000}`, // repeat -> multi-GB join
}
for _, tmpl := range bad {
if _, err := compile(parse(t, tmpl)); err == nil {
t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl)
}
}
// Ordinary values still compile.
if _, err := compile(parse(t, `{"format":"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"}`)); err != nil {
t.Errorf("compile of normal args failed: %v", err)
}
}
func TestBuiltinIBAN(t *testing.T) {
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
f := engine(1)
for cc, n := range wantLen {
tmpl := `{"format":"{iban(` + cc + `)}"}`
for i := 0; i < 100; i++ {
got := mustRender(t, f, tmpl)
if got[:2] != cc || len(got) != n {
t.Fatalf("iban(%s) = %q, want %d chars prefixed %s", cc, got, n, cc)
}
if !ibanValid(got) {
t.Fatalf("iban(%s) = %q fails the mod-97 check", cc, got)
}
}
}
}
// ibanValid checks the IBAN mod-97 rule independently of the generator: move the
// first four chars to the end, map letters A-Z to 10-35, the number mod 97 == 1.
func ibanValid(s string) bool {
r := s[4:] + s[:4]
rem := 0
for i := 0; i < len(r); i++ {
switch c := r[i]; {
case c >= '0' && c <= '9':
rem = (rem*10 + int(c-'0')) % 97
case c >= 'A' && c <= 'Z':
rem = (rem*100 + int(c-'A') + 10) % 97
default:
return false
}
}
return rem == 1
}
// TestRegistryShapes pins the builtin contract compileOps relies on: every entry
// supplies prep, and args parsed at compile only behind a check.
func TestRegistryShapes(t *testing.T) {
for name, b := range builtins {
if b.prep == nil {
t.Errorf("builtin %q has no prep: compileOps would call a nil func", name)
}
if b.arity != 0 && b.check == nil {
t.Errorf("builtin %q parses args in prep with no check", name)
}
}
}
// TestArgGuardsPanic pins the guards that report a builtin arg its check should
// have rejected. No data reaches them — checkFunc runs a builtin's check before
// compileOps ever calls prep — so they are exercised directly.
func TestArgGuardsPanic(t *testing.T) {
for name, call := range map[string]func(){
"atoi on an unvalidated arg": func() { atoi("nope") },
"atof on an unvalidated arg": func() { atof("nope") },
} {
mustPanic(t, name, call)
}
}
// mustPanic fails unless call panics, which is what separates a reported
// invariant break from a silently wrong value.
func mustPanic(t *testing.T, name string, call func()) {
t.Helper()
defer func() {
if recover() == nil {
t.Errorf("%s: no panic, want the invariant reported", name)
}
}()
call()
}