~bigbes/sr-ht-spec

ref: d4b6373d153470b30c9bb6882f71d9bee0a8f416 sr-ht-spec/prosediff/wordlines_test.go -rw-r--r-- 6.3 KiB
d4b6373d — Eugene Blikh graph: seed the test keyset from ecoretest 10 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package prosediff

import (
	"strings"
	"testing"
)

// modifyOf returns the single ChangeModify in a diff of two documents, failing
// when the alignment did not produce one — the tests below are about splitting
// a word script, not about whether the aligner paired the blocks.
func modifyOf(t *testing.T, oldSrc, newSrc string) BlockChange {
	t.Helper()
	for _, c := range Compare([]byte(oldSrc), []byte(newSrc)).Changes {
		if c.Kind == ChangeModify && len(c.Words) > 0 {
			return c
		}
	}
	t.Fatalf("no modified prose block in the diff of:\n%s\n---\n%s", oldSrc, newSrc)
	return BlockChange{}
}

// renderLines renders one side back to plain strings, per line, so a test can assert
// on what a reader would see.
func renderLines(rows []LineWords) []string {
	out := make([]string, len(rows))
	for i, r := range rows {
		var b strings.Builder
		for j, s := range r.Spans {
			if s.Space && j > 0 {
				b.WriteByte(' ')
			}
			b.WriteString(s.Text)
		}
		out[i] = b.String()
	}
	return out
}

// The words of each line must come back on that line, with the source line
// numbers the block actually occupies. Without this the gutter would number
// rows that hold someone else's text.
func TestWordsByLineKeepsEachLinesOwnWords(t *testing.T) {
	const oldSrc = "# H\n\nalpha beta gamma\ndelta epsilon zeta\neta theta iota\n"
	const newSrc = "# H\n\nalpha beta gamma\ndelta CHANGED zeta\neta theta iota\n"

	old, nw, ok := WordsByLine(modifyOf(t, oldSrc, newSrc))
	if !ok {
		t.Fatal("WordsByLine reported the block unsplittable")
	}
	if len(old) != 3 || len(nw) != 3 {
		t.Fatalf("rows = %d old / %d new, want 3 each", len(old), len(nw))
	}

	wantOld := []string{"alpha beta gamma", "delta epsilon zeta", "eta theta iota"}
	wantNew := []string{"alpha beta gamma", "delta CHANGED zeta", "eta theta iota"}
	for i := range wantOld {
		if got := renderLines(old)[i]; got != wantOld[i] {
			t.Errorf("old line %d = %q, want %q", i, got, wantOld[i])
		}
		if got := renderLines(nw)[i]; got != wantNew[i] {
			t.Errorf("new line %d = %q, want %q", i, got, wantNew[i])
		}
	}

	// Line numbers are the block's own, not indices into the slice.
	for i, r := range old {
		if r.Line != old[0].Line+i {
			t.Errorf("old row %d numbered %d, want %d", i, r.Line, old[0].Line+i)
		}
	}
}

// The change must be marked on the line that carries it and nowhere else,
// which is the whole point of splitting rather than marking the block.
func TestTheChangeLandsOnOneLine(t *testing.T) {
	const oldSrc = "# H\n\nalpha beta gamma\ndelta epsilon zeta\neta theta iota\n"
	const newSrc = "# H\n\nalpha beta gamma\ndelta CHANGED zeta\neta theta iota\n"

	old, nw, ok := WordsByLine(modifyOf(t, oldSrc, newSrc))
	if !ok {
		t.Fatal("unsplittable")
	}
	marked := func(rows []LineWords, op Op) []int {
		var hit []int
		for i, r := range rows {
			for _, s := range r.Spans {
				if s.Op == op {
					hit = append(hit, i)
					break
				}
			}
		}
		return hit
	}
	if got := marked(old, OpDelete); len(got) != 1 || got[0] != 1 {
		t.Errorf("deletions on old rows %v, want only row 1", got)
	}
	if got := marked(nw, OpInsert); len(got) != 1 || got[0] != 1 {
		t.Errorf("insertions on new rows %v, want only row 1", got)
	}
}

