Tests: a column of one reference alone takes the datatype and null of the column it reads, in records and structs, and a datatype restating it is refused

This commit is contained in:
2026-09-15 20:17:07 +02:00
parent 3cd3f8f317
commit 802f62a4de
3 changed files with 113 additions and 7 deletions
+14 -7
View File
@@ -30,10 +30,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
`{"format":"{p}","p":{"format":"{n}","n":{"format":"1","datatype":"integer"}}}`: "datatype only types a record column", `{"format":"{p}","p":{"format":"{n}","n":{"format":"1","datatype":"integer"}}}`: "datatype only types a record column",
`{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: "datatype only types a record column", `{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: "datatype only types a record column",
`null`: `so write ""`, `null`: `so write ""`,
`{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`,
`{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: `write it as {"format":"x","datatype":"integer"}`,
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]}`: `give it "datatype": "integer"`,
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]}`: "a column holds one datatype",
} { } {
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want) t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want)
@@ -44,9 +41,18 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
tree := map[string]string{ tree := map[string]string{
"cat": `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x"}]`, "cat": `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x"}]`,
"src": `{"format":"","score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`, "src": `{"format":"","code":[null,"200","2x"],"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
} }
for _, c := range []struct{ name, column, want string }{ for _, c := range []struct{ name, column, want string }{
{"an item beside a typed one", `[{"format":"1","datatype":"integer"},"x"]`, `write it as {"format":"x","datatype":"integer"}`},
{"a weighted item beside a typed one", `[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]`, `give it "datatype": "integer"`},
{"items of two datatypes", `[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]`, "a column holds one datatype"},
{"an item beside a typed column it reads", `["{/src.score}","x"]`, `write it as {"format":"x","datatype":"integer"}`},
{"a typed item beside a string column it reads", `["{/src.code}",{"format":"1","datatype":"integer"}]`, `item "{/src.code}" declares no datatype beside one declaring integer`},
{"a datatype over a typed column", `{"format":"{/src.score}","datatype":"integer"}`, `{/src.score} takes datatype integer from the column it reads; drop "datatype"`},
{"another datatype over a typed column", `{"format":"{/src.score}","datatype":"number"}`, `{/src.score} takes datatype integer from the column it reads; drop "datatype"`},
{"a value of the column it reads", `{"format":"{/src.code}","datatype":"integer"}`, `"2x" is not an integer`},
{"a null read into text", `{"format":"{x}","x":"{/src.score}","datatype":"integer"}`, "reads a null"},
{"a sample with leading zeros", `{"format":"{digits(3)}","datatype":"integer"}`, "{digits(3)} prints text, not an integer"}, {"a sample with leading zeros", `{"format":"{digits(3)}","datatype":"integer"}`, "{digits(3)} prints text, not an integer"},
{"a fraction", `{"format":"{v}","v":["1","1.5"],"datatype":"integer"}`, `"1.5" is not an integer`}, {"a fraction", `{"format":"{v}","v":["1","1.5"],"datatype":"integer"}`, `"1.5" is not an integer`},
{"past int64", `{"format":"9223372036854775808","datatype":"integer"}`, "past the int64 range"}, {"past int64", `{"format":"9223372036854775808","datatype":"integer"}`, "past the int64 range"},
@@ -55,7 +61,6 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
{"a sign before a sample", `{"format":"-{int(1,9)}","datatype":"integer"}`, "is not one value"}, {"a sign before a sample", `{"format":"-{int(1,9)}","datatype":"integer"}`, "is not one value"},
{"a repeat", `{"format":"{int(1,9)}","repeat":2,"separator":",","datatype":"integer"}`, "carries a repeat"}, {"a repeat", `{"format":"{int(1,9)}","repeat":2,"separator":",","datatype":"integer"}`, "carries a repeat"},
{"through a reference", `{"format":"{/cat.code}","datatype":"integer"}`, `"2x" is not an integer`}, {"through a reference", `{"format":"{/cat.code}","datatype":"integer"}`, `"2x" is not an integer`},
{"a null through a reference", `{"format":"{/src.score}","datatype":"integer"}`, "reads a null"},
{"a bare dot", `{"format":".5","datatype":"number"}`, `".5" is not a number`}, {"a bare dot", `{"format":".5","datatype":"number"}`, `".5" is not a number`},
{"a plus sign", `{"format":"+1","datatype":"number"}`, `"+1" is not a number`}, {"a plus sign", `{"format":"+1","datatype":"number"}`, `"+1" is not a number`},
{"a text sample", `{"format":"{hex(4)}","datatype":"number"}`, "{hex(4)} prints text, not a number"}, {"a text sample", `{"format":"{hex(4)}","datatype":"number"}`, "{hex(4)} prints text, not a number"},
@@ -120,9 +125,11 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
`{"format":"{calc(a / (b + 1), 2)}","a":"{int(1,9)}","b":"{digits(2)}","datatype":"number"}`, `{"format":"{calc(a / (b + 1), 2)}","a":"{int(1,9)}","b":"{digits(2)}","datatype":"number"}`,
`{"format":"{calc(a / b, 0)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, `{"format":"{calc(a / b, 0)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`,
`{"format":"{calc(sub * 1.25, 2)}","sub":{"format":"{calc(a * b)}","a":"{int(1,9)}","b":"{float(0,5,2)}"},"datatype":"number"}`, `{"format":"{calc(sub * 1.25, 2)}","sub":{"format":"{calc(a * b)}","a":"{int(1,9)}","b":"{float(0,5,2)}"},"datatype":"number"}`,
`{"format":"{/src.code}","datatype":"integer"}`,
} { } {
row := `{"format":"","col":` + column + `}` row := `{"format":"","col":` + column + `}`
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row})), WithSeed(1)) src := `{"format":"","code":["200","404"]}`
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row, "src": src})), WithSeed(1))
if err != nil { if err != nil {
t.Errorf("%s: New = %v, want it loaded", column, err) t.Errorf("%s: New = %v, want it loaded", column, err)
continue continue
+56
View File
@@ -382,6 +382,62 @@ func TestRecordBareReferenceStaysIndependent(t *testing.T) {
} }
} }
func TestRecordColumnOfOneReferenceIsTheColumnItReads(t *testing.T) {
dir := writeData(t, map[string]string{
"mid": `{"format":"","score":"{/src.score}"}`,
"row": `{"format":"","chain":"{/mid.score}","code":{"format":"{/src.code}","datatype":"integer"},"label":"n={/src.score}","same":"{/src.score}","score":"{/src.score}","text":"{/src.code}"}`,
"src": `{"format":"","code":[null,"200","404"],"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
})
f := newGenerator(t, dir, WithSeed(1))
inline, err := f.NewRecordTemplate(`{"format":"","score":"{/src.score}"}`)
if err != nil {
t.Fatal(err)
}
nulls := map[string]int{}
for i := 0; i < 200; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
cols := map[string]Column{}
for _, c := range r.Columns() {
cols[c.Name] = c
}
score, code := cols["score"], cols["code"]
agree := func(name string, want Column) bool {
return cols[name].Null == want.Null && cols[name].Value == want.Value
}
switch {
case score.DataType != DataTypeInteger || cols["chain"].DataType != DataTypeInteger || code.DataType != DataTypeInteger || cols["label"].DataType != DataTypeString || cols["text"].DataType != DataTypeString:
t.Fatalf("%s: want score, chain and code integer columns, label and text string ones", r.JSON())
case score.Null == (len(score.Value) == 1 && score.Value >= "1" && score.Value <= "9"):
t.Fatalf("score = %+v, want null or a digit from src.score", score)
case code.Null == (code.Value == "200" || code.Value == "404"):
t.Fatalf("code = %+v, want null or a code from src.code", code)
case !agree("same", score) || !agree("chain", score) || !agree("text", code):
t.Fatalf("%s: want every read of one src column one draw, through mid too", r.JSON())
case cols["label"].Null || cols["label"].Value != "n="+score.Value:
t.Fatalf("label = %+v beside score %+v, want the text of the read, a null as \"\"", cols["label"], score)
case strings.Contains(r.JSON(), `"score":null`) != score.Null:
t.Fatalf("JSON() = %s, want a null score written null", r.JSON())
}
ic := inline.Fake().Columns()[0]
if ic.DataType != DataTypeInteger || ic.Null == (len(ic.Value) == 1) {
t.Fatalf("inline score = %+v, want an integer column, null or a digit", ic)
}
for name, c := range map[string]Column{"code": code, "inline": ic, "score": score} {
if c.Null {
nulls[name]++
}
}
}
for _, name := range []string{"code", "inline", "score"} {
if n := nulls[name]; n == 0 || n == 200 {
t.Errorf("%s drew null %d times in 200 records, want both outcomes", name, n)
}
}
}
func TestRecordSQLQuotesIdentifiers(t *testing.T) { func TestRecordSQLQuotesIdentifiers(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"row": `{"format": "", "postal-code": "1", "street-number": "2"}`, "row": `{"format": "", "postal-code": "1", "street-number": "2"}`,
+43
View File
@@ -2,6 +2,7 @@ package fejkdata
import ( import (
"reflect" "reflect"
"strconv"
"strings" "strings"
"testing" "testing"
) )
@@ -53,10 +54,43 @@ func structData(t *testing.T) *Generator {
return newGenerator(t, writeData(t, map[string]string{ return newGenerator(t, writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`, "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"}]`, "place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31"}]`,
"src": `{"format":"","code":[null,"200","404"],"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
"trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`, "trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`,
}), WithSeed(1)) }), WithSeed(1))
} }
func TestFakeStructReadsAColumnWhole(t *testing.T) {
f := structData(t)
nils := map[string]int{}
for i := 0; i < 200; i++ {
var v struct {
Code *string `fake:"src.code"`
Label string `fake:"n={/src.score}"`
Score *int64 `fake:"src.score"`
}
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
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) { func TestFakeStructFillsTaggedFields(t *testing.T) {
a, b := structData(t), structData(t) a, b := structData(t), structData(t)
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"} people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
@@ -166,6 +200,15 @@ func TestFakeStructErrors(t *testing.T) {
{&struct { {&struct {
A int `fake:"[null,\"{int(1,9)}\"]"` A int `fake:"[null,\"{int(1,9)}\"]"`
}{}, "can draw null, which int cannot hold; make it *int"}, }{}, "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 { {&struct {
A int `fake:"{digits(3)}"` A int `fake:"{digits(3)}"`
}{}, ".A (int): {digits(3)} prints text, not an integer"}, }{}, ".A (int): {digits(3)} prints text, not an integer"},