package prosediff
import (
"fmt"
"strings"
"testing"
)
// --- what these measure --------------------------------------------------------
//
// Every proposal view runs Compare over two whole revisions of a specification
// document: both are segmented into blocks with goldmark, the two block
// sequences are aligned, and every block the alignment paired as modified is
// diffed again word by word (or line by line, inside a code fence). It is the
// most expensive thing this service does per request that is not I/O, and it
// grows with the square of the number of blocks in the worst case, so the size
// of the document is the number worth watching.
//
// The corpus below is a specification of the shape this service holds: numbered
// chapters, prose paragraphs, requirement lists, tables and fenced examples.
// The edited revision applies the four edits a proposal actually makes — a
// reworded sentence, an inserted requirement, a deleted paragraph and a moved
// chapter — so the alignment has one of every kind to recognise rather than a
// single wall of insertions.
// benchChapters is how many chapters the synthetic specification has. Around
// 40 blocks each, so the document is a few hundred blocks: the size at which
// the alignment, and not the segmentation, dominates.
const benchChapters = 24
// benchSpec renders a synthetic specification. `shift` rotates the chapter
// bodies by one so that a chapter appears at a different position in the two
// revisions, which is the move the alignment exists to recognise.
func benchSpec(edited bool) []byte {
var b strings.Builder
b.WriteString("---\nid: SPEC-1\ntitle: The synthetic specification\nstatus: draft\n---\n\n")
b.WriteString("# The synthetic specification\n\n")
order := make([]int, 0, benchChapters)
for i := 0; i < benchChapters; i++ {
order = append(order, i)
}
if edited {
// One chapter moved: the last is read first. Exactly one, so the move
// detection has something to find and the rest of the alignment is still
// an ordinary walk.
order = append(order[len(order)-1:], order[:len(order)-1]...)
}
for _, i := range order {
fmt.Fprintf(&b, "## %d. Chapter %d\n\n", i+1, i)
fmt.Fprintf(&b, "This chapter states what the service does with the %dth\n"+
"kind of document, in prose hard-wrapped the way a specification is\n"+
"written, so that a one-word edit rewraps the paragraph and a\n"+
"line-oriented differ would report the whole of it as replaced.\n\n", i)
if edited && i == benchChapters/3 {
// A reworded sentence: same block, edited, so the word-level diff runs.
b.WriteString("A revision **must** carry an id, and the id must be stable\n" +
"across every revision of the document that carries it.\n\n")
} else {
b.WriteString("A revision must carry an id, and that id is stable across\n" +
"every revision of the document that carries it.\n\n")
}
if !(edited && i == benchChapters/2) {
// Deleted in the edited revision: one whole paragraph gone.
fmt.Fprintf(&b, "The paragraph chapter %d loses when the edit lands. It is\n"+
"here so the alignment has a deletion to recognise and not only\n"+
"insertions.\n\n", i)
}
b.WriteString("Requirements:\n\n")
b.WriteString("- the reader must be able to address a revision by its hash\n")
b.WriteString("- the writer must not be able to rewrite an approved branch\n")
b.WriteString("- a proposal must name the revision it was cut from\n")
if edited && i == benchChapters/4 {
b.WriteString("- a proposal must carry a summary of at most one paragraph\n")
}
b.WriteString("\n")
b.WriteString("| field | required | note |\n")
b.WriteString("| ----- | -------- | ---- |\n")
b.WriteString("| id | yes | stable across revisions |\n")
b.WriteString("| title | yes | shown in every listing |\n")
b.WriteString("| status | no | draft when absent |\n\n")
b.WriteString("```yaml\n")
fmt.Fprintf(&b, "id: SPEC-1.%d\n", i)
b.WriteString("title: the example this chapter is about\n")
if edited && i == benchChapters/6 {
// A code fence is diffed line by line, whitespace and all: the other
// half of the split this package exists for.
b.WriteString("status: approved\n")
} else {
b.WriteString("status: draft\n")
}
b.WriteString("```\n\n")
b.WriteString("> A block quote, because a specification always has one.\n\n")
}
return []byte(b.String())
}
// BenchmarkCompare is the proposal view's whole diff: both revisions segmented,
// the block sequences aligned, and every paired block diffed word by word or
// line by line.
func BenchmarkCompare(b *testing.B) {
oldSrc := benchSpec(false)
newSrc := benchSpec(true)
b.ReportAllocs()
b.SetBytes(int64(len(oldSrc) + len(newSrc)))
for b.Loop() {
d := Compare(oldSrc, newSrc)
// Asserted rather than assumed: an alignment that paired nothing would be
// the fastest run here, and so would one that found no edit at all.
if d.Stats.BlocksModified == 0 || d.Stats.BlocksDeleted == 0 ||
d.Stats.BlocksInserted == 0 || d.Stats.BlocksMoved == 0 {
b.Fatalf("the fixture lost a change kind: %+v", d.Stats)
}
}
}
// BenchmarkCompareUnchanged is the same document against itself — the common
// case in a long proposal, where most files a reviewer opens are untouched.
// What it measures is segmentation plus the alignment's cheap path.
func BenchmarkCompareUnchanged(b *testing.B) {
src := benchSpec(false)
b.ReportAllocs()
b.SetBytes(int64(2 * len(src)))
for b.Loop() {
d := Compare(src, src)
if d.Stats.BlocksModified != 0 || d.Stats.BlocksInserted != 0 ||
d.Stats.BlocksDeleted != 0 {
b.Fatalf("a document compared with itself reported edits: %+v", d.Stats)
}
}
}
// BenchmarkSegment is the parse half alone, so a regression can be attributed
// to segmentation or to alignment rather than to "the diff".
func BenchmarkSegment(b *testing.B) {
src := benchSpec(false)
b.ReportAllocs()
b.SetBytes(int64(len(src)))
for b.Loop() {
if len(Segment(src)) == 0 {
b.Fatal("the fixture segmented into no blocks")
}
}
}
// BenchmarkDiffWords is the inline word diff on its own: one modified paragraph
// against its edit, which is what runs once per modified block above.
func BenchmarkDiffWords(b *testing.B) {
oldText := strings.Repeat("A revision must carry an id, and that id is stable "+
"across every revision of the document that carries it. ", 12)
newText := strings.Repeat("A revision must carry an identifier, and the id is "+
"stable across each revision of the document carrying it. ", 12)
b.ReportAllocs()
b.SetBytes(int64(len(oldText) + len(newText)))
for b.Loop() {
if len(DiffWords(oldText, newText)) == 0 {
b.Fatal("two different paragraphs produced no spans")
}
}
}