// An unchanged run spanning a line break is cut at the boundary rather than
// dumped whole onto the line it started on. This is the common case in real
// prose: one long equal run covers most of a paragraph, and if it were not
// split every wrapped paragraph would collapse onto its first line.
func TestARunIsSplitAtTheLineBreak(t *testing.T) {
	const oldSrc = "# H\n\none two three four\nfive six seven eight\nnine ten\n"
	const newSrc = "# H\n\none two three four\nfive six seven eight\nnine ELEVEN\n"

	old, nw, ok := WordsByLine(modifyOf(t, oldSrc, newSrc))
	if !ok {
		t.Fatal("unsplittable")
	}
	for name, side := range map[string][]LineWords{"old": old, "new": nw} {
		if len(side) != 3 {
			t.Fatalf("%s rows = %d, want 3", name, len(side))
		}
		for i, r := range side {
			if len(r.Spans) == 0 {
				t.Errorf("%s row %d got no spans; the equal run was not split", name, i)
			}
		}
	}
	if got := renderLines(old); got[0] != "one two three four" || got[2] != "nine ten" {
		t.Errorf("old = %q", got)
	}
	if got := renderLines(nw); got[0] != "one two three four" || got[2] != "nine ELEVEN" {
		t.Errorf("new = %q", got)
	}
}

// Reflow is invisible to the word differ by design, so a rewrapped paragraph
// with a real edit still splits — onto the NEW line structure, which is what a
// reader of the new revision sees.
func TestRewrappedParagraphStillSplits(t *testing.T) {
	const oldSrc = "# H\n\nthe quick brown fox\njumps over the lazy dog\n"
	const newSrc = "# H\n\nthe quick brown fox jumps\nover the SLEEPY dog\n"

	old, nw, ok := WordsByLine(modifyOf(t, oldSrc, newSrc))
	if !ok {
		t.Fatal("unsplittable")
	}
	if got := strings.Join(renderLines(old), "|"); got != "the quick brown fox|jumps over the lazy dog" {
		t.Errorf("old = %q, want the old wrapping", got)
	}
	if got := strings.Join(renderLines(nw), "|"); got != "the quick brown fox jumps|over the SLEEPY dog" {
		t.Errorf("new = %q, want the new wrapping", got)
	}
}

// Anything that is not a modified prose block has no word script to spread,
// and says so rather than returning empty rows a caller might render.
func TestWordsByLineRefusesWhatItCannotSplit(t *testing.T) {
	d := Compare([]byte("# H\n\nkept\n"), []byte("# H\n\nkept\n\nadded paragraph\n"))
	for _, c := range d.Changes {
		if c.Kind == ChangeModify && len(c.Words) > 0 {
			continue
		}
		if _, _, ok := WordsByLine(c); ok {
			t.Errorf("%s block reported splittable", c.Kind)
		}
	}
}

// Every token of every line is accounted for, on both sides. A dropped token
// would silently delete text from the rendered diff.
func TestNoTokenIsLost(t *testing.T) {
	const oldSrc = "# H\n\nalpha beta gamma\ndelta epsilon zeta\n"
	const newSrc = "# H\n\nalpha BETA gamma\ndelta epsilon ZETA\n"

	c := modifyOf(t, oldSrc, newSrc)
	old, nw, ok := WordsByLine(c)
	if !ok {
		t.Fatal("unsplittable")
	}
	count := func(rows []LineWords) int {
		n := 0
		for _, r := range rows {
			for _, s := range r.Spans {
				n += len(Tokenize(s.Text))
			}
		}
		return n
	}
	if got, want := count(old), len(Tokenize(c.Old.Text)); got != want {
		t.Errorf("old side carries %d tokens, the block has %d", got, want)
	}
	if got, want := count(nw), len(Tokenize(c.New.Text)); got != want {
		t.Errorf("new side carries %d tokens, the block has %d", got, want)
	}
}