Test that every {…} draws afresh and only a name keeps a pick, a field's and a struct's path tags included
This commit is contained in:
+18
-36
@@ -123,13 +123,11 @@ func TestCalcGuardsPanic(t *testing.T) {
|
||||
|
||||
// --- one draw, one value: a calc operand reads the expansion's draw ---
|
||||
|
||||
// TestCalcOperandReadsTheExpansionsDraw pins the one-draw rule for a calc operand.
|
||||
// A field the format renders and a calc reads is drawn once per expansion, so the
|
||||
// operand shown is the operand computed — the correlation the dotted-path rule
|
||||
// already gives a level, applied to a plain sibling.
|
||||
func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
||||
// TestANamedCalcOperandIsTheOneShown pins how a calc computes from the value its format
|
||||
// shows: the format and the calc read one name.
|
||||
func TestANamedCalcOperandIsTheOneShown(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||
"inv": `{"format":"{net as n}{qty as q}{n} x {q} = {calc(n * q, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(3))
|
||||
for i := 0; i < 300; i++ {
|
||||
@@ -144,39 +142,23 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcOperandSharesOneDraw pins the reach of that hold: the draw belongs to the
|
||||
// expansion, not to the calc, so the bare token rendering the same name reads it too.
|
||||
func TestCalcOperandSharesOneDraw(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||
})
|
||||
// TestACalcOperandDrawsAfresh pins that a calc reading a field draws it as any {…} does.
|
||||
func TestACalcOperandDrawsAfresh(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`})
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fake(t, f, "same")
|
||||
if p := strings.Fields(got); len(p) != 2 || p[0] != p[1] {
|
||||
t.Fatalf("same = %q, want one value twice", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFieldNoCalcReadsDrawsEachTime guards the boundary: only a name a calc reads is
|
||||
// held, so an ordinary {w} {w} still draws twice.
|
||||
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 200; i++ {
|
||||
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
|
||||
if p := strings.Fields(fake(t, f, "same")); p[0] != p[1] {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("{w} {w} never differed in 200 draws, want two independent draws")
|
||||
t.Fatal("{w} {calc(w)} never differed in 200 draws, want two independent draws")
|
||||
}
|
||||
|
||||
// TestCalcHoldIsPerExpansion pins the scope of the hold: each repeat iteration is
|
||||
// its own expansion, so it draws again while staying self-consistent.
|
||||
func TestCalcHoldIsPerExpansion(t *testing.T) {
|
||||
// TestANamedCalcOperandPicksEachIteration pins that a name a repeat binds picks again on each
|
||||
// iteration, staying one value within it.
|
||||
func TestANamedCalcOperandPicksEachIteration(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
||||
"rep": `{"format":"{n as m}{m}={calc(m * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(11))
|
||||
varied := false
|
||||
@@ -197,12 +179,12 @@ func TestCalcHoldIsPerExpansion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcHoldIsPerTemplate pins that a nested template holds its own: the inner
|
||||
// {v} and its calc agree with each other, not with the outer pair.
|
||||
func TestCalcHoldIsPerTemplate(t *testing.T) {
|
||||
// TestTwoNamesPickApart pins that a nested template's name is a pick of its own: the inner
|
||||
// pair agrees with itself, not with the outer pair.
|
||||
func TestTwoNamesPickApart(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"nest": `{"format":"{v}={calc(v * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
|
||||
"inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
||||
"nest": `{"format":"{v as a}{a}={calc(a * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
|
||||
"inner":{"format":"{v as b}{b}={calc(b * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(13))
|
||||
differed := false
|
||||
|
||||
+23
-16
@@ -268,7 +268,7 @@ func TestShippedPersonNames(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 2000; i++ {
|
||||
seen[fake(t, f, locale+".sex[f].first-name")] = true
|
||||
got := fakeTemplate(t, f, `{/`+locale+`.person.first}|{/`+locale+`.person.sex}`)
|
||||
got := fakeTemplate(t, f, `{/`+locale+`.person as p}{p.first}|{p.sex}`)
|
||||
first, sex, _ := strings.Cut(got, "|")
|
||||
fake(t, f, locale+".sex["+sex+"].first-name["+first+"]")
|
||||
}
|
||||
@@ -330,35 +330,42 @@ func TestShippedSwedishAddress(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestShippedAddressIsOneNamedPick pins that an address's street, postal code,
|
||||
// locality and region are one place in the geo tables, and that two names bound to
|
||||
// one address category are two addresses.
|
||||
// locality and region are one place in the geo tables, read through a name or a struct's
|
||||
// tags, and that a reference beside the name is another address.
|
||||
func TestShippedAddressIsOneNamedPick(t *testing.T) {
|
||||
f := newGenerator(t, "data", WithSeed(3))
|
||||
for _, c := range []struct{ locale, country, template, largest string }{
|
||||
{"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}||{b.street}", "Stockholm"},
|
||||
{"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}|{b.street}", "New York"},
|
||||
{"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}|", "Stockholm"},
|
||||
{"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}", "New York"},
|
||||
} {
|
||||
places := geoPlaces(t, c.country)
|
||||
apart, localities := false, map[string]int{}
|
||||
for i := 0; i < 2000; i++ {
|
||||
got := fakeTemplate(t, f, `{/`+c.locale+`.address as a}{/`+c.locale+`.address as b}`+c.template)
|
||||
got := fakeTemplate(t, f, `{/`+c.locale+`.address as a}`+c.template+`|{/`+c.locale+`.address.street}`)
|
||||
parts := strings.Split(got, "|")
|
||||
if !places[strings.Join(parts[:4], "|")] {
|
||||
t.Fatalf("%s.address: %q is no street, postal code, locality and region of one place", c.locale, got)
|
||||
}
|
||||
apart = apart || parts[0] != parts[4]
|
||||
localities[parts[2]]++
|
||||
region := "{/" + c.locale + ".address.region}"
|
||||
if c.country == "SE" {
|
||||
region = ""
|
||||
}
|
||||
got = fakeTemplate(t, f, `{/`+c.locale+`.address.street}|{/`+c.locale+`.address.postal-code}|{/`+c.locale+`.address.locality}|`+region)
|
||||
if !places[got] {
|
||||
t.Fatalf("%s.address: field paths %q are no street, postal code, locality and region of one place", c.locale, got)
|
||||
}
|
||||
}
|
||||
if !apart {
|
||||
t.Errorf("%s: two names bound to address drew one street in 2000 renders, want two addresses", c.locale)
|
||||
t.Errorf("%s: a reference beside a name drew the name's street in 2000 renders, want two addresses", c.locale)
|
||||
}
|
||||
if c.country == "SE" {
|
||||
var row struct {
|
||||
Street string `fake:"sv_SE.address.street"`
|
||||
PostalCode string `fake:"sv_SE.address.postal-code"`
|
||||
Locality string `fake:"sv_SE.address.locality"`
|
||||
}
|
||||
for i := 0; i < 200; i++ {
|
||||
if err := f.FakeStruct(&row); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := row.Street + "|" + row.PostalCode + "|" + row.Locality + "|"; !places[got] {
|
||||
t.Fatalf("sv_SE.address: struct tags %q are no street, postal code and locality of one place", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
for l, n := range localities {
|
||||
if n > localities[c.largest] {
|
||||
@@ -545,7 +552,7 @@ func TestShippedUSTitleAgreesWithSex(t *testing.T) {
|
||||
female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true}
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 3000; i++ {
|
||||
got := fakeTemplate(t, f, `{/en_US.person.prefix}|{/en_US.person.sex}`)
|
||||
got := fakeTemplate(t, f, `{/en_US.person as p}{p.prefix}|{p.sex}`)
|
||||
title, sex, _ := strings.Cut(got, "|")
|
||||
title = strings.TrimSpace(title)
|
||||
seen[title] = true
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func fenceCorpus(t *testing.T) *Generator {
|
||||
t.Helper()
|
||||
dir := writeFiles(t, with(geo(), map[string]string{
|
||||
"addr.json": `{"format":"{a} {b} {c}","a":"{/locality.name}, {/municipality.name}","b":{"format":"{/region[12].name} {/place}","drawGroup":"g"},"c":{"format":"{/place.zip} ","repeat":2}}`,
|
||||
"place.json": `{"format":"{zip} {name} {tag} {/w}","rows":"place.tsv","key":"name"}`,
|
||||
"place.tsv": "name\tzip\ttag\nStockholm\t1{digits(2)} {digits(2)}\t{/x}\nTranås\t573 {digits(2)}\t{/region[12].name}\n",
|
||||
"rec.json": `{"format":"","l":"{/locality.name}","m":"{/municipality.name}","t":"{/place[Stockholm].tag}"}`,
|
||||
"w.json": `["x","y"]`,
|
||||
"x.json": `"{/w}"`,
|
||||
}))
|
||||
f, err := New(WithDataPath(dir), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v", err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type fenceRoot struct {
|
||||
label string
|
||||
t *template
|
||||
}
|
||||
|
||||
func fenceRoots(t *testing.T, f *Generator) []fenceRoot {
|
||||
t.Helper()
|
||||
var roots []fenceRoot
|
||||
_ = sitesScope(categorySites(&f.root))(func(path string, n node) error {
|
||||
if tm, ok := n.(*template); ok {
|
||||
roots = append(roots, fenceRoot{path, tm})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
inline, err := f.NewTemplate(`{"format":"{x}|{y}","x":{"format":"{/locality.name}","drawGroup":"g"},"y":"{/region[01].name}"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTemplate = %v", err)
|
||||
}
|
||||
record, err := f.NewRecordTemplate(`{"format":"","a":"{/locality.name}","b":"{/municipality.name}"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRecordTemplate = %v", err)
|
||||
}
|
||||
_ = eachNode(inline.n, "inline", func(path string, n node) error {
|
||||
if tm, ok := n.(*template); ok {
|
||||
roots = append(roots, fenceRoot{path, tm})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return append(roots, fenceRoot{"inline record", record.template})
|
||||
}
|
||||
|
||||
func TestPairsFindWhatAWholeReplayFinds(t *testing.T) {
|
||||
corpus := fenceCorpus(t)
|
||||
roots := fenceRoots(t, corpus)
|
||||
shipped := newShippedWhole(t)
|
||||
_ = sitesScope(categorySites(&shipped.root))(func(path string, n node) error {
|
||||
if tm, ok := n.(*template); ok {
|
||||
roots = append(roots, fenceRoot{path, tm})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
compared := 0
|
||||
fold := newReadFold()
|
||||
for _, root := range roots {
|
||||
compared += wantPairsFindAll(t, root.label, surveyRender(fold, root.t).reads)
|
||||
if root.t.isRecord && len(root.t.fields) > 0 {
|
||||
compared += wantPairsFindAll(t, root.label+" as a record", surveyColumns(fold, root.t, sortedNames(root.t.fields)).reads)
|
||||
}
|
||||
}
|
||||
if compared == 0 {
|
||||
t.Error("no read set rendering together holds two table reads, so the comparison proved nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairsFindWhatAWholeReplayOfSelectedRowsFinds(t *testing.T) {
|
||||
f, err := New(WithDataPath(writeFiles(t, siblings())))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v", err)
|
||||
}
|
||||
var reads []pathRead
|
||||
fold := newReadFold()
|
||||
_ = sitesScope(categorySites(&f.root))(func(_ string, n node) error {
|
||||
tb, isTable := n.(*table)
|
||||
if !isTable || tb.keyIndex < 0 || tb.familyRoot().path != "region" {
|
||||
return nil
|
||||
}
|
||||
for r := 0; r < tb.rowCount(); r++ {
|
||||
inline, err := f.NewTemplate(`"{/` + tb.selectorSpelling(r) + `}"`)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTemplate(%s) = %v", tb.selectorSpelling(r), err)
|
||||
}
|
||||
root, isTemplate := inline.n.(*template)
|
||||
if !isTemplate {
|
||||
t.Fatalf("NewTemplate(%s) compiled to %T", tb.selectorSpelling(r), inline.n)
|
||||
}
|
||||
reads = append(reads, surveyRender(fold, root).reads...)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
conflicts := 0
|
||||
for _, a := range reads {
|
||||
for _, b := range reads {
|
||||
for _, c := range reads {
|
||||
triple := []pathRead{a, b, c}
|
||||
wantPairsFindAll(t, a.a.spelling+", "+b.a.spelling+", "+c.a.spelling, triple)
|
||||
if (&readSurvey{reads: triple}).check() != nil {
|
||||
conflicts++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if conflicts == 0 {
|
||||
t.Error("no triple of selected rows conflicts, so the comparison proved nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// wantPairsFindAll counts the read sets rendering together that hold two table reads or more.
|
||||
func wantPairsFindAll(t *testing.T, label string, reads []pathRead) int {
|
||||
t.Helper()
|
||||
var tabled []pathRead
|
||||
for _, r := range reads {
|
||||
if r.tr != nil {
|
||||
tabled = append(tabled, r)
|
||||
}
|
||||
}
|
||||
pairs := (&readSurvey{reads: reads}).check()
|
||||
var whole error
|
||||
compared := 0
|
||||
eachRenderSet(tabled, func(set []pathRead) {
|
||||
if len(set) > 1 {
|
||||
compared++
|
||||
}
|
||||
var d pinSet
|
||||
for _, r := range set {
|
||||
if err := r.tr.replay(&d); err != nil && whole == nil {
|
||||
whole = err
|
||||
}
|
||||
}
|
||||
})
|
||||
if (pairs == nil) != (whole == nil) {
|
||||
t.Errorf("%s: check = %v, a whole replay of every read set rendering together = %v", label, pairs, whole)
|
||||
}
|
||||
return compared
|
||||
}
|
||||
|
||||
// eachRenderSet calls fn with every maximal set of reads that render together, by Bron–Kerbosch.
|
||||
func eachRenderSet(reads []pathRead, fn func([]pathRead)) {
|
||||
var grow func(set, candidates, excluded []pathRead)
|
||||
grow = func(set, candidates, excluded []pathRead) {
|
||||
if len(candidates) == 0 && len(excluded) == 0 {
|
||||
fn(set)
|
||||
return
|
||||
}
|
||||
for len(candidates) > 0 {
|
||||
r := candidates[0]
|
||||
var nextC, nextX []pathRead
|
||||
for _, c := range candidates[1:] {
|
||||
if coRender(r, c) {
|
||||
nextC = append(nextC, c)
|
||||
}
|
||||
}
|
||||
for _, x := range excluded {
|
||||
if coRender(r, x) {
|
||||
nextX = append(nextX, x)
|
||||
}
|
||||
}
|
||||
grow(append(set[:len(set):len(set)], r), nextC, nextX)
|
||||
candidates, excluded = candidates[1:], append(excluded, r)
|
||||
}
|
||||
}
|
||||
grow(nil, reads, nil)
|
||||
}
|
||||
|
||||
func TestReadSurveyReportsAPinClashAsAnError(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"r.json": {Data: []byte(`"{/y[a].v}"`)},
|
||||
"m.json": {Data: []byte(`{"format":"{g}{f}","f":"{/y[a].v}","g":"{/y[a].v}"}`)},
|
||||
"n.json": {Data: []byte(`{"format":"{f}","f":"{/y[a].v}"}`)},
|
||||
"x.json": {Data: []byte(`{"format":"{v}","rows":"x.tsv","key":"code"}`)},
|
||||
"x.tsv": {Data: []byte("code\tv\n1\tb\n2\tc\n")},
|
||||
"y.json": {Data: []byte(`{"format":"{v}","rows":"y.tsv","key":"code","parent":"x"}`)},
|
||||
"y.tsv": {Data: []byte("code\tv\tx\na\t{/x[2].v}\t1\nb\tz\t2\n")},
|
||||
}
|
||||
g, err := loadDir(dataSource{fsys: fsys}, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("loadDir = %v", err)
|
||||
}
|
||||
if err := categoryBinding(categorySites(g), g.children).link(); err != nil {
|
||||
t.Fatalf("link = %v", err)
|
||||
}
|
||||
r, isTemplate := g.children["r"].(*template)
|
||||
if !isTemplate {
|
||||
t.Fatalf("r is a %T, want a template", g.children["r"])
|
||||
}
|
||||
if err := surveyRender(newReadFold(), r).check(); err == nil || !strings.Contains(err.Error(), "two rows of") {
|
||||
t.Errorf("check = %v, want y[a]'s row of x clashing with x[2] named, whatever order the fences run in", err)
|
||||
}
|
||||
n, isTemplate := g.children["n"].(*template)
|
||||
if !isTemplate {
|
||||
t.Fatalf("n is a %T, want a template", g.children["n"])
|
||||
}
|
||||
if err := surveyRender(newReadFold(), n).check(); err == nil || !strings.HasPrefix(err.Error(), "{f} with {/x[2].v}: x[1] and x[2] are two rows of x") {
|
||||
t.Errorf("check = %v, want the clash below {f} named by the route the root reaches it by", err)
|
||||
}
|
||||
m, isTemplate := g.children["m"].(*template)
|
||||
if !isTemplate {
|
||||
t.Fatalf("m is a %T, want a template", g.children["m"])
|
||||
}
|
||||
if err := surveyRender(newReadFold(), m).check(); err == nil || !strings.HasPrefix(err.Error(), "{f} with") {
|
||||
t.Errorf("check = %v, want the clash of the lowest route named, {f}, whichever the walk reaches first", err)
|
||||
}
|
||||
}
|
||||
|
||||
func surveyRender(f *readFold, t *template) *readSurvey {
|
||||
return survey(f.rootReads(t), t.link.drawGroupKey)
|
||||
}
|
||||
|
||||
func surveyColumns(f *readFold, t *template, columns []string) *readSurvey {
|
||||
return survey(f.columnReads(t, columns), t.link.drawGroupKey)
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
||||
// postal-code prefix that really belongs to it.
|
||||
const swedishPlaces = `{"format":"%s","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`
|
||||
|
||||
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
||||
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
||||
|
||||
func places(format string) string {
|
||||
return strings.Replace(swedishPlaces, "%s", format, 1)
|
||||
}
|
||||
|
||||
// varies renders src until two renders of it satisfy differ, failing after 400 tries.
|
||||
func varies(t *testing.T, f *Generator, src string, differ func(got string) bool) {
|
||||
t.Helper()
|
||||
for i := 0; i < 400; i++ {
|
||||
if differ(mustRender(t, f, src)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("400 renders of %s never drew apart, want each {…} a pick of its own", src)
|
||||
}
|
||||
|
||||
func TestEveryTokenDrawsAfresh(t *testing.T) {
|
||||
f := engine(3)
|
||||
pair := func(got string) bool { p := strings.Split(got, "|"); return p[0] != p[1] }
|
||||
varies(t, f, `{"format":"{w}|{w}","w":["a","b","c","d","e"]}`, pair)
|
||||
varies(t, f, `{"format":"{p.first}|{p.first}","p":{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}}`, pair)
|
||||
varies(t, f, `{"format":"{w}|{uppercase(w)}","w":["a","b","c","d","e"]}`, func(got string) bool {
|
||||
p := strings.Split(got, "|")
|
||||
return strings.ToUpper(p[0]) != p[1]
|
||||
})
|
||||
varies(t, f, `{"format":"{n}|{calc(n * 1)}","n":["1","2","3","4"]}`, pair)
|
||||
varies(t, f, places("{place.postal-code} {place.locality}"), func(got string) bool { return !agree.MatchString(got) })
|
||||
}
|
||||
|
||||
func TestEveryReferenceDrawsAfresh(t *testing.T) {
|
||||
f := newGenerator(t, writeData(t, map[string]string{
|
||||
"person": `{"format":"{first} {last}","first":["Ada","Bo","Cy","Di"],"last":["Byron","Ek","Lind","Ros"]}`,
|
||||
}), WithSeed(4))
|
||||
for _, src := range []string{
|
||||
`"{/person.first}|{/person.first}"`,
|
||||
`"{/person}|{/person.first} {/person.last}"`,
|
||||
`{"format":"{a}|{b}","a":"{/person.last}","b":"{/person.last}"}`,
|
||||
} {
|
||||
differ := false
|
||||
for i := 0; i < 400 && !differ; i++ {
|
||||
p := strings.Split(fakeTemplate(t, f, src), "|")
|
||||
differ = p[0] != p[1]
|
||||
}
|
||||
if !differ {
|
||||
t.Errorf("400 renders of %s never drew apart, want each reference a pick of its own", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestANamedFieldCorrelatesItsReads(t *testing.T) {
|
||||
f := engine(1)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, places("{place as p}{p.postal-code} {p.locality}"))
|
||||
if !agree.MatchString(got) {
|
||||
t.Fatalf("draw %d = %q, want a postcode and locality that pair", i, got)
|
||||
}
|
||||
seen[got[len(got)-4:]] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("500 draws produced only %v, want both localities", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestANamedFieldReadsItsVariantsWhole(t *testing.T) {
|
||||
// One variant holds addr as a plain template where another holds a choice; the name
|
||||
// keeps the variant drawn at every level its reads pass.
|
||||
f := engine(13)
|
||||
tmpl := `{"format":"{p as q}{q.addr.city}/{q.addr.zip}","p":[
|
||||
{"format":"{addr}","addr":{"format":"{city}","city":"Kiruna","zip":"98100"}},
|
||||
{"format":"{addr}","addr":[
|
||||
{"format":"{city}","city":"Malmö","zip":"21100"},
|
||||
{"format":"{city}","city":"Lund","zip":"22100"}]}]}`
|
||||
for i := 0; i < 400; i++ {
|
||||
got := mustRender(t, f, tmpl)
|
||||
if got != "Kiruna/98100" && got != "Malmö/21100" && got != "Lund/22100" {
|
||||
t.Fatalf("draw %d = %q, want a city with its own zip", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestANamedFieldInARepeatPicksEachLine(t *testing.T) {
|
||||
f := engine(5)
|
||||
tmpl := `{"format":"{lines}","lines":` + strings.Replace(places("{place as p}{p.postal-code} {p.locality}"), `"format":`, `"repeat":6,"separator":"\n","format":`, 1) + `}`
|
||||
varied := false
|
||||
for i := 0; i < 40; i++ {
|
||||
lines := strings.Split(mustRender(t, f, tmpl), "\n")
|
||||
if len(lines) != 6 {
|
||||
t.Fatalf("repeat 6 produced %d lines", len(lines))
|
||||
}
|
||||
for _, line := range lines {
|
||||
if !agree.MatchString(line) {
|
||||
t.Fatalf("line %q does not pair", line)
|
||||
}
|
||||
}
|
||||
varied = varied || lines[0] != lines[1] || lines[1] != lines[2]
|
||||
}
|
||||
if !varied {
|
||||
t.Fatal("40 renders of repeat 6 never varied within a render, want a pick per line")
|
||||
}
|
||||
}
|
||||
|
||||
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":"{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`,
|
||||
} {
|
||||
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s = %v, want it refused naming %s", src, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"a": `{"format":"{p.x}","p":{"format":"static","x":"{/b}"}}`,
|
||||
"b": `"{/a}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACycleInALaterVariantIsRejected(t *testing.T) {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}]}`,
|
||||
"hop": `"{/cat}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||
t.Fatalf("New = %v, want the cycle in the later variant rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycleThroughAPathTokenIsRejected(t *testing.T) {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/b}"}}`,
|
||||
"b": `"{/a}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADeepDiamondChainLoads(t *testing.T) {
|
||||
// A diamond chain is 2^n routes through n nodes, so every load walk must visit each
|
||||
// node once, not once per route.
|
||||
files := map[string]string{"l0": `"x"`}
|
||||
for i := 1; i <= 30; i++ {
|
||||
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
||||
`{"format":"{a}{b}","a":"{/l%d}","b":"{/l%d}"}`, i-1, i-1)
|
||||
}
|
||||
files["thing"] = `{"format":"{p.first} {/l30}","p":{"format":"x","first":["A","B"]}}`
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||
t.Fatalf("New = %v, want a deep diamond chain to load", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedChoiceDrawsOneVariant(t *testing.T) {
|
||||
// A choice item may itself be a choice, so a path unwraps until it reaches a value.
|
||||
f := engine(21)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 200; i++ {
|
||||
seen[mustRender(t, f, `{"format":"{p.x}","p":[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]}`)] = true
|
||||
}
|
||||
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
||||
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatingLevelIsNamedInTheError(t *testing.T) {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":"z"}}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
||||
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathIntoARepeatingLevelIsRejected(t *testing.T) {
|
||||
// A path reads one level's draw, so it can never apply that level's repeat, directly or
|
||||
// behind a choice.
|
||||
for _, cat := range []string{
|
||||
`{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":"z"}}`,
|
||||
`{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":"x"},{"format":"{a}","a":"y"}]}`,
|
||||
} {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat})))
|
||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||
t.Errorf("New(%s) = %v, want a path into a repeating level rejected", cat, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathIntoAPlainTemplateNamesTheMissingField(t *testing.T) {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{a.nope}","a":{"format":"x","b":"1"}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `no field "nope"`) {
|
||||
t.Fatalf("New = %v, want it to name the missing field", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADrawIsSeedStable(t *testing.T) {
|
||||
tmpl := places("{place as p}{p.postal-code} {p.locality}")
|
||||
if a, b := mustRender(t, engine(42), tmpl), mustRender(t, engine(42), tmpl); a != b {
|
||||
t.Fatalf("same seed gave %q and %q, want an identical draw", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDottedTokenErrors(t *testing.T) {
|
||||
rejected := map[string]struct {
|
||||
file string
|
||||
want string
|
||||
}{
|
||||
"unknown head": {
|
||||
`{"format":"{nope.x}","place":{"format":"{v}","v":"A"}}`,
|
||||
`no field "nope"`,
|
||||
},
|
||||
"unknown tail": {
|
||||
`{"format":"{place.nope}","place":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}]}`,
|
||||
`"nope"`,
|
||||
},
|
||||
"tail only some variants carry": {
|
||||
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},"x"]}`,
|
||||
`locality`,
|
||||
},
|
||||
"path into a literal": {
|
||||
`{"format":"{x.y}","x":"plain"}`,
|
||||
`"y"`,
|
||||
},
|
||||
"dotted field key": {
|
||||
`{"format":"[{a.b}]","a.b":"V"}`,
|
||||
`contains "."`,
|
||||
},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file})))
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
||||
for name, file := range map[string]string{
|
||||
"trailing dot": `{"format":"[{a.}]","a":"x"}`,
|
||||
"triple dot": `{"format":"[{a...b}]","a":"x"}`,
|
||||
} {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||
if err == nil || !strings.Contains(err.Error(), "empty segment") {
|
||||
t.Errorf("%s: New = %v, want it to name the empty segment", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestASubFieldIsReachableByFake(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"address": places("{place as p}{p.postal-code} {p.locality}")})
|
||||
f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(9))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v", err)
|
||||
}
|
||||
for _, path := range []string{"address", "address.place", "address.place.locality", "address.place.postal-code"} {
|
||||
if _, err := f.Fake(path); err != nil {
|
||||
t.Errorf("Fake(%q) = %v, want it to render", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ func TestSeededOutputIsStable(t *testing.T) {
|
||||
"funcs.json": `"{hex(6)} {int(10,99)} {float(0,1,3)} {nanoid(5)} {seq()}"`,
|
||||
"municipality.json": `{"format":"{name}","rows":"municipality.tsv","key":"code","name":"name","parent":"region"}`,
|
||||
"municipality.tsv": "code\tname\tregion\n0180\tStockholm\t01\n0184\tSolna\t01\n1280\tMalmö\t12\n1281\tLund\t12\n",
|
||||
"named.json": `{"format":"{p as q}{q.a}{q.b}","p":[{"format":"x","a":"1","b":"2"},{"format":"y","a":"A","b":"B"},{"format":"z","a":"a","b":"b"}]}`,
|
||||
"nested.json": `{"format":"{outer}","outer":{"format":"{inner}-{digits(2)}","inner":"i"}}`,
|
||||
"paths.json": `{"format":"{p.a}{p.b}","p":[{"format":"x","a":"1","b":"2"},{"format":"y","a":"A","b":"B"},{"format":"z","a":"a","b":"b"}]}`,
|
||||
"ref.json": `"see {/alt}"`,
|
||||
@@ -30,6 +31,7 @@ func TestSeededOutputIsStable(t *testing.T) {
|
||||
"classes": {"49-74-VY-gj", "38-68-RP-xo", "47-68-IB-hs", "91-89-LP-gk"},
|
||||
"escapes": {"01Aa#!", "01Aa#!", "01Aa#!", "01Aa#!"},
|
||||
"funcs": {"0016a2 33 0.649 k4Kwj 1", "a00c07 84 0.175 6UjGv 2", "f70eb8 12 0.390 p5p6g 3", "9048a3 90 0.531 I3Aqv 4"},
|
||||
"named": {"ab", "ab", "AB", "AB"},
|
||||
"nested": {"i-73", "i-23", "i-67", "i-85"},
|
||||
"paths": {"ab", "ab", "AB", "AB"},
|
||||
"paths.p.a": {"A", "a", "a", "A"},
|
||||
|
||||
-784
@@ -1,784 +0,0 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This file pins the one-draw rule for dotted sibling tokens. A format that
|
||||
// addresses a sibling by path ({place.locality}) draws that sibling once per
|
||||
// expansion, so every {place.*} token in the same format reads the same variant.
|
||||
// That is what makes a correlated pair — a locality and a postal code that agree
|
||||
// — expressible in data, without the row having to be the whole node.
|
||||
|
||||
// swedishPlaces is a two-variant sibling whose variants pair a locality with the
|
||||
// postal-code prefix that really belongs to it.
|
||||
const swedishPlaces = `{"format":"%s","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`
|
||||
|
||||
// agree reports whether a rendered "postcode locality" pair is a real pairing.
|
||||
var agree = regexp.MustCompile(`^(1[0-9]{2} [0-9]{2} Stockholm|573 [0-9]{2} Tranås)$`)
|
||||
|
||||
func places(format string) string {
|
||||
return strings.Replace(swedishPlaces, "%s", format, 1)
|
||||
}
|
||||
|
||||
func TestDottedTokenCorrelatesSiblingFields(t *testing.T) {
|
||||
f := engine(1)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, places("{place.postal-code} {place.locality}"))
|
||||
if !agree.MatchString(got) {
|
||||
t.Fatalf("draw %d = %q, want a postcode and locality that pair", i, got)
|
||||
}
|
||||
seen[got[len(got)-4:]] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("500 draws produced only %v, want both localities", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderingALevelAndReadingIntoItIsRejected(t *testing.T) {
|
||||
// One draw, one spelling. A token that renders a level and one that reads a
|
||||
// path into it name the same draw two ways, and only the path reads what the
|
||||
// other rendered — so the pair is a load error rather than a shape whose two
|
||||
// halves can disagree.
|
||||
rejected := map[string]string{
|
||||
"bare head beside a path": `{"format":"{p} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||
"prefix path beside a path": `{"format":"{p.addr}|{p.addr.city}","p":{"format":"{addr}","addr":[` +
|
||||
`{"format":"{city}","city":["Kiruna","Boden"]},{"format":"{city}","city":["Malmö","Lund"]}]}}`,
|
||||
"either order": `{"format":"{p.first} + {p}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||
}
|
||||
for name, file := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||
if err == nil {
|
||||
t.Errorf("%s: New = nil error, want the overlapping tokens rejected", name)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(err.Error(), "fejkdata: cat: ") || !strings.Contains(err.Error(), "renders a level that") {
|
||||
t.Errorf("%s: New = %v, want it to name the category path and the overlap", name, err)
|
||||
}
|
||||
}
|
||||
// The same path twice is one spelling, so it stays legal.
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{p.first} + {p.first}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||
}))); err != nil {
|
||||
t.Errorf("New = %v, want one path read twice accepted", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcOperandNamingAHeldLevelIsRejected(t *testing.T) {
|
||||
// A calc operand renders its field, so naming a held level in one is the same
|
||||
// overlap as a bare token: {calc(item * 1)} renders what {item.price} reads a
|
||||
// path into, and the two disagree.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{item.price}|{calc(item * 1)}","item":[` +
|
||||
`{"format":"{price}","price":"10"},{"format":"{price}","price":"20"}]}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "renders a level that") {
|
||||
t.Fatalf("New = %v, want the calc operand rejected as an overlap", err)
|
||||
}
|
||||
// Arithmetic over fields no path names is untouched.
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`,
|
||||
}))); err != nil {
|
||||
t.Errorf("New = %v, want plain arithmetic accepted", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceNamingAHeldLevelIsRejected(t *testing.T) {
|
||||
// A reference can name a held level from the data root, which renders it
|
||||
// afresh beside the path that reads its held draw — the same overlap by
|
||||
// another spelling.
|
||||
// A category never references itself, so each reference back into cat sits in a
|
||||
// second category cat renders.
|
||||
rejected := map[string]map[string]string{
|
||||
"reference names the head": {
|
||||
"cat": `{"format":"{p.first}|{/hop}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||
"hop": `"{/cat.p}"`,
|
||||
},
|
||||
"reference names the leaf": {
|
||||
"cat": `{"format":"{p.addr}|{/hop}","p":{"format":"x","addr":["A","B","C","D"]}}`,
|
||||
"hop": `"{/cat.p.addr}"`,
|
||||
},
|
||||
// The reference need not sit in the format that holds the level: any field it renders
|
||||
// reaches the level just the same, however deep.
|
||||
"reference from a sibling field": {
|
||||
"cat": `{"format":"{p.first}|{inner}","p":[` +
|
||||
`{"format":"{first}-{last}","first":"A","last":"1"},{"format":"{first}-{last}","first":"B","last":"2"}],` +
|
||||
`"inner":"{/hop}"}`,
|
||||
"hop": `"{/cat.p}"`,
|
||||
},
|
||||
}
|
||||
for name, files := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, files)))
|
||||
if err == nil || !strings.Contains(err.Error(), "by a second route") {
|
||||
t.Errorf("%s: New = %v, want the reference rejected as an overlap", name, err)
|
||||
}
|
||||
}
|
||||
// A reference to anything this format does not hold is untouched.
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{p.first} {/surname}","p":{"format":"{first}","first":["Anna","Bo"]}}`,
|
||||
"surname": `["Eriksson","Lindqvist"]`,
|
||||
}))); err != nil {
|
||||
t.Errorf("New = %v, want a reference outside the held level accepted", 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. A head whose own format names nothing
|
||||
// would otherwise hide the cycle until render, where it is fatal.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"a": `{"format":"{p.x}","p":{"format":"static","x":"{/b}"}}`,
|
||||
"b": `"{/a}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestALevelRenderedOnlyByAPathTokenIsHeld(t *testing.T) {
|
||||
// {p.a} renders q, so it is a route to the level {q.x} holds — even though p's
|
||||
// own format names nothing.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"thing": `{"format":"{p.a} {q.x}","p":{"format":"static","a":"{/hop}"},` +
|
||||
`"q":{"format":"{x}","x":["1","2"]}}`,
|
||||
"hop": `"{/thing.q}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "by a second route") {
|
||||
t.Fatalf("New = %v, want the second route to q rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestACalcOperandIsHeldAgainstEveryRoute pins what a {calc()} operand's hold
|
||||
// fences. A calc renders its operand whole, so the draw it holds is that one node's
|
||||
// value: another route conflicts only by naming that same node, and then the value
|
||||
// shown is not the value computed. Two names that merely draw from one source are
|
||||
// two draws, as {word} {word} is — the rule everywhere else in the engine.
|
||||
func TestACalcOperandIsHeldAgainstEveryRoute(t *testing.T) {
|
||||
rejected := map[string]struct {
|
||||
files map[string]string
|
||||
want string // the route the error names, spelled as the author wrote it
|
||||
}{
|
||||
// A category never references itself, so each route back into cat sits in a
|
||||
// second category cat renders.
|
||||
"a reference beside the operand": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{/hop} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||
"hop": `"{/cat.net}"`,
|
||||
},
|
||||
`{/hop} renders "net"`,
|
||||
},
|
||||
"a reference one level down": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(net * 2, 2)} {q}","net":["10.00","20.00"],` +
|
||||
`"q":"{/hop}"}`,
|
||||
"hop": `"{/cat.net}"`,
|
||||
},
|
||||
`{q} renders "net"`,
|
||||
},
|
||||
// A reference ending at the operand holds the choice itself, not a variant,
|
||||
// so wrapping the operand in one changes nothing.
|
||||
"a reference to an operand wrapped in a choice": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(n * 2, 2)} {q}","n":{"format":"{v}","v":["1","2"]},` +
|
||||
`"q":"{/hop}"}`,
|
||||
"hop": `"{/cat.n}"`,
|
||||
},
|
||||
`{q} renders "n"`,
|
||||
},
|
||||
// The reaching route can be another operand, which is not a token — so the
|
||||
// error must name it as one rather than invent a {b} the format never wrote.
|
||||
"an operand reaching another operand": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(a + b, 0)}","a":{"format":"{x}","x":["1","2"]},` +
|
||||
`"b":"{/hop}"}`,
|
||||
"hop": `"{/cat.a}"`,
|
||||
},
|
||||
`calc operand "b" renders "a", which {calc(a + b, 0)} holds to one draw for this expansion; stop calc operand "b" reaching "a"`,
|
||||
},
|
||||
// Reaching *into* the operand is the same shape as naming it: the draw the
|
||||
// calc holds settled that value too. The token spelling {net.v} is already
|
||||
// rejected, so the reference spelling has to be.
|
||||
"a reference into the operand": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(net * 2, 2)}|{/hop}",` +
|
||||
`"net":{"format":"{v}","v":["10.00","20.00"]}}`,
|
||||
"hop": `"{/cat.net.v}"`,
|
||||
},
|
||||
`{/hop} renders "net"`,
|
||||
},
|
||||
"a reference into the operand one level down": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(net * 2, 2)}|{q}","net":{"format":"{v}","v":["10.00","20.00"]},` +
|
||||
`"q":"{/hop}"}`,
|
||||
"hop": `"{/cat.net.v}"`,
|
||||
},
|
||||
`{q} renders "net"`,
|
||||
},
|
||||
// Each head is walked against its own seen set. Sharing one across heads
|
||||
// would let the first head's walk mark the only route to the second's draw,
|
||||
// and this violation sits on the later head in sorted order.
|
||||
"a violation on the second of two held heads": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{calc(a + b, 0)} {w}","a":{"format":"{x}","x":["1","2"]},` +
|
||||
`"b":{"format":"{y}","y":["3","4"]},"w":"{/hop}"}`,
|
||||
"hop": `"{/cat.b}"`,
|
||||
},
|
||||
`{w} renders "b"`,
|
||||
},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
||||
if err == nil || !strings.Contains(err.Error(), "holds to one draw for this expansion; ") {
|
||||
t.Errorf("%s: New = %v, want the second route to the operand rejected", name, err)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it to name %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
accepted := map[string]map[string]string{
|
||||
// The spellings that read the one draw: the operand itself, and a bare token.
|
||||
"only the operand and a bare token": {
|
||||
"cat": `{"format":"{net} x 2 = {calc(net * 2, 2)}","net":["10.00","20.00"]}`,
|
||||
},
|
||||
// A sibling the operand's format never renders is no part of its value. A
|
||||
// path head differs — a path may read into anything the level contains —
|
||||
// which is why that half of the fence covers containment.
|
||||
"a reference to a sibling the operand never renders": {
|
||||
"cat": `{"format":"{calc(net * 2, 2)} {unit}",` +
|
||||
`"net":{"format":"{v}","v":["1","2"],"spare":["kg","lb"]},` +
|
||||
`"unit":"{/hop}"}`,
|
||||
"hop": `"{/cat.net.spare}"`,
|
||||
},
|
||||
// Two operands drawing from one source are two names, so two draws: each is
|
||||
// held under its own name and shown once, and neither can disagree.
|
||||
"two operands sharing one source": {
|
||||
"die": `["1","2","3","4","5","6"]`,
|
||||
"cat": `{"format":"{d1} + {d2} = {calc(d1 + d2, 0)}","d1":"{/die}",` +
|
||||
`"d2":"{/die}"}`,
|
||||
},
|
||||
// Likewise a reference drawing from what the operand draws from: {/common}
|
||||
// and net are two names, not two spellings of one field.
|
||||
"a reference to what an operand renders through": {
|
||||
"common": `["1","2"]`,
|
||||
"cat": `{"format":"{calc(net * 2, 2)} {/common}","net":"{/common}"}`,
|
||||
},
|
||||
// A fixed string cannot disagree with itself, so it needs no fence.
|
||||
"a literal operand named twice": {
|
||||
"cat": `{"format":"{calc(n * 2, 0)} {/hop}","n":"5"}`,
|
||||
"hop": `"{/cat.n}"`,
|
||||
},
|
||||
// One node reached twice while walking the operand: the walk must not
|
||||
// revisit it, and the repeat is not a second route to anything.
|
||||
"an operand that renders one field twice": {
|
||||
"cat": `{"format":"{calc(v * 2, 0)}","v":{"format":"{a}{a}","a":["1","2"]}}`,
|
||||
},
|
||||
}
|
||||
for name, files := range accepted {
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||
t.Errorf("%s: New = %v, want it accepted", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPathReachesEveryVariantItMightDraw(t *testing.T) {
|
||||
// A path through a choice may land in any variant, so both walks have to see
|
||||
// all of them. Reaching only the first leaves whatever hides in a later
|
||||
// variant to be found at render — a cycle there is fatal, and a second route
|
||||
// to a held level disagrees silently.
|
||||
rejected := map[string]struct {
|
||||
files map[string]string
|
||||
want string
|
||||
}{
|
||||
"a cycle in a later variant": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{p.x}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}]}`,
|
||||
"hop": `"{/cat}"`,
|
||||
},
|
||||
"reference cycle",
|
||||
},
|
||||
"a second route in a later variant": {
|
||||
map[string]string{
|
||||
"cat": `{"format":"{p.x} {q.y}","p":[{"format":"h","x":"safe"},{"format":"h","x":"{/hop}"}],` +
|
||||
`"q":{"format":"{y}","y":["1","2"]}}`,
|
||||
"hop": `"{/cat.q}"`,
|
||||
},
|
||||
"by a second route",
|
||||
},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, c.files)))
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestALevelAPathNeverRendersIsAccepted(t *testing.T) {
|
||||
// A path token does not expand its head's format, so a reference sitting in
|
||||
// that format is not a second route to anything: it is never rendered by the
|
||||
// path at all. Both orders must load. p and q sit one level down, so they are
|
||||
// one column of thing rather than two that would read one another.
|
||||
accepted := map[string]map[string]string{
|
||||
"reference in the head's own format": {
|
||||
"thing": `{"format":"{inner}","inner":{"format":"{p.first} {q.a}",` +
|
||||
`"p":{"format":"{first} {/hop}","first":["A","B"]},"q":{"format":"{a}","a":["1","2"]}}}`,
|
||||
"hop": `"{/thing.inner.q}"`,
|
||||
},
|
||||
"the mirror shape": {
|
||||
"thing": `{"format":"{inner}","inner":{"format":"{p.first} {q.a}",` +
|
||||
`"p":{"format":"{first}","first":["A","B"]},"q":{"format":"{a} {/hop}","a":["1","2"]}}}`,
|
||||
"hop": `"{/thing.inner.p}"`,
|
||||
},
|
||||
}
|
||||
for name, files := range accepted {
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||
t.Errorf("%s: New = %v, want it accepted", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestADeepDiamondChainLoads(t *testing.T) {
|
||||
// A diamond chain is 2^n routes through n nodes. The search past a held level
|
||||
// must walk each node once, not once per route, or a deep chain never loads.
|
||||
files := map[string]string{"l0": `"x"`}
|
||||
for i := 1; i <= 30; i++ {
|
||||
files[fmt.Sprintf("l%d", i)] = fmt.Sprintf(
|
||||
`{"format":"{a}{b}","a":"{/l%d}","b":"{/l%d}"}`, i-1, i-1)
|
||||
}
|
||||
files["thing"] = `{"format":"{p.first} {/l30}","p":{"format":"x","first":["A","B"]}}`
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))); err != nil {
|
||||
t.Fatalf("New = %v, want a deep diamond chain to load", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestASharedNodeIsWalkedOnce(t *testing.T) {
|
||||
// Two fields reaching one node make the render graph a diamond, not a tree.
|
||||
// The search past a held level must take that in its stride rather than walk
|
||||
// the shared node once per route.
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{p.first}|{q}","p":{"format":"{first}","first":["Anna","Bo"]},` +
|
||||
`"q":{"format":"{a}{b}","a":"{/shared}","b":"{/shared}"}}`,
|
||||
"shared": `"x"`,
|
||||
})), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v, want a shared node accepted", err)
|
||||
}
|
||||
if got, err := f.Fake("cat"); err != nil || !strings.HasSuffix(got, "|xx") {
|
||||
t.Fatalf("Fake(cat) = %q, %v, want it to end in xx", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheEarlierReaderIsNamed(t *testing.T) {
|
||||
// A token and a calc operand can name one level. The one the format writes
|
||||
// first is the one reported, so the error points at the same place a reader
|
||||
// looking at the format would start.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":{"format":"{first}","first":["1","2"]}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
||||
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceToAMatchingStringIsAccepted(t *testing.T) {
|
||||
// A literal renders one fixed string, so no draw of it can disagree with a
|
||||
// held one. Two unrelated literals that merely spell the same text must not
|
||||
// read as the same node — the trap when comparing a value type.
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{p.city} {/other.tag}","p":{"format":"{city}","city":"Stockholm"}}`,
|
||||
"other": `{"format":"x","tag":"Stockholm"}`,
|
||||
}))); err != nil {
|
||||
t.Fatalf("New = %v, want a reference to a matching string accepted", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedChoiceDrawsOneVariant(t *testing.T) {
|
||||
// A choice item may itself be a choice, so drawing a held head unwraps until
|
||||
// it reaches a value. Stopping at one level leaves a choice where the walk
|
||||
// expects a template.
|
||||
f := engine(21)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 200; i++ {
|
||||
seen[mustRender(t, f, `{"format":"{p.x}","p":[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]}`)] = true
|
||||
}
|
||||
if !seen["1"] || !seen["2"] || len(seen) != 2 {
|
||||
t.Fatalf("200 draws produced %v, want 1 and 2", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatingLevelIsNamedInTheError(t *testing.T) {
|
||||
// The level carrying the repeat is the one to fix, so the error names it
|
||||
// rather than the head the path started from.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"[{p.a.b}]","p":{"format":"{a}","a":{"format":"{b}","repeat":3,"separator":",","b":"z"}}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `"p.a"`) {
|
||||
t.Fatalf("New = %v, want it to name the level p.a that carries the repeat", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathIntoARepeatingLevelBehindAChoiceIsRejected(t *testing.T) {
|
||||
// A choice of rows is the shape this feature is for, so the repeat rule has to
|
||||
// reach inside one — otherwise the direct spelling is a load error and the same
|
||||
// mistake behind a choice silently drops the repeat.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"[{p.a}]","p":[{"format":"{a}","repeat":3,"separator":",","a":"x"},{"format":"{a}","a":"y"}]}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||
t.Fatalf("New = %v, want the repeat behind a choice rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathIntoARepeatingLevelIsRejected(t *testing.T) {
|
||||
// A path reads one level's draw, so it can never apply that level's repeat.
|
||||
// The engine rejects options that cannot take effect, and this is one.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","repeat":3,"separator":",","a":"z"}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "repeat") {
|
||||
t.Fatalf("New = %v, want a path into a repeating level rejected", err)
|
||||
}
|
||||
_, err = New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"[{p.a}]","p":{"format":"{a}","a":"{/word.w}","drawGroup":"g"}}`,
|
||||
"word": `{"format":"{w}","w":["x","y"]}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `the level "p" carries a drawGroup`) {
|
||||
t.Fatalf("New = %v, want a path into a level carrying a group rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathIntoAPlainTemplateNamesTheMissingField(t *testing.T) {
|
||||
// A head that is one template, not a choice, reports the missing segment
|
||||
// directly — there are no variants to compare.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{a.nope}","a":{"format":"x","b":"1"}}`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), `no field "nope"`) {
|
||||
t.Fatalf("New = %v, want it to name the missing field", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldWithoutAPathTokenStillDrawsEachTime(t *testing.T) {
|
||||
// A format with no dotted token keeps today's behaviour: every {word} is an
|
||||
// independent draw.
|
||||
f := engine(3)
|
||||
varied := false
|
||||
for i := 0; i < 200 && !varied; i++ {
|
||||
got := mustRender(t, f, `{"format":"{w}{w}{w}","w":["a","b","c","d","e"]}`)
|
||||
varied = got[0] != got[1] || got[1] != got[2]
|
||||
}
|
||||
if !varied {
|
||||
t.Fatal("200 draws of {w}{w}{w} never varied, want independent draws")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoHeldHeadsDrawIndependently(t *testing.T) {
|
||||
// A hold is per head, so two correlated sets in one format compose instead
|
||||
// of needing a cross product of variants.
|
||||
f := engine(4)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 400; i++ {
|
||||
seen[mustRender(t, f, `{"format":"{p.v}{q.v}",
|
||||
"p":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}],
|
||||
"q":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}`)] = true
|
||||
}
|
||||
for _, want := range []string{"A1", "A2", "B1", "B2"} {
|
||||
if !seen[want] {
|
||||
t.Fatalf("400 draws produced %v, want every combination including %q", seen, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatRedrawsTheHold(t *testing.T) {
|
||||
// The hold lives for one expansion, so each repetition is a fresh draw —
|
||||
// and each repetition is internally consistent.
|
||||
f := engine(5)
|
||||
tmpl := strings.Replace(places("{place.postal-code} {place.locality}"), `"format":"{place.postal-code} {place.locality}"`,
|
||||
`"format":"{place.postal-code} {place.locality}","repeat":6,"separator":"\n"`, 1)
|
||||
varied := false
|
||||
for i := 0; i < 40; i++ {
|
||||
got := mustRender(t, f, tmpl)
|
||||
lines := strings.Split(got, "\n")
|
||||
if len(lines) != 6 {
|
||||
t.Fatalf("repeat 6 produced %d lines", len(lines))
|
||||
}
|
||||
for _, line := range lines {
|
||||
if !agree.MatchString(line) {
|
||||
t.Fatalf("repetition %q does not pair", line)
|
||||
}
|
||||
}
|
||||
if lines[0] != lines[1] || lines[1] != lines[2] {
|
||||
varied = true
|
||||
}
|
||||
}
|
||||
if !varied {
|
||||
t.Fatal("40 renders of repeat 6 never varied within a render, want a redraw per repetition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedTemplateKeepsItsOwnHold(t *testing.T) {
|
||||
// A hold belongs to the expansion that made it: an inner template's own
|
||||
// place is not the outer one's.
|
||||
f := engine(6)
|
||||
tmpl := `{"format":"{place.v}{inner}",
|
||||
"place":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}],
|
||||
"inner":{"format":"{place.v}","place":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}]}}`
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 400; i++ {
|
||||
seen[mustRender(t, f, tmpl)] = true
|
||||
}
|
||||
if !seen["AB"] && !seen["BA"] {
|
||||
t.Fatalf("400 draws produced %v, want the inner hold to be independent", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeldDrawIsSeedStable(t *testing.T) {
|
||||
tmpl := places("{place.postal-code} {place.locality}")
|
||||
a := mustRender(t, engine(42), tmpl)
|
||||
b := mustRender(t, engine(42), tmpl)
|
||||
if a != b {
|
||||
t.Fatalf("same seed gave %q and %q, want an identical draw", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDottedTokenErrors(t *testing.T) {
|
||||
// A path token is validated where every other token is: at New, naming what
|
||||
// is wrong, so a typo can never reach a render.
|
||||
rejected := map[string]struct {
|
||||
file string
|
||||
want string
|
||||
}{
|
||||
"unknown head": {
|
||||
`{"format":"{nope.x}","place":{"format":"{v}","v":"A"}}`,
|
||||
`no field "nope"`,
|
||||
},
|
||||
"unknown tail": {
|
||||
`{"format":"{place.nope}","place":[{"format":"{v}","v":"A"},{"format":"{v}","v":"B"}]}`,
|
||||
`"nope"`,
|
||||
},
|
||||
"tail only some variants carry": {
|
||||
`{"format":"{place.locality}","place":[{"format":"{locality}","locality":"A"},"x"]}`,
|
||||
`locality`,
|
||||
},
|
||||
"path into a literal": {
|
||||
`{"format":"{x.y}","x":"plain"}`,
|
||||
`"y"`,
|
||||
},
|
||||
"dotted field key": {
|
||||
`{"format":"[{a.b}]","a.b":"V"}`,
|
||||
`contains "."`,
|
||||
},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": c.file})))
|
||||
if err == nil {
|
||||
t.Errorf("%s: New = nil error, want it rejected at load", name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSamePathReadTwiceReadsOneValue(t *testing.T) {
|
||||
// A path is drawn once per expansion, so reading it twice reads one value —
|
||||
// what a name shown in a display form and again in an address needs.
|
||||
f := engine(7)
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `{"format":"{p.first}|{p.first}","p":{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}}`)
|
||||
parts := strings.Split(got, "|")
|
||||
if parts[0] != parts[1] {
|
||||
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) {
|
||||
// Two tails of one head stay in the same variant, each keeping its own value.
|
||||
f := engine(8)
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `{"format":"{p.a}{p.b}",
|
||||
"p":[{"format":"x","a":"A","b":"1"},{"format":"y","a":"B","b":"2"}]}`)
|
||||
if got != "A1" && got != "B2" {
|
||||
t.Fatalf("draw %d = %q, want a row's own pair", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deepPlaces nests the correlated facts a level down: the row holds an addr, and
|
||||
// the addr holds the city and the zip that belongs to it.
|
||||
const deepPlaces = `{"format":"%s","p":{"format":"{addr}","addr":[{"format":"{city}","city":"Stockholm","zip":"11111"},{"format":"{city}","city":"Kiruna","zip":"98100"}]}}`
|
||||
|
||||
func TestPathHoldsItsDrawAtEveryLevel(t *testing.T) {
|
||||
// A choice below the head is drawn once too, so two paths sharing a prefix
|
||||
// share every level of it — not just the row they started from.
|
||||
f := engine(11)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, strings.Replace(deepPlaces, "%s", "{p.addr.city}/{p.addr.zip}", 1))
|
||||
if got != "Stockholm/11111" && got != "Kiruna/98100" {
|
||||
t.Fatalf("draw %d = %q, want a city and the zip that belongs to it", i, got)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("500 draws produced only %v, want both", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathHoldsItsDrawThreeLevelsDown(t *testing.T) {
|
||||
// The rule does not run out at two: every level a path passes through is held.
|
||||
f := engine(12)
|
||||
tmpl := `{"format":"{p.geo.town.name}/{p.geo.town.zip} {p.geo.region}","p":{"format":"{geo}","geo":[{"format":"{town}","region":"Norrbotten","town":[{"format":"{name}","name":"Kiruna","zip":"98100"},{"format":"{name}","name":"Luleå","zip":"97200"}]},{"format":"{town}","region":"Skåne","town":[{"format":"{name}","name":"Malmö","zip":"21100"},{"format":"{name}","name":"Lund","zip":"22100"}]}]}}`
|
||||
want := map[string]bool{
|
||||
"Kiruna/98100 Norrbotten": true, "Luleå/97200 Norrbotten": true,
|
||||
"Malmö/21100 Skåne": true, "Lund/22100 Skåne": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 600; i++ {
|
||||
got := mustRender(t, f, tmpl)
|
||||
if !want[got] {
|
||||
t.Fatalf("draw %d = %q, want a town with its own zip and region", i, got)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) != len(want) {
|
||||
t.Fatalf("600 draws produced %v, want all four towns", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathWalksVariantsOfDifferentShape(t *testing.T) {
|
||||
// One variant holds addr as a plain template where another holds a choice.
|
||||
// Both carry the path, so both must walk — the walk reads what it drew, never
|
||||
// assuming a shape.
|
||||
f := engine(13)
|
||||
tmpl := `{"format":"{p.addr.city}/{p.addr.zip}","p":[
|
||||
{"format":"{addr}","addr":{"format":"{city}","city":"Kiruna","zip":"98100"}},
|
||||
{"format":"{addr}","addr":[
|
||||
{"format":"{city}","city":"Malmö","zip":"21100"},
|
||||
{"format":"{city}","city":"Lund","zip":"22100"}]}]}`
|
||||
for i := 0; i < 400; i++ {
|
||||
got := mustRender(t, f, tmpl)
|
||||
if got != "Kiruna/98100" && got != "Malmö/21100" && got != "Lund/22100" {
|
||||
t.Fatalf("draw %d = %q, want a city with its own zip", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepPathsUnderOneHeadStayIndependentWhereTheyDiverge(t *testing.T) {
|
||||
// Two paths share only what they actually share: a common prefix is one draw,
|
||||
// and each keeps its own draw past the point they part.
|
||||
f := engine(14)
|
||||
tmpl := `{"format":"{p.a.v}{p.b.v}","p":{"format":"x","a":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}],"b":[{"format":"{v}","v":"1"},{"format":"{v}","v":"2"}]}}`
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 400; i++ {
|
||||
seen[mustRender(t, f, tmpl)] = true
|
||||
}
|
||||
for _, want := range []string{"11", "12", "21", "22"} {
|
||||
if !seen[want] {
|
||||
t.Fatalf("400 draws produced %v, want every combination including %q", seen, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyPathSegmentIsRejected(t *testing.T) {
|
||||
// "{a.}" and "{a...b}" are unfinished paths, and the segment naming nothing is
|
||||
// reported as that rather than as a missing field. An empty name is rejected
|
||||
// where it is authored, so no data can make these resolve.
|
||||
rejected := map[string]string{
|
||||
"trailing dot": `{"format":"[{a.}]","a":"x"}`,
|
||||
"triple dot": `{"format":"[{a...b}]","a":"x"}`,
|
||||
}
|
||||
for name, file := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": file})))
|
||||
if err == nil {
|
||||
t.Errorf("%s: New = nil error, want the unfinished path rejected", name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), "empty segment") {
|
||||
t.Errorf("%s: New = %v, want it to name the empty segment", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycleThroughAPathTokenIsRejected(t *testing.T) {
|
||||
// A path token is a render edge like any other, so a cycle routed through one
|
||||
// must be caught at New. Reaching render would be fatal: the recursion never
|
||||
// terminates, and a stack overflow cannot be recovered.
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"a": `{"format":"{p.x}","p":{"format":"{x}","x":"{/b}"}}`,
|
||||
"b": `"{/a}"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "reference cycle") {
|
||||
t.Fatalf("New = %v, want the cycle through {p.x} rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeldPathIsReachableByFake(t *testing.T) {
|
||||
// Holding changes how a format reads a sibling, not what List and Fake offer:
|
||||
// the sub-fields stay addressable on their own.
|
||||
dir := writeData(t, map[string]string{"address": places("{place.postal-code} {place.locality}")})
|
||||
f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(9))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v", err)
|
||||
}
|
||||
for _, path := range []string{"address", "address.place", "address.place.locality", "address.place.postal-code"} {
|
||||
if _, err := f.Fake(path); err != nil {
|
||||
t.Errorf("Fake(%q) = %v, want it to render", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatedBareTokenOfAHeldNameIsRejected(t *testing.T) {
|
||||
f := newGenerator(t, writeData(t, map[string]string{"w": `"a"`}))
|
||||
for src, want := range map[string]string{
|
||||
`{"format":"{w} {w} {uppercase(w)}","w":["a","b"]}`: "write {w} once",
|
||||
`{"format":"{w} {w|x} {uppercase(w)}","w":["a","b"],"x":["c","d"]}`: "write {w} once",
|
||||
`{"format":"{n} + {n} = {calc(n * 2)}","n":["1","2"]}`: "write {n} once",
|
||||
`{"format":"{p.a} {q} {q} {lowercase(q)}","p":{"format":"{a}","a":"1"},"q":["A","B"]}`: "write {q} once",
|
||||
} {
|
||||
_, err := f.NewTemplate(src)
|
||||
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "holds") {
|
||||
t.Errorf("NewTemplate(%s) = %v, want the repeated token rejected naming %s", src, err, want)
|
||||
}
|
||||
}
|
||||
for _, ok := range []string{
|
||||
`{"format":"{w} {w}","w":["a","b"]}`,
|
||||
`{"format":"{w} {uppercase(w)}","w":["a","b"]}`,
|
||||
`{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00"],"qty":["3","7"]}`,
|
||||
`{"format":"{uppercase(w)} {uppercase(w)}","w":["a","b"]}`,
|
||||
} {
|
||||
if _, err := f.NewTemplate(ok); err != nil {
|
||||
t.Errorf("NewTemplate(%s) = %v, want it accepted", ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileFixesHowEachArmIsRead(t *testing.T) {
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"cat": `{"format":"{w} {w} {p.x} {/other.y} {n} {uppercase(n)}","w":["a","b"],"n":["c","d"],"p":{"format":"{x}","x":"1"}}`,
|
||||
"other": `{"format":"{y}","y":"2"}`,
|
||||
})))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tm, ok := f.root.children["cat"].(*template)
|
||||
if !ok {
|
||||
t.Fatal("cat is not a template")
|
||||
}
|
||||
want := map[string]armKind{"/other.y": refPathRead, "n": heldRead, "p.x": heldRead, "w": freshRead}
|
||||
for _, o := range tm.compiled.ops {
|
||||
for _, a := range append(o.arms, o.operands...) {
|
||||
if k, ok := want[a.spelling]; !ok || a.kind != k {
|
||||
t.Errorf("{%s} compiles to kind %d, want %d", a.spelling, a.kind, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -53,13 +53,13 @@ func TestFakeTemplateJSONArray(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeTemplateCorrelatedReferences(t *testing.T) {
|
||||
func TestFakeTemplateNamedReference(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
got := tmpl(t, f, "{/person.first} {/person.last}")
|
||||
got := tmpl(t, f, "{/person as p}{p.first} {p.last}")
|
||||
if got != "Ada Lovelace" && got != "Bo Ek" {
|
||||
t.Fatalf("correlated references = %q, want one person's first and last", got)
|
||||
}
|
||||
@@ -108,12 +108,6 @@ func TestFakeTemplateErrors(t *testing.T) {
|
||||
{`name: {/no.such.path}`, "no entry"},
|
||||
{`name: {..nope}`, "write {/nope}"},
|
||||
{`{"format":"x"}`, "is a string"},
|
||||
{`{/misc.territory} {/misc.territory.alpha2}`, "{/misc.territory} renders its own draw of what {/misc.territory.alpha2} reads a path through; name the fields you want instead, or move {/misc.territory.alpha2} into a field with a drawGroup"},
|
||||
{`{"format":"{/misc.territory.alpha2} {x}","x":"{/misc.territory}"}`, "renders its own draw of what"},
|
||||
{`{/misc.territory} {/misc.territory} {/misc.territory.alpha2}`, "{/misc.territory} renders its own draw of what {/misc.territory.alpha2} reads a path through; name the fields you want instead, or move {/misc.territory.alpha2} into a field with a drawGroup"},
|
||||
{`{"format":"{uppercase(/misc.territory)} {y}","y":"{/misc.territory}"}`, `{y} renders "/misc.territory", which {uppercase(/misc.territory)} holds to one draw for this expansion; drop {y}, or write {/misc.territory} in this format to read that draw`},
|
||||
{`{"format":"{uppercase(/misc.territory.alpha2)} {lowercase(/misc.territory)} {y}","y":"{/misc.territory}"}`, "which {lowercase(/misc.territory)} holds to one draw for this expansion; drop {y}, or write {/misc.territory} in"},
|
||||
{`{"format":"{/misc.territory.alpha2}","drawGroup":"g"}`, "nothing can reference"},
|
||||
} {
|
||||
_, err := f.FakeTemplate(c.input)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
|
||||
+13
-9
@@ -184,26 +184,30 @@ func TestNameErrors(t *testing.T) {
|
||||
`name "w" is a field of the root template too`},
|
||||
{"an option", map[string]string{"word": `["a","b"]`, "card": `"{/word as format}{format}"`},
|
||||
`"format" is an option and can never be a name`},
|
||||
{"no reference", map[string]string{"card": `{"format":"{word as w}{w}","word":["a","b"]}`},
|
||||
`"word" is no reference`},
|
||||
{"no reference or field", map[string]string{"card": `{"format":"{word as w}{w}{w}","words":["a","b"]}`},
|
||||
`no field "word"; a name binds a pick of a field, or of 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 by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`},
|
||||
`nothing reads name "w"`},
|
||||
{"read in two draw groups", map[string]string{"nm.json": `{"format":"{first}","rows":"nm.tsv"}`, "nm.tsv": "first\tlast\nAda\tByron\nBo\tEk\n", "person": `{"format":"{first} {last}","first":"{/nm.first}","last":"{/nm.last}"}`, "card": `{"format":"{b}|{a}","a":"{/person as p}{p}","b":{"format":"{p.first}","drawGroup":"g"}}`},
|
||||
`name "p" is read in two draw groups`},
|
||||
{"read in a repeat", map[string]string{"nm.json": `{"format":"{first}","rows":"nm.tsv"}`, "nm.tsv": "first\tlast\nAda\tByron\nBo\tEk\n", "person": `{"format":"{first} {last}","first":"{/nm.first}","last":"{/nm.last}"}`, "card": `{"format":"{/person as p}{p}{each}","each":{"format":"{p.first}","repeat":2}}`},
|
||||
`name "p" is read inside a repeat`},
|
||||
{"read in a repeat", map[string]string{"nm.json": `{"format":"{first}","rows":"nm.tsv"}`, "nm.tsv": "first\tlast\nAda\tByron\nBo\tEk\n", "person": `{"format":"{first} {last}","first":"{/nm.first}","last":"{/nm.last}"}`, "card": `{"format":"{/person as p}{each}","each":{"format":"{p.first}","repeat":2}}`},
|
||||
""},
|
||||
{"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 beside its reference path", map[string]string{"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word as a}{a.w} {/word.w}"`},
|
||||
""},
|
||||
{"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}"]`},
|
||||
`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}"}`},
|
||||
`write {/word}`},
|
||||
`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}"`},
|
||||
|
||||
+1
-28
@@ -38,45 +38,18 @@ func TestInertObjectIsRejected(t *testing.T) {
|
||||
`{"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":{"format":"{y}","y":"v","drawGroup":""}}`: `drawGroup "" is the default`,
|
||||
`{"format":"{x}","x":{"format":"{y}","y":"v","drawGroup":1}}`: "drawGroup must be a string",
|
||||
} {
|
||||
if _, err := linked(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)}"`, `{"format":"{/cat.x}","drawGroup":"g"}`} {
|
||||
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 TestAGroupThatSplitsNothingIsRejected(t *testing.T) {
|
||||
files := map[string]string{"mail": `"{/word.w}@example.com"`, "word": `{"format":"{w}","w":["a","b"]}`}
|
||||
for src, want := range map[string]string{
|
||||
`{"format":"{x}","x":{"format":"{y}","y":["a","b"],"drawGroup":"g"}}`: `drawGroup "g" splits nothing`,
|
||||
`{"format":"{x}","x":{"format":"{/word}","drawGroup":"g"}}`: `drawGroup "g" splits nothing`,
|
||||
`{"format":"{x}","x":{"format":"{r}","drawGroup":"g","r":{"format":"{/word.w}","repeat":2}}}`: `drawGroup "g" splits nothing`,
|
||||
`{"format":"{x}","x":{"format":"{y}","drawGroup":"g","y":{"format":"{/word.w}","drawGroup":"h"}}}`: `drawGroup "g" splits nothing`,
|
||||
`{"format":"{x}","x":{"format":"{y}","y":"{/word.w}","repeat":2,"drawGroup":"g"}}`: `drawGroup "g" on a repeat`,
|
||||
`{"format":"{x}","x":{"format":"{/word.w} {y}","drawGroup":"g","y":{"format":"{/word.w}!","drawGroup":"g"}}}`: `"y" names drawGroup "g", the draw group this template draws in already`,
|
||||
`{"format":"{/word.w}","drawGroup":"g"}`: "",
|
||||
`{"format":"{x}","x":{"format":"{/mail}","drawGroup":"g"}}`: "",
|
||||
`{"format":"{x}","x":{"format":"{/word.w} {y}","drawGroup":"g","y":{"format":"{/word.w}!","drawGroup":"h"}}}`: "",
|
||||
`{"format":"{x}","x":{"format":"{/word.w} {r}","drawGroup":"g","r":{"format":"{y}","repeat":2,"y":{"format":"{/word.w}","drawGroup":"g"}}}}`: "",
|
||||
} {
|
||||
files["cat"] = src
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, files)))
|
||||
switch {
|
||||
case want == "" && err != nil:
|
||||
t.Errorf("New(%s) = %v, want a group over a reference path accepted, however deep", src, err)
|
||||
case want != "" && (err == nil || !strings.Contains(err.Error(), want)):
|
||||
t.Errorf("New(%s) = %v, want an error mentioning %s", src, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInlineFolderSigilsAreRejected(t *testing.T) {
|
||||
f := shipped(t)
|
||||
for _, input := range []string{"{.sv_SE.person.last}", "{..sv_SE.person.last}"} {
|
||||
|
||||
@@ -29,13 +29,6 @@ func TestPathCheckChoiceConsumesNoSegment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathCoverStopsAtAChoice(t *testing.T) {
|
||||
n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`)
|
||||
if cover := compilePath(n, []string{"f"}).cover; cover != n {
|
||||
t.Fatalf("cover through a choice = %v, want the choice itself", cover)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawStepsPanicsOnAStepItsNodeLacks(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil || !strings.Contains(fmt.Sprint(r), `no field "f"`) {
|
||||
|
||||
+16
-16
@@ -35,13 +35,13 @@ func TestNoRenderAllocRegression(t *testing.T) {
|
||||
json string
|
||||
base float64
|
||||
}{
|
||||
{"nested depth 25", nestedJSON(25), 27},
|
||||
{"nested depth 100", nestedJSON(100), 102},
|
||||
{"wide 500 tokens", wideTokenJSON(500), 9},
|
||||
{"two paths from one held draw", `{"format":"{p.a} {p.b}","p":[{"format":"x","a":"1","b":"2"},{"format":"y","a":"A","b":"B"}]}`, 3},
|
||||
{"ten paths from one held draw", `{"format":"{r.a}{r.b}{r.c}{r.d}{r.e}{r.f}{r.g}{r.h}{r.i}{r.j}","r":[
|
||||
{"nested depth 25", nestedJSON(25), 26},
|
||||
{"nested depth 100", nestedJSON(100), 101},
|
||||
{"wide 500 tokens", wideTokenJSON(500), 8},
|
||||
{"two paths into one field", `{"format":"{p.a} {p.b}","p":[{"format":"x","a":"1","b":"2"},{"format":"y","a":"A","b":"B"}]}`, 2},
|
||||
{"ten paths into one field", `{"format":"{r.a}{r.b}{r.c}{r.d}{r.e}{r.f}{r.g}{r.h}{r.i}{r.j}","r":[
|
||||
{"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"},
|
||||
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`, 6},
|
||||
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`, 3},
|
||||
}
|
||||
for _, s := range shapes {
|
||||
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}}))
|
||||
@@ -55,16 +55,15 @@ func TestNoRenderAllocRegression(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The repeat shape prices what keeps a render's draws off the heap: a copy dropped costs an alloc an iteration.
|
||||
// The repeat shape prices what keeps a render's scope off the heap: a copy dropped costs an alloc an iteration.
|
||||
func TestNoReferenceAllocRegression(t *testing.T) {
|
||||
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
|
||||
for _, s := range []struct {
|
||||
name, json string
|
||||
base float64
|
||||
}{
|
||||
{"a repeat of a reference path", `{"format":"{r}","r":{"format":"{/word.w}","repeat":20,"separator":", "}}`, 66},
|
||||
{"a named draw group", `{"format":"{a}","a":{"format":"{/word.w}","drawGroup":"g"}}`, 11},
|
||||
{"a repeat of a name", `{"format":"{r}","r":{"format":"{/word as n}{n.w}{n.w}","repeat":20,"separator":", "}}`, 205},
|
||||
{"a repeat of a reference path", `{"format":"{r}","r":{"format":"{/word.w}","repeat":20,"separator":", "}}`, 25},
|
||||
{"a repeat of a name", `{"format":"{r}","r":{"format":"{/word as n}{n.w}{n.w}","repeat":20,"separator":", "}}`, 161},
|
||||
} {
|
||||
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
|
||||
"word.json": {Data: []byte(word)},
|
||||
@@ -74,12 +73,12 @@ func TestNoReferenceAllocRegression(t *testing.T) {
|
||||
t.Fatalf("New(%s): %v", s.name, err)
|
||||
}
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*1.10 {
|
||||
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a render's draws reaching the heap is the usual cause", s.name, allocs, s.base*1.10, s.base)
|
||||
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a render's scope reaching the heap is the usual cause", s.name, allocs, s.base*1.10, s.base)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A table read pins a row in the render's draws and reads its cells in place.
|
||||
// A table read pins a row inline and reads its cells in place.
|
||||
func TestNoTableAllocRegression(t *testing.T) {
|
||||
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
|
||||
"region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)},
|
||||
@@ -96,7 +95,7 @@ func TestNoTableAllocRegression(t *testing.T) {
|
||||
{"a table drawn", "region", 2},
|
||||
{"a column of a drawn row", "region.name", 1},
|
||||
{"a row selected", "region[12]", 2},
|
||||
{"two columns of one draw", "x", 10},
|
||||
{"two columns, each drawn", "x", 3},
|
||||
} {
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.Fake(s.path) }); allocs > s.base*1.10 {
|
||||
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a row lookup built per draw is the usual cause", s.name, allocs, s.base*1.10, s.base)
|
||||
@@ -117,7 +116,7 @@ func TestNoTableAllocRegression(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const base = 13.0
|
||||
const base = 3.0
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("addr") }); allocs > base*1.10 {
|
||||
t.Errorf("five linked tables: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a pin spilling past the inline set is the usual cause", allocs, base*1.10, base)
|
||||
}
|
||||
@@ -136,7 +135,7 @@ func TestNoRecordAllocRegression(t *testing.T) {
|
||||
if _, err := f.FakeRecord("x"); err != nil {
|
||||
t.Fatalf("FakeRecord(%s): %v", s.name, err) // else the gate would measure the error path
|
||||
}
|
||||
const base = 4.0
|
||||
const base = 3.0
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 {
|
||||
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base)
|
||||
}
|
||||
@@ -160,7 +159,8 @@ func TestNoStructAllocRegression(t *testing.T) {
|
||||
if err := f.FakeStruct(&v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const base = 5.0
|
||||
// The three tags read one pick of x: its frame and memo cost two allocations.
|
||||
const base = 7.0
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.FakeStruct(&v) }); allocs > base*1.10 {
|
||||
t.Errorf("FakeStruct: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); compiling the type per call is the usual cause", allocs, base*1.10, base)
|
||||
}
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestEveryReadARenderMakesIsGathered(t *testing.T) {
|
||||
f := fenceCorpus(t)
|
||||
roots := fenceRoots(t, f)
|
||||
traced := 0
|
||||
fold := newReadFold()
|
||||
for _, root := range roots {
|
||||
traced += wantGathered(t, root.label, surveyRender(fold, root.t).reads, func(trace renderTrace) { renderRoot(f.rand, root.t, trace) })
|
||||
if !root.t.isRecord || len(root.t.fields) == 0 {
|
||||
continue
|
||||
}
|
||||
_, columns, err := recordOf(root.t)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: recordOf = %v", root.label, err)
|
||||
}
|
||||
traced += wantGathered(t, root.label+" as a record", surveyColumns(fold, root.t, sortedNames(root.t.fields)).reads, func(trace renderTrace) { renderRecordRoot(f.rand, root.t, columns, trace) })
|
||||
}
|
||||
if traced == 0 {
|
||||
t.Error("no render traced a reference read, so the comparison proved nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func wantGathered(t *testing.T, label string, gathered []pathRead, renderWith func(renderTrace)) int {
|
||||
t.Helper()
|
||||
var reads []pathRead
|
||||
type traceKey struct {
|
||||
group, path string
|
||||
row renderedRow
|
||||
}
|
||||
seen := map[traceKey]bool{}
|
||||
trace := func(group string, row renderedRow, a arm) {
|
||||
if !isRef(a.head) {
|
||||
return
|
||||
}
|
||||
r := pathRead{group: group, a: a}
|
||||
if row.t != nil {
|
||||
r.branches.pins.add(row.t, row.index)
|
||||
}
|
||||
if k := (traceKey{group, a.path, row}); !seen[k] {
|
||||
seen[k] = true
|
||||
reads = append(reads, r)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
renderWith(trace)
|
||||
}
|
||||
for _, r := range reads {
|
||||
if !gathers(gathered, r) {
|
||||
t.Errorf("%s: the render read {%s} in draw group %q from %s, and the fold gathered no such read", label, r.a.spelling, r.group, spellPins(&r.branches.pins))
|
||||
}
|
||||
}
|
||||
return len(reads)
|
||||
}
|
||||
|
||||
func gathers(gathered []pathRead, r pathRead) bool {
|
||||
for _, g := range gathered {
|
||||
if g.group == r.group && g.a.path == r.a.path && !g.branches.pins.differs(&r.branches.pins) && !g.branches.wholePins.differs(&r.branches.pins) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func spellPins(p *pinSet) string {
|
||||
var rows []string
|
||||
p.each(func(t *table, r int) { rows = append(rows, t.selectorSpelling(r)) })
|
||||
if len(rows) == 0 {
|
||||
return "no row"
|
||||
}
|
||||
return "row " + strings.Join(rows, ", ")
|
||||
}
|
||||
|
||||
func renderRoot(s *generatorState, t *template, trace renderTrace) {
|
||||
draws := renderDraws{trace: trace}
|
||||
sc, _ := renderScope{draws: &draws}.enter(t, &draws.unnamed.memo)
|
||||
switch {
|
||||
case t.site.isFormat():
|
||||
tbl := t.site.table
|
||||
render(s, tbl, sc)
|
||||
for r := 0; r < tbl.rowCount(); r++ {
|
||||
draws := renderDraws{trace: trace}
|
||||
sc := renderScope{draws: &draws}
|
||||
sc.groupDraws().pins.pin(tbl, r)
|
||||
render(s, tbl.rowNode, sc.at(tbl.rowNode, &sc.groupDraws().pins))
|
||||
}
|
||||
case t.repeat > 1:
|
||||
expand(s, t, sc)
|
||||
default:
|
||||
render(s, t, sc)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRecordRoot(s *generatorState, t *template, columns []Column, trace renderTrace) {
|
||||
draws := renderDraws{trace: trace}
|
||||
sc := renderScope{draws: &draws}
|
||||
if t.site.isFormat() {
|
||||
t.site.table.drawIn(s, &sc.groupDraws().pins)
|
||||
}
|
||||
renderRecord(s, t, columns, sc)
|
||||
}
|
||||
|
||||
func TestReadFoldWalksACellByEveryRoute(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"g.json": {Data: []byte(`{"format":"{/x.v}","drawGroup":"g2"}`)},
|
||||
"r.json": {Data: []byte(`"{/g} {/x}"`)},
|
||||
"x.json": {Data: []byte(`{"format":"{v}","rows":"x.tsv","key":"code"}`)},
|
||||
"x.tsv": {Data: []byte("code\tv\n1\t{/z}\n2\t{/z.a}\n")},
|
||||
"z.json": {Data: []byte(`{"format":"{a}","a":"hi"}`)},
|
||||
}
|
||||
g, err := loadDir(dataSource{fsys: fsys}, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("loadDir = %v", err)
|
||||
}
|
||||
if err := categoryBinding(categorySites(g), g.children).link(); err != nil {
|
||||
t.Fatalf("link = %v", err)
|
||||
}
|
||||
r, isTemplate := g.children["r"].(*template)
|
||||
if !isTemplate {
|
||||
t.Fatalf("r is a %T, want a template", g.children["r"])
|
||||
}
|
||||
var pinned, whole bool
|
||||
for _, read := range surveyRender(newReadFold(), r).reads {
|
||||
if read.a.path != "/z" {
|
||||
continue
|
||||
}
|
||||
pinned = pinned || read.branches.pins.used > 0
|
||||
whole = whole || read.branches.wholePins.used > 0
|
||||
}
|
||||
if !pinned || !whole {
|
||||
t.Errorf("{/z} gathered under a pinned row %v and a whole read's row %v, want both: {/g} reaches x's cells from the root, and {/x} renders them whole", pinned, whole)
|
||||
}
|
||||
}
|
||||
+30
-55
@@ -199,46 +199,23 @@ func TestRecordWritesTypedAndNullColumns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) {
|
||||
func TestRecordColumnsReadingOneReferenceLoad(t *testing.T) {
|
||||
cat := `{"format":"{a}","a":[{"format":"A={b}","b":"1"},{"format":"A={b}","b":"2"}]}`
|
||||
mid := `"{/cat.a}"`
|
||||
for _, c := range []struct{ name, row string }{
|
||||
{"through a column repeat, which draws anew", `{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b}","repeat":2,"separator":"-"}}`},
|
||||
{"in a group of its own", `{"format":"","whole":{"format":"{/cat.a}","drawGroup":"g"},"inner":"{/cat.a.b}"}`},
|
||||
for _, row := range []string{
|
||||
`{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b}","repeat":2,"separator":"-"}}`,
|
||||
`{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`,
|
||||
`{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b} {x}","x":"1"}}`,
|
||||
`{"format":"","whole":"{/cat.a}","inner":[{"format":"{/cat.a.b} {x}","x":"1"},{"format":"{/cat.a.b}! {x}","x":"2"}]}`,
|
||||
`{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`,
|
||||
`{"format":"","whole":"{/cat}","inner":"{/cat.a.b}"}`,
|
||||
`{"format":"","whole":"{/mid}","inner":"{/cat.a.b}"}`,
|
||||
} {
|
||||
f := newGenerator(t, writeData(t, map[string]string{"cat": cat, "mid": mid, "row": c.row}), WithSeed(1))
|
||||
f := newGenerator(t, writeData(t, map[string]string{"cat": cat, "mid": mid, "row": row}), WithSeed(1))
|
||||
if _, err := f.FakeRecord("row"); err != nil {
|
||||
t.Errorf("%s: FakeRecord = %v, want it accepted", c.name, err)
|
||||
t.Errorf("%s: FakeRecord = %v, want it accepted", row, err)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct{ name, row string }{
|
||||
{"sibling columns", `{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`},
|
||||
{"through a nested template", `{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b} {x}","x":"1"}}`},
|
||||
{"through a choice variant", `{"format":"","whole":"{/cat.a}","inner":[{"format":"{/cat.a.b} {x}","x":"1"},{"format":"{/cat.a.b}! {x}","x":"2"}]}`},
|
||||
{"as a builtin operand", `{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`},
|
||||
{"a bare reference beside a path", `{"format":"","whole":"{/cat}","inner":"{/cat.a.b}"}`},
|
||||
{"a column reaching back through another category", `{"format":"","whole":"{/mid}","inner":"{/cat.a.b}"}`},
|
||||
} {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "mid": mid, "row": c.row})))
|
||||
if err == nil || !strings.Contains(err.Error(), "renders its own draw of what") {
|
||||
t.Errorf("%s: New = %v, want the overlap refused at load, the way one format is", c.name, err)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), `"whole"`) || !strings.Contains(err.Error(), `"inner"`) {
|
||||
t.Errorf("%s: error %q names neither column; it must name both", c.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInlineRecordRejectsOverlappingColumns(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"cat": `{"format":"","a":[{"format":"A={b}","b":"1"},{"format":"A={b}","b":"2"}]}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
_, err := f.FakeRecordTemplate(`{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`)
|
||||
if err == nil || !strings.Contains(err.Error(), "renders its own draw of what") {
|
||||
t.Fatalf("inline record over an overlapping pair = %v, want the inline entry point to refuse it too", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordRejectsAColumnReadingItsOwnRecord(t *testing.T) {
|
||||
@@ -276,15 +253,16 @@ func TestRecordBareReferenceStaysIndependentAsAnOperand(t *testing.T) {
|
||||
if !sawMismatch {
|
||||
t.Error("two bare-reference operand columns never disagreed; a bare reference draws on its own, as the plain spelling does")
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := 0; i < 200; i++ {
|
||||
v, err := f.FakeTemplate("{/cur}|{uppercase(/cur)}")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if parts := strings.Split(v, "|"); !strings.EqualFold(parts[0], parts[1]) {
|
||||
t.Fatalf("one format rendered %q; within an expansion a bare reference is still one draw", v)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("{/cur}|{uppercase(/cur)} never disagreed in 200 renders; each reference draws on its own")
|
||||
}
|
||||
|
||||
func TestRecordSQLInsert(t *testing.T) {
|
||||
@@ -315,10 +293,10 @@ func TestRecordRejectsFieldDescent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordSharesAReferenceAcrossColumns(t *testing.T) {
|
||||
func TestRecordColumnsReadOneName(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€","name":"Euro"}]`,
|
||||
"price": `{"format":"{code} {symbol}","code":"{/currency.code}","symbol":"{/currency.symbol}"}`,
|
||||
"price": `{"format":"{code} {symbol}","code":"{/currency as c}{c.code}","symbol":"{c.symbol}"}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
symbols := map[string]string{"AUD": "$", "EUR": "€"}
|
||||
@@ -341,9 +319,9 @@ func TestRecordSharesAReferenceAcrossColumns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatIterationsDrawReferencesAnew(t *testing.T) {
|
||||
func TestRepeatIterationsPickNamesAnew(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"party": `{"format":"{host}: {guests}","guests":{"format":"{/person.first} {/person.last}","repeat":3,"separator":", "},"host":"{/person.first} {/person.last}"}`,
|
||||
"party": `{"format":"{host}: {guests}","guests":{"format":"{/person as p}{p.first} {p.last}","repeat":3,"separator":", "},"host":"{/person as h}{h.first} {h.last}"}`,
|
||||
"person": drawPeople,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
@@ -379,10 +357,10 @@ func TestRepeatIterationsDrawReferencesAnew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordGroupsDrawApart(t *testing.T) {
|
||||
func TestRecordNamesDrawApart(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"person": drawPeople,
|
||||
"transfer": `{"format":"{from_first} {from_last} to {to_first} {to_last}","from_first":{"format":"{/person.first}","drawGroup":"from"},"from_last":{"format":"{/person.last}","drawGroup":"from"},"to_first":{"format":"{/person.first}","drawGroup":"to"},"to_last":{"format":"{/person.last}","drawGroup":"to"}}`,
|
||||
"transfer": `{"format":"{from_first} {from_last} to {to_first} {to_last}","from_first":"{/person as from}{from.first}","from_last":"{from.last}","to_first":"{/person as to}{to.first}","to_last":"{to.last}"}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
apart := map[string]bool{}
|
||||
@@ -398,14 +376,14 @@ func TestRecordGroupsDrawApart(t *testing.T) {
|
||||
from, to, _ := strings.Cut(fake(t, f, "transfer"), " to ")
|
||||
for view, pair := range map[string][2]string{"FakeRecord": {m["from_first"] + " " + m["from_last"], m["to_first"] + " " + m["to_last"]}, "Fake": {from, to}} {
|
||||
if !onePerson(pair[0]) || !onePerson(pair[1]) {
|
||||
t.Fatalf("%s drew %q and %q, want each group one person", view, pair[0], pair[1])
|
||||
t.Fatalf("%s drew %q and %q, want each name one person", view, pair[0], pair[1])
|
||||
}
|
||||
apart[view] = apart[view] || pair[0] != pair[1]
|
||||
}
|
||||
}
|
||||
for _, view := range []string{"FakeRecord", "Fake"} {
|
||||
if !apart[view] {
|
||||
t.Errorf("%s: groups from and to drew one person in 100 renders, want a draw each", view)
|
||||
t.Errorf("%s: names from and to drew one person in 100 renders, want a pick each", view)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,22 +410,19 @@ func TestRecordColumnOfOneReferenceIsTheColumnItReads(t *testing.T) {
|
||||
cols[c.Name] = c
|
||||
}
|
||||
score, code := cols["score"], cols["code"]
|
||||
agree := func(name string, want Column) bool {
|
||||
return cols[name].Null == want.Null && cols[name].Value == want.Value
|
||||
}
|
||||
digit := func(c Column) bool { return len(c.Value) == 1 && c.Value >= "1" && c.Value <= "9" }
|
||||
isScore := func(name string) bool { return cols[name].Null != digit(cols[name]) }
|
||||
switch {
|
||||
case score.DataType != DataTypeInteger || cols["chain"].DataType != DataTypeInteger || code.DataType != DataTypeInteger || cols["pick"].DataType != DataTypeInteger || cols["label"].DataType != DataTypeString || cols["mixed"].DataType != DataTypeString || cols["text"].DataType != DataTypeString:
|
||||
t.Fatalf("%s: want score, chain, code and pick integer columns, label, mixed and text string ones", r.JSON())
|
||||
case score.Null == (len(score.Value) == 1 && score.Value >= "1" && score.Value <= "9"):
|
||||
t.Fatalf("score = %+v, want null or a digit from src.score", score)
|
||||
case !isScore("score") || !isScore("same") || !isScore("chain") || !isScore("dot"):
|
||||
t.Fatalf("%s: want score, same, chain and dot each null or a digit from src.score", r.JSON())
|
||||
case code.Null == (code.Value == "200" || code.Value == "404"):
|
||||
t.Fatalf("code = %+v, want null or a code from src.code", code)
|
||||
case !agree("same", score) || !agree("chain", score) || !agree("dot", score) || !agree("text", code):
|
||||
t.Fatalf("%s: want every read of one src column one draw, through mid too", r.JSON())
|
||||
case cols["pick"].Value != "7" && !agree("pick", score), cols["mixed"].Null || cols["mixed"].Value != "n/a" && cols["mixed"].Value != score.Value:
|
||||
t.Fatalf("pick = %+v, mixed = %+v beside score %+v: want pick 7 or the score's draw, mixed n/a or the score's text", cols["pick"], cols["mixed"], score)
|
||||
case cols["label"].Null || cols["label"].Value != "n="+score.Value:
|
||||
t.Fatalf("label = %+v beside score %+v, want the text of the read, a null as \"\"", cols["label"], score)
|
||||
case cols["pick"].Value != "7" && !isScore("pick"), cols["mixed"].Null || cols["mixed"].Value != "n/a" && cols["mixed"].Value != "" && !digit(cols["mixed"]):
|
||||
t.Fatalf("pick = %+v, mixed = %+v: want pick 7 or a score, mixed n/a or a score's text", cols["pick"], cols["mixed"])
|
||||
case cols["label"].Null || !strings.HasPrefix(cols["label"].Value, "n="):
|
||||
t.Fatalf("label = %+v, want the text of the read, a null as \"\"", cols["label"])
|
||||
case strings.Contains(r.JSON(), `"score":null`) != score.Null:
|
||||
t.Fatalf("JSON() = %s, want a null score written null", r.JSON())
|
||||
}
|
||||
|
||||
+16
-128
@@ -218,25 +218,6 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferencePathIsHeld(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg","born":"1980"}]`,
|
||||
"card": `"{/person.first} {/person.last}"`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(3))
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 50; i++ {
|
||||
got := fake(t, f, "card")
|
||||
if got != "Anna Andersson" && got != "Bo Berg" {
|
||||
t.Fatalf("card = %q, want one person's first and last name", got)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) != 2 {
|
||||
t.Fatalf("card only ever rendered %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceThroughChoiceNeedsEveryVariant(t *testing.T) {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"who": `[{"format":"{f}{h}","f":"1","h":"x"},{"format":"{g}{h}","g":"2","h":"y"}]`,
|
||||
@@ -261,43 +242,23 @@ func TestBareReferenceDrawsEachTime(t *testing.T) {
|
||||
t.Fatal("two bare references always agreed; each should be its own draw")
|
||||
}
|
||||
|
||||
func TestReferenceOverlapIsRejected(t *testing.T) {
|
||||
// A category never references itself, so the reads sit in a second category.
|
||||
func TestReferencesIntoOneLevelLoad(t *testing.T) {
|
||||
cat := `{"format":"x","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`
|
||||
for name, row := range map[string]string{
|
||||
"head beside a path": `"{/cat.p} {/cat.p.first}"`,
|
||||
"sibling fields reading a level and a path into it": `{"format":"{a} {b}","a":"{/cat.p}","b":"{/cat.p.first}"}`,
|
||||
"a field rendering the level a nested reference reads into": `{"format":"{x} {p}","x":"{/cat.p.first}","p":"{/cat.p}"}`,
|
||||
"a bare reference beside a path into what it never renders": `{"format":"{a} {b}","a":"{/cat}","b":"{/cat.p.first}"}`,
|
||||
for _, files := range []map[string]string{
|
||||
{"cat": cat, "row": `"{/cat.p} {/cat.p.first}"`},
|
||||
{"cat": cat, "row": `{"format":"{a} {b}","a":"{/cat.p}","b":"{/cat.p.first}"}`},
|
||||
{"cat": cat, "row": `{"format":"{x} {p}","x":"{/cat.p.first}","p":"{/cat.p}"}`},
|
||||
{"cat": cat, "row": `{"format":"{a} {b}","a":"{/cat}","b":"{/cat.p.first}"}`},
|
||||
{"row": `{"format":"{p.first} {/hop}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`, "hop": `"{/row.p.last}"`},
|
||||
{"sv_SE/person": cat, "sv_SE/mail": `"{.person} <{/sv_SE.person.p.first}>"`},
|
||||
} {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row})))
|
||||
if err == nil || !strings.Contains(err.Error(), "renders its own draw of what") {
|
||||
t.Errorf("%s: New = %v, want the overlap rejected", name, err)
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeData(t, files)), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Errorf("New(%v) = %v, want each read a draw of its own", files, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
for row, suffix := range map[string]string{
|
||||
`{"format":"{a} {b}","a":"{/cat}","b":"{/cat.p.first}"}`: "or give {b} a drawGroup",
|
||||
`{"format":"{a}","a":"{/cat} {/cat.p.first}"}`: "name the fields you want instead",
|
||||
} {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row})))
|
||||
if err == nil || !strings.HasSuffix(err.Error(), suffix) {
|
||||
t.Errorf("New(%s) = %v, want it to end %q", row, err, suffix)
|
||||
}
|
||||
}
|
||||
// A sibling path and a reference into the level it holds are the same overlap,
|
||||
// and the reference reaches it from a category row renders.
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"row": `{"format":"{p.first} {/hop}","p":[{"format":"{first}","first":"A","last":"1"},{"format":"{first}","first":"B","last":"2"}]}`,
|
||||
"hop": `"{/row.p.last}"`,
|
||||
}))); err == nil || !strings.Contains(err.Error(), "by a second route") {
|
||||
t.Errorf("New = %v, want a sibling path beside a reference into it rejected", err)
|
||||
}
|
||||
for _, row := range []string{
|
||||
`{"format":"{a} {b}","a":{"format":"{/cat.p}","drawGroup":"g"},"b":"{/cat.p.first}"}`,
|
||||
`{"format":"{a} {b}","a":"{/cat}","b":{"format":"{/cat.p.first}","drawGroup":"g"}}`,
|
||||
} {
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row}))); err != nil {
|
||||
t.Errorf("New(%s) = %v, want a level and a path into it accepted in groups of their own", row, err)
|
||||
for path := range files {
|
||||
fake(t, f, strings.ReplaceAll(path, "/", "."))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,33 +271,13 @@ func onePerson(name string) bool {
|
||||
return last != "" && map[string]string{"Ada": "Lovelace", "Bo": "Ek", "Cy": "Young"}[first] == last
|
||||
}
|
||||
|
||||
func TestAReferencePathIsOneDrawPerRender(t *testing.T) {
|
||||
func TestFieldsReadOneNameTheirCategoryBinds(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"apart": `{"format":"{a} & {b}","a":{"format":"{/person.first} {/person.last}","drawGroup":"x"},"b":{"format":"{/person.first} {/person.last}","drawGroup":"y"}}`,
|
||||
"caller": `{"format":"{a} & {b}","a":{"format":"{/person.first} {/person.last}","drawGroup":"x"},"b":"{/pay}"}`,
|
||||
"contact": `{"format":"{first} {last} <{email}>","email":"{lowercase(/person.first)}.{lowercase(/person.last)}@example.com","first":"{/person.first}","last":"{/person.last}"}`,
|
||||
"iterations": `{"format":"{/person.first} {r}","drawGroup":"outer","r":{"format":"{a}={b}","repeat":3,"separator":",","a":"{/person.first}","b":{"format":"{/person.first}","drawGroup":"outer"}}}`,
|
||||
"nested": `{"format":"{/person.first} {inner}","inner":"{/person.last}"}`,
|
||||
"pair": `{"format":"{a} & {b}","a":"{/person.first} {/person.last}","b":"{/person.first} {/person.last}"}`,
|
||||
"pay": `{"format":"{p}","p":{"format":"{/person.first} {/person.last}","drawGroup":"x"}}`,
|
||||
"contact": `{"format":"{first} {last} <{email}>","email":"{lowercase(p.first)}.{lowercase(p.last)}@example.com","first":"{/person as p}{p.first}","last":"{p.last}"}`,
|
||||
"person": drawPeople,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
apart, local, noGroup := false, false, false
|
||||
for i := 0; i < 100; i++ {
|
||||
a, b, _ := strings.Cut(fake(t, f, "caller"), " & ")
|
||||
if !onePerson(a) || !onePerson(b) {
|
||||
t.Fatalf("caller = %q & %q, want each one person", a, b)
|
||||
}
|
||||
local = local || a != b
|
||||
_, iterations, _ := strings.Cut(fake(t, f, "iterations"), " ")
|
||||
for _, pair := range strings.Split(iterations, ",") {
|
||||
a, b, _ := strings.Cut(pair, "=")
|
||||
noGroup = noGroup || a != b
|
||||
}
|
||||
if got := fake(t, f, "nested"); !onePerson(got) {
|
||||
t.Fatalf("nested = %q, want a nested template's path one draw with its parent's", got)
|
||||
}
|
||||
name, email, _ := strings.Cut(fake(t, f, "contact"), " <")
|
||||
first, last, _ := strings.Cut(name, " ")
|
||||
if !onePerson(name) || email != strings.ToLower(first)+"."+strings.ToLower(last)+"@example.com>" {
|
||||
@@ -350,24 +291,6 @@ func TestAReferencePathIsOneDrawPerRender(t *testing.T) {
|
||||
if !onePerson(c[1].Value+" "+c[2].Value) || c[0].Value != strings.ToLower(c[1].Value)+"."+strings.ToLower(c[2].Value)+"@example.com" {
|
||||
t.Fatalf("contact record %s, want first, last and email one person", r.JSON())
|
||||
}
|
||||
a, b, _ = strings.Cut(fake(t, f, "pair"), " & ")
|
||||
if !onePerson(a) || a != b {
|
||||
t.Fatalf("pair = %q & %q, want both fields one person", a, b)
|
||||
}
|
||||
a, b, _ = strings.Cut(fake(t, f, "apart"), " & ")
|
||||
if !onePerson(a) || !onePerson(b) {
|
||||
t.Fatalf("apart = %q & %q, want each group one person", a, b)
|
||||
}
|
||||
apart = apart || a != b
|
||||
}
|
||||
if !apart {
|
||||
t.Error("groups x and y drew one person in 100 renders, want a draw each")
|
||||
}
|
||||
if !local {
|
||||
t.Error("group x in caller and group x in the pay it references drew one person in 100 renders; a group name is local to its category")
|
||||
}
|
||||
if !noGroup {
|
||||
t.Error("an iteration's plain read and its group outer always agreed; a repeat iteration renders in no group, so they are a draw each")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,19 +313,6 @@ func TestRelativeReferences(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelativeAndRootSpellingsBindOneDraw(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"sv_SE/person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg","born":"1980"}]`,
|
||||
"sv_SE/card": `"{.person.first} {/sv_SE.person.last}"`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(3))
|
||||
for i := 0; i < 50; i++ {
|
||||
if got := fake(t, f, "sv_SE.card"); got != "Anna Andersson" && got != "Bo Berg" {
|
||||
t.Fatalf("card = %q, want both spellings to read one person", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceSigilErrors(t *testing.T) {
|
||||
for name, files := range map[string]map[string]string{
|
||||
"no folder above the root": {"a": `"{..b}"`, "b": `"x"`},
|
||||
@@ -418,28 +328,6 @@ func TestReferenceSigilErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpellingsOfOneReferenceAreOneLevel(t *testing.T) {
|
||||
person := `[{"format":"{first} {last}","first":"Ada","last":"Byron"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
|
||||
"sv_SE/person": person,
|
||||
"sv_SE/mail": `"{.person} <{/sv_SE.person.first}>"`,
|
||||
})))
|
||||
if err == nil || !strings.Contains(err.Error(), "renders its own draw of what") || !strings.Contains(err.Error(), "{.person}") {
|
||||
t.Errorf("New = %v, want the bare spelling rejected beside the path spelling", err)
|
||||
}
|
||||
dir := writeData(t, map[string]string{
|
||||
"sv_SE/word": `["alpha","beta","gamma"]`,
|
||||
"sv_SE/loud": `"{/sv_SE.word} {uppercase(.word)}"`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(2))
|
||||
for i := 0; i < 50; i++ {
|
||||
got := strings.Fields(fake(t, f, "sv_SE.loud"))
|
||||
if len(got) != 2 || strings.ToUpper(got[0]) != got[1] {
|
||||
t.Fatalf("loud = %q, want one draw under both spellings", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlashAfterAReferenceSigilIsRejected(t *testing.T) {
|
||||
for name, files := range map[string]map[string]string{
|
||||
"after ..": {"sv_SE/person": `"Ada"`, "sv_SE/deep/a": `"{../person}"`},
|
||||
|
||||
+4
-5
@@ -42,8 +42,8 @@ func TestGrowIsALowerBound(t *testing.T) {
|
||||
t.Fatalf("format %q did not compile to a template", format)
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
var draws renderDraws
|
||||
if got := len(expand(f.rand, tmpl, renderScope{draws: &draws})); got < tmpl.compiled.grow {
|
||||
var frames frameStack
|
||||
if got := len(expand(f.rand, tmpl, renderScope{frames: &frames})); got < tmpl.compiled.grow {
|
||||
t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.compiled.grow)
|
||||
}
|
||||
}
|
||||
@@ -111,10 +111,9 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) {
|
||||
"contained": func(n node) { contained(n) },
|
||||
"paths": func(n node) { paths(n) },
|
||||
"prove": func(n node) { (&valueProof{}).prove(n) },
|
||||
"reads": func(n node) { newReadFold().reads(n) },
|
||||
"render": func(n node) {
|
||||
var draws renderDraws
|
||||
render(engine(1).rand, n, renderScope{draws: &draws, row: renderedRow{tbl, 0}})
|
||||
var frames frameStack
|
||||
render(engine(1).rand, n, renderScope{frames: &frames, row: renderedRow{tbl, 0}})
|
||||
},
|
||||
"renderEdges": func(n node) { renderEdges(n) },
|
||||
"stepInto": func(n node) { _, _ = stepInto(n, "x") },
|
||||
|
||||
+34
-14
@@ -93,8 +93,10 @@ func TestFakeStructFieldTaggedWithAColumnIsThatColumn(t *testing.T) {
|
||||
switch {
|
||||
case show(v.Del) != "nil":
|
||||
t.Fatalf("Del = %s, want nil from a column only ever null", show(v.Del))
|
||||
case show(v.Mixed) != "x" && show(v.Mixed) != score, show(v.Pair) != "5" && show(v.Pair) != score, show(v.Codes) != score && show(v.Codes) != code:
|
||||
t.Fatalf("Mixed %s, Pair %s, Codes %s beside score %s and code %s: want each the literal or the draw of the column it reads", show(v.Mixed), show(v.Pair), show(v.Codes), score, code)
|
||||
case !digitOrNil(show(v.Mixed)) && show(v.Mixed) != "x", !digitOrNil(show(v.Pair)) && show(v.Pair) != "5", !digitOrNil(show(v.Codes)) && show(v.Codes) != "200" && show(v.Codes) != "404":
|
||||
t.Fatalf("Mixed %s, Pair %s, Codes %s: want each the literal or a draw of the column it reads", show(v.Mixed), show(v.Pair), show(v.Codes))
|
||||
case score != "nil" && !digitOrNil(score), code != "nil" && code != "200" && code != "404":
|
||||
t.Fatalf("Score %s, Code %s: want each nil or a draw of its column", score, code)
|
||||
}
|
||||
switch {
|
||||
case v.Code == nil:
|
||||
@@ -102,11 +104,11 @@ func TestFakeStructFieldTaggedWithAColumnIsThatColumn(t *testing.T) {
|
||||
case *v.Code != "200" && *v.Code != "404":
|
||||
t.Fatalf("Code = %q, want nil or a code, never a null's \"\"", *v.Code)
|
||||
}
|
||||
switch {
|
||||
case v.Score == nil && v.Label == "n=":
|
||||
if v.Score == nil {
|
||||
nils["score"]++
|
||||
case v.Score == nil || *v.Score < 1 || *v.Score > 9 || v.Label != "n="+strconv.FormatInt(*v.Score, 10):
|
||||
t.Fatalf("%+v, want Score nil beside Label n=, or one digit in both", v)
|
||||
}
|
||||
if label, _ := strings.CutPrefix(v.Label, "n="); !strings.HasPrefix(v.Label, "n=") || label != "" && !digitOrNil(label) {
|
||||
t.Fatalf("Label %q, want n= and a draw of src.score, a null as \"\"", v.Label)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"code", "score"} {
|
||||
@@ -116,6 +118,28 @@ func TestFakeStructFieldTaggedWithAColumnIsThatColumn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func digitOrNil(s string) bool {
|
||||
return s == "nil" || len(s) == 1 && s >= "1" && s <= "9"
|
||||
}
|
||||
|
||||
func TestFakeStructPathTagsReadOnePickOfTheirCategory(t *testing.T) {
|
||||
f := structData(t)
|
||||
var v struct {
|
||||
Leg string `fake:"trip.leg"`
|
||||
To string `fake:"trip.leg.to"`
|
||||
Name string `fake:"person"`
|
||||
Last string `fake:"person.last"`
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
if err := f.FakeStruct(&v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Leg != v.To || v.Name != "Ada "+v.Last && v.Name != "Bo "+v.Last {
|
||||
t.Fatalf("%+v, want the tags into trip one leg, and into person one person", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeStructFillsTaggedFields(t *testing.T) {
|
||||
a, b := structData(t), structData(t)
|
||||
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
|
||||
@@ -133,8 +157,8 @@ func TestFakeStructFillsTaggedFields(t *testing.T) {
|
||||
switch {
|
||||
case !reflect.DeepEqual(u, twin):
|
||||
t.Fatalf("same seed diverged: %+v != %+v", u, twin)
|
||||
case people[u.First] != u.Last || u.Email != strings.ToLower(u.First)+"@example.com":
|
||||
t.Fatalf("person fields %q %q %q, want one person drawn across the struct", u.First, u.Last, u.Email)
|
||||
case people[u.First] != u.Last || u.Email != "ada@example.com" && u.Email != "bo@example.com":
|
||||
t.Fatalf("person fields %q %q %q, want one person across the path tags, and the template its own draw", u.First, u.Last, u.Email)
|
||||
case zips[u.Home.City] != u.Home.Zip || u.Work == nil || zips[u.Work.City] != u.Work.Zip:
|
||||
t.Fatalf("places %+v, %+v, want each nested struct one place, the pointer allocated", u.Home, u.Work)
|
||||
case u.ID != int64(i+1) || u.Age < 18 || u.Age > 99 || u.Score < 0 || u.Score > 1 || u.Rank == nil || (*u.Rank != 1 && *u.Rank != 2):
|
||||
@@ -162,8 +186,8 @@ func TestFakeStructFillsEmbeddedFieldsIntoItsRecord(t *testing.T) {
|
||||
if err := f.FakeStruct(&e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e.StructFamily == nil || people[e.First] != e.Last || e.Email != strings.ToLower(e.First)+"@example.com" {
|
||||
t.Fatalf("%+v, %+v: want the promoted fields one person with the struct's own, the embedded pointer allocated", e, e.StructFamily)
|
||||
if e.StructFamily == nil || people[e.First] != e.Last {
|
||||
t.Fatalf("%+v, %+v: want the promoted fields one person, the embedded pointer allocated", e, e.StructFamily)
|
||||
}
|
||||
}
|
||||
var skipped struct {
|
||||
@@ -293,10 +317,6 @@ func TestFakeStructErrors(t *testing.T) {
|
||||
{&struct {
|
||||
A string `fake:"{x}"`
|
||||
}{}, `no field "x"`},
|
||||
{&struct {
|
||||
A string `fake:"trip.leg"`
|
||||
B string `fake:"trip.leg.to"`
|
||||
}{}, "renders its own draw of what"},
|
||||
{&struct {
|
||||
Trip struct {
|
||||
A int `fake:"{digits(3)}"`
|
||||
|
||||
+96
-229
@@ -1,7 +1,6 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -237,13 +236,10 @@ func TestTableDescendsToALinkedTable(t *testing.T) {
|
||||
|
||||
func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
||||
files := with(geo(), map[string]string{
|
||||
"drawn.json": `"{/locality.code}|{/locality..municipality.code}|{/locality..municipality..region.code}"`,
|
||||
"named.json": `"{/locality as l}{l.code}|{l..municipality.code}|{l..municipality..region.code}"`,
|
||||
"down.json": `"{/locality.code}|{/locality..municipality.locality.code}"`,
|
||||
"named-down.json": `"{/locality as l}{l.code}|{l..municipality.locality.code}"`,
|
||||
"twice.json": `"{/locality..municipality.locality.code}|{/locality..municipality.locality.name}"`,
|
||||
"named-twice.json": `"{/locality as l}{l..municipality.locality.code}|{l..municipality.locality.name}"`,
|
||||
"record.json": `{"format":"","l":"{/locality.code}","m":"{/locality..municipality.code}"}`,
|
||||
"record.json": `{"format":"","l":"{/locality as x}{x.code}","m":"{x..municipality.code}"}`,
|
||||
})
|
||||
f := newGenerator(t, writeFiles(t, files), WithSeed(4))
|
||||
for path, want := range map[string]string{
|
||||
@@ -264,11 +260,8 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
||||
names := map[string]string{"L1": "Stockholm", "L2": "Solna", "L3": "Malmö", "L4": "Lund", "L5": "Göteborg", "L6": "Hisingen", "L7": "Sandby", "L8": "Sandby"}
|
||||
afresh := map[string]bool{}
|
||||
for i := 0; i < 200; i++ {
|
||||
for _, path := range []string{"drawn", "named"} {
|
||||
parts := strings.Split(fake(t, f, path), "|")
|
||||
if municipalityOf[parts[0]] != parts[1] || regionOf[parts[1]] != parts[2] {
|
||||
t.Fatalf("%s = %v, want the locality's own municipality and region", path, parts)
|
||||
}
|
||||
if parts := strings.Split(fake(t, f, "named"), "|"); municipalityOf[parts[0]] != parts[1] || regionOf[parts[1]] != parts[2] {
|
||||
t.Fatalf("named = %v, want the locality's own municipality and region", parts)
|
||||
}
|
||||
if l := fake(t, f, "locality[L4]..municipality.locality.code"); l != "L4" && l != "L7" {
|
||||
t.Fatalf("locality[L4]..municipality.locality.code = %q, outside Lund", l)
|
||||
@@ -278,17 +271,11 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
||||
if m := fake(t, f, "locality[L4]..municipality..region.municipality.code"); regionOf[m] != "12" {
|
||||
t.Fatalf("locality[L4]..municipality..region.municipality.code = %q, outside 12", m)
|
||||
}
|
||||
for _, path := range []string{"down", "named-down"} {
|
||||
parts := strings.Split(fake(t, f, path), "|")
|
||||
if municipalityOf[parts[0]] != municipalityOf[parts[1]] {
|
||||
t.Fatalf("%s = %v, want a locality of the first one's municipality", path, parts)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"twice", "named-twice"} {
|
||||
parts := strings.Split(fake(t, f, path), "|")
|
||||
if names[parts[0]] != parts[1] {
|
||||
t.Fatalf("%s = %v, want both reads of one step down to read one locality", path, parts)
|
||||
if parts := strings.Split(fake(t, f, "named-down"), "|"); municipalityOf[parts[0]] != municipalityOf[parts[1]] {
|
||||
t.Fatalf("named-down = %v, want a locality of the first one's municipality", parts)
|
||||
}
|
||||
if parts := strings.Split(fake(t, f, "named-twice"), "|"); names[parts[0]] != parts[1] {
|
||||
t.Fatalf("named-twice = %v, want both reads of one step down under a name to read one locality", parts)
|
||||
}
|
||||
down, err := f.FakeRecord("locality[L4]..municipality.locality")
|
||||
if err != nil {
|
||||
@@ -316,11 +303,6 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
|
||||
if err := f.FakeStruct(&tagged); err != nil || tagged.M != "Lund" {
|
||||
t.Errorf("a struct tag stepping up = %q, %v; want Lund", tagged.M, err)
|
||||
}
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{
|
||||
"x.json": `"{/locality..municipality.code}|{/locality[L1].name}"`,
|
||||
})))); err == nil || !strings.Contains(err.Error(), "{/locality[L1]..municipality.code}") {
|
||||
t.Errorf("New = %v, want the fence to name {/locality[L1]..municipality.code}", err)
|
||||
}
|
||||
for _, p := range f.List() {
|
||||
if strings.Contains(p, "..") {
|
||||
t.Fatalf("List() advertises %q; it lists direct descents only", p)
|
||||
@@ -359,11 +341,12 @@ func TestTableStepUpRefusals(t *testing.T) {
|
||||
|
||||
func TestLinkedTablesDrawConsistently(t *testing.T) {
|
||||
spellings := map[string]string{
|
||||
"leaf first": `{"format":"{l}|{m}|{r}","l":"{/locality.code}","m":"{/municipality.code}","r":"{/region.code}"}`,
|
||||
"root first": `{"format":"{r}|{m}|{l}","r":"{/region.code}","m":"{/municipality.code}","l":"{/locality.code}"}`,
|
||||
"skipping one": `{"format":"{r}|{l}","r":"{/region.code}","l":"{/locality.code}"}`,
|
||||
"nested": `{"format":"{a}|{l}","a":{"format":"{r}|{m}","r":"{/region.code}","m":"{/municipality.code}"},"l":"{/locality.code}"}`,
|
||||
"a record": `{"format":"","l":"{/locality.code}","m":"{/municipality.code}","r":"{/region.code}"}`,
|
||||
"leaf first": `{"format":"{l}|{m}|{r}","l":"{/locality as x}{x.code}","m":"{x..municipality.code}","r":"{x..municipality..region.code}"}`,
|
||||
"root first": `{"format":"{r}|{m}|{l}","r":"{/region as x}{x.code}","m":"{x.municipality.code}","l":"{x.municipality.locality.code}"}`,
|
||||
"skipping one": `{"format":"{r}|{l}","r":"{/region as x}{x.code}","l":"{x.locality.code}"}`,
|
||||
"a skip read first": `{"format":"{l}|{m}|{r}","l":"{/region as x}{x.locality.code}","m":"{x.municipality.code}","r":"{x.code}"}`,
|
||||
"nested": `{"format":"{a}|{l}","a":{"format":"{r}|{m}","r":"{x..municipality..region.code}","m":"{x..municipality.code}"},"l":"{/locality as x}{x.code}"}`,
|
||||
"a record": `{"format":"","l":"{/locality as x}{x.code}","m":"{x..municipality.code}","r":"{x..municipality..region.code}"}`,
|
||||
}
|
||||
for name, addr := range spellings {
|
||||
f := newGenerator(t, writeFiles(t, with(geo(), map[string]string{"addr.json": addr})), WithSeed(5))
|
||||
@@ -406,9 +389,9 @@ func TestLinkedTablesDrawConsistently(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkedTablesDrawApartAcrossGroupsAndRepeats(t *testing.T) {
|
||||
func TestLinkedTablesDrawApartAcrossReadsAndRepeats(t *testing.T) {
|
||||
files := with(geo(), map[string]string{
|
||||
"groups.json": `{"format":"{a}|{b}","a":{"format":"{/locality.code}","drawGroup":"g"},"b":"{/locality.code}"}`,
|
||||
"groups.json": `"{/locality.code}|{/region.locality.code}"`,
|
||||
"many.json": `{"format":"{x}","x":{"format":"{/locality.code}","repeat":8,"separator":"|"}}`,
|
||||
})
|
||||
f := newGenerator(t, writeFiles(t, files), WithSeed(9))
|
||||
@@ -421,14 +404,14 @@ func TestLinkedTablesDrawApartAcrossGroupsAndRepeats(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !differ {
|
||||
t.Fatalf("%s: every draw agreed in 100 renders, want groups and repeat iterations drawn apart", path)
|
||||
t.Fatalf("%s: every draw agreed in 100 renders, want reads and repeat iterations drawn apart", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiblingTablesDrawInsideOneAncestor(t *testing.T) {
|
||||
files := with(siblings(), map[string]string{
|
||||
"addr.json": `{"format":"{s}|{p}|{l}","s":"{/street.name}","p":"{/postal-code.code}","l":"{/locality.code}"}`,
|
||||
"addr.json": `{"format":"{s}|{p}|{l}","s":"{/locality as x}{x.street.name}","p":"{x.postal-code.code}","l":"{x.code}"}`,
|
||||
})
|
||||
f := newGenerator(t, writeFiles(t, files), WithSeed(3))
|
||||
seen := map[string]bool{}
|
||||
@@ -463,7 +446,7 @@ func TestSiblingTablesDrawInsideOneAncestor(t *testing.T) {
|
||||
|
||||
func TestTableSelectorInAReference(t *testing.T) {
|
||||
files := with(geo(), map[string]string{
|
||||
"x.json": `{"format":"{a} {b} {c}","a":"{/region[12].name}","b":"{/region[12].municipality.code}","c":"{/region[12].locality.code}"}`,
|
||||
"x.json": `{"format":"{a} {b} {c}","a":"{/region[12] as x}{x.name}","b":"{x.municipality.code}","c":"{x.locality.code}"}`,
|
||||
})
|
||||
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
@@ -487,47 +470,31 @@ func TestTableSelectorInAReference(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableSelectionFences(t *testing.T) {
|
||||
rejected := map[string]struct{ json, want string }{
|
||||
"bare beside a path": {`"{/region} {/region.name}"`, "renders its own draw of what"},
|
||||
"bare beside a linked path": {`"{/region} {/locality.name}"`, "renders its own draw of what"},
|
||||
"selected beside unselected": {`"{/region[12].municipality.name} {/municipality.code}"`, "{/region[12].municipality.code}"},
|
||||
"relative selected beside unselected": {`"{.region[12].municipality.name} {.municipality.code}"`, "{.region[12].municipality.code}"},
|
||||
"two selections": {`"{/region[12].name} {/region[14].name}"`, "drawGroup"},
|
||||
"selection beside a descendant": {`"{/region[12].name} {/locality.code}"`, "{/region[12].locality.code}"},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json}))))
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it rejected mentioning %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
accepted := map[string]string{
|
||||
"a descent beside a path into it": `"{/region.municipality} {/region.municipality.name}"`,
|
||||
"selections in two groups": `{"format":"{a} {b}","a":{"format":"{/region[12].name}","drawGroup":"a"},"b":"{/region[14].name}"}`,
|
||||
"one selection twice": `"{/region[12].name} {/region[12].timezone} {/region[12].locality.name}"`,
|
||||
"a bare table in another group": `{"format":"{a} {b}","a":{"format":"{/region}","drawGroup":"a"},"b":"{/locality.name}"}`,
|
||||
}
|
||||
for name, json := range accepted {
|
||||
func TestTableReadsBesideEachOtherLoad(t *testing.T) {
|
||||
for _, json := range []string{
|
||||
`"{/region} {/region.name}"`,
|
||||
`"{/region} {/locality.name}"`,
|
||||
`"{/region[12].municipality.name} {/municipality.code}"`,
|
||||
`"{.region[12].municipality.name} {.municipality.code}"`,
|
||||
`"{/region[12].name} {/region[14].name}"`,
|
||||
`"{/region[12].name} {/locality.code}"`,
|
||||
`"{/region.municipality} {/region.municipality.name}"`,
|
||||
`"{/region[12].name} {/region[12].timezone} {/region[12].locality.name}"`,
|
||||
`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`,
|
||||
`"{/region[14].name} {/locality[L4].name}"`,
|
||||
} {
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Errorf("%s: New = %v, want it accepted", name, err)
|
||||
t.Errorf("%s: New = %v, want each read a draw of its own", json, err)
|
||||
continue
|
||||
}
|
||||
fake(t, f, "x")
|
||||
}
|
||||
f := newGenerator(t, writeFiles(t, with(geo(), map[string]string{"x.json": accepted["a descent beside a path into it"]})), WithSeed(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
if parts := strings.Fields(fake(t, f, "x")); parts[0] != parts[1] {
|
||||
t.Fatalf("x = %v, want the descended row and the path into it to agree", parts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The family fence replays each read's selectors through the walk the render uses, so
|
||||
// spellings that pin the same rows by different routes agree, and two rows of one
|
||||
// table are refused at load rather than found at render.
|
||||
func TestTableFamilyFenceReplaysPins(t *testing.T) {
|
||||
// A selector names one row wherever it is written, so spellings that select the same rows by
|
||||
// different routes agree.
|
||||
func TestSelectedRowsAgreeAcrossSpellings(t *testing.T) {
|
||||
accepted := map[string]string{
|
||||
"skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`,
|
||||
"descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`,
|
||||
@@ -548,16 +515,6 @@ func TestTableFamilyFenceReplaysPins(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rejected := map[string]struct{ json, want string }{
|
||||
"two rows of one table": {`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, "drawGroup"},
|
||||
"a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "drawGroup"},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json}))))
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: New = %v, want it rejected mentioning %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableFences(t *testing.T) {
|
||||
@@ -585,11 +542,6 @@ func TestTableFences(t *testing.T) {
|
||||
"name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"},
|
||||
"a name repeating inside one parent row": {with(siblings(), map[string]string{"street.json": `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality"}`, "street.tsv": "name\tlocality\nAvenyn\tL1\nAvenyn\tL1\nStorgatan\tL2\nStorgatan\tL3\nStorgatan\tL4\nStorgatan\tL5\nStorgatan\tL6\nStorgatan\tL7\nStorgatan\tL8\n"}), `"Avenyn"`},
|
||||
"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 cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}), "family"},
|
||||
"a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"},
|
||||
"a format's reference beside a path into it": {map[string]string{"x.json": `{"format":"{v} {/z}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "z.json": `{"format":"{a}","a":["p","q"]}`, "r.json": `"{/x} {/z.a}"`}, "renders its own draw of what {/z.a}"},
|
||||
"a selected row's format selecting another row": {map[string]string{"x.json": `{"format":"{v} {/y[1].w}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `{"format":"{w}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tw\n1\tm\n2\tn\n", "r.json": `"{/x[1]} {/y[2].w}"`}, "two rows of y"},
|
||||
"a whole table's format selecting another row": {map[string]string{"x.json": `{"format":"{v} {/y[1].w}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `{"format":"{w}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tw\n1\tm\n2\tn\n", "r.json": `"{/x} {/y[2].w}"`}, "two rows of 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"},
|
||||
@@ -601,10 +553,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"},
|
||||
"a table reaching its family through a template": {with(geo(), map[string]string{"addr.json": `"{/locality.name}"`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
|
||||
"a table reaching its family through a repeat": {with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name} ","repeat":3}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
|
||||
"a table reaching its family through a group": {with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name}","drawGroup":"g"}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
|
||||
"a selected row reaching its family's other row": {with(geo(), map[string]string{"a.json": `"{/region[01].note}"`, "addr.json": `{"format":"{/region[12].name}","drawGroup":"g"}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
|
||||
"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"},
|
||||
@@ -619,13 +567,11 @@ func TestTableFences(t *testing.T) {
|
||||
"rows in a choice item": {map[string]string{"t.json": `[{"format":"{a}","rows":"t.tsv"},"y"]`, "t.tsv": "a\nx\ny\n"}, "category"},
|
||||
"unknown table option": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","fields":"a"}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
|
||||
"repeat on a table": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","repeat":2}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
|
||||
"drawGroup on a table": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","drawGroup":"g"}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
|
||||
"option not a string": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":1}`, "t.tsv": "a\nx\ny\n"}, "string"},
|
||||
"format names no column": {map[string]string{"t.json": `{"format":"{b}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `no column "b"`},
|
||||
"format reads into a column": {map[string]string{"t.json": `{"format":"{a.x}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `"a"`},
|
||||
"a reference into a column": {with(base, map[string]string{"t.json": `"{/region.name.x}"`}), "column"},
|
||||
"a category referencing itself": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n{/t.a}\ny\n"}, "names the category it sits in"},
|
||||
"a cell repeating a reference": {with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n{/region} {/region} {uppercase(/region)}\n"}), "t.a, line 3: token {/region} is repeated"},
|
||||
}
|
||||
for name, c := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, c.files)))
|
||||
@@ -637,6 +583,29 @@ func TestTableFences(t *testing.T) {
|
||||
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
accepted := map[string]map[string]string{
|
||||
"a cell reading its family": with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}),
|
||||
"a format reading its family": with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}),
|
||||
"a format's reference beside a path into it": map[string]string{"x.json": `{"format":"{v} {/z}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "z.json": `{"format":"{a}","a":["p","q"]}`, "r.json": `"{/x} {/z.a}"`},
|
||||
"a selected row's format selecting another row": map[string]string{"x.json": `{"format":"{v} {/y[1].w}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `{"format":"{w}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tw\n1\tm\n2\tn\n", "r.json": `"{/x[1]} {/y[2].w}"`},
|
||||
"a whole table's format selecting another row": map[string]string{"x.json": `{"format":"{v} {/y[1].w}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `{"format":"{w}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tw\n1\tm\n2\tn\n", "r.json": `"{/x} {/y[2].w}"`},
|
||||
"a table reaching its family through a template": with(geo(), map[string]string{"addr.json": `"{/locality.name}"`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
|
||||
"a table reaching its family through a repeat": with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name} ","repeat":3}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
|
||||
"a selected row reaching its family's other row": with(geo(), map[string]string{"a.json": `"{/region[01].note}"`, "addr.json": `{"format":"{/region[12].name}","drawGroup":"g"}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
|
||||
"a cell repeating a reference": with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n{/region} {/region} {uppercase(/region)}\n"}),
|
||||
}
|
||||
for name, files := range accepted {
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files)), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Errorf("%s: New = %v, want each read a draw of its own", name, err)
|
||||
continue
|
||||
}
|
||||
for _, path := range f.List() {
|
||||
if _, err := f.Fake(path); err != nil {
|
||||
t.Errorf("%s: Fake(%s) = %v", name, path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := New(WithoutShippedData(), WithDataFS(os.DirFS(writeFiles(t, base)))); err != nil {
|
||||
t.Fatalf("New(WithDataFS) = %v, want a table loaded from any fs.FS", err)
|
||||
}
|
||||
@@ -774,7 +743,8 @@ func TestTableRowsAreAlternatives(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rejected := map[string]map[string]string{
|
||||
twoRows := map[string]map[string]string{
|
||||
"two draws landing on cells selecting two rows": {"z.json": `"{/country}|{/country}"`},
|
||||
"one cell selecting two rows": {"country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/cur[EUR].sym}{/cur[SEK].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
|
||||
"two tables' cells in one render": {"z.json": `"{/country.money} {/y.sym}"`},
|
||||
"the format beside a cell": {"country.json": `{"format":"{money} {/cur[SEK].sym}","rows":"country.tsv","key":"alpha2"}`},
|
||||
@@ -782,10 +752,20 @@ func TestTableRowsAreAlternatives(t *testing.T) {
|
||||
"a whole read beside a path": {"country.json": `{"format":"{a} {b}","rows":"country.tsv","key":"alpha2"}`, "country.tsv": "alpha2\ta\tb\nFI\t{/cur}\t{/cur[EUR].sym}\nSE\t{/cur}\t{/cur[SEK].sym}\n"},
|
||||
"a cell reaching another's cells": {"y.json": `{"format":"{sym}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tsym\n1\t{/cur[SEK].sym}\n2\t{/cur[USD].sym}\n", "cur.tsv": "code\tsym\nEUR\t€\nSEK\tkr\nUSD\t$\n", "country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/y[1].sym} {/cur[EUR].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
|
||||
}
|
||||
for name, more := range rejected {
|
||||
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))))
|
||||
if err == nil || !strings.Contains(err.Error(), "drawGroup") {
|
||||
t.Errorf("%s: New = %v, want it refused", name, err)
|
||||
for name, more := range twoRows {
|
||||
g, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Errorf("%s: New = %v, want each read a draw of its own", name, err)
|
||||
continue
|
||||
}
|
||||
paths := []string{"country"}
|
||||
if _, defined := more["z.json"]; defined {
|
||||
paths = append(paths, "z")
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
for _, path := range paths {
|
||||
fake(t, g, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -913,9 +893,9 @@ func TestShippedTables(t *testing.T) {
|
||||
t.Fatalf("territory draws %d distinct rows in 5000, want the full register", len(count))
|
||||
}
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fakeTemplate(t, f, `{/territory.alpha2} {/timezone.territory}`)
|
||||
got := fakeTemplate(t, f, `{/timezone as z}{z..territory.alpha2} {z.territory}`)
|
||||
if drawn, zone, _ := strings.Cut(got, " "); drawn != zone {
|
||||
t.Fatalf("%q: a territory and a timezone in one render disagree on the territory", got)
|
||||
t.Fatalf("%q: a timezone and the territory it steps up to disagree", got)
|
||||
}
|
||||
}
|
||||
var row struct {
|
||||
@@ -1110,86 +1090,20 @@ func TestAmbiguousNameNamesARunnablePath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnteredRowsAgreeWithPinning holds what a fence walk decides about the rows it entered, and
|
||||
// the rows a render draws, to what pinning refuses: drift between them is data that loads and then
|
||||
// renders a family that disagrees. A row rendered whole is drawn over the whole table, apart from
|
||||
// every pin, so it is an alternative only to another row of its own draw.
|
||||
func TestEnteredRowsAgreeWithPinning(t *testing.T) {
|
||||
// TestAStepSitsAtTheSegmentItConsumes holds a path's compiled steps to its segments.
|
||||
func TestAStepSitsAtTheSegmentItConsumes(t *testing.T) {
|
||||
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
|
||||
var tables []*table
|
||||
for _, category := range []string{"region", "municipality", "locality"} {
|
||||
tbl, isTable := f.root.children[category].(*table)
|
||||
if !isTable {
|
||||
t.Fatalf("%s is not a table", category)
|
||||
}
|
||||
tables = append(tables, tbl)
|
||||
}
|
||||
var none pinSet
|
||||
for _, entered := range tables {
|
||||
for row := 0; row < entered.rowCount(); row++ {
|
||||
whole := pathRead{}
|
||||
whole.branches.wholePins.add(entered, row)
|
||||
s := none.entered(entered, row)
|
||||
for _, cand := range tables {
|
||||
for i := 0; i < 20; i++ {
|
||||
drawing := s.clone()
|
||||
if r := cand.drawIn(f.rand, &drawing); s.clash(cand, r) != nil {
|
||||
t.Errorf("a render draws %s beside %s, which pinning it there refuses", cand.selectorSpelling(r), entered.selectorSpelling(row))
|
||||
}
|
||||
}
|
||||
for cr := 0; cr < cand.rowCount(); cr++ {
|
||||
beside := s.clone()
|
||||
refused := beside.pinRow(cand, cr) != nil
|
||||
candAt := pathRead{branches: branches{pins: none.entered(cand, cr)}}
|
||||
if got := coRender(pathRead{branches: branches{pins: s}}, candAt); got == refused {
|
||||
t.Errorf("coRender(%s, %s) = %v, but pinning both refuses = %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), got, refused)
|
||||
}
|
||||
if !coRender(whole, candAt) || !coRender(candAt, whole) {
|
||||
t.Errorf("%s rendered whole is an alternative to %s pinned, though the whole draw ignores the pin", entered.selectorSpelling(row), cand.selectorSpelling(cr))
|
||||
}
|
||||
other := pathRead{}
|
||||
other.branches.wholePins.add(cand, cr)
|
||||
if want := cand == entered && cr != row; coRender(whole, other) == want {
|
||||
t.Errorf("coRender(%s, %s), both rendered whole, = %v, want %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), want, !want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var spilled pinSet
|
||||
for i := 0; i <= len(spilled.inline); i++ {
|
||||
spilled = spilled.entered(&table{path: strconv.Itoa(i)}, 0)
|
||||
}
|
||||
_ = spilled.entered(&table{path: "past"}, 0)
|
||||
n := 0
|
||||
spilled.each(func(*table, int) { n++ })
|
||||
if n != len(spilled.inline)+1 {
|
||||
t.Errorf("entering a row beside a spilled pin set leaves it holding %d rows, want %d", n, len(spilled.inline)+1)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableReadReportsTheTablesADrawPins holds a path's compiled steps to the render: the tables
|
||||
// its table read says it draws are those a draw of it pins beyond what its selectors pin, each
|
||||
// selector is credited to the table it selects a row of, and a step naming a segment sits at it.
|
||||
func TestTableReadReportsTheTablesADrawPins(t *testing.T) {
|
||||
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
|
||||
for _, c := range []struct{ path, sels string }{
|
||||
{"locality.code", ""},
|
||||
{"municipality.locality.name", ""},
|
||||
{"municipality[1281].locality.name", "municipality=municipality[1281]"},
|
||||
{"municipality.locality[L4].name", "locality=municipality.locality[L4]"},
|
||||
{"region", ""},
|
||||
{"region.municipality", ""},
|
||||
{"region.municipality.name", ""},
|
||||
{"region.locality.name", ""},
|
||||
{"region[12].locality", "region=region[12]"},
|
||||
{"region[12].locality.code", "region=region[12]"},
|
||||
{"region[12].locality[L4].name", "region=region[12] locality=region[12].locality[L4]"},
|
||||
{"region[12].municipality.locality.code", "region=region[12]"},
|
||||
{"region[12].municipality[1281].locality.code", "region=region[12] municipality=region[12].municipality[1281]"},
|
||||
{"region[12].name", "region=region[12]"},
|
||||
for _, path := range []string{
|
||||
"locality.code",
|
||||
"municipality.locality.name",
|
||||
"municipality[1281].locality.name",
|
||||
"municipality.locality[L4].name",
|
||||
"region.municipality.name",
|
||||
"region[12].locality[L4].name",
|
||||
"region[12].municipality[1281].locality.code",
|
||||
"region[12].name",
|
||||
} {
|
||||
a := splitArm(c.path, nil)
|
||||
a := splitArm(path, nil)
|
||||
head := f.root.children[a.head].(*table)
|
||||
a.steps = compilePath(head, a.tail).steps
|
||||
for _, st := range a.steps {
|
||||
@@ -1203,75 +1117,28 @@ func TestTableReadReportsTheTablesADrawPins(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if st.at >= len(a.tail) || a.tail[st.at] != seg {
|
||||
t.Errorf("%s: step %q sits at %d, want the segment it consumes", c.path, seg, st.at)
|
||||
}
|
||||
}
|
||||
tr := tableReadOf(head, a, nil)
|
||||
var sels []string
|
||||
for _, s := range tr.sels {
|
||||
sels = append(sels, s.t.segment+"="+s.spelling)
|
||||
}
|
||||
if got := strings.Join(sels, " "); got != c.sels {
|
||||
t.Errorf("%s: the table read credits the selectors %q, want %q", c.path, got, c.sels)
|
||||
}
|
||||
selected, drawn := tr.pins, tr.drawn
|
||||
for i := 0; i < 20; i++ {
|
||||
var pins pinSet
|
||||
drawSteps(f.rand, head, a.steps, &pins, nil, nil)
|
||||
want := map[*table]bool{}
|
||||
pins.each(func(tbl *table, _ int) {
|
||||
if _, isSelected := selected.pinned(tbl); !isSelected {
|
||||
want[tbl] = true
|
||||
}
|
||||
})
|
||||
if !maps.Equal(drawn, want) {
|
||||
t.Fatalf("%s: the table read reports %v drawn, a draw pins %v", c.path, segmentsOf(drawn), segmentsOf(want))
|
||||
t.Errorf("%s: step %q sits at %d, want the segment it consumes", path, seg, st.at)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func segmentsOf(tables map[*table]bool) []string {
|
||||
var out []string
|
||||
for tbl := range tables {
|
||||
out = append(out, tbl.segment)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// TestCellReadsMeetWhereTheRenderPairsTheRows reads what the TSVs below do not show: region 01's
|
||||
// note holds {/addr} and 1280's, under region 12, holds {/addr.city}, so the two meet only where
|
||||
// no read pins a region.
|
||||
func TestCellReadsMeetWhereTheRenderPairsTheRows(t *testing.T) {
|
||||
// TestCellReadsLoadAndRender reads what the TSVs below do not show: region 01's note holds
|
||||
// {/addr} and 1280's holds {/addr.city}, and each read of a cell draws on its own.
|
||||
func TestCellReadsLoadAndRender(t *testing.T) {
|
||||
cells := with(geo(), map[string]string{
|
||||
"addr.json": `{"format":"{city}","city":["Lund","Malmö"]}`,
|
||||
"municipality.tsv": "code\tname\tregion\tpopulation\tnote\n0180\tStockholm\t01\t980000\t-\n0184\tSolna\t01\t85000\t-\n1280\tMalmö\t12\t360000\t{/addr.city}\n1281\tLund\t12\t130000\t-\n1480\tGöteborg\t14\t590000\t-\n",
|
||||
"region.tsv": "code\tname\tpopulation\tnote\n01\tStockholms län\t2400000\t{/addr}\n12\tSkåne län\t1400000\t-\n14\tVästra Götalands län\t1750000\t-\n",
|
||||
})
|
||||
paths := with(cells, map[string]string{"x.json": `"{/region.note} {/municipality.note}"`})
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, paths)), WithSeed(1))
|
||||
for _, x := range []string{`"{/region.note} {/municipality.note}"`, `"{/region}|{/municipality}"`} {
|
||||
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(cells, map[string]string{"x.json": x}))), WithSeed(1))
|
||||
if err != nil {
|
||||
t.Fatalf("New = %v, want the two cells accepted", err)
|
||||
t.Fatalf("New(%s) = %v, want the cells accepted", x, err)
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
fake(t, f, "x")
|
||||
whole := with(cells, map[string]string{
|
||||
"municipality.json": `{"format":"{name}={note}","rows":"municipality.tsv","key":"code","name":"name","parent":"region","weight":"population"}`,
|
||||
"region.json": `{"format":"{name}={note}","rows":"region.tsv","key":"code","name":"name","weight":"population"}`,
|
||||
"x.json": `"{/region}|{/municipality}"`,
|
||||
})
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, whole)), WithSeed(1)); err == nil || !strings.HasSuffix(err.Error(), "or move {/municipality} into a field with a drawGroup") {
|
||||
t.Fatalf("New = %v, want the two drawn rows held to one draw of addr, and the drawGroup named on a field around the read", err)
|
||||
}
|
||||
nested := with(whole, map[string]string{
|
||||
"municipality.tsv": "code\tname\tregion\tpopulation\tnote\n0180\tStockholm\t01\t980000\t-\n0184\tSolna\t01\t85000\t-\n1280\tMalmö\t12\t360000\t{/shop}\n1281\tLund\t12\t130000\t{/addr.city}\n1480\tGöteborg\t14\t590000\t-\n",
|
||||
"region.tsv": "code\tname\tpopulation\tnote\n01\tStockholms län\t2400000\t-\n12\tSkåne län\t1400000\t-\n14\tVästra Götalands län\t1750000\t-\n",
|
||||
"shop.json": `{"format":"{name}={note}","rows":"shop.tsv","key":"code","name":"name"}`,
|
||||
"shop.tsv": "code\tname\tnote\nS1\tKiosk\t{/addr}\nS2\tBod\t-\n",
|
||||
"x.json": `"{/municipality}"`,
|
||||
})
|
||||
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, nested)), WithSeed(1)); err != nil {
|
||||
t.Fatalf("New = %v, want 1280's shop and 1281's city kept apart, one draw of municipality rendering one of them", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -15,9 +15,9 @@ func TestTransforms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformReadsTheHeldDraw(t *testing.T) {
|
||||
func TestTransformReadsTheNamedDraw(t *testing.T) {
|
||||
f := engine(2)
|
||||
src := `{"format":"{p.first}={lowercase(p.first)}","p":[{"format":"{first}","first":"Anna"},{"format":"{first}","first":"Bo"}]}`
|
||||
src := `{"format":"{p as q}{q.first}={lowercase(q.first)}","p":[{"format":"{first}","first":"Anna"},{"format":"{first}","first":"Bo"}]}`
|
||||
for i := 0; i < 50; i++ {
|
||||
if got := mustRender(t, f, src); got != "Anna=anna" && got != "Bo=bo" {
|
||||
t.Fatalf("render = %q, want the transform over the same draw", got)
|
||||
@@ -28,7 +28,7 @@ func TestTransformReadsTheHeldDraw(t *testing.T) {
|
||||
func TestTransformOverAReference(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"person": `[{"format":"{first} {last}","first":"Åsa","last":"Öberg"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`,
|
||||
"email": `"{/person.first} {/person.last} <{lowercase(ascii(/person.first))}.{lowercase(ascii(/person.last))}@example.com>"`,
|
||||
"email": `"{/person as p}{p.first} {p.last} <{lowercase(ascii(p.first))}.{lowercase(ascii(p.last))}@example.com>"`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 50; i++ {
|
||||
@@ -45,7 +45,6 @@ func TestTransformArgs(t *testing.T) {
|
||||
`{"format":"{lowercase(nope)}","x":"v"}`,
|
||||
`{"format":"{ascii(x,y)}","x":"v","y":"w"}`,
|
||||
`{"format":"{lowercase(hex(2))}","x":"v"}`,
|
||||
`{"format":"{lowercase(x)} {x.a}","x":{"format":"{a}","a":"1"}}`,
|
||||
} {
|
||||
if _, err := f.NewTemplate(bad); err == nil {
|
||||
t.Errorf("NewTemplate(%s) = nil error, want it rejected", bad)
|
||||
|
||||
Reference in New Issue
Block a user