Test a declared datatype over a column read, repeats counting as written, null alone on stdin, one leading slash, and the CLI's lone reference against the grammar
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -175,7 +175,6 @@ 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)
|
||||
@@ -187,6 +186,7 @@ func TestIsTemplate(t *testing.T) {
|
||||
"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 "\""`,
|
||||
|
||||
+3
-23
@@ -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"},
|
||||
"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
@@ -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"]`,
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-3
@@ -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" {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
if _, err := resolved(t, `[{"format":"a","weight":2}, "b"]`); err != nil {
|
||||
t.Errorf("compile(weighted a, b) = %v", err)
|
||||
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 TestInertObjectIsRejected(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 TestASeparatorWithoutARepeatIsRejected(t *testing.T) {
|
||||
for src, want := range map[string]string{
|
||||
`{"format":"{x}","x":"v","separator":","}`: "separator",
|
||||
} {
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user