Files
felhom-controller/controller/internal/web/i18n_parity_test.go
T
admin ff68b0b433
gates / gates (push) Successful in 20s
v0.248.0: i18n slice 1 release A — apps and settings in English, Hungarian byte-identical (R-556)
Ten pages converted against fixtures captured from unconverted templates (6be55a0); marker
coverage test; English page mask narrowed; English retrieval-promise stems; common keys;
title keys and infraMeta from the bundle; old wording tests read the Hungarian expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-17 17:36:08 +02:00

524 lines
21 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"bytes"
"flag"
"fmt"
"html/template"
"io/fs"
"path"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// ── i18n (v0.247.0) — THE RELEASE GATE: Hungarian renders byte-for-byte as it did ──────────────────
//
// The fixtures under testdata/i18n_parity/ were captured from the templates BEFORE any of them carried
// a {{T}} marker (`go test -run TestI18nParity -update-i18n-golden` on the unconverted tree, commit
// recorded in felhom-controller/REPORT.md). Every conversion since must reproduce them exactly.
//
// Normalisation, and it is the ONLY one: relative ages („3 napja") are rewritten to „# napja", because
// they are computed from the wall clock and the fixtures carry fixed dates. Nothing else is touched.
//
// RED BEFORE THE FIXTURE EXISTS: a missing fixture file fails the test; it is never silently written.
// RED-PROOF (recorded in REPORT.md): change one byte of one launcher value in locales/hu.json and
// TestI18nParity fails naming the page and the first differing line.
var updateI18nGolden = flag.Bool("update-i18n-golden", false, "write MISSING testdata/i18n_parity fixtures from the current templates (an existing fixture is never overwritten)")
var i18nDumpDir = flag.String("i18n-dump-dir", "", "TestI18nEnglishPages: also write every case's hu and en render to this directory (evidence, per-page review)")
var i18nGoldenOnly = flag.String("i18n-golden-only", "", "with -update-i18n-golden: only cases whose name matches this regexp")
var relativeAgeRe = regexp.MustCompile(`\d+ (napja|órája|perce)`)
type i18nCase struct {
name string
tmpl string
data func() map[string]interface{}
}
func i18nFixtureTime() time.Time { return time.Date(2026, 9, 14, 3, 12, 0, 0, time.UTC) }
// i18nTitleKeys mirrors the handlers' data["TitleKey"]: a page whose Go-side title has a key renders it
// translated in English, exactly as addLanguageData does in production.
var i18nTitleKeys = map[string]string{
"Indítópult": "page.title.launcher",
"Biztonsági mentés": "page.title.backups",
"Vezérlőpult": "page.title.dashboard",
"Alkalmazások": "page.title.stacks",
"Rendszermonitor": "page.title.monitoring",
"Beállítások": "page.title.settings",
"Értesítések": "page.title.settings_notifications",
"Biztonság és hozzáférés": "page.title.settings_security",
}
func i18nLayoutData(page, title string) map[string]interface{} {
d := map[string]interface{}{
"Page": page, "Title": title, "CustomerName": "Teszt Ügyfél", "Domain": "example.hu",
"Version": "0.247.0", "AuthEnabled": true, "DebugMode": false,
"CSRFField": "", "CSRFToken": "tok",
}
if k, ok := i18nTitleKeys[title]; ok {
d["TitleKey"] = k
}
return d
}
func i18nCases() []i18nCase {
base := []i18nCase{
{"launcher_full", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["ClaimLegacyOpen"] = true
d["RecoveryBanner"] = true
d["RecoveryBannerBack"] = "/launcher"
d["RecoveryAbandonDays"] = 12
d["RecoveryAbandonDate"] = "2026-09-29"
d["RecoveryAbandonRetrievalOffered"] = true
d["Apps"] = []LauncherApp{
{Name: "immich", DisplayName: "Immich", Slug: "immich", State: stacks.StateRunning, Subdomain: "fotok"},
{Name: "privatebin", DisplayName: "PrivateBin", Slug: "privatebin", State: stacks.StateUnhealthy, Subdomain: "paste", OpenPath: "/"},
{Name: "kimai", DisplayName: "Kimai", Slug: "kimai", State: stacks.StateStopped, Subdomain: "ido"},
}
d["ShareEnabled"] = true
d["ShareURL"] = "https://felhom.example.hu/s/abc"
d["SharePasswordSet"] = true
d["ShareFlash"] = "A megosztás bekapcsolva."
return d
}},
{"launcher_empty", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryBannerBack"] = "/launcher"
d["RecoveryReminderTier"] = 14
d["EscrowBanner"] = true
d["EscrowBannerBack"] = "/launcher"
return d
}},
{"launcher_nopassword", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryAbandonDays"] = 3
d["RecoveryAbandonDate"] = "2026-09-20"
d["Alerts"] = []Alert{
{ID: "a", Level: "warning", Message: "A rendszer meghajtója 91%-on áll.", Link: "/monitoring", LinkText: "Rendszermonitor"},
{ID: "b", Level: "info", Message: "Frissítés érhető el."},
{ID: "c", Level: "error", Message: "Csak a műszerfalon.", PageOnly: []string{"dashboard"}},
}
d["Apps"] = []LauncherApp{{Name: "gokapi", DisplayName: "Gokapi", Slug: "gokapi", State: stacks.StateStarting, Subdomain: "fajl"}}
d["ShareEnabled"] = true
d["ShareURL"] = "https://felhom.example.hu/s/xyz"
return d
}},
{"launcher_reminder_tiers", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 7
return d
}},
{"launcher_reminder_tier3", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 3
d["AuthEnabled"] = false
return d
}},
{"launcher_reminder_tier0", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 0 // the handler always sets an int; a missing key cannot reach `ge`
return d
}},
{"backups_full", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{
"FlashSuccess": "A mentés elindult.", "FlashError": "", "SingleCopyWarning": "Csak egy másolat létezik.",
"DumpFiles": []string{"a.sql", "b.sql"},
"LastDBDump": map[string]interface{}{"Success": true, "Results": []int{1, 2, 3}},
}
d["BackupTarget"] = map[string]interface{}{
"Message": "A rendszermentésnek nincs helye.", "OfferPath": "/mnt/hdd2",
"OfferText": "Kijelölheted a második meghajtót.", "OfferLabel": "HDD 2 (1 TB)",
}
d["SystemInfo"] = map[string]interface{}{"DiskPercent": 42.0, "DiskUsedGB": 42.5, "DiskTotalGB": 100.0}
d["StorageBars"] = []map[string]interface{}{
{"Label": "Adatok", "Disconnected": false, "Percent": 71.3, "UsedGB": 713.0, "TotalGB": 1000.0},
{"Label": "Régi lemez", "Disconnected": true},
}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": true, "Success": true, "StartedAt": at, "SizeUnknown": false,
"SizeBytes": int64(5 << 30), "Target": "PBS (helyi)", "Due": false, "AgeHours": 7,
"HasRestoreTest": true, "RestorePass": true, "RestoreTestedAt": at,
"Tiers": []map[string]interface{}{
{"Label": "Helyi tároló (local)", "NotSetUp": false, "HasSuccess": true, "SuccessAt": at, "SizeKnown": true, "SizeBytes": int64(3 << 30), "Current": true, "FailedAfter": true, "FailedAt": at},
{"Label": "Távoli szerver (PBS)", "NotSetUp": false, "HasSuccess": false, "Current": false},
{"Label": "Harmadik szint", "NotSetUp": true},
},
"Running": true, "Phase": "vzdump", "CanTrigger": true, "Offsite": true,
}
d["BackupWindow"] = "01:30"
d["BackupLegDB"] = "01:30"
d["BackupLegTier2"] = "02:00"
d["BackupLegOffbox"] = "02:30"
d["BackupGateFrom"] = "03:00"
d["BackupGateTo"] = "05:00"
d["DBSectionState"] = "dumps"
d["EscrowBanner"] = true
return d
}},
{"backups_empty", "backups", func() map[string]interface{} {
return i18nLayoutData("backups", "Biztonsági mentés")
}},
{"backups_degraded", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{
"FlashSuccess": "", "FlashError": "A mentés nem sikerült.",
"LastDBDump": map[string]interface{}{"Success": false, "Results": []int{}},
}
d["BackupTarget"] = map[string]interface{}{"Message": "A rendszermentés helye nem elérhető."}
d["GuestBackup"] = map[string]interface{}{"Available": false, "Note": "Az ügynök nem elérhető."}
d["DBSectionState"] = "embedded"
_ = at
return d
}},
{"backups_nobackup_yet", "backups", func() map[string]interface{} {
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{"DumpFiles": []string{}}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": false, "Due": true, "HasRestoreTest": false, "CanTrigger": false,
}
d["DBSectionState"] = "pending"
return d
}},
{"backups_tier_due", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{"DumpFiles": []string{"a.sql"}}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": true, "Success": false, "StartedAt": at, "SizeUnknown": true,
"Target": "PBS (helyi)", "Due": true, "HasRestoreTest": true, "RestorePass": false, "RestoreTestedAt": at,
"Tiers": []map[string]interface{}{
{"Label": "Helyi tároló (local)", "NotSetUp": false, "HasSuccess": true, "SuccessAt": at, "SizeKnown": false, "Current": false},
},
"Running": false, "CanTrigger": true, "Offsite": false,
}
d["DBSectionState"] = "dumps"
return d
}},
{"app_info_installable", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: false, State: stacks.StateNotDeployed}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök",
Resources: stacks.ResourceHints{MemRequest: "64M"}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
return d
}},
{"app_info_deployed", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: true, State: stacks.StateRunning, HoldReason: "A frissítés megállt: nincs friss mentés."}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök", Description: "Titkosított jegyzetek.",
Resources: stacks.ResourceHints{MemRequest: "64M", NeedsHDD: true, PiCompatible: false, HungarianUI: true},
AppInfo: stacks.AppInfo{Tagline: "Titkosított szövegmegosztás.", UseCases: []string{"Jelszó átadása"}, FirstSteps: []string{"Nyisd meg"},
Prerequisites: []string{"Semmi"}, DefaultCreds: "nincs", DocsURL: "https://privatebin.info"}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = true
d["EffectiveSubdomain"] = "paste"
d["MissingStorageLabel"] = "HDD 1"
d["DataPathCards"] = []DataPathCard{{Label: "Feltöltött fájlok", Link: "/files/x", Consequence: "Mentve minden éjjel.", IsImport: true, FreeSpace: "12 GB szabad"}}
d["MigrateTargets"] = []map[string]interface{}{{"Path": "/mnt/hdd2", "Label": "HDD 2"}}
d["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin", Note: "A jelszót a Felhom állította be."}
d["InitialCredsHasPassword"] = true
return d
}},
{"app_info_available", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: false, State: stacks.StateNotDeployed}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök", Description: "Titkosított jegyzetek.",
Lifecycle: "abandoned", Resources: stacks.ResourceHints{MemRequest: "64M", PiCompatible: true}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = false
return d
}},
{"app_info_operator_creds", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "Fájlkezelő app")
st := stacks.Stack{Name: "filebrowser", Deployed: true, State: stacks.StateStopped, Orphaned: true, Updating: false, UpdateError: "A frissítés sikertelen."}
st.Meta = stacks.Metadata{DisplayName: "Fájlkezelő app", Slug: "filebrowser"}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = true
d["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin"}
d["InitialCredsOperatorSet"] = true
return d
}},
{"app_info_updating", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: true, State: stacks.StateDeploying, Updating: true, UpdatePhase: "pull", UpdatePhaseLabel: "Letöltés folyamatban…"}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin"}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
return d
}},
}
// Slice 1 (R-556): one file of cases per release, each captured from its unconverted templates.
base = append(base, i18nCasesA()...)
return base
}
func renderI18nCase(t *testing.T, s *Server, lang string, c i18nCase) string {
t.Helper()
var buf bytes.Buffer
data := c.data()
// The production choke point's language step (executeTemplate → addLanguageData), with a request that
// carries no ?lang= — so the switch and the translated title behave exactly as on a real page.
s.addLanguageData(data, httptest.NewRequest("GET", "/i18n-fixture", nil), lang)
if err := s.templatesFor(lang).ExecuteTemplate(&buf, c.tmpl, data); err != nil {
t.Fatalf("%s [%s]: render: %v", c.name, lang, err)
}
return relativeAgeRe.ReplaceAllString(buf.String(), "# $1")
}
func i18nTestServer(t *testing.T) *Server {
s := testServer(t)
s.loadTemplates()
return s
}
// TestI18nParity — the Hungarian pages are byte-identical to the fixtures captured before conversion.
func TestI18nParity(t *testing.T) {
s := i18nTestServer(t)
dir := filepath.Join("testdata", "i18n_parity")
for _, c := range i18nCases() {
got := renderI18nCase(t, s, "hu", c)
p := filepath.Join(dir, c.name+".html")
if *updateI18nGolden {
if *i18nGoldenOnly != "" && !regexp.MustCompile(*i18nGoldenOnly).MatchString(c.name) {
continue
}
if _, err := os.Stat(p); err == nil {
t.Logf("%s: fixture exists — NOT rewritten (fixtures are captured once, from the unconverted template)", c.name)
continue
}
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(got), 0o644); err != nil {
t.Fatal(err)
}
t.Logf("%s: fixture WRITTEN", c.name)
continue
}
want, err := os.ReadFile(p)
if err != nil {
t.Fatalf("%s: no parity fixture (%v) — fixtures are captured from UNCONVERTED templates, never regenerated to make a conversion pass", c.name, err)
}
if got != string(want) {
gl, wl := strings.Split(got, "\n"), strings.Split(string(want), "\n")
for i := 0; i < len(gl) || i < len(wl); i++ {
var g, w string
if i < len(gl) {
g = gl[i]
}
if i < len(wl) {
w = wl[i]
}
if g != w {
t.Errorf("%s: Hungarian page CHANGED at line %d\n fixture: %q\n now: %q", c.name, i+1, w, g)
break
}
}
}
}
}
// huLetter reports a Hungarian accented letter. ASCII source: the letters are escapes.
func huLetter(s string) bool {
return strings.ContainsAny(s, "áéíóöőúüűÁÉÍÓÖŐÚÜŰ")
}
// fixtureStrings collects every string the fixture DATA carries (catalog copy, Go-side messages the
// handler would build). Those are not template copy: they are slices 2 and 5 of the plan, and the
// English page check subtracts them rather than pretending the template could translate them.
func fixtureStrings(v reflect.Value, out map[string]bool) {
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
if !v.IsNil() {
fixtureStrings(v.Elem(), out)
}
case reflect.String:
if v.String() != "" {
out[v.String()] = true
}
case reflect.Map:
for _, k := range v.MapKeys() {
fixtureStrings(v.MapIndex(k), out)
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
fixtureStrings(v.Index(i), out)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).IsExported() {
fixtureStrings(v.Field(i), out)
}
}
}
}
var i18nKeyLikeRe = regexp.MustCompile(`\b[a-z][a-z_]*\.[a-z0-9_.]+\b`)
var emptyElemRe = regexp.MustCompile(`<(h[1-6]|button|label|li|strong|option|p)(\s[^>]*)?>\s*</(h[1-6]|button|label|li|strong|option|p)>`)
// TestI18nEnglishPages — the three pages in English: no Hungarian letter from the TEMPLATE copy, no
// raw key, no marker, and no element that is empty in English but not in Hungarian.
func TestI18nEnglishPages(t *testing.T) {
s := i18nTestServer(t)
// Controls for the letter detector, so a zero below means something.
if !huLetter("Indítópult") {
t.Fatal("control: the detector does not flag a Hungarian word")
}
if huLetter("Backup completed at 03:00, next run tomorrow.") {
t.Fatal("control: the detector flags plain English")
}
for _, c := range i18nCases() {
hu := renderI18nCase(t, s, "hu", c)
en := renderI18nCase(t, s, "en", c)
if *i18nDumpDir != "" {
_ = os.MkdirAll(*i18nDumpDir, 0o755)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".hu.html"), []byte(hu), 0o644)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".en.html"), []byte(en), 0o644)
}
if !huLetter(hu) {
t.Fatalf("%s: positive control failed — the Hungarian render carries no Hungarian letter", c.name)
}
if strings.Contains(en, `{{T `) {
t.Errorf("%s: an unexpanded marker reached the English page", c.name)
}
for _, m := range i18nKeyLikeRe.FindAllString(en, -1) {
if s.i18n.Has(i18n.Default, m) {
t.Errorf("%s: a raw bundle key reached the English page: %q", c.name, m)
break
}
}
if a, b := len(emptyElemRe.FindAllString(hu, -1)), len(emptyElemRe.FindAllString(en, -1)); b > a {
t.Errorf("%s: English has %d empty elements, Hungarian %d — a translation rendered blank", c.name, b, a)
}
data := map[string]bool{}
fixtureStrings(reflect.ValueOf(c.data()), data)
stripped := en
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) })
for _, k := range keys {
// NARROWED (slice 1): only a data string long enough to be unmistakable is masked — two or
// more words, or 12+ characters. A one-word Hungarian value („Indítópult") equal to template
// copy is no longer able to hide that copy; fixture data therefore carries no one-word
// Hungarian values.
if len(strings.Fields(k)) < 2 && len([]rune(k)) < 12 {
continue
}
stripped = strings.ReplaceAll(stripped, k, "")
}
// Script/style bodies are included on purpose: inline JS copy is page copy.
for i, line := range strings.Split(stripped, "\n") {
if huLetter(line) {
t.Errorf("%s: Hungarian template copy on the English page, line %d: %q", c.name, i+1, strings.TrimSpace(line))
}
}
}
}
// TestI18nParityCoversEveryMarker — every {{T}} marker OCCURRENCE in every template is rendered by at
// least one parity case. A branch no case reaches carries copy the parity and English tests never saw,
// so a page is not done while one exists.
//
// How: a probe template set is parsed in which each occurrence is expanded to an invisible, unique
// sentinel (U+2063 … U+2063) followed by its Hungarian text; every case renders through it; any sentinel
// absent from every output names the file and the key. The sentinel is literal template text, so it
// survives html/template untouched in every context.
//
// RED-PROOF (REPORT.md): a marker placed in a launcher.html branch no case reaches → this test fails
// naming launcher.html and the key.
func TestI18nParityCoversEveryMarker(t *testing.T) {
s := i18nTestServer(t)
b, err := i18n.Shared()
if err != nil {
t.Fatal(err)
}
names, err := fs.Glob(templateFS, "templates/*.html")
if err != nil {
t.Fatal(err)
}
probe := template.New("").Funcs(s.templateFuncMap())
occ := map[string]string{}
for _, n := range names {
raw, err := templateFS.ReadFile(n)
if err != nil {
t.Fatal(err)
}
base := path.Base(n)
idx := 0
src := i18nMarkerRe.ReplaceAllStringFunc(string(raw), func(m string) string {
key := i18nMarkerRe.FindStringSubmatch(m)[1]
id := fmt.Sprintf("⁣%s#%d⁣", base, idx)
idx++
occ[id] = base + " " + key
text, _, ok := b.Text(i18n.Default, key)
if !ok {
t.Fatalf("%s: key %q not in hu.json", base, key)
}
return id + text
})
tmpl := probe
if base != probe.Name() {
tmpl = probe.New(base)
}
if _, err := tmpl.Parse(src); err != nil {
t.Fatalf("probe parse %s: %v", base, err)
}
}
if len(occ) < 250 {
t.Fatalf("only %d marker occurrences found — the probe is not reading the converted templates", len(occ))
}
var all strings.Builder
for _, c := range i18nCases() {
data := c.data()
s.addLanguageData(data, httptest.NewRequest("GET", "/i18n-fixture", nil), i18n.Default)
if err := probe.ExecuteTemplate(&all, c.tmpl, data); err != nil {
t.Fatalf("%s: probe render: %v", c.name, err)
}
}
out := all.String()
var missing []string
for id, where := range occ {
if !strings.Contains(out, id) {
missing = append(missing, where)
}
}
sort.Strings(missing)
for _, m := range missing {
t.Errorf("marker never rendered by any parity case: %s", m)
}
}