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
11 changed files with 94 additions and 82 deletions
Showing only changes of commit 052a52fc7a - Show all commits
+5 -14
View File
@@ -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
} {
+18 -13
View File
@@ -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...)
-6
View File
@@ -27,12 +27,6 @@ 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 ""`,
} {
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)
+3 -4
View File
@@ -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)
}
}
+2 -1
View File
@@ -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"`},
} {
+2 -12
View File
@@ -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"},
+1 -1
View File
@@ -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}"`},
+5 -7
View File
@@ -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
+40 -19
View File
@@ -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ö"`,
+7 -1
View File
@@ -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"},
+9 -2
View File
@@ -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"`},