Tests for a dotted sibling token drawing its head once per expansion
This commit is contained in:
+192
@@ -0,0 +1,192 @@
|
|||||||
|
package fakes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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":{"format":"#100 00"}},
|
||||||
|
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"#5#7#3 00"}}
|
||||||
|
]}`
|
||||||
|
|
||||||
|
// 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 TestBareTokenBindsWhenTheFormatAlsoTakesAPath(t *testing.T) {
|
||||||
|
// A head any dotted token addresses is drawn once, so a bare {place} in the
|
||||||
|
// same format reads that same draw rather than a second one.
|
||||||
|
f := engine(2)
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
got := mustRender(t, f, places("{place.postal-code} {place}"))
|
||||||
|
if !agree.MatchString(got) {
|
||||||
|
t.Fatalf("draw %d = %q, want the bare token to read the bound draw", i, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestTwoBoundHeadsDrawIndependently(t *testing.T) {
|
||||||
|
// Binding 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 TestRepeatRedrawsTheBinding(t *testing.T) {
|
||||||
|
// The binding 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 TestNestedTemplateKeepsItsOwnBinding(t *testing.T) {
|
||||||
|
// A binding 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 binding to be independent", seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoundDrawIsSeedStable(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"},{"format":"x"}]}`,
|
||||||
|
`locality`,
|
||||||
|
},
|
||||||
|
"path into a literal": {
|
||||||
|
`{"format":"{x.y}","x":"plain"}`,
|
||||||
|
`"y"`,
|
||||||
|
},
|
||||||
|
"dotted field key": {
|
||||||
|
`{"format":"[{a.b}]","a.b":["V"]}`,
|
||||||
|
`contains a dot`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, c := range rejected {
|
||||||
|
_, err := New([]string{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 TestBoundPathIsReachableByFake(t *testing.T) {
|
||||||
|
// Binding 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([]string{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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,7 +120,6 @@ func TestNewErrors(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":["Anvil"],"Weight":["7"]}`}, "a", "Anvil 7kg"},
|
"option name as a field": {map[string]string{"a": `{"format":"{name} {Weight}kg","name":["Anvil"],"Weight":["7"]}`}, "a", "Anvil 7kg"},
|
||||||
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":["PDF"]}`}, "a", "PDF"},
|
"format spelling as a field": {map[string]string{"a": `{"format":"{Format}","Format":["PDF"]}`}, "a", "PDF"},
|
||||||
"dotted field via its token": {map[string]string{"a": `{"format":"[{a.b}]","a.b":["V"]}`}, "a", "[V]"},
|
|
||||||
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":["1"]}`}, "a.x-y", "1"},
|
"hyphenated field": {map[string]string{"a": `{"format":"{x-y}","x-y":["1"]}`}, "a.x-y", "1"},
|
||||||
"category named Format": {map[string]string{"Format": `["1"]`}, "Format", "1"},
|
"category named Format": {map[string]string{"Format": `["1"]`}, "Format", "1"},
|
||||||
"folder named Repeat": {map[string]string{"Repeat/cat": `["1"]`}, "Repeat.cat", "1"},
|
"folder named Repeat": {map[string]string{"Repeat/cat": `["1"]`}, "Repeat.cat", "1"},
|
||||||
|
|||||||
Reference in New Issue
Block a user