diff --git a/builtins_test.go b/builtins_test.go index b6d237e..2092704 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -271,25 +271,19 @@ func TestBuiltinDateAndTime(t *testing.T) { } } -// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order, -// the layout is quoted, names a field, and for time names no date field. +// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order, and the +// layout is quoted. func TestBuiltinDateArgs(t *testing.T) { for tmpl, want := range map[string]string{ `"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01", `"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after", - `"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "write it as text", `"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'", `"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'", `"{date(1990-01-01,1990-12-31,\"January 2, 2006\")}"`: "quoted: 'January 2, 2006'", - `"{date(1990-01-01,1990-12-31,'x')}"`: "text", - `"{date(1990-01-01,1990-12-31,'')}"`: "text", `"{date(1990-01-01,1990-12-31)}"`: "3 arguments", `"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'", `"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'", `"{time(15:04)}"`: "'15:04'", - `"{time('2006-01-02 15:04')}"`: "date(", - `"{time('x')}"`: "text", - `"{date(1990-01-01,1990-12-31,'15:04')}"`: "time('15:04')", } { _, err := resolved(t, tmpl) if err == nil || !strings.Contains(err.Error(), want) { @@ -327,7 +321,7 @@ func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) { } // TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it -// draws every second of it, and without one it could only emit one value. +// draws every second of it, and without one it prints that day. func TestBuiltinDateSpansOneDay(t *testing.T) { f := engine(1) seen := map[string]bool{} @@ -341,9 +335,8 @@ func TestBuiltinDateSpansOneDay(t *testing.T) { if len(seen) < 400 { t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen)) } - _, err := compile(parse(t, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`)) - if err == nil || !strings.Contains(err.Error(), "write it as text") { - t.Fatalf("one day in a date-only layout = %v, want it named a constant", err) + if got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`); got != "2026-01-01" { + t.Fatalf("one day in a date-only layout = %q, want that day", got) } } @@ -398,8 +391,6 @@ func TestBuiltinCompileErrors(t *testing.T) { `"{float(Inf,Inf,2)}"`, // same-sign infinities `"{float(1,NaN,2)}"`, // one NaN bound `"{float(-Inf,1,2)}"`, // one infinite bound - `"{int(5,5)}"`, // a constant is written as text - `"{float(1,1,2)}"`, // a constant is written as text `"{iban(US)}"`, // unsupported country `"{seq(a,b)}"`, // seq takes at most one name } { diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index e93490b..2fd421d 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -492,11 +492,6 @@ func TestRunTemplateMisuse(t *testing.T) { "{bad": "unterminated", "name: {/no.such.path}": "no entry", "a } b": "}}", - "42": "number", - "42\n": "number", - "true\n": "boolean", - "null\n": "jq", - " null ": "jq", "{//sv_SE.person}": "write {/sv_SE.person}", } { code, out, errb := runOut(stdin, "--seed", "1") @@ -548,11 +543,24 @@ func TestRunRepeatIsBounded(t *testing.T) { } } -func TestRunListTakesNoRepeatOrSeparator(t *testing.T) { - for _, args := range [][]string{{"--list", "-n", "3"}, {"--list", "--separator", ","}} { - code, _, errb := runOut("", args...) - if code != 2 || !strings.Contains(errb, "--list takes no") { - t.Errorf("run(%v) = %d, %q, want misuse", args, code, errb) +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) + } + } +} + +func TestRunRendersAnyJSONValue(t *testing.T) { + for stdin, want := range map[string]string{"42": "42", "42\n": "42", "true\n": "true", "null\n": "", " null ": ""} { + if code, out, errb := runOut(stdin); code != 0 || out != want { + t.Errorf("run(%q) = %d, %q, %q, want %q", stdin, code, out, errb, want) } } } @@ -679,9 +687,6 @@ func TestRunRecordMisuse(t *testing.T) { want string }{ {[]string{"--format", "yaml"}, "--format takes csv, json, ndjson, sql or text"}, - {[]string{"--format", "json", "--separator", ","}, "--separator joins text values"}, - {[]string{"--table", "t"}, "--table names the INSERT target"}, - {[]string{"--format", "json", "--table", "t"}, "--table names the INSERT target"}, {[]string{"--format", "sql", "--table", ""}, "--table names the INSERT target"}, } { code, out, errb := runOut("{/users}", c.args...) diff --git a/datatype_test.go b/datatype_test.go index 7bdc0c2..2d4c149 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -25,14 +25,8 @@ func TestDatatypeSitsOnlyInAColumn(t *testing.T) { } } for src, want := range map[string]string{ - `{"format":"","n":{"format":"1","datatype":"int"}}`: `datatype takes "string", "integer", "number" or "boolean", got "int"`, - `{"format":"","n":{"format":"1","datatype":1}}`: "datatype must be a string", - `{"format":"{int(1,9)}","datatype":"integer"}`: "datatype only types a record column", - `[{"format":"1","datatype":"integer"},"x"]`: "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"}}`: `so it has no effect here; drop "datatype"`, - `null`: `so write ""`, - `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, + `{"format":"","n":{"format":"1","datatype":"int"}}`: `datatype takes "string", "integer", "number" or "boolean", got "int"`, + `{"format":"","n":{"format":"1","datatype":1}}`: "datatype must be a string", } { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want) diff --git a/internal/datafiles/datafiles_test.go b/internal/datafiles/datafiles_test.go index 7c33eca..e419529 100644 --- a/internal/datafiles/datafiles_test.go +++ b/internal/datafiles/datafiles_test.go @@ -80,10 +80,9 @@ func TestReadRowsReadsOnlyAFileBesideTheCategory(t *testing.T) { } } -func TestWalkRefusesARowsFileNoCategoryNames(t *testing.T) { - _, err := walk(t, Source{fsys: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, "a.tsv": {Data: []byte("key\n")}}, label: "lbl"}) - if err == nil || !strings.HasPrefix(err.Error(), "lbl/a.tsv: no category names it in its rows") { - t.Errorf("Walk = %v, want a.tsv refused as named by no category", err) +func TestWalkIgnoresARowsFileNoCategoryNames(t *testing.T) { + if _, err := walk(t, Source{fsys: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, "a.tsv": {Data: []byte("key\n")}}, label: "lbl"}); err != nil { + t.Errorf("Walk = %v, want a.tsv ignored", err) } } diff --git a/internal/rows/rows_test.go b/internal/rows/rows_test.go index bc5c911..d61626b 100644 --- a/internal/rows/rows_test.go +++ b/internal/rows/rows_test.go @@ -37,7 +37,8 @@ func TestParseRefuses(t *testing.T) { {"no rows", "a\n", Options{}, "has no rows below its header"}, {"a short row", "a\tb\n1\t2\n3\n", Options{}, "line 3 has fewer cells"}, {"a repeated key", "k\nx\nx\n", Options{Key: "k"}, `key "x" repeats line 2`}, - {"a weight of zero", "k\tw\nx\t1\ny\t0\n", Options{Weight: "w"}, `weight "0" is not a positive number`}, + {"every weight zero", "k\tw\nx\t0\ny\t0\n", Options{Weight: "w"}, "every weight is 0"}, + {"a negative weight", "k\tw\nx\t1\ny\t-1\n", Options{Weight: "w"}, `weight "-1" is not a number of 0 or more`}, {"a key a selector cannot spell", "k\nx\ny|z\n", Options{Key: "k"}, `contains "|"`}, {"an option naming no column", "k\nx\ny\n", Options{Key: "nope"}, `key names no column "nope"`}, } { diff --git a/loading_test.go b/loading_test.go index 2854d3c..e85e266 100644 --- a/loading_test.go +++ b/loading_test.go @@ -224,18 +224,6 @@ func TestNewErrors(t *testing.T) { files map[string]string want string }{ - "separator without repeat": { - map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`}, - `drop "separator"`, - }, - "separator with an explicit repeat of 1": { - map[string]string{"a": `{"format":"{x}","x":"1","separator":","}`}, - `drop "separator"`, - }, - "weight outside a choice": { - map[string]string{"a": `{"format":"x","weight":5}`}, - `drop "weight"`, - }, "non-numeric weight outside a choice": { map[string]string{"a": `{"format":"x","weight":"bad"}`}, "takes a number, not a string", @@ -341,6 +329,8 @@ 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 a02f9e7..2af1da6 100644 --- a/name_test.go +++ b/name_test.go @@ -191,7 +191,7 @@ func TestNameErrors(t *testing.T) { {"read once through a field path that does not resolve", map[string]string{"card": `{"format":"{place as p}{p.zz}","place":{"format":"{x}","x":["a","b"]}}`}, `no field "zz"`}, {"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`}, - `nothing reads name "w"`}, + ""}, {"one arm read twice in an alternation", map[string]string{"cat": `{"format":"{w|w}","w":["a","b"]}`, "card": `"{/cat as n}{n} {n.w}"`}, ""}, {"a field and a path into it in one alternation", map[string]string{"cat": `{"format":"{w|w.x}","w":{"format":"{x}","x":["a","b"]}}`, "card": `"{/cat as n}{n} {n.w}"`}, diff --git a/node_test.go b/node_test.go index d405dde..c9bbef5 100644 --- a/node_test.go +++ b/node_test.go @@ -2,14 +2,14 @@ package fejkdata import ( "regexp" - "strings" "testing" ) -func TestWeightZeroIsRejected(t *testing.T) { - _, err := resolved(t, `[{"format":"X","weight":0},"Y"]`) - if err == nil || !strings.Contains(err.Error(), "weight 0") || !strings.Contains(err.Error(), "remove") { - t.Fatalf("compile(weight 0) = %v, want it rejected naming the fix", err) +func TestAChoiceWithNothingToDrawIsRejected(t *testing.T) { + for _, src := range []string{`[{"format":"A","weight":0},{"format":"B","weight":0}]`, `[]`} { + if _, err := resolved(t, src); err == nil { + t.Errorf("compile(%s) = nil error, want a choice with nothing to draw refused", src) + } } } @@ -56,8 +56,6 @@ func TestNodeCompileErrors(t *testing.T) { for _, bad := range []string{ `{"x":"Q"}`, // object without "format" `{"format":"{y}","x":1}`, // a field is a bare number - `[1, 2]`, // a choice of numbers - `5`, // unsupported node type `[]`, // empty choice `[{"format":"A","weight":-1},"B"]`, // negative weight `[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero diff --git a/spellings_test.go b/spellings_test.go index d26b94b..e49a7e7 100644 --- a/spellings_test.go +++ b/spellings_test.go @@ -31,16 +31,47 @@ func TestOneItemChoiceIsItsItem(t *testing.T) { } } -func TestAWeightWithNothingToDrawAgainstIsRejected(t *testing.T) { - for _, src := range []string{ - `{"format":"a","weight":2}`, - `{"format":"{x}","x":{"format":"a","weight":2}}`, - `[{"format":"a","weight":2}]`, - `{"format":"a","weight":0}`, +func TestAPartThatDoesNothingIsIgnored(t *testing.T) { + for long, short := range map[string]string{ + `{"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"],"separator":","}`: `{"format":"{x}","x":["a","b"]}`, + `{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: `{"format":"{n}","repeat":2,"n":"1"}`, + `[{"format":"1","datatype":"integer"},"x"]`: `["1","x"]`, + `null`: `""`, + `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `{"format":"{p}","p":{"format":"{x}","x":["","a"]}}`, + `[{"format":"a","weight":0},"b"]`: `"b"`, + `"{int(5,5)}"`: `"5"`, + `"{float(1,1,2)}"`: `"1.00"`, + `"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: `"1990-01-01"`, + `"{date(1990-01-01,1990-12-31,'x')}"`: `"x"`, + `"{time('x')}"`: `"x"`, } { - _, err := resolved(t, src) - if err == nil || !strings.Contains(err.Error(), `drop "weight"`) || strings.Contains(err.Error(), "can never be a field") || strings.Contains(err.Error(), "only skews a choice's items") { - t.Errorf("compile(%s) = %v, want the weight refused as drawn against nothing, naming the drop", src, err) + sameRenders(t, long, short) + } + for _, src := range []string{`"{date(1990-01-01,1990-12-31,'15:04')}"`, `"{time('2006-01-02 15:04')}"`} { + if _, err := resolved(t, src); err != nil { + t.Errorf("compile(%s) = %v, want a layout of either kind rendered", src, err) + } + } +} + +func TestAJSONNumberOrBooleanIsItsText(t *testing.T) { + f := newGenerator(t, writeData(t, map[string]string{ + "n": `{"format":"{a} {b} {c} {d}","a":5,"b":1.50,"c":12345678901234567890,"d":true}`, + "l": `[1e3]`, + }), WithSeed(1)) + if v := fake(t, f, "n"); v != "5 1.50 12345678901234567890 true" { + t.Errorf("n = %q, want each number and boolean as written", v) + } + if v := fake(t, f, "l"); v != "1e3" { + t.Errorf("l = %q, want 1e3 as written", v) + } + for in, want := range map[string]string{"42": "42", " false ": "false", "[7]": "7"} { + if v, err := f.FakeTemplate(in); err != nil || v != want { + t.Errorf("FakeTemplate(%q) = %q, %v, want %q", in, v, err, want) } } } @@ -62,16 +93,6 @@ func TestARepeatedItemCountsAsWritten(t *testing.T) { } } -func TestASeparatorWithoutARepeatIsRejected(t *testing.T) { - for src, want := range map[string]string{ - `{"format":"{x}","x":"v","separator":","}`: `drop "separator"`, - } { - if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want) - } - } -} - func TestADefaultWrittenOutIsTheDefault(t *testing.T) { for long, short := range map[string]string{ `{"format":"Malmö"}`: `"Malmö"`, diff --git a/struct_test.go b/struct_test.go index 863e4c0..92c4d17 100644 --- a/struct_test.go +++ b/struct_test.go @@ -253,6 +253,13 @@ func TestFakeStructTakesTheDatatypeItsGoTypeSets(t *testing.T) { } } +func TestFakeStructWithNoTagsFillsNothing(t *testing.T) { + v := struct{ A string }{A: "kept"} + if err := structData(t).FakeStruct(&v); err != nil || v.A != "kept" { + t.Errorf("FakeStruct = %+v, %v, want nothing filled", v, err) + } +} + func TestFakeStructErrors(t *testing.T) { f := structData(t) for _, c := range []struct { @@ -263,7 +270,6 @@ func TestFakeStructErrors(t *testing.T) { {(*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"}, diff --git a/table_test.go b/table_test.go index c0869ce..938c8e2 100644 --- a/table_test.go +++ b/table_test.go @@ -531,6 +531,15 @@ func TestATableOfOneRowOrNamedByItsKeyLoads(t *testing.T) { } } +func TestARowOfWeightZeroIsNeverDrawn(t *testing.T) { + f := newGenerator(t, writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t0\ny\t2\n"}), WithSeed(1)) + for i := 0; i < 50; i++ { + if v := fake(t, f, "t"); v != "y" { + t.Fatalf("t = %q, want only the row of a positive weight", v) + } + } +} + func TestTableFences(t *testing.T) { base := map[string]string{ "region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`, @@ -541,7 +550,6 @@ func TestTableFences(t *testing.T) { want string }{ "rows names a missing file": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`}, "t.tsv"}, - "a TSV nothing names": {with(base, map[string]string{"stray.tsv": "a\nx\n"}), "stray.tsv"}, "rows outside its folder": {with(base, map[string]string{"t.json": `{"format":"{code}","rows":"../region.tsv"}`}), "beside"}, "rows not a tsv": {with(base, map[string]string{"t.json": `{"format":"{code}","rows":"region.txt"}`, "region.txt": "code\n1\n"}), ".tsv"}, "key names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`}, @@ -557,7 +565,6 @@ func TestTableFences(t *testing.T) { "a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`}, "a descendant named like an ancestor's column": {with(geo(), map[string]string{"region.tsv": "code\tname\tpopulation\tlocality\n01\tStockholms län\t2400000\tx\n12\tSkåne län\t1400000\ty\n14\tVästra Götalands län\t1750000\tz\n"}), `"locality"`}, "weight not a number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\tmany\ny\t2\n"}, `"many"`}, - "weight zero": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t0\ny\t2\n"}, "0"}, "weight negative": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t-1\ny\t2\n"}, "-1"}, "reserved column name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\tb.c\nx\ty\n"}, `"b.c"`}, "duplicate column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\ta\nx\ty\n"}, `"a"`},