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 146 additions and 71 deletions
Showing only changes of commit 1a022dbd79 - Show all commits
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"testing"
"github.com/larvit/fejkdata/internal/grammar"
)
// grammarLoneReference is the path a format reads when the grammar parses it as one path
// read of one reference from the root, and nothing else.
func grammarLoneReference(text string) (string, bool) {
toks, err := grammar.ParseFormat(text)
if err != nil || len(toks) != 1 || toks[0].Kind != grammar.PathRead || len(toks[0].Arms) != 1 {
return "", false
}
sigil, path, err := grammar.RefShape(toks[0].Arms[0])
if err != nil || sigil != "/" {
return "", false
}
return path, true
}
func TestLoneReferenceAgreesWithTheGrammar(t *testing.T) {
for _, text := range []string{
"{/sv_SE.person}",
"{/misc.currency[US Dollar (Next day)]}",
"{/t[a as b].c}",
"{/t[f(x)]}",
"{/a}{/b}",
"{/x}}",
"{/}",
"{/.x}",
" {/x}",
"{/a|/b}",
"{/x as p}",
"{//x}",
"{/x(1)}",
"{.x}",
"x",
} {
path, lone := loneReference(text)
want, wantLone := grammarLoneReference(text)
if lone != wantLone || path != want {
t.Errorf("loneReference(%q) = %q, %v; the grammar reads %q, %v", text, path, lone, want, wantLone)
}
}
}
+3
View File
@@ -249,6 +249,7 @@ func TestRunTakesNoOperand(t *testing.T) {
want string
}{
{[]string{"sv_SE.person"}, "echo '{/sv_SE.person}' | fejkdata"},
{[]string{"/en_US.address"}, "echo '{/en_US.address}' | fejkdata"},
{[]string{"{/en_US.person}"}, "echo '{/en_US.person}' | fejkdata"},
{[]string{"name: {/sv_SE.person.last}"}, "echo 'name: {/sv_SE.person.last}' | fejkdata"},
{[]string{"geo.US.locality[O'Fallon].name"}, `echo '{/geo.US.locality[O'\''Fallon].name}' | fejkdata`},
@@ -494,6 +495,8 @@ func TestRunTemplateMisuse(t *testing.T) {
"42": "number",
"42\n": "number",
"true\n": "boolean",
"null\n": "only null",
" null ": "only null",
"{//sv_SE.person}": "write {/sv_SE.person}",
} {
code, out, errb := runOut(stdin, "--seed", "1")
+29 -6
View File
@@ -48,17 +48,16 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
{"a weighted item beside a typed one", `[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]`, `give it "datatype": "integer"`},
{"an item beside a typed column it reads", `["{/src.score}","5"]`, `write it as {"format":"5","datatype":"integer"}`},
{"a typed item beside a string column it reads", `["{/src.code}",{"format":"1","datatype":"integer"}]`, `write it as {"format":"{/src.code}","datatype":"integer"}`},
{"text beside a typed column it reads", `["{/src.score}","n/a"]`, `to read that column as text, write {"format":"{text}","text":"{/src.score}"}`},
{"a datatype over a typed column", `{"format":"{/src.score}","datatype":"integer"}`, `{/src.score} takes datatype integer from the column it reads; drop "datatype"`},
{"text beside a typed column it reads", `["{/src.score}","n/a"]`, `to read that column as text, write "{/src.score}" as {"format":"{/src.score}","datatype":"string"}`},
{"a datatype a typed column's values reject", `{"format":"{/src.score}","datatype":"boolean"}`, "{int(1,9)} prints an integer, not a boolean"},
{"two typed columns it reads", `["{/src.score}","{/src.flag}"]`, `so to read "{/src.score}" as text, write {"format":"{text}","text":"{/src.score}"}`},
{"two typed columns it reads", `["{/src.score}","{/src.flag}"]`, `so to read "{/src.score}" as text, write "{/src.score}" as {"format":"{/src.score}","datatype":"string"}`},
{"a typed column read that holds the datatype declared beside it", `[{"format":"1.5","datatype":"number"},"{/src.score}"]`, `so write "{/src.score}" as {"format":"{/src.score}","datatype":"number"}`},
{"a typed column read holding the datatype of another", `["{/src.ratio}","{/src.score}"]`, `so write "{/src.ratio}" as {"format":"{/src.ratio}","datatype":"integer"}`},
{"text before a typed column it reads", `["n/a","{/src.score}"]`, `to read that column as text, write {"format":"{text}","text":"{/src.score}"}`},
{"text before a typed column it reads", `["n/a","{/src.score}"]`, `to read that column as text, write "{/src.score}" as {"format":"{/src.score}","datatype":"string"}`},
{"a weighted item beside a typed column it reads", `["{/src.score}",{"format":"5","weight":2}]`, `give it "datatype": "integer"`},
{"a typed column read before the datatype it holds", `["{/src.score}",{"format":"1.5","datatype":"number"}]`, `so write "{/src.score}" as {"format":"{/src.score}","datatype":"number"}`},
{"a typed column read beside a datatype it does not hold", `["{/src.score}",{"format":"true","datatype":"boolean"}]`, `so to read "{/src.score}" as text, write {"format":"{text}","text":"{/src.score}"}`},
{"text beside a weighted typed column read", `[{"format":"{/src.score}","weight":3},"n/a"]`, `to read that column as text, set its "format" to "{text}" and add "text": "{/src.score}"`},
{"a typed column read beside a datatype it does not hold", `["{/src.score}",{"format":"true","datatype":"boolean"}]`, `so to read "{/src.score}" as text, write "{/src.score}" as {"format":"{/src.score}","datatype":"string"}`},
{"text beside a weighted typed column read", `[{"format":"{/src.score}","weight":3},"n/a"]`, `to read that column as text, give "{/src.score}" "datatype": "string"`},
{"a value of the column it reads", `{"format":"{/src.code}","datatype":"integer"}`, `"2x" is not an integer`},
{"a null read into text", `{"format":"{x}","x":"{/src.score}","datatype":"integer"}`, "reads a null"},
{"a sample with leading zeros", `{"format":"{digits(3)}","datatype":"integer"}`, "{digits(3)} prints text, not an integer"},
@@ -233,3 +232,27 @@ func TestASignedZeroIsZero(t *testing.T) {
t.Errorf("columns = %+v, want each written as its format", c)
}
}
func TestADeclaredDatatypeOverAColumnRead(t *testing.T) {
f := newGenerator(t, writeData(t, map[string]string{
"src": `{"format":"","score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
"row": `{"format":"","same":{"format":"{/src.score}","datatype":"integer"},"text":{"format":"{/src.score}","datatype":"string"}}`,
}), WithSeed(1))
empty := 0
for i := 0; i < 50; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
same, text := r.Columns()[0], r.Columns()[1]
if same.DataType != DataTypeInteger || text.DataType != DataTypeString || text.Null {
t.Fatalf("columns = %+v, want an integer column restated and a string column never null", r.Columns())
}
if text.Value == "" {
empty++
}
}
if empty == 0 {
t.Error(`50 records never read a null as "", want the string column to render one so`)
}
}
+7
View File
@@ -40,6 +40,9 @@ func TestPackageImports(t *testing.T) {
"internal/proven": {"internal/datatype", "internal/grammar", "internal/invariant"},
"internal/rows": {"internal/drawstate", "internal/grammar", "internal/invariant"},
}
testsAlso := map[string][]string{
"cmd/fejkdata": {"internal/grammar"},
}
seen := map[string]bool{}
eachPackage(t, func(dir string, pkg *build.Package) {
seen[dir] = true
@@ -49,6 +52,10 @@ func TestPackageImports(t *testing.T) {
return
}
for _, imp := range slices.Concat(pkg.Imports, pkg.TestImports, pkg.XTestImports) {
may := may
if !slices.Contains(pkg.Imports, imp) {
may = slices.Concat(may, testsAlso[dir])
}
if imp != module && !strings.HasPrefix(imp, module+"/") {
continue
}
+10 -10
View File
@@ -175,22 +175,22 @@ func TestIsTemplate(t *testing.T) {
"{//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
"[abc]": `starts with "["`,
"[abc].field": `starts with "["`,
"x[1]y": `"]"`,
"x[[1]]": `"["`,
"x[]": "empty",
"//sv_SE.person": "write /sv_SE.person",
"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)
+4 -24
View File
@@ -302,27 +302,6 @@ func TestNewErrors(t *testing.T) {
map[string]string{"a": `{"format":"{x}","x":"1","-":"2"}`},
`field "-" is reserved`,
},
// A repeated arm skews an alternation, which weight is the spelling for.
"repeated alternation arm": {
map[string]string{"a": `{"format":"{x|x}","x":"1"}`},
`arm "x" is repeated`,
},
"repeated arm among others": {
map[string]string{"a": `{"format":"{x|y|x}","x":"1","y":"2"}`},
`arm "x" is repeated`,
},
"repeated path arm": {
map[string]string{"a": `{"format":"{p.v|p.v}","p":{"format":"{v}","v":"1"}}`},
`arm "p.v" is repeated`,
},
// A reference arm is the only kind that reaches the repeat check by passing
// the per-arm checks rather than falling through them.
"repeated reference arm": {
map[string]string{"a": `"x"`, "b": `"{/a|/a}"`},
`arm "/a" is repeated`,
},
// An arm that is broken on its own terms is reported as that, not as a
// repeat: the repeat is a consequence of the real mistake.
"repeated arm with no path": {
map[string]string{"a": `"{/|/}"`},
"reference has no path",
@@ -361,9 +340,10 @@ func TestNewErrors(t *testing.T) {
"folder named Repeat": {map[string]string{"Repeat/cat": `"1"`}, "Repeat.cat", "1"},
"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 separate tokens is two independent draws, not a repeated
// arm; only a repeat within one alternation is rejected.
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":"1"}`}, "a", "11"},
"one name in two tokens": {map[string]string{"a": `{"format":"{x}{x}","x":"1"}`}, "a", "11"},
"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"},
}
for name, c := range accepted {
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
+22 -14
View File
@@ -186,8 +186,6 @@ func TestNameErrors(t *testing.T) {
`"format" is an option and can never be a name`},
{"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"]}}`},
""},
{"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"]}}`},
@@ -198,22 +196,10 @@ 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}"`},
""},
{"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}"`},
""},
{"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}"`},
""},
{"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}"]`},
`name "p" is bound at an item of the root choice, inside a repeat`},
{"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}"}`},
""},
{"read once whole, as a transform's operand", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}{uppercase(w)}"`},
""},
{"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"},
@@ -258,6 +244,28 @@ func TestNameErrors(t *testing.T) {
}
}
func TestANameReadOnceLoads(t *testing.T) {
for name, files := range map[string]map[string]string{
"through a path": {"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word as a}{a.w} {/word.w}"`},
"whole, bound to a reference path": {"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word.w as c}{c} {/word.w}"`},
"through a step up": {"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}"`},
"as a calc operand": {"word": `["1","2"]`, "card": `"{/word as w}{calc(w * 2)}"`},
"whole": {"word": `["a","b"]`, "card": `{"format":"{/word as w}{x}","x":"{w}"}`},
"whole, as a transform's operand": {"word": `["a","b"]`, "card": `"{/word as w}{uppercase(w)}"`},
} {
withJSON := map[string]string{}
for file, body := range files {
if !strings.Contains(file, ".") {
file += ".json"
}
withJSON[file] = body
}
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, withJSON))); err != nil {
t.Errorf("%s: New() = %v, want it to load", name, err)
}
}
}
func TestANameBindsAReferenceFromItsFolder(t *testing.T) {
dir := writeData(t, map[string]string{
"sv/word": `["a","b","c","d","e","f","g","h"]`,
-1
View File
@@ -59,7 +59,6 @@ func TestNodeCompileErrors(t *testing.T) {
`[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
+5 -4
View File
@@ -187,10 +187,11 @@ func TestFakePathNavigation(t *testing.T) {
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)
}
if v := fake(t, f, "/person"); v != "Ada" {
t.Errorf("Fake(/person) = %q, want Ada", v)
}
if _, err := f.Fake("//person"); err == nil || !strings.Contains(err.Error(), "write /person") {
t.Errorf("Fake(//person) = %v, want it refused naming /person", err)
}
r, err := f.FakeRecord("/person")
if err != nil || len(r.Columns()) != 1 || r.Columns()[0].Value != "Ada" {
+16 -12
View File
@@ -29,25 +29,29 @@ func TestOneItemChoiceIsItsItem(t *testing.T) {
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")
}
if _, err := resolved(t, `[{"format":"a","weight":2}]`); err == nil || !strings.Contains(err.Error(), "no effect here") {
t.Errorf("a weight on a one-item choice's item = %v, want it refused as doing nothing", err)
}
}
func TestRepeatedChoiceItemIsRejected(t *testing.T) {
for src, want := range map[string]string{
`["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`,
`[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item",
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
`{"format":"","w":[null,null,"a"]}`: "a null takes no weight",
} {
if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
func TestARepeatedItemCountsAsWritten(t *testing.T) {
for _, src := range []string{`["a", "a", "b"]`, `[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`, `{"format":"{w}","w":["", "", "x"]}`, `{"format":"","w":[null,null,"a"]}`, `{"format":"{x|x|y}","x":"1","y":"2"}`} {
if _, err := resolved(t, src); err != nil {
t.Errorf("compile(%s) = %v, want it loaded", src, err)
}
}
if _, err := resolved(t, `[{"format":"a","weight":2}, "b"]`); err != nil {
t.Errorf("compile(weighted a, b) = %v", err)
f, n, as := engine(1), compiled(t, `["a", "a", "b"]`), 0
for i := 0; i < 600; i++ {
if renderOnce(f.drawState, n) == "a" {
as++
}
}
if as < 340 || as > 460 {
t.Errorf(`["a","a","b"] drew a %d times in 600, want about 400`, as)
}
}
func TestInertObjectIsRejected(t *testing.T) {
func TestASeparatorWithoutARepeatIsRejected(t *testing.T) {
for src, want := range map[string]string{
`{"format":"{x}","x":"v","separator":","}`: "separator",
} {
+3
View File
@@ -269,6 +269,9 @@ func TestFakeStructErrors(t *testing.T) {
{&struct {
A int `fake:"{\"format\":\"{int(1,9)}\",\"datatype\":\"integer\"}"`
}{}, `its Go type int sets the datatype; drop "datatype"`},
{&struct {
A string `fake:"{\"format\":\"x\",\"datatype\":\"string\"}"`
}{}, `its Go type string sets the datatype; drop "datatype"`},
{&struct {
A int `fake:"[null,\"{int(1,9)}\"]"`
}{}, "can draw null, which int cannot hold; make it *int"},