Files
fejkdata/struct_test.go

334 lines
12 KiB
Go

package fejkdata
import (
"reflect"
"strconv"
"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"`
Level uint8 `fake:"{float(0,255,0)}"`
Nick *string `fake:"[null,\"bo\"]"`
Note string
Rank *int `fake:"{\"format\":\"{r}\",\"r\":[\"1\",\"2\"]}"`
Score float32 `fake:"{float(0,1,2)}"`
Skip *structPlace `fake:"-"`
Work *structPlace
hidden structPlace
}
type structGiven struct {
First string `fake:"person.first"`
}
type StructFamily struct {
Last string `fake:"person.last"`
}
type structEmployee struct {
structGiven
*StructFamily
Email string `fake:"{lowercase(/person.first)}@example.com"`
}
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","born":"1815"}]`,
"place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31","region":"F"}]`,
"mid": `{"format":"","score":["{/src.score}",{"format":"5","datatype":"integer"}]}`,
"src": `{"format":"","code":[null,"200","404"],"del":null,"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
"trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`,
}), WithSeed(1))
}
func TestFakeStructFieldTaggedWithAColumnIsThatColumn(t *testing.T) {
f := structData(t)
nils := map[string]int{}
show := func(p any) string {
switch p := p.(type) {
case *string:
if p != nil {
return *p
}
case *int64:
if p != nil {
return strconv.FormatInt(*p, 10)
}
}
return "nil"
}
for i := 0; i < 200; i++ {
var v struct {
Code *string `fake:"src.code"`
Codes *string `fake:"[\"{/src.score}\",\"{/src.code}\"]"`
Del *int64 `fake:"src.del"`
Label string `fake:"n={/src.score}"`
Mixed *string `fake:"[\"{/src.score}\",\"x\"]"`
Pair *int64 `fake:"[\"{/src.score}\",\"5\"]"`
Score *int64 `fake:"src.score"`
}
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
score, code := show(v.Score), show(v.Code)
switch {
case show(v.Del) != "nil":
t.Fatalf("Del = %s, want nil from a column only ever null", show(v.Del))
case show(v.Mixed) != "x" && show(v.Mixed) != score, show(v.Pair) != "5" && show(v.Pair) != score, show(v.Codes) != score && show(v.Codes) != code:
t.Fatalf("Mixed %s, Pair %s, Codes %s beside score %s and code %s: want each the literal or the draw of the column it reads", show(v.Mixed), show(v.Pair), show(v.Codes), score, code)
}
switch {
case v.Code == nil:
nils["code"]++
case *v.Code != "200" && *v.Code != "404":
t.Fatalf("Code = %q, want nil or a code, never a null's \"\"", *v.Code)
}
switch {
case v.Score == nil && v.Label == "n=":
nils["score"]++
case v.Score == nil || *v.Score < 1 || *v.Score > 9 || v.Label != "n="+strconv.FormatInt(*v.Score, 10):
t.Fatalf("%+v, want Score nil beside Label n=, or one digit in both", v)
}
}
for _, name := range []string{"code", "score"} {
if n := nils[name]; n == 0 || n == 200 {
t.Errorf("%s was nil %d times in 200 fills, want both outcomes", name, n)
}
}
}
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++ {
oldNick, oldTwinNick := "old", "old"
u, twin := structUser{Nick: &oldNick, Note: "keep"}, structUser{Nick: &oldTwinNick, 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{}), u.Skip != nil, oldNick != "old":
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 TestFakeStructFillsEmbeddedFieldsIntoItsRecord(t *testing.T) {
f := structData(t)
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
for i := 0; i < 100; i++ {
var e structEmployee
if err := f.FakeStruct(&e); err != nil {
t.Fatal(err)
}
if e.StructFamily == nil || people[e.First] != e.Last || e.Email != strings.ToLower(e.First)+"@example.com" {
t.Fatalf("%+v, %+v: want the promoted fields one person with the struct's own, the embedded pointer allocated", e, e.StructFamily)
}
}
var skipped struct {
structGiven `fake:"-"`
Email string `fake:"{lowercase(/person.first)}@example.com"`
}
if err := f.FakeStruct(&skipped); err != nil || skipped.First != "" || skipped.Email == "" {
t.Errorf("FakeStruct = %v, %+v; want the embedded struct under fake:\"-\" left unfilled beside the filled field", err, skipped)
}
}
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 int64 `fake:"src.score"`
}{}, "can draw null, which int64 cannot hold; make it *int64"},
{&struct {
A string `fake:"src.code"`
}{}, "can draw null, which string cannot hold; make it *string"},
{&struct {
A *bool `fake:"src.score"`
}{}, ".A (*bool): {int(1,9)} prints an integer, not a boolean"},
{&struct {
A int64 `fake:"mid.score"`
}{}, "can draw null, which int64 cannot hold; make it *int64"},
{&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; narrow it to that range, or make the field int64`},
{&struct {
A uint `fake:"{int(-1,5)}"`
}{}, `"{int(-1,5)}" is not proven within uint; narrow it to that range, or make the field int64`},
{&struct {
A float32 `fake:"[\"1\",\"1e39\"]"`
}{}, `"1e39" is not proven within float32; narrow it to that range, or make the field float64`},
{&struct {
A int32 `fake:"{seq()}"`
}{}, `"{seq()}" is not proven within int32; narrow it to that range, or make the field int64`},
{&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}"`
}{}, "is the path person.first written as a template; write person.first"},
{&struct {
A string `fake:"\"{/person.first}\""`
}{}, "is the path person.first written as a template; write person.first"},
{&struct {
A string `fake:"a|b"`
}{}, `contains "|"`},
{&struct {
A string `fake:""`
}{}, "is empty"},
{&struct {
A string `fake:"[abc]"`
}{}, `"["`},
{&struct {
A string `fake:"{.person.first} x"`
}{}, "write {/person.first}"},
{&struct {
A string `fake:"/person.first"`
}{}, "write person.first"},
{&struct {
A string `fake:"-"`
}{}, `fake:"-" leaves a struct field unfilled`},
{&struct{ *structGiven }{}, "an unexported embedded pointer field cannot be set"},
{&struct {
structGiven
First string `fake:"person.last"`
}{}, "struct.structGiven.First: hidden by another field named 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)}"`
}
}{}, ": struct.Trip.A (int): {digits(3)} prints text"},
} {
err := f.FakeStruct(c.v)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("FakeStruct(%T) = %v, want an error containing %q", c.v, err, c.want)
continue
}
if again := f.FakeStruct(c.v); again == nil || again.Error() != err.Error() {
t.Errorf("FakeStruct(%T) again = %v, want the first call's error, %v", c.v, again, err)
}
}
}
func TestFakeStructBoundsTheStructsATypeReaches(t *testing.T) {
f := structData(t)
place := reflect.TypeOf(structPlace{})
tree := place
for depth := 1; depth <= 8; depth++ {
tree = reflect.StructOf([]reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}})
}
fields := []reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}, {Name: "P", Type: place}}
if err := f.FakeStruct(reflect.New(reflect.StructOf(fields)).Interface()); err != nil {
t.Fatalf("a type reaching 1024 structs: FakeStruct = %v, want it filled", err)
}
embedded := reflect.StructField{Name: "StructFamily", Type: reflect.TypeOf(StructFamily{}), Anonymous: true}
err := f.FakeStruct(reflect.New(reflect.StructOf(append(fields, embedded))).Interface())
if err == nil || !strings.Contains(err.Error(), "more than 1024 structs") || !strings.Contains(err.Error(), `leave a struct field unfilled with fake:"-"`) {
t.Errorf("a type reaching 1025 structs, the last embedded: FakeStruct = %v, want it refused naming the cap and fake:\"-\"", err)
}
}