From d4ff3b242ee3924426dd1f16a0df2a37c0002d3e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 16:23:00 +0200 Subject: [PATCH] Tests: FakeStruct fills tagged fields as one record, draws a nested struct apart, refuses a tag its field cannot hold, and compiles a type once --- perf_test.go | 19 +++++ struct_test.go | 187 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 struct_test.go diff --git a/perf_test.go b/perf_test.go index 0ee6dcb..ab92408 100644 --- a/perf_test.go +++ b/perf_test.go @@ -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 BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") } func BenchmarkWideTokens100(b *testing.B) { diff --git a/struct_test.go b/struct_test.go new file mode 100644 index 0000000..3fbaa37 --- /dev/null +++ b/struct_test.go @@ -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) + } + } +}