Trace the reads a render makes, gather a render's draws in renderDraws and columnDraws, and drop the done item

This commit is contained in:
2026-09-26 14:36:39 +02:00
committed by Lillem4n
parent fc8bb34242
commit 1a6111d67d
6 changed files with 50 additions and 19 deletions
+14 -6
View File
@@ -69,14 +69,19 @@ func (c *drawCheck) checkDraws(path string, n node) error {
if !c.readsPath(t) { if !c.readsPath(t) {
return nil return nil
} }
if err := renderDraws(t).check(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// renderDraws gathers the reads one render of t makes.
func renderDraws(t *template) *drawWalk {
w := newDrawWalk() w := newDrawWalk()
for _, e := range renderEdges(t) { for _, e := range renderEdges(t) {
w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}}) w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}})
} }
if err := w.check(); err != nil { return w
return fmt.Errorf("%s: %w", path, err)
}
return nil
} }
// checkRecordDraws fences the columns of a record — a template compiled at the top without a // checkRecordDraws fences the columns of a record — a template compiled at the top without a
@@ -172,12 +177,15 @@ func refRead(n node, label string) (arm, bool) {
} }
// checkColumnDraws fences a record's columns as one render. // checkColumnDraws fences a record's columns as one render.
func checkColumnDraws(t *template, columns []string) error { func checkColumnDraws(t *template, columns []string) error { return columnDraws(t, columns).check() }
// columnDraws gathers the reads one record render of t's columns makes.
func columnDraws(t *template, columns []string) *drawWalk {
w := newDrawWalk() w := newDrawWalk()
for _, name := range columns { for _, name := range columns {
w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
} }
return w.check() return w
} }
// drawWalk gathers the references one render reads, by draw group, for check to compare. // drawWalk gathers the references one render reads, by draw group, for check to compare.
+20 -10
View File
@@ -10,7 +10,7 @@ import (
func fenceCorpus(t *testing.T) *Generator { func fenceCorpus(t *testing.T) *Generator {
t.Helper() t.Helper()
dir := writeFiles(t, with(geo(), map[string]string{ dir := writeFiles(t, with(geo(), map[string]string{
"addr.json": `{"format":"{a} {b} {c}","a":{"format":"{/locality.name}, {/municipality.name}"},"b":{"format":"{/region[12].name} {/place}","drawGroup":"g"},"c":{"format":"{/place.zip} ","repeat":2}}`, "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.json": `{"format":"{zip} {name} {tag}","rows":"place.tsv","key":"name"}`,
"place.tsv": "name\tzip\ttag\nStockholm\t1{digits(2)} {digits(2)}\t{/w}\nTranås\t573 {digits(2)}\t{/region[12].name}\n", "place.tsv": "name\tzip\ttag\nStockholm\t1{digits(2)} {digits(2)}\t{/w}\nTranås\t573 {digits(2)}\t{/region[12].name}\n",
"rec.json": `{"format":"","l":"{/locality.name}","m":"{/municipality.name}","t":"{/place[Stockholm].tag}"}`, "rec.json": `{"format":"","l":"{/locality.name}","m":"{/municipality.name}","t":"{/place[Stockholm].tag}"}`,
@@ -67,13 +67,23 @@ func TestEveryReadARenderMakesIsGathered(t *testing.T) {
} }
// wantGathered renders a root five times over the seeded stream and fails where a reference the // wantGathered renders a root five times over the seeded stream and fails where a reference the
// render read is not among gathered: same draw group and path, under rows that can render together. // render read is not among gathered: same draw group and path, and where the read sits in a cell,
// gathered from that row or from no row of its table.
func wantGathered(t *testing.T, s *session, label string, gathered []pathRead, renderOnce func()) { func wantGathered(t *testing.T, s *session, label string, gathered []pathRead, renderOnce func()) {
t.Helper() t.Helper()
var reads []pathRead var reads []pathRead
s.trace = &renderTrace{read: func(_ *template, group string, pins pinSet, a arm) { seen := map[readKey]bool{}
if s.trace.depth == 0 && isRef(a.key) { s.trace = &renderTrace{read: func(from *template, group string, a arm) {
reads = append(reads, pathRead{at: drawAt{group: group, pins: pins}, a: a}) if s.trace.depth != 0 || !isRef(a.key) {
return
}
r := pathRead{at: drawAt{group: group}, a: a}
if from.cellOf != nil {
r.at.pins.add(from.cellOf, from.cellRow)
}
if k := (readKey{group, a.path, r.at.rowsKey()}); !seen[k] {
seen[k] = true
reads = append(reads, r)
} }
}} }}
defer func() { s.trace = nil }() defer func() { s.trace = nil }()
@@ -82,27 +92,27 @@ func wantGathered(t *testing.T, s *session, label string, gathered []pathRead, r
} }
for _, r := range reads { for _, r := range reads {
if !gathers(gathered, r) { if !gathers(gathered, r) {
t.Errorf("%s: the render read {%s} in draw group %q with %s pinned, and drawWalk gathered no such read", label, r.a.spelling, r.at.group, pinned(&r.at.pins)) 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 { func gathers(gathered []pathRead, r pathRead) bool {
for _, g := range gathered { for _, g := range gathered {
if g.at.group == r.at.group && g.a.path == r.a.path && !g.at.pins.differs(&r.at.pins) { 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 true
} }
} }
return false return false
} }
func pinned(p *pinSet) string { func spellPins(p *pinSet) string {
var rows []string var rows []string
p.each(func(t *table, r int) { rows = append(rows, t.selectorSpelling(r)) }) p.each(func(t *table, r int) { rows = append(rows, t.selectorSpelling(r)) })
if len(rows) == 0 { if len(rows) == 0 {
return "no row" return "no cell"
} }
return strings.Join(rows, ", ") return "the cell of " + strings.Join(rows, ", ")
} }
// renderRoot renders t the way the render reaches it: a table's format through its table, a cell in // renderRoot renders t the way the render reaches it: a table's format through its table, a cell in
+1
View File
@@ -45,6 +45,7 @@ type Generator struct {
type session struct { type session struct {
*rand.Rand *rand.Rand
counters map[string]uint64 counters map[string]uint64
trace *renderTrace
} }
func (s *session) next(key string) uint64 { func (s *session) next(key string) uint64 {
+11
View File
@@ -2,6 +2,7 @@ package fejkdata
import ( import (
"fmt" "fmt"
"strings"
) )
// hold is what has already been drawn for held names: the variant each was drawn as, so every // hold is what has already been drawn for held names: the variant each was drawn as, so every
@@ -27,6 +28,9 @@ type draw struct {
// drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and // drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and
// linkRefs prove every step, so the walk cannot fail. // linkRefs prove every step, so the walk cannot fail.
func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw { func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw {
if s.trace != nil {
s.trace.read(t, strings.Clone(sc.group), a)
}
if !t.held[a.key] { if !t.held[a.key] {
if len(a.tail) > 0 { if len(a.tail) > 0 {
panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.spelling, a.key)) panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.spelling, a.key))
@@ -45,6 +49,13 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw
return r return r
} }
// renderTrace is a test's view of a render: each field a format reads, and how many repeat
// iterations, each a render of its own, the read sits inside. nil outside a test.
type renderTrace struct {
read func(from *template, group string, a arm)
depth int
}
// readHold is the hold a held read keeps its draw in: for a reference that reads a path, // readHold is the hold a held read keeps its draw in: for a reference that reads a path,
// the render's hold for its group, so its draw spans the render; for a sibling, or a // the render's hold for its group, so its draw spans the render; for a sibling, or a
// reference read whole, held. // reference read whole, held.
+4
View File
@@ -35,6 +35,10 @@ func renderOnce(s *session, n node) string {
//go:noinline //go:noinline
func expandAnew(s *session, t *template) string { func expandAnew(s *session, t *template) string {
var set holdSet var set holdSet
if s.trace != nil {
s.trace.depth++
defer func() { s.trace.depth-- }()
}
return expand(s, t, renderScope{set: &set}) return expand(s, t, renderScope{set: &set})
} }
-3
View File
@@ -21,9 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming
This round, in order: This round, in order:
- Hold the draw fences to what render does: a test rendering seeded draws that fails
where a read the render makes falls outside what `drawWalk` gathered. Today the two
models agree by a comment on `renderEdges`, and a new render route drifts silently.
- Give "whole" one meaning: `doc.go` defines it as a read landing on a `tableRow`, - Give "whole" one meaning: `doc.go` defines it as a read landing on a `tableRow`,
while `drawAt.wholeTable`, `checkFamilyPair` and `checkOwnFamily`'s comment use it for while `drawAt.wholeTable`, `checkFamilyPair` and `checkOwnFamily`'s comment use it for
a bare read landing on a `*table`, the one that draws without pinning. a bare read landing on a `*table`, the one that draws without pinning.