Files
fejkdata/drawfence_test.go
T

310 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}","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, map[string]*table) {
t.Helper()
var roots []fenceRoot
tables := map[string]*table{}
_ = walkNodes(f.categories, func(path string, n node) error {
switch n := n.(type) {
case *template:
roots = append(roots, fenceRoot{path, n})
case *table:
tables[n.path] = n
}
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}), tables
}
func TestEveryReadARenderMakesIsGathered(t *testing.T) {
f := fenceCorpus(t)
roots, tables := fenceRoots(t, f)
for _, root := range roots {
wantGathered(t, f.rand, tables, root.label, renderDraws(root.t).reads, func() { renderRoot(f.rand, root.t) })
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)
}
wantGathered(t, f.rand, tables, root.label+" as a record", columnDraws(root.t, sortedNames(root.t.fields)).reads, func() { renderRecordRoot(f.rand, root.t, columns) })
}
}
func wantGathered(t *testing.T, s *session, tables map[string]*table, label string, gathered []pathRead, renderOnce func()) {
t.Helper()
var reads []pathRead
type traceKey struct {
group, path, table string
row int
}
seen := map[traceKey]bool{}
s.trace = &renderTrace{read: func(group, table string, row int, a arm) {
if s.trace.repeatDepth != 0 || !isRef(a.key) {
return
}
r := pathRead{at: drawAt{group: group}, a: a}
if table != "" {
r.at.pins.add(tables[table], row)
}
if k := (traceKey{group, a.path, table, row}); !seen[k] {
seen[k] = true
reads = append(reads, r)
}
}}
defer func() { s.trace = nil }()
for i := 0; i < 5; i++ {
renderOnce()
}
for _, r := range reads {
if !gathers(gathered, r) {
t.Errorf("%s: the render read {%s} in draw group %q from %s, and drawWalk gathered no such read", label, r.a.spelling, r.at.group, spellPins(&r.at.pins))
}
}
}
func gathers(gathered []pathRead, r pathRead) bool {
for _, g := range gathered {
if g.at.group == r.at.group && g.a.path == r.a.path && !g.at.pins.differs(&r.at.pins) && !g.at.wholePins.differs(&r.at.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 *session, t *template) {
var set holdSet
sc := renderScope{set: &set}
switch {
case t.table != nil:
render(s, t.table, sc)
for r := 0; r < t.table.rowCount(); r++ {
var set holdSet
sc := renderScope{set: &set}
sc.groupHold().pins.pin(t.table, r)
render(s, t.table.pinnedRow, sc.at(t.table.pinnedRow, &sc.groupHold().pins))
}
case t.repeat > 1:
expand(s, t, sc)
default:
render(s, t, sc)
}
}
func renderRecordRoot(s *session, t *template, columns []Column) {
var set holdSet
sc := renderScope{set: &set}
if t.table != nil {
t.table.drawIn(s, &sc.groupHold().pins)
}
renderRecord(s, t, columns, sc)
}
func TestReplayPairsFindsWhatAWholeReplayFinds(t *testing.T) {
corpus := fenceCorpus(t)
roots, _ := fenceRoots(t, corpus)
shipped, err := New()
if err != nil {
t.Fatalf("New() = %v", err)
}
_ = walkNodes(shipped.categories, func(path string, n node) error {
if tm, ok := n.(*template); ok {
roots = append(roots, fenceRoot{path, tm})
}
return nil
})
compared := 0
for _, root := range roots {
compared += wantPairsFindAll(t, root.label, renderDraws(root.t).reads)
if root.t.isRecord && len(root.t.fields) > 0 {
compared += wantPairsFindAll(t, root.label+" as a record", columnDraws(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 TestReplayPairsFindsWhatAWholeReplayOfSelectedRowsFinds(t *testing.T) {
f, err := New(WithDataPath(writeFiles(t, siblings())))
if err != nil {
t.Fatalf("New = %v", err)
}
var reads []pathRead
_ = walkNodes(f.categories, 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, renderDraws(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 replayPairs(triple) != 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 := replayPairs(reads)
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: replayPairs = %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)) {
together := func(a, b pathRead) bool { return a.at.group == b.at.group && !alternatives(a.at, b.at) }
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 together(r, c) {
nextC = append(nextC, c)
}
}
for _, x := range excluded {
if together(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 TestDrawWalkWalksACellByEveryRoute(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":"{/g}","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 := treeBinding(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 renderDraws(r).reads {
if read.a.path != "/z" {
continue
}
pinned = pinned || read.at.pins.used > 0
whole = whole || read.at.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 from inside {/x}", pinned, whole)
}
}