Struct-filling #14

Merged
lilleman merged 10 commits from struct-filling into main 2026-09-15 17:52:29 +02:00
2 changed files with 206 additions and 0 deletions
Showing only changes of commit d4ff3b242e - Show all commits
+19
View File
@@ -75,6 +75,25 @@ func TestNoRecordAllocRegression(t *testing.T) {
} }
} }
func TestNoStructAllocRegression(t *testing.T) {
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(`{"format":"","a":"x","b":"y","c":"z"}`)}}))
if err != nil {
t.Fatal(err)
}
var v struct {
A string `fake:"x.a"`
B string `fake:"x.b"`
C string `fake:"x.c"`
}
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
const base = 5.0
if allocs := testing.AllocsPerRun(10000, func() { f.FakeStruct(&v) }); allocs > base*1.10 {
t.Errorf("FakeStruct: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); compiling the type per call is the usual cause", allocs, base*1.10, base)
}
}
func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") } func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") }
func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") } func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") }
func BenchmarkWideTokens100(b *testing.B) { func BenchmarkWideTokens100(b *testing.B) {
+187
View File
@@ -0,0 +1,187 @@
package fejkdata
import (
"reflect"
"strings"
"testing"
)
type structPlace struct {
City string `fake:"place.city"`
Zip string `fake:"place.zip"`
}
type structUser struct {
Active bool `fake:"[\"true\",\"false\"]"`
Age uint8 `fake:"{int(18,99)}"`
Email string `fake:"{lowercase(/person.first)}@example.com"`
First string `fake:"person.first"`
Home structPlace
ID int64 `fake:"{seq()}"`
Last string `fake:"person.last"`
Nick *string `fake:"[null,\"bo\"]"`
Note string
Rank *int `fake:"{\"format\":\"{r}\",\"r\":[\"1\",\"2\"]}"`
Score float32 `fake:"{float(0,1,2)}"`
Work *structPlace
hidden structPlace
}
type structLink struct {
Name string `fake:"person.first"`
Next *structLink
}
func structData(t *testing.T) *Generator {
t.Helper()
return newGenerator(t, writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`,
"place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31"}]`,
"trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`,
}), WithSeed(1))
}
func TestFakeStructFillsTaggedFields(t *testing.T) {
a, b := structData(t), structData(t)
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
zips := map[string]string{"Stockholm": "111 22", "Tranås": "573 31"}
actives, nils := 0, 0
for i := 0; i < 100; i++ {
u, twin := structUser{Note: "keep"}, structUser{Note: "keep"}
if err := a.FakeStruct(&u); err != nil {
t.Fatal(err)
}
if err := b.FakeStruct(&twin); err != nil {
t.Fatal(err)
}
switch {
case !reflect.DeepEqual(u, twin):
t.Fatalf("same seed diverged: %+v != %+v", u, twin)
case people[u.First] != u.Last || u.Email != strings.ToLower(u.First)+"@example.com":
t.Fatalf("person fields %q %q %q, want one person drawn across the struct", u.First, u.Last, u.Email)
case zips[u.Home.City] != u.Home.Zip || u.Work == nil || zips[u.Work.City] != u.Work.Zip:
t.Fatalf("places %+v, %+v, want each nested struct one place, the pointer allocated", u.Home, u.Work)
case u.ID != int64(i+1) || u.Age < 18 || u.Age > 99 || u.Score < 0 || u.Score > 1 || u.Rank == nil || (*u.Rank != 1 && *u.Rank != 2):
t.Fatalf("typed fields %+v, want each the value its tag draws", u)
case u.Nick != nil && *u.Nick != "bo", u.Note != "keep", u.hidden != (structPlace{}):
t.Fatalf("%+v: want Nick nil or bo, and the untagged fields left as they were", u)
}
if u.Active {
actives++
}
if u.Nick == nil {
nils++
}
}
if actives == 0 || actives == 100 || nils == 0 || nils == 100 {
t.Errorf("100 draws gave %d active and %d nil nicks, want both outcomes of each", actives, nils)
}
}
func TestFakeStructDrawsANestedStructApart(t *testing.T) {
f := structData(t)
for i := 0; i < 100; i++ {
var trip struct{ From, To structPlace }
if err := f.FakeStruct(&trip); err != nil {
t.Fatal(err)
}
if trip.From.City != trip.To.City {
return
}
}
t.Error("From and To drew one place in 100 trips; a nested struct is a record of its own, so each draws apart")
}
func TestFakeStructLeavesAPointerBackAlone(t *testing.T) {
var l structLink
if err := structData(t).FakeStruct(&l); err != nil || l.Name == "" || l.Next != nil {
t.Errorf("FakeStruct = %v, %+v; want Name filled and Next, a pointer back to the struct being filled, left nil", err, l)
}
}
func TestFakeStructErrors(t *testing.T) {
f := structData(t)
for _, c := range []struct {
v any
want string
}{
{structUser{}, "fills a struct through a non-nil pointer, got fejkdata.structUser"},
{(*structUser)(nil), "through a non-nil pointer"},
{new(int), "through a non-nil pointer"},
{nil, "through a non-nil pointer"},
{&struct{ A string }{}, "has no fake tags"},
{&struct {
a string `fake:"person.first"`
}{}, ".a: unexported"},
{&struct {
A []string `fake:"person.first"`
}{}, ".A: a fake tag fills a string, bool, integer or float field, or a pointer to one, not []string"},
{&struct {
A **int `fake:"{int(1,9)}"`
}{}, "not **int"},
{&struct {
A structPlace `fake:"place"`
}{}, ".A: a struct field fills from the tags on its own fields"},
{&struct {
A int `fake:"{\"format\":\"{int(1,9)}\",\"datatype\":\"integer\"}"`
}{}, `its Go type int sets the datatype; drop "datatype"`},
{&struct {
A int `fake:"[null,\"{int(1,9)}\"]"`
}{}, "can draw null, which int cannot hold; make it *int"},
{&struct {
A int `fake:"{digits(3)}"`
}{}, ".A (int): {digits(3)} prints text, not an integer"},
{&struct {
A int `fake:"person.first"`
}{}, `"Ada" is not an integer`},
{&struct {
A int8 `fake:"{int(0,300)}"`
}{}, `"{int(0,300)}" is not proven within int8`},
{&struct {
A uint `fake:"{int(-1,5)}"`
}{}, `"{int(-1,5)}" is not proven within uint`},
{&struct {
A float32 `fake:"[\"1\",\"1e39\"]"`
}{}, `"1e39" is not proven within float32`},
{&struct {
A bool `fake:"{int(0,1)}"`
}{}, "prints an integer, not a boolean"},
{&struct {
A string `fake:"nope.x"`
}{}, `no entry "nope"`},
{&struct {
A string `fake:"{/person.first}"`
}{}, `write fake:"person.first"`},
{&struct {
A string `fake:"\"{/person.first}\""`
}{}, `write fake:"person.first"`},
{&struct {
A string `fake:"a|b"`
}{}, `contains "|"`},
{&struct {
A string `fake:""`
}{}, "is empty"},
{&struct {
A string `fake:"[abc]"`
}{}, `holds a "["`},
{&struct {
A string `fake:"{.person.first}"`
}{}, "write {/person.first}"},
{&struct {
A string `fake:"{x}"`
}{}, `no field "x"`},
{&struct {
A string `fake:"trip.leg"`
B string `fake:"trip.leg.to"`
}{}, "reads a path into"},
{&struct {
Trip struct {
A int `fake:"{digits(3)}"`
}
}{}, ".Trip.A (int): {digits(3)} prints text"},
} {
if err := f.FakeStruct(c.v); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("FakeStruct(%T) = %v, want an error containing %q", c.v, err, c.want)
}
}
}