From c337dc4436dc75b4d04309759bb9f43df8f0a5f1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 8 Oct 2026 12:11:14 +0200 Subject: [PATCH] Test what stability round 1 found untested: a parent's zero-weight rows, a frame per draw of a choice item, ignored flags against the run without them, two leading slashes, and an option too close to 0 --- cmd/fejkdata/main_test.go | 25 +++++++++++++++++-------- datatype_test.go | 2 -- loading_test.go | 2 -- name_test.go | 32 ++++++++++++-------------------- node_test.go | 24 ++++++++++++++++++++++-- path_test.go | 12 ++++++++++++ spellings_test.go | 34 +++++++++++++++++----------------- struct_test.go | 21 ++++++++++++++------- table_test.go | 2 ++ 9 files changed, 96 insertions(+), 58 deletions(-) diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index c3dbc0b..551c8bb 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -565,15 +565,24 @@ func TestRunRepeatIsBounded(t *testing.T) { } func TestRunIgnoresAFlagWithNoEffect(t *testing.T) { - for _, args := range [][]string{{"--list", "-n", "3"}, {"--list", "--separator", ","}, {"--list", "--format", "json"}, {"--list", "--table", "t"}} { - if code, out, errb := runOut("", args...); code != 0 || !strings.Contains(out, "sv_SE.person\n") { - t.Errorf("run(%v) = %d, stderr %q, want the list", args, code, errb) - } - } dir := recordDir(t) - for _, args := range [][]string{{"--format", "json", "--separator", ","}, {"--table", "t"}, {"--format", "json", "--table", "t"}} { - if code, out, errb := runOut("{/users}", append(args, "--data-path", dir)...); code != 0 || out == "" { - t.Errorf("run(%v) = %d, %q, %q, want the render", args, code, out, errb) + for _, c := range []struct { + stdin string + base, ignored []string + }{ + {"", []string{"--list"}, []string{"-n", "3"}}, + {"", []string{"--list"}, []string{"--separator", ","}}, + {"", []string{"--list"}, []string{"--format", "json"}}, + {"", []string{"--list"}, []string{"--table", "t"}}, + {"{/users}", []string{"--format", "json", "-n", "2"}, []string{"--separator", ","}}, + {"{/users}", nil, []string{"--table", "t"}}, + {"{/users}", []string{"--format", "json"}, []string{"--table", "t"}}, + } { + args := append([]string{"--seed", "1", "--data-path", dir}, c.base...) + _, want, _ := runOut(c.stdin, args...) + code, got, errb := runOut(c.stdin, append(args, c.ignored...)...) + if code != 0 || want == "" || got != want { + t.Errorf("run(%v + %v) = %d, %q, %q; want what %v alone prints, %q", c.base, c.ignored, code, got, errb, c.base, want) } } } diff --git a/datatype_test.go b/datatype_test.go index bcc4d7a..7c1a40f 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -17,8 +17,6 @@ func TestDatatypeSitsOnlyInAColumn(t *testing.T) { `{"format":"","middle":[null,"Ann","Eva"]}`, `{"format":"","age":[null,{"format":"{int(18,99)}","datatype":"integer","weight":9}]}`, `{"format":"","pick":[[null,"a"],"b"]}`, - `{"format":"","n":{"format":"-0","datatype":"integer"}}`, - `{"format":"","n":{"format":"-0.00","datatype":"number"}}`, } { if _, err := resolved(t, src); err != nil { t.Errorf("compile(%s) = %v, want a column to take a datatype and null", src, err) diff --git a/loading_test.go b/loading_test.go index c4fa6d5..bd15eb9 100644 --- a/loading_test.go +++ b/loading_test.go @@ -323,8 +323,6 @@ func TestNewErrors(t *testing.T) { "field with a closing paren": {map[string]string{"a": `{"format":"{b)c}","b)c":"2"}`}, "a", "2"}, "repeat without a separator": {map[string]string{"a": `{"format":"{x}","repeat":3,"x":"1"}`}, "a", "111"}, "one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":"1"}`}, "a", "11"}, - "separator without repeat": {map[string]string{"a": `{"format":"{x}","x":"1","separator":",","repeat":1}`}, "a", "1"}, - "weight outside a choice": {map[string]string{"a": `{"format":"x","weight":5}`}, "a", "x"}, "repeated alternation arm": {map[string]string{"a": `{"format":"{x|x}","x":"1"}`}, "a", "1"}, "repeated path arm": {map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":"1"}}`}, "a", "1"}, "repeated reference arm": {map[string]string{"a": `"x"`, "b": `"{/a|/a}"`}, "b", "x"}, diff --git a/name_test.go b/name_test.go index 4245053..4d763f2 100644 --- a/name_test.go +++ b/name_test.go @@ -1,7 +1,6 @@ package fejkdata import ( - "fmt" "strings" "testing" ) @@ -168,28 +167,19 @@ func TestANameInAnInlineTemplate(t *testing.T) { } func TestAChoiceItemKeepsTheNamesItBinds(t *testing.T) { - f, n, both := engine(1), compiled(t, `{"format":"{x}","x":[{"format":"{v as w}{w}{w}","v":["a","b"]},"c"]}`), 0 + f, n, apart := engine(1), compiled(t, `{"format":"{x}|{x}","x":[{"format":"{v as w}{w}{w}","v":["a","b"]},"c"]}`), false for i := 0; i < 100; i++ { - switch renderOnce(f.drawState, n) { - case "aa", "bb": - both++ - case "c": - default: - t.Fatal("a name bound in a choice's item read two picks") + got := renderOnce(f.drawState, n) + halves := strings.Split(got, "|") + for _, h := range halves { + if h != "aa" && h != "bb" && h != "c" { + t.Fatalf("render %q holds %q, want each draw of the item to read one pick twice", got, h) + } } + apart = apart || halves[0] != halves[1] } - if both == 0 { - t.Error("100 renders never drew the item binding a name") - } -} - -func TestTheRootScopeIsTheCategory(t *testing.T) { - root := compiled(t, `{"format":"{x as n}{n}{y}","x":"a","y":{"format":"{z as m}{m}","z":"b","repeat":2}}`).(*template) - if k := fmt.Sprint(root.nameScope.kind()); k != "category" { - t.Errorf("root scope kind = %q, want category", k) - } - if k := fmt.Sprint(root.fields["y"].(*template).nameScope.kind()); k != "repeat" { - t.Errorf("repeat scope kind = %q, want repeat", k) + if !apart { + t.Error("100 renders of {x}|{x} never drew two different halves, want a pick per draw of the item") } } @@ -203,6 +193,8 @@ func TestNameErrors(t *testing.T) { `name "w" is bound twice outside any repeat`}, {"bound inside a repeat written first", map[string]string{"word": `["a","b"]`, "card": `{"format":"{a}{z}","a":{"format":"{/word as w}{w}","repeat":2},"z":"{/word as w}{w}"}`}, `name "w" is bound outside this repeat too`}, + {"bound twice in one repeat", map[string]string{"word": `["a","b"]`, "card": `{"format":"{x}","x":{"format":"{/word as n}{/word as n}{n}","repeat":2}}`}, + `name "n" is bound twice in one repeat`}, {"bound in a choice's item", map[string]string{"word": `["a","b"]`, "card": `{"format":"{x}{w}","x":["{/word as w}","b"]}`}, `name "w" is bound at field "x", inside an item of a choice, which a read outside that item cannot see`}, {"bound outside the repeat too", map[string]string{"word": `["a","b"]`, "card": `{"format":"{/word as w}{x}","x":{"format":"{/word as w}{w}","repeat":2}}`}, diff --git a/node_test.go b/node_test.go index a7e32ad..b37111e 100644 --- a/node_test.go +++ b/node_test.go @@ -7,8 +7,15 @@ import ( ) func TestAnOptionOfTheWrongKindIsNamedByItsJSONKind(t *testing.T) { - if _, err := resolved(t, `{"format":"x","repeat":2,"separator":5}`); err == nil || !strings.Contains(err.Error(), "separator must be a string, not a number") { - t.Errorf("separator 5 = %v, want the kind named", err) + for src, want := range map[string]string{ + `{"format":"x","repeat":2,"separator":5}`: "separator must be a string, not a number", + `{"format":"x","repeat":true}`: "repeat must be a number, not a boolean", + `{"format":"x","repeat":2,"separator":{}}`: "separator must be a string, not an object", + `[{"format":"a","weight":-1}]`: "weight must be a finite number of 0 or more", + } { + if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("compile(%s) = %v, want %q", src, err, want) + } } _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":5}`, "t.tsv": "a\nx\n"}))) if err == nil || !strings.Contains(err.Error(), "key must be a string, not a number") { @@ -16,6 +23,19 @@ func TestAnOptionOfTheWrongKindIsNamedByItsJSONKind(t *testing.T) { } } +func TestAnOptionTooCloseToZeroIsRefused(t *testing.T) { + for _, src := range []string{`[{"format":"a","weight":1e-400},"b"]`, `{"format":"{x}","x":"a","repeat":1e-400}`} { + if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), "1e-400") { + t.Errorf("compile(%s) = %v, want the number too close to 0 named", src, err) + } + } + for _, src := range []string{`[{"format":"a","weight":0.0},"b"]`, `{"format":"x","repeat":0e5}`} { + if _, err := resolved(t, src); err != nil { + t.Errorf("compile(%s) = %v, want a zero written as zero loaded", src, err) + } + } +} + func TestWeightSkewsDistribution(t *testing.T) { f := engine(9) heavy := 0 diff --git a/path_test.go b/path_test.go index 270f4d4..6f70458 100644 --- a/path_test.go +++ b/path_test.go @@ -197,4 +197,16 @@ func TestALeadingSlashIsThePath(t *testing.T) { if err != nil || len(r.Columns()) != 1 || r.Columns()[0].Value != "Ada" { t.Errorf("FakeRecord(/person) = %+v, %v, want the record person", r, err) } + if _, err := f.FakeRecord("//person"); err == nil || !strings.Contains(err.Error(), "write /person") { + t.Errorf("FakeRecord(//person) = %v, want it refused naming /person", err) + } + if _, err := f.FakeRecord("person["); err == nil || !strings.Contains(err.Error(), "never closes") { + t.Errorf("FakeRecord(person[) = %v, want the open selector named", err) + } + var v struct { + A string `fake:"//person"` + } + if err := f.FakeStruct(&v); err == nil || !strings.Contains(err.Error(), "write /person") { + t.Errorf("FakeStruct(//person tag) = %v, want it refused naming /person", err) + } } diff --git a/spellings_test.go b/spellings_test.go index 022e8a2..1c2630b 100644 --- a/spellings_test.go +++ b/spellings_test.go @@ -37,21 +37,20 @@ func TestOneItemChoiceIsItsItem(t *testing.T) { func TestAPartWithNoEffectRendersAsWithoutIt(t *testing.T) { for long, short := range map[string]string{ - `{"format":"Malmö"}`: `"Malmö"`, - `{"format":"{digits(3)}"}`: `"{digits(3)}"`, - `[{"format":"a","weight":1},"b"]`: `["a","b"]`, - `[{"format":"a","weight":0},"b"]`: `"b"`, - `{"format":"a","weight":2}`: `"a"`, - `{"format":"a","weight":0}`: `"a"`, - `{"format":"{x}","x":{"format":"a","weight":2}}`: `{"format":"{x}","x":"a"}`, - `[{"format":"a","weight":2}]`: `"a"`, - `{"format":"{x}","x":["a","b"],"repeat":1}`: `{"format":"{x}","x":["a","b"]}`, - `{"format":"{x}","x":["a","b"],"separator":","}`: `{"format":"{x}","x":["a","b"]}`, - `{"format":"{x}","x":["a","b"],"repeat":2,"separator":""}`: `{"format":"{x}","x":["a","b"],"repeat":2}`, - `{"format":"","n":{"format":"{x}","x":["1","2"],"datatype":"string"}}`: `{"format":"","n":{"format":"{x}","x":["1","2"]}}`, - `{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: `{"format":"{n}","repeat":2,"n":"1"}`, - `[{"format":"1","datatype":"integer"},"x"]`: `["1","x"]`, - `{"format":"{word as w}x","word":["a","b"]}`: `"x"`, + `{"format":"Malmö"}`: `"Malmö"`, + `{"format":"{digits(3)}"}`: `"{digits(3)}"`, + `[{"format":"a","weight":1},"b"]`: `["a","b"]`, + `[{"format":"a","weight":0},"b"]`: `"b"`, + `{"format":"a","weight":2}`: `"a"`, + `{"format":"a","weight":0}`: `"a"`, + `{"format":"{x}","x":{"format":"a","weight":2}}`: `{"format":"{x}","x":"a"}`, + `[{"format":"a","weight":2}]`: `"a"`, + `{"format":"{x}","x":["a","b"],"repeat":1}`: `{"format":"{x}","x":["a","b"]}`, + `{"format":"{x}","x":["a","b"],"separator":","}`: `{"format":"{x}","x":["a","b"]}`, + `{"format":"{x}","x":["a","b"],"repeat":2,"separator":""}`: `{"format":"{x}","x":["a","b"],"repeat":2}`, + `{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: `{"format":"{n}","repeat":2,"n":"1"}`, + `[{"format":"1","datatype":"integer"},"x"]`: `["1","x"]`, + `{"format":"{word as w}x","word":["a","b"]}`: `"x"`, `null`: `""`, `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `{"format":"{p}","p":{"format":"{x}","x":["","a"]}}`, `"{int(5,5)}"`: `"5"`, @@ -65,8 +64,9 @@ func TestAPartWithNoEffectRendersAsWithoutIt(t *testing.T) { } { sameRenders(t, long, short) } - if r := compiled(t, `{"format":"","n":{"format":"1","datatype":"string"}}`).(*template); !r.isRecord || *r.fields["n"].(*template).datatype != DataTypeString { - t.Error("a column of datatype string is no string column of a record") + r, err := shipped(t).NewRecordTemplate(`{"format":"","n":{"format":"{x}","x":["1","2"],"datatype":"string"}}`) + if err != nil || r.Fake().Columns()[0].DataType != DataTypeString { + t.Errorf("a column of datatype string = %v, want a string column", err) } if got := mustRender(t, engine(1), `"{date(1990-01-01,1990-12-31,'15:04')}"`); !regexp.MustCompile(`^\d\d:\d\d$`).MatchString(got) { t.Errorf("date with a clock-only layout = %q, want a clock", got) diff --git a/struct_test.go b/struct_test.go index afbe924..d032ea7 100644 --- a/struct_test.go +++ b/struct_test.go @@ -254,13 +254,20 @@ func TestFakeStructTakesTheDatatypeItsGoTypeSets(t *testing.T) { } func TestFakeStructTypesABareNumberOrBooleanByItsGoType(t *testing.T) { - v := struct { - N int `fake:"[5, 6]"` - B bool `fake:"[true, false]"` - S string `fake:"[7.50]"` - }{} - if err := structData(t).FakeStruct(&v); err != nil || (v.N != 5 && v.N != 6) || v.S != "7.50" { - t.Errorf("FakeStruct = %+v, %v, want each value typed by its field", v, err) + f, seen := structData(t), map[bool]bool{} + for i := 0; i < 30; i++ { + v := struct { + N int `fake:"[5, 6]"` + B bool `fake:"[true, false]"` + S string `fake:"[7.50]"` + }{} + if err := f.FakeStruct(&v); err != nil || (v.N != 5 && v.N != 6) || v.S != "7.50" { + t.Fatalf("FakeStruct = %+v, %v, want each value typed by its field", v, err) + } + seen[v.B] = true + } + if len(seen) != 2 { + t.Errorf("B took %v in 30 fills, want true and false", seen) } } diff --git a/table_test.go b/table_test.go index ddd4362..e6ef200 100644 --- a/table_test.go +++ b/table_test.go @@ -590,6 +590,8 @@ func TestTableFences(t *testing.T) { "format names no column": {map[string]string{"t.json": `{"format":"{b}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `no column "b"`}, "format reads into a column": {map[string]string{"t.json": `{"format":"{a.x}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `"a"`}, "a reference into a column": {with(base, map[string]string{"t.json": `"{/region.name.x}"`}), "column"}, + "every row under one parent weighing 0": {map[string]string{"p.json": `{"format":"{k}","rows":"p.tsv","key":"k"}`, "p.tsv": "k\nA\nB\n", "c.json": `{"format":"{n}","rows":"c.tsv","parent":"p","weight":"w"}`, "c.tsv": "n\tp\tw\nx\tA\t0\ny\tB\t1\n"}, `every row under p "A" weighs 0`}, + "weights summing past the largest number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t1e308\ny\t1e308\n"}, "past the largest number"}, "a category referencing itself": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n{/t.a}\ny\n"}, "cycle"}, } for name, c := range rejected {