package markup
import (
"fmt"
"strings"
"testing"
"time"
"golang.org/x/net/html/atom"
)
const atomH2 = atom.H2
// bigReadme is a plausible large README: sections of prose with inline
// markup and links, lists, a table, a fence and a quote, repeated to
// about 1 MB. Headings repeat, as they do in real documents.
func bigReadme(size int) []byte {
var b strings.Builder
for i := 0; b.Len() < size; i++ {
// Real READMEs repeat headings ("Usage", "Example" per section);
// each repeat needs its own anchor id.
fmt.Fprintf(&b, "## Example\n\n")
fmt.Fprintf(&b, "Some prose with **bold**, *italic*, `code %d`, a [link](https://example.com/%d), "+
"an autolink https://example.org/p/%d and a reference to abcdef%04d0123.\n"+
"A second sentence that runs on for a while so paragraphs have some weight to them.\n\n", i, i, i, i)
fmt.Fprintf(&b, "- item %d one\n- item %d two\n - nested %d\n- [x] done %d\n\n", i, i, i, i)
fmt.Fprintf(&b, "| name | qty | note |\n|---|--:|---|\n| a%d | %d | first |\n| b%d | %d | second |\n\n", i, i, i, i*3)
fmt.Fprintf(&b, "```go\nfunc f%d() int { return %d }\n```\n\n> quoted line %d\n\n", i, i, i)
fmt.Fprintf(&b, "\n\n", i)
}
return []byte(b.String())
}
func BenchmarkRenderLarge(b *testing.B) {
src := bigReadme(1 << 20)
b.SetBytes(int64(len(src)))
for i := 0; i < b.N; i++ {
if _, err := Render(src, fakeRes{}); err != nil {
b.Fatal(err)
}
}
}
// A repository owner controls the README, and it renders on every page
// view: a megabyte must not cost seconds. The budget is generous — the
// point is to catch a super-linear pass, not to time a machine: the
// quadratic heading-id loop this guards took minutes at this size.
const budget = 5 * time.Second * raceSlowdown
// headingDense is a megabyte of tiny sections under the same heading —
// the shape that makes anchor generation quadratic.
func headingDense(size int) []byte {
return []byte(strings.Repeat("## Notes\n\ntext\n\n", size/len("## Notes\n\ntext\n\n")+1))
}
func TestRenderHeadingDenseStaysWithinBudget(t *testing.T) {
src := headingDense(1 << 20)
start := time.Now()
if _, err := Render(src, nil); err != nil {
t.Fatal(err)
}
if d := time.Since(start); d > budget {
t.Fatalf("Render of %d bytes took %v, budget %v", len(src), d, budget)
}
}
func TestRenderLargeStaysWithinBudget(t *testing.T) {
src := bigReadme(1 << 20)
start := time.Now()
if _, err := Render(src, fakeRes{}); err != nil {
t.Fatal(err)
}
if d := time.Since(start); d > budget {
t.Fatalf("Render of %d bytes took %v, budget %v", len(src), d, budget)
}
}
// Repeated headings get distinct anchors, numbered in order, and an
// explicit heading whose slug already looks like a numbered anchor
// is not handed the same one twice.
func TestRepeatedHeadingIDsAreDistinct(t *testing.T) {
src := "## Usage\n\n## Usage\n\n## Usage-1\n\n## Usage\n\n## Usage\n"
ids := map[string]bool{}
// The renderer demotes every heading a level: ## arrives as h3.
for _, h := range findAll(parseBody(t, src), atom.H3) {
id := attr(h, "id")
if id == "" || ids[id] {
t.Fatalf("id %q empty or repeated", id)
}
ids[id] = true
}
for _, want := range []string{"usage", "usage-1", "usage-1-1", "usage-2", "usage-3"} {
if !ids[want] {
t.Errorf("missing id %s in %v", want, ids)
}
}
}