From eb0c5dc066b6706456c0c6f1a0ce4a624a574b54 Mon Sep 17 00:00:00 2001 From: M Date: Sat, 29 Aug 2026 14:55:50 +0200 Subject: [PATCH] Tests for a path holding its draw at every level --- bench_test.go | 9 ++++++ bound_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/bench_test.go b/bench_test.go index 85c2665..3d47162 100644 --- a/bench_test.go +++ b/bench_test.go @@ -82,6 +82,15 @@ func BenchmarkBoundDeep(b *testing.B) { benchPath(b, dir, "addr") } +// BenchmarkBoundWide reads ten paths from one row: the point where the maps an +// expansion holds outgrow a single bucket. +func BenchmarkBoundWide(b *testing.B) { + dir := tmpData(b, "row", `{"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"}]}`) + benchPath(b, dir, "row") +} + // BenchmarkNew measures load+compile+validate of the whole shipped tree. func BenchmarkNew(b *testing.B) { b.ReportAllocs() diff --git a/bound_test.go b/bound_test.go index 98af42c..b907e42 100644 --- a/bound_test.go +++ b/bound_test.go @@ -202,6 +202,92 @@ func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) { } } +// 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 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