From 5112832fe1e8fb5c101544f56871741facaf9154 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:54:54 +0200 Subject: [PATCH] Assert what the tag reader does where a page ends and at the edges [#44] The strict read of a produced page walks the bytes itself. Nine places in that walk test an index and then read the byte after it, and no case ended a document at any of them, so a mutation turning one bound into its neighbour read past the end of the input and every test still passed. What that guards is the checker crashing on the page it exists to judge, which takes the whole gate down instead of naming one broken tag. Three more places had the same shape without the index: an empty raw text element, whose end tag sits at offset zero; a page whose first heading is not the top level; and the deepest heading level, which stops being a heading if the bound moves by one. All three are pages a template writes. Found by re-taking the mutation run this issue records, over the third scoped area, which had no score in it. Thirty mutants survived at ad8b25c and fourteen do now, with efficacy at 84.62% against 66.67%. The cases are raw fragments rather than wrapped ones where the point is the end of the input, because the wrapper writes a closing body after the byte each of those cases is about. The well-formed neighbour of every refusal is asserted beside it: a reader that refused one of these is repaired by somebody loosening the rule. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- internal/markup/markup_test.go | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/internal/markup/markup_test.go b/internal/markup/markup_test.go index 364c201..74848e2 100644 --- a/internal/markup/markup_test.go +++ b/internal/markup/markup_test.go @@ -176,3 +176,78 @@ func TestABrokenPageIsReportedOnceRatherThanGuessedAt(t *testing.T) { t.Errorf("the one problem reported is %s rather than the structure it stopped at", got[0].Kind) } } + +// A page that stops in the middle of a tag. Every case here is a raw fragment +// rather than a wrapped one, and deliberately: what is being read is what the +// tag reader does when the bytes run out under it, and wrapping would put a +// closing body after the point each case is about. +// +// A template that stops writing produces exactly these. What they must not do +// is index past the end of what was written: the walk reads the byte after the +// one it tested at nine places, and each place is a byte the page does not +// have. +func TestAPageThatEndsInTheMiddleOfATagIsRefusedRatherThanReadPastItsEnd(t *testing.T) { + for name, fragment := range map[string]string{ + "a tag name that runs to the end": "`, + "spaces around the equals sign": `
`, + "an attribute carrying no value": ` `, + "a void element closed with a slash": `
`, + "a raw text element with nothing inside": ` `, + "a comment holding what looks like a tag": `

A paragraph.

`, + "a comment ending on a stray bracket": `

A paragraph.

`, + } { + if got := Read(page(body)); len(got) != 0 { + t.Errorf("%s: %q was refused: %v", name, body, got) + } + } +} + +// The heading rule refuses a level that was skipped and nothing else. The three +// cases it must leave alone are the first heading on a page, whatever level it +// is, the level directly under the last one, and the deepest level reached that +// way; the fourth is the jump it exists for, and it is here so that the pair is +// one case rather than two files apart. +func TestOnlyASkippedHeadingLevelIsRefused(t *testing.T) { + for name, body := range map[string]string{ + "a first heading that is not the top level": `

A section

`, + "the level directly under the last one": `

A title

A section

`, + "the deepest level under the one above it": `

A

B
C
`, + } { + if got := Of(Heading, Read(page(body))); len(got) != 0 { + t.Errorf("%s: %q was refused: %v", name, body, got) + } + } + if got := Of(Heading, Read(page(`

A title

Six
`))); len(got) != 1 { + t.Errorf("a jump from the top level to the deepest was read as %d problem(s): %v", len(got), got) + } +}