From 1a6111d67d4c3ed5d1b3982929b81d11030c570e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 26 Sep 2026 14:36:39 +0200 Subject: [PATCH] Trace the reads a render makes, gather a render's draws in renderDraws and columnDraws, and drop the done item --- drawfence.go | 20 ++++++++++++++------ drawfence_test.go | 30 ++++++++++++++++++++---------- fejkdata.go | 1 + hold.go | 11 +++++++++++ holdset.go | 4 ++++ todo.md | 3 --- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/drawfence.go b/drawfence.go index 9e0fa2a..9211afc 100644 --- a/drawfence.go +++ b/drawfence.go @@ -69,14 +69,19 @@ func (c *drawCheck) checkDraws(path string, n node) error { if !c.readsPath(t) { 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() for _, e := range renderEdges(t) { w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}}) } - if err := w.check(); err != nil { - return fmt.Errorf("%s: %w", path, err) - } - return nil + return w } // 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. -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() for _, name := range columns { 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. diff --git a/drawfence_test.go b/drawfence_test.go index b785070..477eb47 100644 --- a/drawfence_test.go +++ b/drawfence_test.go @@ -10,7 +10,7 @@ import ( func fenceCorpus(t *testing.T) *Generator { t.Helper() 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.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}"}`, @@ -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 -// 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()) { t.Helper() var reads []pathRead - s.trace = &renderTrace{read: func(_ *template, group string, pins pinSet, a arm) { - if s.trace.depth == 0 && isRef(a.key) { - reads = append(reads, pathRead{at: drawAt{group: group, pins: pins}, a: a}) + seen := map[readKey]bool{} + s.trace = &renderTrace{read: func(from *template, group string, a arm) { + 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 }() @@ -82,27 +92,27 @@ func wantGathered(t *testing.T, s *session, label string, gathered []pathRead, r } for _, r := range reads { 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 { 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 false } -func pinned(p *pinSet) string { +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 "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 diff --git a/fejkdata.go b/fejkdata.go index d8bb4cc..def390f 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -45,6 +45,7 @@ type Generator struct { type session struct { *rand.Rand counters map[string]uint64 + trace *renderTrace } func (s *session) next(key string) uint64 { diff --git a/hold.go b/hold.go index 36909d9..b5d752d 100644 --- a/hold.go +++ b/hold.go @@ -2,6 +2,7 @@ package fejkdata import ( "fmt" + "strings" ) // 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 // linkRefs prove every step, so the walk cannot fail. 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 len(a.tail) > 0 { 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 } +// 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, // the render's hold for its group, so its draw spans the render; for a sibling, or a // reference read whole, held. diff --git a/holdset.go b/holdset.go index db1e022..28e5ec4 100644 --- a/holdset.go +++ b/holdset.go @@ -35,6 +35,10 @@ func renderOnce(s *session, n node) string { //go:noinline func expandAnew(s *session, t *template) string { var set holdSet + if s.trace != nil { + s.trace.depth++ + defer func() { s.trace.depth-- }() + } return expand(s, t, renderScope{set: &set}) } diff --git a/todo.md b/todo.md index 8c1298b..bb5e0d4 100644 --- a/todo.md +++ b/todo.md @@ -21,9 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming 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`, while `drawAt.wholeTable`, `checkFamilyPair` and `checkOwnFamily`'s comment use it for a bare read landing on a `*table`, the one that draws without pinning.