From 91b65e3ded724e5e97566e79bcfecd4ed5ea82bc Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 7 Oct 2026 22:23:18 +0200 Subject: [PATCH] Test that each spelling goal 5.7 no longer refuses loads as its shorter form --- builtins_test.go | 27 +++++++----- cmd/fejkdata/main_test.go | 12 ++++- datatype_test.go | 21 ++++++--- inline_test.go | 80 +++++++++++++++++----------------- internal/proven/proven_test.go | 5 +-- internal/rows/rows_test.go | 13 +++++- name_test.go | 12 ++--- node_test.go | 19 +++----- one_spelling_test.go | 55 ++++++++++++++++------- path_test.go | 13 ++++++ pick_test.go | 21 ++++++--- struct_test.go | 33 +++++++++----- table_test.go | 46 ++++++++----------- 13 files changed, 214 insertions(+), 143 deletions(-) diff --git a/builtins_test.go b/builtins_test.go index 1162ae7..b6d237e 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -398,8 +398,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 - `"{digits(05)}"`, // no leading zero - `"{int(+1,5)}"`, // a bound is a plain integer `"{int(5,5)}"`, // a constant is written as text `"{float(1,1,2)}"`, // a constant is written as text `"{iban(US)}"`, // unsupported country @@ -412,15 +410,20 @@ func TestBuiltinCompileErrors(t *testing.T) { } func TestArgErrorsNameTheSpelling(t *testing.T) { - for src, want := range map[string]string{ - `"{float(1,2,02)}"`: "write 2", - `{"format":"{calc(a,02)}","a":"1"}`: "write 2", - `"{digits(+5)}"`: "write 5", - `"{hex(99999999999999999999)}"`: "exceeds the maximum", - `"{int(007,9)}"`: "write 7", - } { - if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("compile(%s) = %v, want an error saying %q", src, err, want) - } + if _, err := resolved(t, `"{hex(99999999999999999999)}"`); err == nil || !strings.Contains(err.Error(), "exceeds the maximum") { + t.Errorf("compile(hex past the range) = %v, want the maximum named", err) + } +} + +func TestASignOrLeadingZeroIsTheInteger(t *testing.T) { + for long, short := range map[string]string{ + `"{float(1,2,02)}"`: `"{float(1,2,2)}"`, + `{"format":"{calc(a,02)}","a":"1"}`: `{"format":"{calc(a,2)}","a":"1"}`, + `"{digits(+5)}"`: `"{digits(5)}"`, + `"{digits(05)}"`: `"{digits(5)}"`, + `"{int(007,9)}"`: `"{int(7,9)}"`, + `"{int(+1,5)}"`: `"{int(1,5)}"`, + } { + sameRenders(t, long, short) } } diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index a08e81b..8637126 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -475,10 +475,20 @@ func TestRunInlineTemplate(t *testing.T) { } } +func TestRunFormatTakesOnlyALoneReferenceAsARecord(t *testing.T) { + for _, stdin := range []string{"{//sv_SE.person}", "{/sv_SE.person|/misc.uuid}", "{/sv_SE.person as p}", "{/}", "{/.sv_SE.person}"} { + if code, out, errb := runOut(stdin, "--format", "json"); code != 2 || out != "" { + t.Errorf("run(%q, --format json) = %d, %q, %q; want misuse", stdin, code, out, errb) + } + } + if code, out, errb := runOut(`{"format":"x"}`); code != 0 || out != "x" { + t.Errorf(`run({"format":"x"}) = %d, %q, %q; want x`, code, out, errb) + } +} + func TestRunTemplateMisuse(t *testing.T) { for stdin, want := range map[string]string{ "{bad": "unterminated", - `{"format":"x"}`: "is a string", "name: {/no.such.path}": "no entry", "a } b": "}}", "42": "number", diff --git a/datatype_test.go b/datatype_test.go index 07fc3f4..1c1bfa2 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) { +func TestDatatypeSitsOnlyInAColumn(t *testing.T) { for _, src := range []string{ `{"format":"","age":{"format":"{int(18,99)}","datatype":"integer"}}`, `{"format":"","n":{"format":"42","datatype":"integer"}}`, @@ -17,20 +17,20 @@ func TestDatatypeAndNullSitOnlyInAColumn(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) } } for src, want := range map[string]string{ - `{"format":"","n":{"format":"1","datatype":"int"}}`: `datatype takes "integer", "number" or "boolean", got "int"`, + `{"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"}}`: "datatype only types a record column", - `null`: `so write ""`, - `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, } { 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) @@ -89,8 +89,6 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { {"an operand past the limit", `{"format":"{calc(a)}","a":"{digits(400)}","datatype":"number"}`, "is not proven within 1e300"}, {"a whole calc past int64", `{"format":"{calc(a * 2)}","a":"{seq()}","datatype":"integer"}`, "{calc(a * 2)} is not proven within int64"}, {"a whole float past int64", `{"format":"{float(0,1e19,0)}","datatype":"integer"}`, "{float(0,1e19,0)} is not proven within int64"}, - {"a signed zero integer", `{"format":"-0","datatype":"integer"}`, `"-0" is zero written with a sign; write "0"`}, - {"a signed zero number", `{"format":"-0.00","datatype":"number"}`, `"-0.00" is zero written with a sign; write "0.00"`}, } { row := `{"format":"","col":` + c.column + `}` files := map[string]string{"row": row} @@ -224,3 +222,14 @@ func TestNullColumn(t *testing.T) { func TestDisagreementRefusesTwoTextItems(t *testing.T) { mustPanic(t, "two text items", func() { _ = disagreement(&template{}, DataTypeString, &template{}, DataTypeString) }) } + +func TestASignedZeroIsZero(t *testing.T) { + f := newGenerator(t, writeData(t, map[string]string{"row": `{"format":"","i":{"format":"-0","datatype":"integer"},"n":{"format":"-0.00","datatype":"number"}}`})) + r, err := f.FakeRecord("row") + if err != nil { + t.Fatal(err) + } + if c := r.Columns(); c[0].Value != "-0" || c[1].Value != "-0.00" { + t.Errorf("columns = %+v, want each written as its format", c) + } +} diff --git a/inline_test.go b/inline_test.go index ae9612f..98d7cd0 100644 --- a/inline_test.go +++ b/inline_test.go @@ -107,7 +107,6 @@ func TestFakeTemplateErrors(t *testing.T) { {`"{digits(0)}"`, "must be positive"}, {`name: {/no.such.path}`, "no entry"}, {`name: {..nope}`, "write {/nope}"}, - {`{"format":"x"}`, "is a string"}, } { _, err := f.FakeTemplate(c.input) if err == nil || !strings.Contains(err.Error(), c.want) { @@ -124,12 +123,15 @@ func TestFakeTemplateJSONString(t *testing.T) { } } -func TestPaddedJSONIsRejected(t *testing.T) { +func TestPaddedJSONIsTheJSON(t *testing.T) { f := shipped(t) in := `{"format":"{x}","x":["a","b"]}` - _, err := f.NewTemplate(" " + in + " ") - if err == nil || !strings.Contains(err.Error(), "write "+in) { - t.Fatalf("NewTemplate(padded JSON) = %v, want an error naming the unpadded spelling", err) + padded, err := f.NewTemplate(" \t" + in + "\n ") + if err != nil { + t.Fatalf("NewTemplate(padded JSON) = %v, want the JSON it pads", err) + } + if got := padded.Fake(); got != "a" && got != "b" { + t.Fatalf("padded JSON rendered %q, want a or b with no padding", got) } } @@ -150,47 +152,45 @@ func TestNewTemplateReusable(t *testing.T) { func TestIsTemplate(t *testing.T) { for arg, want := range map[string]bool{ - "sv_SE.person": false, - "person.last": false, - "name: {x}": true, - `{"format":"x"}`: true, - `["a","b"]`: true, - `[1, 2]`: true, - ` ["a","b"]`: true, // padding is the template's own error, not a shape verdict - `"hello"`: true, - "{/a}{/b}": true, - "{/a|/b}": true, - "{uppercase(/a)}": true, - "{{/a}}": true, - `"{/a} x"`: true, - "x[1]": false, // a row selected by key or name - "x[St. Louis].y": false, + "sv_SE.person": false, + "person.last": false, + "name: {x}": true, + `{"format":"x"}`: true, + `["a","b"]`: true, + `[1, 2]`: true, + ` ["a","b"]`: true, // padding is the template's own error, not a shape verdict + `"hello"`: true, + "{/a}{/b}": true, + "{/a|/b}": true, + "{uppercase(/a)}": true, + "{{/a}}": true, + `"{/a} x"`: true, + "x[1]": false, // a row selected by key or name + "x[St. Louis].y": false, + "{/sv_SE.person.last}": true, + `"{/sv_SE.person}"`: true, + "{.person.last}": true, + ` "{/sv_SE.person}"`: true, + `{"format":"{/sv_SE.person}"}`: true, + "{//sv_SE.person}": true, + "{/misc.currency[US Dollar (Next day)]}": true, + "/sv_SE.person": false, + "//sv_SE.person": false, } { if got, err := IsTemplate(arg); err != nil || got != want { t.Errorf("IsTemplate(%q) = %v, %v; want %v", arg, got, err, want) } } for arg, want := range map[string]string{ - "[abc]": `starts with "["`, - "[abc].field": `starts with "["`, - "x[1]y": `"]"`, - "x[[1]]": `"["`, - "x[]": "empty", - "a]b": `holds a "]"`, - "a}b": `holds a "}"`, - `"abc`: `holds a "\""`, - `"a]b`: `holds a "\""`, // the opener the reader typed, not the bracket behind it - "{/sv_SE.person.last}": "{/sv_SE.person.last} is the path sv_SE.person.last written as a template; write sv_SE.person.last", - `"{/sv_SE.person}"`: "write sv_SE.person", - "{.person.last}": "write person.last", - "/sv_SE.person": "write sv_SE.person", - ` "{/sv_SE.person}"`: "write sv_SE.person", - `{"format":"{/sv_SE.person}"}`: "write sv_SE.person", - "//sv_SE.person": "write sv_SE.person", - "{//sv_SE.person}": "{//sv_SE.person} is the path sv_SE.person written as a template; write sv_SE.person", - "/sv_SE/person": `path "sv_SE/person" contains "/"`, - "{/-}": `path "-" is reserved`, - "{/misc.currency[US Dollar (Next day)]}": "write misc.currency[US Dollar (Next day)]", + "[abc]": `starts with "["`, + "[abc].field": `starts with "["`, + "x[1]y": `"]"`, + "x[[1]]": `"["`, + "x[]": "empty", + "a]b": `holds a "]"`, + "a}b": `holds a "}"`, + `"abc`: `holds a "\""`, + `"a]b`: `holds a "\""`, // the opener the reader typed, not the bracket behind it } { if _, err := IsTemplate(arg); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("IsTemplate(%q) = %v; want it rejected naming %s", arg, err, want) diff --git a/internal/proven/proven_test.go b/internal/proven/proven_test.go index b1fdb0e..661cd46 100644 --- a/internal/proven/proven_test.go +++ b/internal/proven/proven_test.go @@ -17,7 +17,7 @@ func TestLiteralSaysWhichDatatypesItsTextIs(t *testing.T) { {"1.5", [datatype.Count]bool{datatype.Integer: true, datatype.Boolean: true}}, {"true", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true}}, {"007", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true, datatype.Boolean: true}}, - {"-0", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true, datatype.Boolean: true}}, + {"-0", [datatype.Count]bool{datatype.Boolean: true}}, } { v := Literal(c.text) for d := datatype.Integer; d < datatype.Count; d++ { @@ -26,9 +26,6 @@ func TestLiteralSaysWhichDatatypesItsTextIs(t *testing.T) { } } } - if v := Literal("-0"); v.Not[datatype.Integer] != `"-0" is zero written with a sign; write "0"` { - t.Errorf("Literal(-0) = %q, want the unsigned spelling named", v.Not[datatype.Integer]) - } } func TestOrKeepsTheBoundsOfBothAndTheFirstReason(t *testing.T) { diff --git a/internal/rows/rows_test.go b/internal/rows/rows_test.go index 23c419e..bc5c911 100644 --- a/internal/rows/rows_test.go +++ b/internal/rows/rows_test.go @@ -34,10 +34,9 @@ func TestParseRefuses(t *testing.T) { want string }{ {"no header", "", Options{}, "no header line"}, - {"one row", "a\n1\n", Options{}, "has one row"}, + {"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`}, - {"the key named as name", "k\nx\ny\n", Options{Key: "k", Name: "k"}, "name names the key column"}, {"a weight of zero", "k\tw\nx\t1\ny\t0\n", Options{Weight: "w"}, `weight "0" is not a positive number`}, {"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"`}, @@ -48,6 +47,16 @@ func TestParseRefuses(t *testing.T) { } } +func TestParseAccepts(t *testing.T) { + if tb := parse(t, "t", "a\n1\n", Options{}); tb.Len() != 1 { + t.Errorf("one row: Len = %d, want 1", tb.Len()) + } + tb := parse(t, "t", "k\nx\ny\n", Options{Key: "k", Name: "k"}) + if r, err := tb.find("y", nil); err != nil || r != 1 { + t.Errorf("the key named as name: find(y) = %d, %v, want row 1", r, err) + } +} + func TestLinkRefusesAParentRowNoChildLinksTo(t *testing.T) { country := parse(t, "country", "code\nSE\nNO\n", Options{Key: "code"}) city := parse(t, "city", "country\tname\nSE\tLund\nSE\tBorg\n", Options{Parent: "country"}) diff --git a/name_test.go b/name_test.go index 801fdaf..63d5660 100644 --- a/name_test.go +++ b/name_test.go @@ -187,7 +187,7 @@ func TestNameErrors(t *testing.T) { {"no reference or field", map[string]string{"card": `{"format":"{word as w}{w}{w}","words":["a","b"]}`}, `no field "word"; a binding names a field, or a reference`}, {"read once through a field", map[string]string{"card": `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`}, - `name "p" is read once, so it keeps no pick for another read; write {place.x} where it is read`}, + ""}, {"read once through a path that does not resolve", map[string]string{"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word as a}{a.zz}"`}, `no field "zz"`}, {"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"]}}`}, @@ -199,11 +199,11 @@ func TestNameErrors(t *testing.T) { {"a misspelt name", map[string]string{"word": `["a","b"]`, "card": `"{/word as p}{p}{p}{pp}"`}, `no field or name "pp"; the names bound here are "p"`}, {"read once through a path", map[string]string{"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word as a}{a.w} {/word.w}"`}, - `write {/word.w} where it is read`}, + ""}, {"read once whole, bound to a reference path", map[string]string{"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word.w as c}{c} {/word.w}"`}, - `write {/word.w} where it is read`}, + ""}, {"read once through a step up", map[string]string{"region.json": nameTables()["region.json"], "region.tsv": nameTables()["region.tsv"], "municipality.json": nameTables()["municipality.json"], "municipality.tsv": nameTables()["municipality.tsv"], "card": `"{/municipality as m}{m..region.name}"`}, - `write {/municipality..region.name} where it is read`}, + ""}, {"read once as a calc operand", map[string]string{"word": `["1","2"]`, "card": `"{/word as w}{calc(w * 2)}"`}, ""}, {"read outside a repeat in the root choice", map[string]string{"word": `["a","b"]`, "card": `[{"format":"{/word as p}{p}{p}","repeat":2},"{p}"]`}, @@ -211,9 +211,9 @@ func TestNameErrors(t *testing.T) { {"an empty name", map[string]string{"word": `["a","b"]`, "card": `"{/word as }"`}, "a binding names nothing"}, {"read once whole", map[string]string{"word": `["a","b"]`, "card": `{"format":"{/word as w}{x}","x":"{w}"}`}, - `write {/word} where it is read`}, + ""}, {"read once whole, as a transform's operand", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}{uppercase(w)}"`}, - `write /word where it is read`}, + ""}, {"rendered twice where a path reads it", map[string]string{"word": `{"format":"{w}-{w}","w":["a","b"]}`, "card": `"{/word as n}{n}|{n.w}"`}, `{n} renders field "w" twice`}, {"a binding in a cell", map[string]string{"word": `["a","b"]`, "t.json": `{"format":"{k}","rows":"t.tsv","key":"k"}`, "t.tsv": "k\tc\nx\t{/word as w}{w}{w}\ny\tz\n"}, diff --git a/node_test.go b/node_test.go index 74f8de9..c2f13aa 100644 --- a/node_test.go +++ b/node_test.go @@ -54,17 +54,13 @@ func TestNodeCompileErrors(t *testing.T) { // Every structural problem is caught up front, at compile/New time, never // deferred to a random render that happens to hit the bad branch. 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 - `["x"]`, // a one-item choice is its item - `["a","a"]`, // a repeated item is a weight - `{"format":"x"}`, // an object holding only a format is a string - `[{"format":"a","weight":1},"b"]`, // weight 1 is the default - `{"format":"{x}","x":"v","repeat":1}`, // repeat 1 is the default - `[{"format":"A","weight":-1},"B"]`, // negative weight + `{"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 + `["a","a"]`, // a repeated item is a weight + `[{"format":"A","weight":-1},"B"]`, // negative weight `[{"format":"A","weight":0},{"format":"B","weight":0}]`, // weights sum to zero `[{"format":"A","weight":1e308},{"format":"B","weight":1e308}]`, // weights overflow to +Inf `{"format":"A","weight":"heavy"}`, // non-numeric weight @@ -73,7 +69,6 @@ func TestNodeCompileErrors(t *testing.T) { `{"format":"x","repeat":1.5}`, // non-integer repeat `{"format":"x","repeat":"two"}`, // non-numeric repeat `{"format":"x","repeat":2,"separator":5}`, // non-string separator - `{"format":"{x}","x":"v","repeat":2,"separator":""}`, // separator "" is the default } { if _, err := resolved(t, bad); err == nil { t.Errorf("compile(%s) = nil error, want error", bad) diff --git a/one_spelling_test.go b/one_spelling_test.go index 2c9d11e..c4ea6ed 100644 --- a/one_spelling_test.go +++ b/one_spelling_test.go @@ -5,14 +5,32 @@ import ( "testing" ) -func TestOneItemChoiceIsRejected(t *testing.T) { - for _, src := range []string{`["x"]`, `[{"format":"{d}","d":"x"}]`, `{"format":"{w}","w":["only"]}`} { - if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), "one-item choice") { - t.Errorf("compile(%s) = %v, want the one-item choice rejected", src, err) +// sameRenders holds long to render as short does, draw for draw under one seed. +func sameRenders(t *testing.T, long, short string) { + t.Helper() + l, s := compiled(t, long), compiled(t, short) + fl, fs := engine(1), engine(1) + for i := 0; i < 20; i++ { + if a, b := renderOnce(fl.drawState, l), renderOnce(fs.drawState, s); a != b { + t.Fatalf("%s rendered %q where %s rendered %q", long, a, short, b) } } } +func TestOneItemChoiceIsItsItem(t *testing.T) { + for long, short := range map[string]string{ + `["x"]`: `"x"`, + `[{"format":"{d}","d":["x","y"]}]`: `{"format":"{d}","d":["x","y"]}`, + `{"format":"{w}","w":["only"]}`: `{"format":"{w}","w":"only"}`, + `{"format":"{w}","w":[["a","b"]]}`: `{"format":"{w}","w":["a","b"]}`, + } { + sameRenders(t, long, short) + } + if r, isTemplate := compiled(t, `[{"format":"","d":"x"}]`).(*template); !isTemplate || !r.isRecord { + t.Error("a one-item choice of a record is no record, want the record it holds") + } +} + func TestRepeatedChoiceItemIsRejected(t *testing.T) { for src, want := range map[string]string{ `["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`, @@ -31,22 +49,29 @@ func TestRepeatedChoiceItemIsRejected(t *testing.T) { func TestInertObjectIsRejected(t *testing.T) { for src, want := range map[string]string{ - `{"format":"Malmö"}`: `write "Malmö"`, - `{"format":"{digits(3)}"}`: `write "{digits(3)}"`, - `[{"format":"a","weight":1},"b"]`: "weight 1", - `{"format":"{x}","x":"v","repeat":1}`: "repeat 1", - `{"format":"{x}","x":"v","separator":","}`: "separator", - `{"format":"{x}","x":"v","repeat":2,"separator":""}`: "default", - `{"format":"","n":{"format":"1","datatype":"string"}}`: `datatype "string" is the default`, + `{"format":"{x}","x":"v","separator":","}`: "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) } } - for _, ok := range []string{`[{"format":"a","weight":2},"b"]`, `{"format":"ab","repeat":2}`, `{"format":"{x}","x":"v"}`, `"{digits(3)}"`} { - if _, err := compile(parse(t, ok)); err != nil { - t.Errorf("compile(%s) = %v", ok, err) - } +} + +func TestADefaultWrittenOutIsTheDefault(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":"{x}","x":["a","b"],"repeat":1}`: `{"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"]}}`, + `null`: `""`, + `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `{"format":"{p}","p":{"format":"{x}","x":["","a"]}}`, + } { + 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") } } diff --git a/path_test.go b/path_test.go index 98ebe8e..008758d 100644 --- a/path_test.go +++ b/path_test.go @@ -184,3 +184,16 @@ func TestFakePathNavigation(t *testing.T) { t.Error("Fake(missing) = nil error, want unknown-category error") } } + +func TestALeadingSlashIsThePath(t *testing.T) { + f := newGenerator(t, writeData(t, map[string]string{"person": `{"format":"{first}","first":"Ada"}`}), WithSeed(1)) + for _, path := range []string{"/person", "//person.first"} { + if v := fake(t, f, path); v != "Ada" { + t.Errorf("Fake(%s) = %q, want Ada", path, v) + } + } + r, err := f.FakeRecord("/person") + if err != nil || len(r.Columns()) != 1 || r.Columns()[0].Value != "Ada" { + t.Errorf("FakeRecord(/person) = %+v, %v, want the record person", r, err) + } +} diff --git a/pick_test.go b/pick_test.go index dd12dbb..7d6ef1d 100644 --- a/pick_test.go +++ b/pick_test.go @@ -117,12 +117,8 @@ func TestANamedFieldInARepeatPicksEachLine(t *testing.T) { func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) { for src, want := range map[string]string{ - `{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`, - `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `write {place.x} where it is read`, - `{"format":"{place as p}{p}","place":["x","y"]}`: `write {place} where it is read`, - `{"format":"{n}|{x}","x":{"format":"{a as n}{a}","a":["1","2"]}}`: `write {x.a} where it is read`, - `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`, - `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, + `{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`, + `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, } { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s = %v, want it refused naming %s", src, err, want) @@ -130,6 +126,19 @@ func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) { } } +func TestANameReadOnceIsWhatItBinds(t *testing.T) { + for long, short := range map[string]string{ + `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `{"format":"{place.x}","place":{"format":"{x}","x":["a","b"]}}`, + `{"format":"{place as p}{p}","place":["x","y"]}`: `{"format":"{place}","place":["x","y"]}`, + `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `{"format":"{calc(place * 2)}","place":["1","2"]}`, + } { + sameRenders(t, long, short) + } + if _, err := resolved(t, `{"format":"{n}|{x}","x":{"format":"{a as n}{a}","a":["1","2"]}}`); err != nil { + t.Errorf("a name bound in a field and read once above it = %v, want it loaded", err) + } +} + func TestCycleReachedOnlyByAPathTokenIsRejected(t *testing.T) { // A path token renders what it lands on, not the level it started from, so the // cycle walk has to follow it there. diff --git a/struct_test.go b/struct_test.go index bec3cb3..287464f 100644 --- a/struct_test.go +++ b/struct_test.go @@ -222,6 +222,27 @@ func TestFakeStructLeavesAPointerBackAlone(t *testing.T) { } } +func TestFakeStructReadsAPathInEachSpelling(t *testing.T) { + v := struct { + Path string `fake:"person.first"` + Ref string `fake:"{/person.first}"` + Quoted string `fake:"\"{/person.first}\""` + Slash string `fake:"/person.first"` + Kept string `fake:"-"` + }{Kept: "kept"} + if err := structData(t).FakeStruct(&v); err != nil { + t.Fatal(err) + } + for name, got := range map[string]string{"Path": v.Path, "Ref": v.Ref, "Quoted": v.Quoted, "Slash": v.Slash} { + if got != "Ada" && got != "Bo" { + t.Errorf("%s = %q, want a first name", name, got) + } + } + if v.Kept != "kept" { + t.Errorf(`Kept = %q, want fake:"-" to leave it as it was`, v.Kept) + } +} + func TestFakeStructErrors(t *testing.T) { f := structData(t) for _, c := range []struct { @@ -287,12 +308,6 @@ func TestFakeStructErrors(t *testing.T) { {&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 "|"`}, @@ -305,12 +320,6 @@ func TestFakeStructErrors(t *testing.T) { {&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 diff --git a/table_test.go b/table_test.go index 389e7a3..c0869ce 100644 --- a/table_test.go +++ b/table_test.go @@ -518,6 +518,19 @@ func TestSelectedRowsAgreeAcrossSpellings(t *testing.T) { } } +func TestATableOfOneRowOrNamedByItsKeyLoads(t *testing.T) { + f := newGenerator(t, writeFiles(t, map[string]string{ + "one.json": `{"format":"{a}","rows":"one.tsv"}`, "one.tsv": "a\nx\n", + "kn.json": `{"format":"{a}","rows":"kn.tsv","key":"a","name":"a"}`, "kn.tsv": "a\nx\ny\n", + }), WithSeed(1)) + if v := fake(t, f, "one"); v != "x" { + t.Errorf("one = %q, want its one row", v) + } + if v := fake(t, f, "kn[y]"); v != "y" { + t.Errorf("kn[y] = %q, want the row keyed y", v) + } +} + func TestTableFences(t *testing.T) { base := map[string]string{ "region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`, @@ -535,7 +548,6 @@ func TestTableFences(t *testing.T) { "name names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`}, "weight names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`}, "parent names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`}, - "key equals name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"a"}`, "t.tsv": "a\nx\ny\n"}, "drop"}, "duplicate key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\nx\nx\n"}, `"x"`}, "empty key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\tb\n\ty\nx\tz\n"}, "empty"}, "a bracket in a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\nx[1]\ny\n"}, `"["`}, @@ -554,7 +566,6 @@ func TestTableFences(t *testing.T) { "no rows": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n"}, "no rows"}, "a blank line after the rows": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\tb\nx\t1\ny\t2\n\n"}, "line 4"}, "a blank line in one column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n\ny\n"}, "line 3"}, - "one row": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n"}, "one row"}, "empty file": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": ""}, "header"}, "parent is not a table": {map[string]string{"p.json": `"x"`, "t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\tx\n"}, "not a table"}, "parent does not exist": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\tx\n"}, `"p"`}, @@ -647,34 +658,15 @@ func TestTableFences(t *testing.T) { } } -func TestSameShapedChoiceIsATable(t *testing.T) { +func TestSameShapedChoiceIsAChoice(t *testing.T) { rows := `[{"format":"{name}","name":"Sweden","alpha2":"SE"},{"format":"{name}","name":"Norway","alpha2":"NO"},{"format":"{name}","name":"Denmark","alpha2":"DK"}]` - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"country": rows}))) - for _, want := range []string{"country.tsv", `"rows"`, `alpha2\tname`, "3 "} { - if err == nil || !strings.Contains(err.Error(), want) { - t.Fatalf("New(same-shaped choice) = %v, want it refused mentioning %q", err, want) - } - } weighted := `[{"format":"{name}","name":"Sweden","weight":2},{"format":"{name}","name":"Norway"}]` - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"country": weighted}))); err == nil || !strings.Contains(err.Error(), "weight") { - t.Fatalf("New(weighted same-shaped choice) = %v, want it refused naming a weight column", err) + f := newGenerator(t, writeData(t, map[string]string{"country": rows, "weighted": weighted}), WithSeed(1)) + if v := fake(t, f, "country"); v != "Sweden" && v != "Norway" && v != "Denmark" { + t.Fatalf("country = %q, want one of its items", v) } - accepted := map[string]string{ - "nested": `{"format":"{c}","c":` + rows + `}`, - "a choice field": `[{"format":"{maker} {model}","maker":"BMW","model":["X3","X5"]},{"format":"{maker} {model}","maker":"Ford","model":["Focus","Fiesta"]}]`, - "different formats": `[{"format":"{a}-{b}","a":"1","b":"2"},{"format":"{b}-{a}","a":"3","b":"4"}]`, - "different fields": `[{"format":"{a}","a":"1","b":"2"},{"format":"{a}","a":"3"}]`, - "a string item": `[{"format":"{a}","a":"1"},"x"]`, - "strings": `["a","b","c"]`, - } - for name, json := range accepted { - if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"x": json}))); err != nil { - t.Errorf("%s: New = %v, want it accepted", name, err) - } - } - f := newGenerator(t, writeData(t, map[string]string{"w": `"x"`})) - if _, err := f.NewTemplate(rows); err != nil { - t.Fatalf("NewTemplate(same-shaped choice) = %v, want an inline template exempt", err) + if v := fake(t, f, "weighted"); v != "Sweden" && v != "Norway" { + t.Fatalf("weighted = %q, want one of its items", v) } }