Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
13 changed files with 214 additions and 143 deletions
Showing only changes of commit 91b65e3ded - Show all commits
+15 -12
View File
@@ -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)
}
}
+11 -1
View File
@@ -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",
+15 -6
View File
@@ -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)
}
}
+40 -40
View File
@@ -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)
+1 -4
View File
@@ -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) {
+11 -2
View File
@@ -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"})
+6 -6
View File
@@ -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"},
+7 -12
View File
@@ -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)
+40 -15
View File
@@ -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")
}
}
+13
View File
@@ -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)
}
}
+15 -6
View File
@@ -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.
+21 -12
View File
@@ -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
+19 -27
View File
@@ -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)
}
}