detached dca8245

test(oracle): add differential oracle and isolate benchmark setup

Seeded workloads drive the public VT, history, codec, ANSI and HTML pipeline and hash every observable result into a golden file. Benchmarks no longer allocate a Screen or input slices inside the measured loop.
commit
dca82451e2bd7b65049428e6e80ab65422ba42f9
author
brice · · 2026-10-02 09:12 UTC
parent
8f2d811
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +1090 -4
M benchmark_test.go +6 -4
@@ -248,8 +248,8 @@ func BenchmarkScreenKittyAPC(b *testing.B) {
248248 b.ReportAllocs()
249249 for b.Loop() {
250250 if fragmented {
251- for _, part := range apc {
252- s.Write([]byte{part})
251+ for i := range apc {
252+ s.Write(apc[i : i+1 : i+1])
253253 }
254254 } else {
255255 s.Write(apc)
@@ -301,9 +301,10 @@ func BenchmarkScreenCaptureDamage(b *testing.B) {
301301
302302 func BenchmarkScreenShellRedrawBurst(b *testing.B) {
303303 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")
304+ s := NewScreen(120, 40)
305+ s.ClearDamage()
304306 b.ReportAllocs()
305307 for b.Loop() {
306- s := NewScreen(120, 40)
307308 for range 200 {
308309 s.Write(chunk)
309310 s.ClearDamage()
@@ -313,9 +314,10 @@ func BenchmarkScreenShellRedrawBurst(b *testing.B) {
313314
314315 func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {
315316 chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")
317+ s := NewScreen(120, 40)
318+ s.ClearDamage()
316319 b.ReportAllocs()
317320 for b.Loop() {
318- s := NewScreen(120, 40)
319321 for range 500 {
320322 s.Write(chunk)
321323 s.ClearDamage()
@@ -248,8 +248,8 @@ func BenchmarkScreenKittyAPC(b *testing.B) {
248 b.ReportAllocs()248 b.ReportAllocs()
249 for b.Loop() {249 for b.Loop() {
250 if fragmented {250 if fragmented {
251- for _, part := range apc {251+ for i := range apc {
252- s.Write([]byte{part})252+ s.Write(apc[i : i+1 : i+1])
253 }253 }
254 } else {254 } else {
255 s.Write(apc)255 s.Write(apc)
@@ -301,9 +301,10 @@ func BenchmarkScreenCaptureDamage(b *testing.B) {
301 301
302 func BenchmarkScreenShellRedrawBurst(b *testing.B) {302 func BenchmarkScreenShellRedrawBurst(b *testing.B) {
303 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")303 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")
304+ s := NewScreen(120, 40)
305+ s.ClearDamage()
304 b.ReportAllocs()306 b.ReportAllocs()
305 for b.Loop() {307 for b.Loop() {
306- s := NewScreen(120, 40)
307 for range 200 {308 for range 200 {
308 s.Write(chunk)309 s.Write(chunk)
309 s.ClearDamage()310 s.ClearDamage()
@@ -313,9 +314,10 @@ func BenchmarkScreenShellRedrawBurst(b *testing.B) {
313 314
314 func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {315 func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {
315 chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")316 chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")
317+ s := NewScreen(120, 40)
318+ s.ClearDamage()
316 b.ReportAllocs()319 b.ReportAllocs()
317 for b.Loop() {320 for b.Loop() {
318- s := NewScreen(120, 40)
319 for range 500 {321 for range 500 {
320 s.Write(chunk)322 s.Write(chunk)
321 s.ClearDamage()323 s.ClearDamage()
A oracle_test.go +616 -0
@@ -0,0 +1,616 @@
1+package vt_test
2+
3+import (
4+ "bufio"
5+ "bytes"
6+ "crypto/sha256"
7+ "encoding/hex"
8+ "flag"
9+ "fmt"
10+ "hash"
11+ "math/rand/v2"
12+ "os"
13+ "path/filepath"
14+ "strings"
15+ "testing"
16+
17+ vt "github.com/bnema/vev-vt"
18+ "github.com/bnema/vev-vt/ansi"
19+ core "github.com/bnema/vev-vt/core"
20+ "github.com/bnema/vev-vt/graphics"
21+ "github.com/bnema/vev-vt/html"
22+)
23+
24+// The differential oracle drives seeded terminal workloads through the public
25+// pipeline (VT parsing, damage, history, codecs, ANSI and HTML renderers) and
26+// hashes every observable result at checkpoints. Performance work must keep
27+// the golden file byte-identical; behavior changes must regenerate it with
28+// -oracle.update and justify the difference.
29+var (
30+ oracleUpdate = flag.Bool("oracle.update", false, "rewrite testdata/oracle/golden.txt")
31+ oracleDump = flag.String("oracle.dump", "", "write full plain-text transcripts to this directory")
32+)
33+
34+const (
35+ oracleScenarios = 36
36+ oracleSteps = 240
37+ oracleCheckEvery = 20
38+ oracleGolden = "testdata/oracle/golden.txt"
39+)
40+
41+type oracleScenario struct {
42+ seed uint64
43+ cols int
44+ rows int
45+ history *vt.HistoryConfig
46+ byteSplit bool
47+ evictHook bool
48+}
49+
50+func oracleScenarioFor(i int) oracleScenario {
51+ r := rand.New(rand.NewPCG(uint64(i), 0x5eed))
52+ sc := oracleScenario{seed: uint64(i), cols: 8 + r.IntN(110), rows: 3 + r.IntN(40)}
53+ switch i % 4 {
54+ case 1:
55+ sc.history = &vt.HistoryConfig{MaxRows: 50 + r.IntN(400), ChunkRows: 1 + r.IntN(16)}
56+ case 2:
57+ sc.history = &vt.HistoryConfig{MaxBytes: uint64(64<<10 + r.IntN(512<<10)), ChunkRows: 1 + r.IntN(64)}
58+ case 3:
59+ sc.history = &vt.HistoryConfig{MaxRows: 2000, MaxBytes: 4 << 20}
60+ }
61+ sc.byteSplit = i%5 == 0
62+ sc.evictHook = i%3 == 0
63+ return sc
64+}
65+
66+type oracleRun struct {
67+ t *testing.T
68+ sc oracleScenario
69+ r *rand.Rand
70+ screen *vt.Screen
71+ ansiR *ansi.Renderer
72+ ansi256 *ansi.Renderer
73+ htmlR *html.Renderer
74+ replay *vt.Screen
75+ // mirror reproduces the vev daemon consumer: one core.Frame reused and
76+ // mutated in place between draws, so renderer shadows must not alias it.
77+ mirror core.Frame
78+ h hash.Hash
79+ dump *bufio.Writer
80+ events []string
81+
82+ replayMismatches int
83+}
84+
85+func (o *oracleRun) emit(format string, args ...any) {
86+ line := fmt.Sprintf(format, args...)
87+ o.h.Write([]byte(line))
88+ o.h.Write([]byte{'\n'})
89+ if o.dump != nil {
90+ o.dump.WriteString(line)
91+ o.dump.WriteByte('\n')
92+ }
93+}
94+
95+func formatCell(c core.Cell) string {
96+ return fmt.Sprintf("%q|%+v|%q|%q|%t", c.Rune, c.Style, c.Payload.Grapheme(), c.Payload.Hyperlink(), c.Continuation)
97+}
98+
99+// formatRow is a compact exact digest of a row: every Cell field contributes.
100+// Full text is only produced for dumps, where it is needed for diagnosis.
101+func formatRow(row []core.Cell) string {
102+ h := sha256.New()
103+ var buf []byte
104+ for _, c := range row {
105+ buf = fmt.Appendf(buf[:0], "%d|%+v|%s|%s|%t;", c.Rune, c.Style, c.Payload.Grapheme(), c.Payload.Hyperlink(), c.Continuation)
106+ h.Write(buf)
107+ }
108+ return hex.EncodeToString(h.Sum(nil)[:12])
109+}
110+
111+func rowText(row []core.Cell) string {
112+ var b strings.Builder
113+ for _, c := range row {
114+ if c.Continuation {
115+ continue
116+ }
117+ if g := c.Payload.Grapheme(); g != "" {
118+ b.WriteString(g)
119+ } else if c.Rune != 0 {
120+ b.WriteRune(c.Rune)
121+ }
122+ }
123+ return b.String()
124+}
125+
126+func (o *oracleRun) genChunk() []byte {
127+ r := o.r
128+ var b bytes.Buffer
129+ for range 1 + r.IntN(6) {
130+ switch op := r.IntN(100); {
131+ case op < 22:
132+ words := []string{"hello", "world", "vev", "ls -la", "❯", "foo/bar", "λx", "0123456789"}
133+ for range 1 + r.IntN(8) {
134+ b.WriteString(words[r.IntN(len(words))])
135+ b.WriteByte(' ')
136+ }
137+ if r.IntN(2) == 0 {
138+ b.WriteString("\r\n")
139+ }
140+ case op < 30:
141+ uni := []string{"界", "🙂", "é", "e\u0301", "한국어", "👍🏽", "a\u200db", "ff"}
142+ for range 1 + r.IntN(10) {
143+ b.WriteString(uni[r.IntN(len(uni))])
144+ }
145+ case op < 42:
146+ b.WriteString(randomSGR(r))
147+ case op < 50:
148+ fmt.Fprintf(&b, "\x1b[%d;%dH", 1+r.IntN(o.sc.rows+2), 1+r.IntN(o.sc.cols+2))
149+ case op < 56:
150+ seqs := []string{"\x1b[K", "\x1b[1K", "\x1b[2K", "\x1b[J", "\x1b[2J", "\x1b[3@", "\x1b[2P", "\x1b[L", "\x1b[2M", "\x1b[4X", "\x1b[S", "\x1b[2T", "\x1bM", "\x1bD", "\x1bE", "\t", "\b"}
151+ b.WriteString(seqs[r.IntN(len(seqs))])
152+ case op < 64:
153+ for range 1 + r.IntN(3*o.sc.rows) {
154+ fmt.Fprintf(&b, "line %d %s\r\n", r.IntN(1000), strings.Repeat("=", r.IntN(o.sc.cols+5)))
155+ }
156+ case op < 68:
157+ top := 1 + r.IntN(o.sc.rows)
158+ bottom := top + r.IntN(o.sc.rows-top+1)
159+ fmt.Fprintf(&b, "\x1b[%d;%dr\x1b[%d;1H", top, bottom, bottom)
160+ for range 1 + r.IntN(6) {
161+ b.WriteString("region\n")
162+ }
163+ if r.IntN(2) == 0 {
164+ b.WriteString("\x1b[r")
165+ }
166+ case op < 71:
167+ if r.IntN(2) == 0 {
168+ b.WriteString("\x1b[?1049h")
169+ } else {
170+ b.WriteString("\x1b[?1049l")
171+ }
172+ case op < 74:
173+ fmt.Fprintf(&b, "\x1b]8;;https://example.invalid/%d\x1b\\link\x1b]8;;\x1b\\", r.IntN(9))
174+ case op < 76:
175+ fmt.Fprintf(&b, "\x1b]0;title %d\x07", r.IntN(99))
176+ case op < 81:
177+ b.WriteString(randomKitty(r))
178+ case op < 84:
179+ queries := []string{"\x1b[6n", "\x1b[c", "\x1b[?2004$p", "\x1b[5n"}
180+ b.WriteString(queries[r.IntN(len(queries))])
181+ case op < 86:
182+ if r.IntN(2) == 0 {
183+ b.WriteString("\x1b[?7l")
184+ } else {
185+ b.WriteString("\x1b[?7h")
186+ }
187+ case op < 91:
188+ // High-cardinality truecolor burst.
189+ for range 1 + r.IntN(40) {
190+ fmt.Fprintf(&b, "\x1b[38;2;%d;%d;%dm#", r.IntN(256), r.IntN(256), r.IntN(256))
191+ }
192+ b.WriteString("\x1b[0m")
193+ case op < 94:
194+ // Styled blanks and erases inherit the current background.
195+ fmt.Fprintf(&b, "\x1b[48;5;%dm\x1b[K \x1b[0m", r.IntN(256))
196+ default:
197+ fmt.Fprintf(&b, "\r\x1b[K❯ abc\x1b[90m suggestion\x1b[39m\r\x1b[%dC", r.IntN(8))
198+ }
199+ }
200+ return b.Bytes()
201+}
202+
203+func randomSGR(r *rand.Rand) string {
204+ parts := []string{"0", "1", "2", "3", "4", "4:3", "5", "7", "9", "22", "23", "24", "27", "39", "49", "59"}
205+ switch r.IntN(6) {
206+ case 0:
207+ return fmt.Sprintf("\x1b[%dm", 30+r.IntN(8))
208+ case 1:
209+ return fmt.Sprintf("\x1b[38;5;%d;48;5;%dm", r.IntN(256), r.IntN(256))
210+ case 2:
211+ return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r.IntN(256), r.IntN(256), r.IntN(256))
212+ case 3:
213+ return fmt.Sprintf("\x1b[58;2;%d;%d;%d;4:%dm", r.IntN(256), r.IntN(256), r.IntN(256), r.IntN(6))
214+ default:
215+ return "\x1b[" + parts[r.IntN(len(parts))] + "m"
216+ }
217+}
218+
219+func randomKitty(r *rand.Rand) string {
220+ switch r.IntN(4) {
221+ case 0:
222+ w, h := 1+r.IntN(4), 1+r.IntN(4)
223+ pixels := make([]byte, w*h*4)
224+ for i := range pixels {
225+ pixels[i] = byte(r.IntN(256))
226+ }
227+ enc := encodeBase64(pixels, r.IntN(2) == 0)
228+ return fmt.Sprintf("\x1b_Ga=T,i=%d,f=32,s=%d,v=%d,c=%d,r=%d,q=%d;%s\x1b\\", 1+r.IntN(4), w, h, 1+r.IntN(3), 1+r.IntN(2), r.IntN(3), enc)
229+ case 1:
230+ // Chunked transmission.
231+ pixels := make([]byte, 4*4*4)
232+ for i := range pixels {
233+ pixels[i] = byte(r.IntN(256))
234+ }
235+ enc := encodeBase64(pixels, true)
236+ mid := (len(enc) / 8) * 4
237+ return fmt.Sprintf("\x1b_Ga=t,i=%d,f=32,s=4,v=4,m=1;%s\x1b\\\x1b_Gm=0;%s\x1b\\\x1b_Ga=p,i=%d\x1b\\", 5+r.IntN(3), enc[:mid], enc[mid:], 5+r.IntN(3))
238+ case 2:
239+ return fmt.Sprintf("\x1b_Ga=d,d=%s\x1b\\", []string{"a", "A", "i", "I"}[r.IntN(4)])
240+ default:
241+ // Invalid base64 must be rejected identically.
242+ return "\x1b_Ga=T,f=32,s=1,v=1;!!!\x1b\\"
243+ }
244+}
245+
246+func encodeBase64(data []byte, padded bool) string {
247+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
248+ var b strings.Builder
249+ for i := 0; i < len(data); i += 3 {
250+ var chunk [3]byte
251+ n := copy(chunk[:], data[i:])
252+ v := uint(chunk[0])<<16 | uint(chunk[1])<<8 | uint(chunk[2])
253+ for j := range 4 {
254+ if j <= n {
255+ b.WriteByte(alphabet[(v>>(18-6*j))&63])
256+ } else if padded {
257+ b.WriteByte('=')
258+ }
259+ }
260+ }
261+ return b.String()
262+}
263+
264+func (o *oracleRun) write(data []byte) {
265+ if o.sc.byteSplit {
266+ for i := range data {
267+ o.screen.Write(data[i : i+1 : i+1])
268+ }
269+ return
270+ }
271+ for len(data) > 0 {
272+ n := 1 + o.r.IntN(len(data))
273+ if o.r.IntN(3) != 0 {
274+ n = len(data)
275+ }
276+ o.screen.Write(data[:n:n])
277+ data = data[n:]
278+ }
279+}
280+
281+func (o *oracleRun) maybeResize(step int) {
282+ if o.r.IntN(40) != 0 {
283+ return
284+ }
285+ cols, rows := 8+o.r.IntN(110), 3+o.r.IntN(40)
286+ o.emit("step %d resize %dx%d", step, cols, rows)
287+ o.screen.Resize(cols, rows)
288+}
289+
290+func (o *oracleRun) checkScreen() {
291+ s := o.screen
292+ o.emit("screen %dx%d cursor=%d,%d vis=%t title=%q alt=%t", s.Columns(), s.Rows(), s.CursorRow(), s.CursorCol(), s.CursorVisible(), s.TerminalTitle(), s.AltScreenActive())
293+ snap := s.Snapshot()
294+ o.emit("modes %+v cursor %+v next=%d", snap.Modes(), snap.Cursor(), snap.NextRowID())
295+ for y := range s.Rows() {
296+ o.emit("row %d id=%d bound=%+v %s %q", y, s.RowID(y), snap.Bound(y), formatRow(s.RowCells(y)), rowText(s.RowCells(y)))
297+ snapRow := snap.Row(y)
298+ if formatRow(snapRow) != formatRow(s.RowCells(y)) {
299+ o.t.Fatalf("seed %d: snapshot row %d differs from live row", o.sc.seed, y)
300+ }
301+ }
302+ o.emit("bounds %+v", s.LineBounds())
303+}
304+
305+func (o *oracleRun) checkGraphics() {
306+ g := o.screen.GraphicsSnapshot()
307+ if g == nil {
308+ o.emit("graphics nil")
309+ return
310+ }
311+ o.emit("graphics gen=%d usage=%+v", g.Generation(), g.Usage())
312+ for _, a := range g.Assets() {
313+ blob := a.Blob()
314+ sum := sha256.Sum256(blob.Encoded)
315+ o.emit("asset fmt=%v %dx%d px=%d sha=%x", blob.Format, blob.Width, blob.Height, blob.DecodedPixels, sum[:8])
316+ }
317+ for _, p := range g.Placements() {
318+ o.emit("placement %+v", p.Spec())
319+ }
320+ o.emit("fragments %+v", g.VisibleCellFragments(graphics.CellRect{Width: int64(o.screen.Columns()), Height: int64(o.screen.Rows())}))
321+}
322+
323+func (o *oracleRun) checkHistory(deep bool) {
324+ h := o.screen.History()
325+ if h == nil {
326+ return
327+ }
328+ o.emit("history len=%d cells=%d bytes=%d next=%d stats=%+v", h.Len(), h.Cells(), h.LogicalBytes(), h.NextRowID(), h.CompressionStats())
329+ if !deep {
330+ return
331+ }
332+ snapView := h.SnapshotView()
333+ tail, err := vt.MarshalHistoryTail(snapView)
334+ if err != nil {
335+ o.t.Fatalf("seed %d: MarshalHistoryTail: %v", o.sc.seed, err)
336+ }
337+ sum := sha256.Sum256(tail)
338+ o.emit("snapview len=%d chunks=%d tail=%x", snapView.Len(), snapView.ChunkCount(), sum)
339+ var sealed [][]byte
340+ for i := range snapView.ChunkCount() {
341+ blob, err := vt.MarshalHistoryChunk(snapView.Chunk(i))
342+ if err != nil {
343+ o.t.Fatalf("seed %d: MarshalHistoryChunk: %v", o.sc.seed, err)
344+ }
345+ sum := sha256.Sum256(blob)
346+ o.emit("chunk %d %x", i, sum)
347+ sealed = append(sealed, blob)
348+ }
349+ view := h.View()
350+ var rows []string
351+ var texts []string
352+ if err := view.Range(func(row []core.Cell) bool {
353+ rows = append(rows, formatRow(row))
354+ texts = append(texts, rowText(row))
355+ return true
356+ }); err != nil {
357+ o.t.Fatalf("seed %d: history Range: %v", o.sc.seed, err)
358+ }
359+ for i, row := range rows {
360+ o.emit("hrow %d id=%d bound=%+v %s %q", i, view.RowID(i), view.Bound(i), row, texts[i])
361+ if got := formatRow(view.Row(i)); got != row {
362+ o.t.Fatalf("seed %d: history Row(%d) differs from Range", o.sc.seed, i)
363+ }
364+ dst := make([]core.Cell, view.RowWidth(i))
365+ view.CopyRow(i, dst)
366+ if formatRow(dst) != row {
367+ o.t.Fatalf("seed %d: history CopyRow(%d) differs from Range", o.sc.seed, i)
368+ }
369+ }
370+ restored, err := vt.HistoryFromBlobs(h.Limits(), sealed, tail)
371+ if err != nil {
372+ o.t.Fatalf("seed %d: HistoryFromBlobs: %v", o.sc.seed, err)
373+ }
374+ rview := restored.View()
375+ if rview.Len() != view.Len() || restored.NextRowID() != h.NextRowID() {
376+ o.t.Fatalf("seed %d: restored history len=%d next=%d want len=%d next=%d", o.sc.seed, rview.Len(), restored.NextRowID(), view.Len(), h.NextRowID())
377+ }
378+ for i, row := range rows {
379+ if formatRow(rview.Row(i)) != row || rview.RowID(i) != view.RowID(i) || rview.Bound(i) != view.Bound(i) {
380+ o.t.Fatalf("seed %d: restored history row %d differs", o.sc.seed, i)
381+ }
382+ }
383+ transcript, err := o.screen.RecoveryTranscriptSnapshot().Marshal()
384+ if err != nil {
385+ o.t.Fatalf("seed %d: recovery transcript: %v", o.sc.seed, err)
386+ }
387+ tsum := sha256.Sum256(transcript)
388+ o.emit("transcript %x", tsum)
389+ recovered, err := vt.NewScreenWithRecoveryTranscript(o.screen.Columns(), o.screen.Rows(), h.Limits(), sealed, tail, transcript)
390+ if err != nil {
391+ o.t.Fatalf("seed %d: NewScreenWithRecoveryTranscript: %v", o.sc.seed, err)
392+ }
393+ rh := recovered.History()
394+ o.emit("recovered len=%d next=%d", rh.Len(), rh.NextRowID())
395+ if rh.Len() > 0 {
396+ rv := rh.View()
397+ o.emit("recovered last %s", formatRow(rv.Row(rv.Len()-1)))
398+ }
399+}
400+
401+func (o *oracleRun) render(step int) {
402+ s := o.screen
403+ damage := append([]core.Damage(nil), s.Damage()...)
404+ capture := s.CaptureDamage()
405+ if len(capture.Damage) != len(damage) {
406+ o.t.Fatalf("seed %d: CaptureDamage length %d want %d", o.sc.seed, len(capture.Damage), len(damage))
407+ }
408+ o.emit("damage %+v", damage)
409+
410+ if o.mirror.Width != s.Columns() || o.mirror.Height != s.Rows() {
411+ o.mirror = core.NewFrame(s.Columns(), s.Rows())
412+ }
413+ for y := range s.Rows() {
414+ for x := range s.Columns() {
415+ o.mirror.Set(x, y, s.Cell(x, y))
416+ }
417+ }
418+ var source core.CellSource = o.mirror
419+ switch o.sc.seed % 3 {
420+ case 1:
421+ source = &o.mirror
422+ case 2:
423+ source = s // generic CellSource path
424+ }
425+ prepared, err := o.ansiR.Prepare(source, damage, false)
426+ if err != nil {
427+ o.t.Fatalf("seed %d step %d: ansi Prepare: %v", o.sc.seed, step, err)
428+ }
429+ out := prepared.Bytes()
430+ o.emit("ansi %q", out)
431+ if o.r.IntN(7) == 0 {
432+ // Discarded draw: renderer must keep its committed shadow and damage
433+ // must remain pending for the next draw.
434+ o.emit("ansi discard")
435+ } else {
436+ prepared.Commit()
437+ o.applyReplay(out, step)
438+ if !s.AcknowledgeDamage(capture.Generation) {
439+ o.t.Fatalf("seed %d: acknowledge failed", o.sc.seed)
440+ }
441+ }
442+ other, err := o.ansi256.Draw(source, damage)
443+ if err != nil {
444+ o.t.Fatalf("seed %d: ansi256 Draw: %v", o.sc.seed, err)
445+ }
446+ o.emit("ansi256 %q", other)
447+
448+ snap := s.Snapshot()
449+ cur := snap.Cursor()
450+ var htmlSource core.CellSource = o.mirror
451+ if o.sc.seed%3 == 1 {
452+ htmlSource = snap
453+ }
454+ hp, err := o.htmlR.Prepare(htmlSource, damage, false, html.Cursor{Row: cur.Row, Column: cur.Col, Visible: cur.Visible, Style: html.CursorStyle(cur.Style), StyleSet: cur.StyleSet})
455+ if err != nil {
456+ o.emit("html err %v", err)
457+ return
458+ }
459+ o.emit("html %s", hp.JSON())
460+ if o.r.IntN(6) == 0 {
461+ if err := hp.Abort(); err != nil {
462+ o.t.Fatalf("seed %d: html abort: %v", o.sc.seed, err)
463+ }
464+ o.emit("html abort")
465+ } else if err := hp.Commit(); err != nil {
466+ o.t.Fatalf("seed %d: html commit: %v", o.sc.seed, err)
467+ }
468+}
469+
470+// applyReplay feeds committed ANSI output to an independent VT and compares
471+// the visible grid with the source. Mismatches are known renderer defects on
472+// the baseline (for example two scroll regions in one damage batch), so they
473+// are hashed into the transcript instead of failing: a performance change must
474+// neither add nor remove one. The replay is then resynchronized from a full
475+// draw, matching the renderer's committed belief.
476+func (o *oracleRun) applyReplay(out []byte, step int) {
477+ s := o.screen
478+ if o.replay == nil || o.replay.Columns() != s.Columns() || o.replay.Rows() != s.Rows() {
479+ o.replay = vt.NewScreen(s.Columns(), s.Rows())
480+ }
481+ o.replay.Write(out)
482+ for y := range s.Rows() {
483+ for x := range s.Columns() {
484+ want, got := s.Cell(x, y), o.replay.Cell(x, y)
485+ if want.Rune != got.Rune || want.Continuation != got.Continuation || !want.Style.Equal(got.Style) {
486+ o.replayMismatches++
487+ o.emit("replay-mismatch step %d at (%d,%d): got %s want %s", step, x, y, formatCell(got), formatCell(want))
488+ o.replay = vt.NewScreen(s.Columns(), s.Rows())
489+ full, err := ansi.New(ansi.Capabilities{}).Draw(s, []core.Damage{core.FullRedraw()})
490+ if err != nil {
491+ o.t.Fatalf("seed %d: resync draw: %v", o.sc.seed, err)
492+ }
493+ o.replay.Write(full)
494+ return
495+ }
496+ }
497+ }
498+}
499+
500+func runOracleScenario(t *testing.T, idx int) string {
501+ sc := oracleScenarioFor(idx)
502+ o := &oracleRun{t: t, sc: sc, r: rand.New(rand.NewPCG(sc.seed, 0xdecaf)), h: sha256.New()}
503+ if *oracleDump != "" {
504+ if err := os.MkdirAll(*oracleDump, 0o755); err != nil {
505+ t.Fatal(err)
506+ }
507+ f, err := os.Create(filepath.Join(*oracleDump, fmt.Sprintf("scenario-%02d.txt", idx)))
508+ if err != nil {
509+ t.Fatal(err)
510+ }
511+ defer f.Close()
512+ o.dump = bufio.NewWriter(f)
513+ defer o.dump.Flush()
514+ }
515+ if sc.history != nil {
516+ o.screen = vt.NewScreenWithHistory(sc.cols, sc.rows, *sc.history)
517+ } else {
518+ o.screen = vt.NewScreen(sc.cols, sc.rows)
519+ }
520+ o.screen.OnResponse = func(b []byte) { o.events = append(o.events, fmt.Sprintf("response %q", b)) }
521+ o.screen.OnBell = func() { o.events = append(o.events, "bell") }
522+ if sc.evictHook {
523+ // The callback owns each row: retain it, verify it is never rewritten by
524+ // later evictions, then scribble on it to prove VT kept its own copy.
525+ var retained []core.Cell
526+ var retainedDigest string
527+ o.screen.OnLineEvicted = func(row []core.Cell) {
528+ if retained != nil && formatRow(retained) != retainedDigest {
529+ t.Fatalf("seed %d: evicted row storage was reused after the callback", sc.seed)
530+ }
531+ digest := formatRow(row)
532+ o.events = append(o.events, "evicted "+digest+" "+rowText(row))
533+ if prev := retained; prev != nil {
534+ for i := range prev {
535+ prev[i] = core.Cell{Rune: 'X'}
536+ }
537+ }
538+ retained, retainedDigest = row, digest
539+ }
540+ }
541+ o.ansiR = ansi.New(ansi.Capabilities{SynchronizedOutput: idx%2 == 0})
542+ o.ansi256 = ansi.NewWithColorProfile(ansi.Capabilities{}, ansi.ColorProfileANSI256)
543+ var err error
544+ o.htmlR, err = html.New(html.Options{})
545+ if err != nil {
546+ t.Fatal(err)
547+ }
548+ var historyConfig vt.HistoryConfig
549+ if sc.history != nil {
550+ historyConfig = *sc.history
551+ }
552+ o.emit("scenario %d %dx%d history=%+v byteSplit=%t evictHook=%t", idx, sc.cols, sc.rows, historyConfig, sc.byteSplit, sc.evictHook)
553+
554+ var digests []string
555+ for step := range oracleSteps {
556+ o.maybeResize(step)
557+ chunk := o.genChunk()
558+ o.emit("step %d write %q", step, chunk)
559+ o.write(chunk)
560+ for _, e := range o.events {
561+ o.emit("%s", e)
562+ }
563+ o.events = o.events[:0]
564+ o.render(step)
565+ if step%oracleCheckEvery == oracleCheckEvery-1 {
566+ o.checkScreen()
567+ o.checkGraphics()
568+ o.checkHistory(step%(2*oracleCheckEvery) == 2*oracleCheckEvery-1)
569+ digests = append(digests, fmt.Sprintf("%02d/%03d %s", idx, step, hex.EncodeToString(o.h.Sum(nil))[:24]))
570+ }
571+ }
572+ digests = append(digests, fmt.Sprintf("%02d replay-mismatches=%d", idx, o.replayMismatches))
573+ return strings.Join(digests, "\n")
574+}
575+
576+func TestDifferentialOracle(t *testing.T) {
577+ if testing.Short() {
578+ t.Skip("differential oracle skipped in -short mode")
579+ }
580+ got := make([]string, oracleScenarios)
581+ t.Run("scenarios", func(t *testing.T) {
582+ for i := range oracleScenarios {
583+ t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
584+ t.Parallel()
585+ got[i] = runOracleScenario(t, i)
586+ })
587+ }
588+ })
589+ if t.Failed() {
590+ return
591+ }
592+ text := strings.Join(got, "\n") + "\n"
593+ if *oracleUpdate {
594+ if err := os.MkdirAll(filepath.Dir(oracleGolden), 0o755); err != nil {
595+ t.Fatal(err)
596+ }
597+ if err := os.WriteFile(oracleGolden, []byte(text), 0o644); err != nil {
598+ t.Fatal(err)
599+ }
600+ return
601+ }
602+ want, err := os.ReadFile(oracleGolden)
603+ if err != nil {
604+ t.Fatalf("read golden (run with -oracle.update on a trusted baseline): %v", err)
605+ }
606+ if string(want) == text {
607+ return
608+ }
609+ wantLines, gotLines := strings.Split(string(want), "\n"), strings.Split(text, "\n")
610+ for i := range min(len(wantLines), len(gotLines)) {
611+ if wantLines[i] != gotLines[i] {
612+ t.Fatalf("oracle diverged at checkpoint %q (want %q); rerun with -oracle.dump=DIR on both trees and diff the scenario transcript", gotLines[i], wantLines[i])
613+ }
614+ }
615+ t.Fatalf("oracle checkpoint count differs: got %d want %d", len(gotLines), len(wantLines))
616+}
@@ -0,0 +1,616 @@
1+package vt_test
2+
3+import (
4+ "bufio"
5+ "bytes"
6+ "crypto/sha256"
7+ "encoding/hex"
8+ "flag"
9+ "fmt"
10+ "hash"
11+ "math/rand/v2"
12+ "os"
13+ "path/filepath"
14+ "strings"
15+ "testing"
16+
17+ vt "github.com/bnema/vev-vt"
18+ "github.com/bnema/vev-vt/ansi"
19+ core "github.com/bnema/vev-vt/core"
20+ "github.com/bnema/vev-vt/graphics"
21+ "github.com/bnema/vev-vt/html"
22+)
23+
24+// The differential oracle drives seeded terminal workloads through the public
25+// pipeline (VT parsing, damage, history, codecs, ANSI and HTML renderers) and
26+// hashes every observable result at checkpoints. Performance work must keep
27+// the golden file byte-identical; behavior changes must regenerate it with
28+// -oracle.update and justify the difference.
29+var (
30+ oracleUpdate = flag.Bool("oracle.update", false, "rewrite testdata/oracle/golden.txt")
31+ oracleDump = flag.String("oracle.dump", "", "write full plain-text transcripts to this directory")
32+)
33+
34+const (
35+ oracleScenarios = 36
36+ oracleSteps = 240
37+ oracleCheckEvery = 20
38+ oracleGolden = "testdata/oracle/golden.txt"
39+)
40+
41+type oracleScenario struct {
42+ seed uint64
43+ cols int
44+ rows int
45+ history *vt.HistoryConfig
46+ byteSplit bool
47+ evictHook bool
48+}
49+
50+func oracleScenarioFor(i int) oracleScenario {
51+ r := rand.New(rand.NewPCG(uint64(i), 0x5eed))
52+ sc := oracleScenario{seed: uint64(i), cols: 8 + r.IntN(110), rows: 3 + r.IntN(40)}
53+ switch i % 4 {
54+ case 1:
55+ sc.history = &vt.HistoryConfig{MaxRows: 50 + r.IntN(400), ChunkRows: 1 + r.IntN(16)}
56+ case 2:
57+ sc.history = &vt.HistoryConfig{MaxBytes: uint64(64<<10 + r.IntN(512<<10)), ChunkRows: 1 + r.IntN(64)}
58+ case 3:
59+ sc.history = &vt.HistoryConfig{MaxRows: 2000, MaxBytes: 4 << 20}
60+ }
61+ sc.byteSplit = i%5 == 0
62+ sc.evictHook = i%3 == 0
63+ return sc
64+}
65+
66+type oracleRun struct {
67+ t *testing.T
68+ sc oracleScenario
69+ r *rand.Rand
70+ screen *vt.Screen
71+ ansiR *ansi.Renderer
72+ ansi256 *ansi.Renderer
73+ htmlR *html.Renderer
74+ replay *vt.Screen
75+ // mirror reproduces the vev daemon consumer: one core.Frame reused and
76+ // mutated in place between draws, so renderer shadows must not alias it.
77+ mirror core.Frame
78+ h hash.Hash
79+ dump *bufio.Writer
80+ events []string
81+
82+ replayMismatches int
83+}
84+
85+func (o *oracleRun) emit(format string, args ...any) {
86+ line := fmt.Sprintf(format, args...)
87+ o.h.Write([]byte(line))
88+ o.h.Write([]byte{'\n'})
89+ if o.dump != nil {
90+ o.dump.WriteString(line)
91+ o.dump.WriteByte('\n')
92+ }
93+}
94+
95+func formatCell(c core.Cell) string {
96+ return fmt.Sprintf("%q|%+v|%q|%q|%t", c.Rune, c.Style, c.Payload.Grapheme(), c.Payload.Hyperlink(), c.Continuation)
97+}
98+
99+// formatRow is a compact exact digest of a row: every Cell field contributes.
100+// Full text is only produced for dumps, where it is needed for diagnosis.
101+func formatRow(row []core.Cell) string {
102+ h := sha256.New()
103+ var buf []byte
104+ for _, c := range row {
105+ buf = fmt.Appendf(buf[:0], "%d|%+v|%s|%s|%t;", c.Rune, c.Style, c.Payload.Grapheme(), c.Payload.Hyperlink(), c.Continuation)
106+ h.Write(buf)
107+ }
108+ return hex.EncodeToString(h.Sum(nil)[:12])
109+}
110+
111+func rowText(row []core.Cell) string {
112+ var b strings.Builder
113+ for _, c := range row {
114+ if c.Continuation {
115+ continue
116+ }
117+ if g := c.Payload.Grapheme(); g != "" {
118+ b.WriteString(g)
119+ } else if c.Rune != 0 {
120+ b.WriteRune(c.Rune)
121+ }
122+ }
123+ return b.String()
124+}
125+
126+func (o *oracleRun) genChunk() []byte {
127+ r := o.r
128+ var b bytes.Buffer
129+ for range 1 + r.IntN(6) {
130+ switch op := r.IntN(100); {
131+ case op < 22:
132+ words := []string{"hello", "world", "vev", "ls -la", "❯", "foo/bar", "λx", "0123456789"}
133+ for range 1 + r.IntN(8) {
134+ b.WriteString(words[r.IntN(len(words))])
135+ b.WriteByte(' ')
136+ }
137+ if r.IntN(2) == 0 {
138+ b.WriteString("\r\n")
139+ }
140+ case op < 30:
141+ uni := []string{"界", "🙂", "é", "e\u0301", "한국어", "👍🏽", "a\u200db", "ff"}
142+ for range 1 + r.IntN(10) {
143+ b.WriteString(uni[r.IntN(len(uni))])
144+ }
145+ case op < 42:
146+ b.WriteString(randomSGR(r))
147+ case op < 50:
148+ fmt.Fprintf(&b, "\x1b[%d;%dH", 1+r.IntN(o.sc.rows+2), 1+r.IntN(o.sc.cols+2))
149+ case op < 56:
150+ seqs := []string{"\x1b[K", "\x1b[1K", "\x1b[2K", "\x1b[J", "\x1b[2J", "\x1b[3@", "\x1b[2P", "\x1b[L", "\x1b[2M", "\x1b[4X", "\x1b[S", "\x1b[2T", "\x1bM", "\x1bD", "\x1bE", "\t", "\b"}
151+ b.WriteString(seqs[r.IntN(len(seqs))])
152+ case op < 64:
153+ for range 1 + r.IntN(3*o.sc.rows) {
154+ fmt.Fprintf(&b, "line %d %s\r\n", r.IntN(1000), strings.Repeat("=", r.IntN(o.sc.cols+5)))
155+ }
156+ case op < 68:
157+ top := 1 + r.IntN(o.sc.rows)
158+ bottom := top + r.IntN(o.sc.rows-top+1)
159+ fmt.Fprintf(&b, "\x1b[%d;%dr\x1b[%d;1H", top, bottom, bottom)
160+ for range 1 + r.IntN(6) {
161+ b.WriteString("region\n")
162+ }
163+ if r.IntN(2) == 0 {
164+ b.WriteString("\x1b[r")
165+ }
166+ case op < 71:
167+ if r.IntN(2) == 0 {
168+ b.WriteString("\x1b[?1049h")
169+ } else {
170+ b.WriteString("\x1b[?1049l")
171+ }
172+ case op < 74:
173+ fmt.Fprintf(&b, "\x1b]8;;https://example.invalid/%d\x1b\\link\x1b]8;;\x1b\\", r.IntN(9))
174+ case op < 76:
175+ fmt.Fprintf(&b, "\x1b]0;title %d\x07", r.IntN(99))
176+ case op < 81:
177+ b.WriteString(randomKitty(r))
178+ case op < 84:
179+ queries := []string{"\x1b[6n", "\x1b[c", "\x1b[?2004$p", "\x1b[5n"}
180+ b.WriteString(queries[r.IntN(len(queries))])
181+ case op < 86:
182+ if r.IntN(2) == 0 {
183+ b.WriteString("\x1b[?7l")
184+ } else {
185+ b.WriteString("\x1b[?7h")
186+ }
187+ case op < 91:
188+ // High-cardinality truecolor burst.
189+ for range 1 + r.IntN(40) {
190+ fmt.Fprintf(&b, "\x1b[38;2;%d;%d;%dm#", r.IntN(256), r.IntN(256), r.IntN(256))
191+ }
192+ b.WriteString("\x1b[0m")
193+ case op < 94:
194+ // Styled blanks and erases inherit the current background.
195+ fmt.Fprintf(&b, "\x1b[48;5;%dm\x1b[K \x1b[0m", r.IntN(256))
196+ default:
197+ fmt.Fprintf(&b, "\r\x1b[K❯ abc\x1b[90m suggestion\x1b[39m\r\x1b[%dC", r.IntN(8))
198+ }
199+ }
200+ return b.Bytes()
201+}
202+
203+func randomSGR(r *rand.Rand) string {
204+ parts := []string{"0", "1", "2", "3", "4", "4:3", "5", "7", "9", "22", "23", "24", "27", "39", "49", "59"}
205+ switch r.IntN(6) {
206+ case 0:
207+ return fmt.Sprintf("\x1b[%dm", 30+r.IntN(8))
208+ case 1:
209+ return fmt.Sprintf("\x1b[38;5;%d;48;5;%dm", r.IntN(256), r.IntN(256))
210+ case 2:
211+ return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r.IntN(256), r.IntN(256), r.IntN(256))
212+ case 3:
213+ return fmt.Sprintf("\x1b[58;2;%d;%d;%d;4:%dm", r.IntN(256), r.IntN(256), r.IntN(256), r.IntN(6))
214+ default:
215+ return "\x1b[" + parts[r.IntN(len(parts))] + "m"
216+ }
217+}
218+
219+func randomKitty(r *rand.Rand) string {
220+ switch r.IntN(4) {
221+ case 0:
222+ w, h := 1+r.IntN(4), 1+r.IntN(4)
223+ pixels := make([]byte, w*h*4)
224+ for i := range pixels {
225+ pixels[i] = byte(r.IntN(256))
226+ }
227+ enc := encodeBase64(pixels, r.IntN(2) == 0)
228+ return fmt.Sprintf("\x1b_Ga=T,i=%d,f=32,s=%d,v=%d,c=%d,r=%d,q=%d;%s\x1b\\", 1+r.IntN(4), w, h, 1+r.IntN(3), 1+r.IntN(2), r.IntN(3), enc)
229+ case 1:
230+ // Chunked transmission.
231+ pixels := make([]byte, 4*4*4)
232+ for i := range pixels {
233+ pixels[i] = byte(r.IntN(256))
234+ }
235+ enc := encodeBase64(pixels, true)
236+ mid := (len(enc) / 8) * 4
237+ return fmt.Sprintf("\x1b_Ga=t,i=%d,f=32,s=4,v=4,m=1;%s\x1b\\\x1b_Gm=0;%s\x1b\\\x1b_Ga=p,i=%d\x1b\\", 5+r.IntN(3), enc[:mid], enc[mid:], 5+r.IntN(3))
238+ case 2:
239+ return fmt.Sprintf("\x1b_Ga=d,d=%s\x1b\\", []string{"a", "A", "i", "I"}[r.IntN(4)])
240+ default:
241+ // Invalid base64 must be rejected identically.
242+ return "\x1b_Ga=T,f=32,s=1,v=1;!!!\x1b\\"
243+ }
244+}
245+
246+func encodeBase64(data []byte, padded bool) string {
247+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
248+ var b strings.Builder
249+ for i := 0; i < len(data); i += 3 {
250+ var chunk [3]byte
251+ n := copy(chunk[:], data[i:])
252+ v := uint(chunk[0])<<16 | uint(chunk[1])<<8 | uint(chunk[2])
253+ for j := range 4 {
254+ if j <= n {
255+ b.WriteByte(alphabet[(v>>(18-6*j))&63])
256+ } else if padded {
257+ b.WriteByte('=')
258+ }
259+ }
260+ }
261+ return b.String()
262+}
263+
264+func (o *oracleRun) write(data []byte) {
265+ if o.sc.byteSplit {
266+ for i := range data {
267+ o.screen.Write(data[i : i+1 : i+1])
268+ }
269+ return
270+ }
271+ for len(data) > 0 {
272+ n := 1 + o.r.IntN(len(data))
273+ if o.r.IntN(3) != 0 {
274+ n = len(data)
275+ }
276+ o.screen.Write(data[:n:n])
277+ data = data[n:]
278+ }
279+}
280+
281+func (o *oracleRun) maybeResize(step int) {
282+ if o.r.IntN(40) != 0 {
283+ return
284+ }
285+ cols, rows := 8+o.r.IntN(110), 3+o.r.IntN(40)
286+ o.emit("step %d resize %dx%d", step, cols, rows)
287+ o.screen.Resize(cols, rows)
288+}
289+
290+func (o *oracleRun) checkScreen() {
291+ s := o.screen
292+ o.emit("screen %dx%d cursor=%d,%d vis=%t title=%q alt=%t", s.Columns(), s.Rows(), s.CursorRow(), s.CursorCol(), s.CursorVisible(), s.TerminalTitle(), s.AltScreenActive())
293+ snap := s.Snapshot()
294+ o.emit("modes %+v cursor %+v next=%d", snap.Modes(), snap.Cursor(), snap.NextRowID())
295+ for y := range s.Rows() {
296+ o.emit("row %d id=%d bound=%+v %s %q", y, s.RowID(y), snap.Bound(y), formatRow(s.RowCells(y)), rowText(s.RowCells(y)))
297+ snapRow := snap.Row(y)
298+ if formatRow(snapRow) != formatRow(s.RowCells(y)) {
299+ o.t.Fatalf("seed %d: snapshot row %d differs from live row", o.sc.seed, y)
300+ }
301+ }
302+ o.emit("bounds %+v", s.LineBounds())
303+}
304+
305+func (o *oracleRun) checkGraphics() {
306+ g := o.screen.GraphicsSnapshot()
307+ if g == nil {
308+ o.emit("graphics nil")
309+ return
310+ }
311+ o.emit("graphics gen=%d usage=%+v", g.Generation(), g.Usage())
312+ for _, a := range g.Assets() {
313+ blob := a.Blob()
314+ sum := sha256.Sum256(blob.Encoded)
315+ o.emit("asset fmt=%v %dx%d px=%d sha=%x", blob.Format, blob.Width, blob.Height, blob.DecodedPixels, sum[:8])
316+ }
317+ for _, p := range g.Placements() {
318+ o.emit("placement %+v", p.Spec())
319+ }
320+ o.emit("fragments %+v", g.VisibleCellFragments(graphics.CellRect{Width: int64(o.screen.Columns()), Height: int64(o.screen.Rows())}))
321+}
322+
323+func (o *oracleRun) checkHistory(deep bool) {
324+ h := o.screen.History()
325+ if h == nil {
326+ return
327+ }
328+ o.emit("history len=%d cells=%d bytes=%d next=%d stats=%+v", h.Len(), h.Cells(), h.LogicalBytes(), h.NextRowID(), h.CompressionStats())
329+ if !deep {
330+ return
331+ }
332+ snapView := h.SnapshotView()
333+ tail, err := vt.MarshalHistoryTail(snapView)
334+ if err != nil {
335+ o.t.Fatalf("seed %d: MarshalHistoryTail: %v", o.sc.seed, err)
336+ }
337+ sum := sha256.Sum256(tail)
338+ o.emit("snapview len=%d chunks=%d tail=%x", snapView.Len(), snapView.ChunkCount(), sum)
339+ var sealed [][]byte
340+ for i := range snapView.ChunkCount() {
341+ blob, err := vt.MarshalHistoryChunk(snapView.Chunk(i))
342+ if err != nil {
343+ o.t.Fatalf("seed %d: MarshalHistoryChunk: %v", o.sc.seed, err)
344+ }
345+ sum := sha256.Sum256(blob)
346+ o.emit("chunk %d %x", i, sum)
347+ sealed = append(sealed, blob)
348+ }
349+ view := h.View()
350+ var rows []string
351+ var texts []string
352+ if err := view.Range(func(row []core.Cell) bool {
353+ rows = append(rows, formatRow(row))
354+ texts = append(texts, rowText(row))
355+ return true
356+ }); err != nil {
357+ o.t.Fatalf("seed %d: history Range: %v", o.sc.seed, err)
358+ }
359+ for i, row := range rows {
360+ o.emit("hrow %d id=%d bound=%+v %s %q", i, view.RowID(i), view.Bound(i), row, texts[i])
361+ if got := formatRow(view.Row(i)); got != row {
362+ o.t.Fatalf("seed %d: history Row(%d) differs from Range", o.sc.seed, i)
363+ }
364+ dst := make([]core.Cell, view.RowWidth(i))
365+ view.CopyRow(i, dst)
366+ if formatRow(dst) != row {
367+ o.t.Fatalf("seed %d: history CopyRow(%d) differs from Range", o.sc.seed, i)
368+ }
369+ }
370+ restored, err := vt.HistoryFromBlobs(h.Limits(), sealed, tail)
371+ if err != nil {
372+ o.t.Fatalf("seed %d: HistoryFromBlobs: %v", o.sc.seed, err)
373+ }
374+ rview := restored.View()
375+ if rview.Len() != view.Len() || restored.NextRowID() != h.NextRowID() {
376+ o.t.Fatalf("seed %d: restored history len=%d next=%d want len=%d next=%d", o.sc.seed, rview.Len(), restored.NextRowID(), view.Len(), h.NextRowID())
377+ }
378+ for i, row := range rows {
379+ if formatRow(rview.Row(i)) != row || rview.RowID(i) != view.RowID(i) || rview.Bound(i) != view.Bound(i) {
380+ o.t.Fatalf("seed %d: restored history row %d differs", o.sc.seed, i)
381+ }
382+ }
383+ transcript, err := o.screen.RecoveryTranscriptSnapshot().Marshal()
384+ if err != nil {
385+ o.t.Fatalf("seed %d: recovery transcript: %v", o.sc.seed, err)
386+ }
387+ tsum := sha256.Sum256(transcript)
388+ o.emit("transcript %x", tsum)
389+ recovered, err := vt.NewScreenWithRecoveryTranscript(o.screen.Columns(), o.screen.Rows(), h.Limits(), sealed, tail, transcript)
390+ if err != nil {
391+ o.t.Fatalf("seed %d: NewScreenWithRecoveryTranscript: %v", o.sc.seed, err)
392+ }
393+ rh := recovered.History()
394+ o.emit("recovered len=%d next=%d", rh.Len(), rh.NextRowID())
395+ if rh.Len() > 0 {
396+ rv := rh.View()
397+ o.emit("recovered last %s", formatRow(rv.Row(rv.Len()-1)))
398+ }
399+}
400+
401+func (o *oracleRun) render(step int) {
402+ s := o.screen
403+ damage := append([]core.Damage(nil), s.Damage()...)
404+ capture := s.CaptureDamage()
405+ if len(capture.Damage) != len(damage) {
406+ o.t.Fatalf("seed %d: CaptureDamage length %d want %d", o.sc.seed, len(capture.Damage), len(damage))
407+ }
408+ o.emit("damage %+v", damage)
409+
410+ if o.mirror.Width != s.Columns() || o.mirror.Height != s.Rows() {
411+ o.mirror = core.NewFrame(s.Columns(), s.Rows())
412+ }
413+ for y := range s.Rows() {
414+ for x := range s.Columns() {
415+ o.mirror.Set(x, y, s.Cell(x, y))
416+ }
417+ }
418+ var source core.CellSource = o.mirror
419+ switch o.sc.seed % 3 {
420+ case 1:
421+ source = &o.mirror
422+ case 2:
423+ source = s // generic CellSource path
424+ }
425+ prepared, err := o.ansiR.Prepare(source, damage, false)
426+ if err != nil {
427+ o.t.Fatalf("seed %d step %d: ansi Prepare: %v", o.sc.seed, step, err)
428+ }
429+ out := prepared.Bytes()
430+ o.emit("ansi %q", out)
431+ if o.r.IntN(7) == 0 {
432+ // Discarded draw: renderer must keep its committed shadow and damage
433+ // must remain pending for the next draw.
434+ o.emit("ansi discard")
435+ } else {
436+ prepared.Commit()
437+ o.applyReplay(out, step)
438+ if !s.AcknowledgeDamage(capture.Generation) {
439+ o.t.Fatalf("seed %d: acknowledge failed", o.sc.seed)
440+ }
441+ }
442+ other, err := o.ansi256.Draw(source, damage)
443+ if err != nil {
444+ o.t.Fatalf("seed %d: ansi256 Draw: %v", o.sc.seed, err)
445+ }
446+ o.emit("ansi256 %q", other)
447+
448+ snap := s.Snapshot()
449+ cur := snap.Cursor()
450+ var htmlSource core.CellSource = o.mirror
451+ if o.sc.seed%3 == 1 {
452+ htmlSource = snap
453+ }
454+ hp, err := o.htmlR.Prepare(htmlSource, damage, false, html.Cursor{Row: cur.Row, Column: cur.Col, Visible: cur.Visible, Style: html.CursorStyle(cur.Style), StyleSet: cur.StyleSet})
455+ if err != nil {
456+ o.emit("html err %v", err)
457+ return
458+ }
459+ o.emit("html %s", hp.JSON())
460+ if o.r.IntN(6) == 0 {
461+ if err := hp.Abort(); err != nil {
462+ o.t.Fatalf("seed %d: html abort: %v", o.sc.seed, err)
463+ }
464+ o.emit("html abort")
465+ } else if err := hp.Commit(); err != nil {
466+ o.t.Fatalf("seed %d: html commit: %v", o.sc.seed, err)
467+ }
468+}
469+
470+// applyReplay feeds committed ANSI output to an independent VT and compares
471+// the visible grid with the source. Mismatches are known renderer defects on
472+// the baseline (for example two scroll regions in one damage batch), so they
473+// are hashed into the transcript instead of failing: a performance change must
474+// neither add nor remove one. The replay is then resynchronized from a full
475+// draw, matching the renderer's committed belief.
476+func (o *oracleRun) applyReplay(out []byte, step int) {
477+ s := o.screen
478+ if o.replay == nil || o.replay.Columns() != s.Columns() || o.replay.Rows() != s.Rows() {
479+ o.replay = vt.NewScreen(s.Columns(), s.Rows())
480+ }
481+ o.replay.Write(out)
482+ for y := range s.Rows() {
483+ for x := range s.Columns() {
484+ want, got := s.Cell(x, y), o.replay.Cell(x, y)
485+ if want.Rune != got.Rune || want.Continuation != got.Continuation || !want.Style.Equal(got.Style) {
486+ o.replayMismatches++
487+ o.emit("replay-mismatch step %d at (%d,%d): got %s want %s", step, x, y, formatCell(got), formatCell(want))
488+ o.replay = vt.NewScreen(s.Columns(), s.Rows())
489+ full, err := ansi.New(ansi.Capabilities{}).Draw(s, []core.Damage{core.FullRedraw()})
490+ if err != nil {
491+ o.t.Fatalf("seed %d: resync draw: %v", o.sc.seed, err)
492+ }
493+ o.replay.Write(full)
494+ return
495+ }
496+ }
497+ }
498+}
499+
500+func runOracleScenario(t *testing.T, idx int) string {
501+ sc := oracleScenarioFor(idx)
502+ o := &oracleRun{t: t, sc: sc, r: rand.New(rand.NewPCG(sc.seed, 0xdecaf)), h: sha256.New()}
503+ if *oracleDump != "" {
504+ if err := os.MkdirAll(*oracleDump, 0o755); err != nil {
505+ t.Fatal(err)
506+ }
507+ f, err := os.Create(filepath.Join(*oracleDump, fmt.Sprintf("scenario-%02d.txt", idx)))
508+ if err != nil {
509+ t.Fatal(err)
510+ }
511+ defer f.Close()
512+ o.dump = bufio.NewWriter(f)
513+ defer o.dump.Flush()
514+ }
515+ if sc.history != nil {
516+ o.screen = vt.NewScreenWithHistory(sc.cols, sc.rows, *sc.history)
517+ } else {
518+ o.screen = vt.NewScreen(sc.cols, sc.rows)
519+ }
520+ o.screen.OnResponse = func(b []byte) { o.events = append(o.events, fmt.Sprintf("response %q", b)) }
521+ o.screen.OnBell = func() { o.events = append(o.events, "bell") }
522+ if sc.evictHook {
523+ // The callback owns each row: retain it, verify it is never rewritten by
524+ // later evictions, then scribble on it to prove VT kept its own copy.
525+ var retained []core.Cell
526+ var retainedDigest string
527+ o.screen.OnLineEvicted = func(row []core.Cell) {
528+ if retained != nil && formatRow(retained) != retainedDigest {
529+ t.Fatalf("seed %d: evicted row storage was reused after the callback", sc.seed)
530+ }
531+ digest := formatRow(row)
532+ o.events = append(o.events, "evicted "+digest+" "+rowText(row))
533+ if prev := retained; prev != nil {
534+ for i := range prev {
535+ prev[i] = core.Cell{Rune: 'X'}
536+ }
537+ }
538+ retained, retainedDigest = row, digest
539+ }
540+ }
541+ o.ansiR = ansi.New(ansi.Capabilities{SynchronizedOutput: idx%2 == 0})
542+ o.ansi256 = ansi.NewWithColorProfile(ansi.Capabilities{}, ansi.ColorProfileANSI256)
543+ var err error
544+ o.htmlR, err = html.New(html.Options{})
545+ if err != nil {
546+ t.Fatal(err)
547+ }
548+ var historyConfig vt.HistoryConfig
549+ if sc.history != nil {
550+ historyConfig = *sc.history
551+ }
552+ o.emit("scenario %d %dx%d history=%+v byteSplit=%t evictHook=%t", idx, sc.cols, sc.rows, historyConfig, sc.byteSplit, sc.evictHook)
553+
554+ var digests []string
555+ for step := range oracleSteps {
556+ o.maybeResize(step)
557+ chunk := o.genChunk()
558+ o.emit("step %d write %q", step, chunk)
559+ o.write(chunk)
560+ for _, e := range o.events {
561+ o.emit("%s", e)
562+ }
563+ o.events = o.events[:0]
564+ o.render(step)
565+ if step%oracleCheckEvery == oracleCheckEvery-1 {
566+ o.checkScreen()
567+ o.checkGraphics()
568+ o.checkHistory(step%(2*oracleCheckEvery) == 2*oracleCheckEvery-1)
569+ digests = append(digests, fmt.Sprintf("%02d/%03d %s", idx, step, hex.EncodeToString(o.h.Sum(nil))[:24]))
570+ }
571+ }
572+ digests = append(digests, fmt.Sprintf("%02d replay-mismatches=%d", idx, o.replayMismatches))
573+ return strings.Join(digests, "\n")
574+}
575+
576+func TestDifferentialOracle(t *testing.T) {
577+ if testing.Short() {
578+ t.Skip("differential oracle skipped in -short mode")
579+ }
580+ got := make([]string, oracleScenarios)
581+ t.Run("scenarios", func(t *testing.T) {
582+ for i := range oracleScenarios {
583+ t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
584+ t.Parallel()
585+ got[i] = runOracleScenario(t, i)
586+ })
587+ }
588+ })
589+ if t.Failed() {
590+ return
591+ }
592+ text := strings.Join(got, "\n") + "\n"
593+ if *oracleUpdate {
594+ if err := os.MkdirAll(filepath.Dir(oracleGolden), 0o755); err != nil {
595+ t.Fatal(err)
596+ }
597+ if err := os.WriteFile(oracleGolden, []byte(text), 0o644); err != nil {
598+ t.Fatal(err)
599+ }
600+ return
601+ }
602+ want, err := os.ReadFile(oracleGolden)
603+ if err != nil {
604+ t.Fatalf("read golden (run with -oracle.update on a trusted baseline): %v", err)
605+ }
606+ if string(want) == text {
607+ return
608+ }
609+ wantLines, gotLines := strings.Split(string(want), "\n"), strings.Split(text, "\n")
610+ for i := range min(len(wantLines), len(gotLines)) {
611+ if wantLines[i] != gotLines[i] {
612+ t.Fatalf("oracle diverged at checkpoint %q (want %q); rerun with -oracle.dump=DIR on both trees and diff the scenario transcript", gotLines[i], wantLines[i])
613+ }
614+ }
615+ t.Fatalf("oracle checkpoint count differs: got %d want %d", len(gotLines), len(wantLines))
616+}
A testdata/oracle/golden.txt +468 -0
@@ -0,0 +1,468 @@
1+00/019 55a72c4c8d1c1a0e00550a15
2+00/039 4beaddfc77e4110f80524b38
3+00/059 a5c2085be8baec4f551edd92
4+00/079 e0ab87b780d0639fe8b96d12
5+00/099 b079ef26a0bb72ced471f499
6+00/119 f1ab642fcb4bfd4b3a7a03e6
7+00/139 27629663856dfa65244e102d
8+00/159 b3aaf605acd5327d41968195
9+00/179 9bb85c80ee152ab540d7ddab
10+00/199 6b4469404f496d823ad9e125
11+00/219 729e470dc8268f158fd3fe65
12+00/239 4c7ace24a712cd597c20d2e4
13+00 replay-mismatches=1
14+01/019 92d98f04143705d463e00d5a
15+01/039 0c887b0db21e564acbeb140b
16+01/059 4a1d21d32fb2e2dbb8a0a048
17+01/079 8c494398433b3f3292105b68
18+01/099 878ad5680b0b65efb2788c78
19+01/119 eba0201db6d4a0a544c7bc26
20+01/139 c366adc9bda774bbe629aa7f
21+01/159 82c5bf666042fdf83dea91dc
22+01/179 6c8e00e2c5851df5fc4d4531
23+01/199 e02a755cc1869e5c2135b076
24+01/219 d641dd26646f5cd455575cb9
25+01/239 7b59cd0114a7b8a5d044dd5b
26+01 replay-mismatches=1
27+02/019 3e82372b6cb2ef730fbd402c
28+02/039 398deed5e521d0788101775d
29+02/059 21401dc4b8bee3c8c6dce2ff
30+02/079 22edd985876e2267fd4a6896
31+02/099 e6c6c10fcee0f3be94cdc63e
32+02/119 4cc31105a1fd3951dbc19e78
33+02/139 ff2da75e170b01030791f703
34+02/159 ee1b1a0baa2e690f004bac7c
35+02/179 70044ab48fec67d54592058a
36+02/199 73e6310d3e4c2a02a0aac70f
37+02/219 38ffab48e284151fed6243a1
38+02/239 dee6444cff6f5bdda9f190a1
39+02 replay-mismatches=0
40+03/019 23e37ad503fc5be744f00967
41+03/039 ec34adbb74097df59cc75e32
42+03/059 85c664d4a3ad1d484c5118b3
43+03/079 902369e3cf92b7cada2e9f20
44+03/099 008d777b9692c94433ca31e0
45+03/119 83d72044067f5276a4f75a4b
46+03/139 0cdea02f941f932699118d8e
47+03/159 dfd6d2abbad3a1be1e3d6ecc
48+03/179 99e59e60677d6d5ec88631a0
49+03/199 8fe8fc36ce8901c3bf214f23
50+03/219 20b7f922c656a2e197d4589e
51+03/239 045ae146ae096692a82f7211
52+03 replay-mismatches=1
53+04/019 4faa96c672855d38d5f009b9
54+04/039 45daf6da341e0be752176143
55+04/059 b210cfeb355ca92703eb5a7d
56+04/079 567e6224445b47f639d67406
57+04/099 36dfe5e3817a756a77610280
58+04/119 28f266be513510780e5298a0
59+04/139 77159aaa349724f6a3468083
60+04/159 454932ee14333813f68147b2
61+04/179 5dc2da5983a46d4d8c045463
62+04/199 33b567344c5ba4cbb11073b7
63+04/219 98d41d64e511dc4f992f9ddd
64+04/239 a873589f7cbf01ab97712424
65+04 replay-mismatches=0
66+05/019 68d508ff98ffe9c97d491512
67+05/039 22833aa6ac5590fd5c51a10c
68+05/059 f37316a5530d28ea4d386599
69+05/079 72697ed3d723a5e4e2bf127e
70+05/099 b8128a9baa2e24f9cc2b10d0
71+05/119 4f7d25dbe1b50ecf1ff67901
72+05/139 c269b4f1d67c5238d12fabc0
73+05/159 1ebdd92e38aeadf77e3d6c72
74+05/179 ffc342703ae6c3a8c4cd9f57
75+05/199 cb964d13d6ed459f435eca69
76+05/219 945a552afa3fc038c0d0d49d
77+05/239 bb39be7fce9c2c1f3a7dd6a3
78+05 replay-mismatches=0
79+06/019 9f70eb84e027a0da12cf789e
80+06/039 fd1b4d60360d797d01dcf980
81+06/059 e957cf36ed0da231cc781732
82+06/079 ef5a1e45228ec8d02b4339fb
83+06/099 8aee61a050bf383a74a62a8b
84+06/119 b63b5ac60fb59413621ca130
85+06/139 08d32c2a7d6a3d47faf8ad7e
86+06/159 ae180c731417adc39a3ac92e
87+06/179 50ecc3cc8de94af4fb6256e6
88+06/199 bcad1567aa6b13757337ca65
89+06/219 6d9e62599ea93901fbb98bda
90+06/239 12ad1caedd5346ab013bd936
91+06 replay-mismatches=0
92+07/019 771e7f8f79ce9d77f0bb56ff
93+07/039 031ed4ce8ac59a36ac7c1365
94+07/059 2516663f39582c63aa218218
95+07/079 3cd9f1f67cef906806422bdd
96+07/099 c3709b3f1a08717fc304d159
97+07/119 9ca36d78bfa961ac711edb07
98+07/139 fea50cc9d78951ce47ab7b5c
99+07/159 ac33e3c1365cc8c449a0d77b
100+07/179 92874af432d8106e7d39dd73
101+07/199 6a9cdfd7f92ce4e2fc62bc5d
102+07/219 b3f6403504ede15a9d1f283b
103+07/239 a38515a32d34854dc6e2f4f7
104+07 replay-mismatches=1
105+08/019 29e2816bc5d015ce2d23c05c
106+08/039 2a86fe7ff64d840cd8569737
107+08/059 51239ba60dc3b8f1b5078c45
108+08/079 abb48c671fe1388ddd3dbe91
109+08/099 bcfe3784a4f3cbe24d417126
110+08/119 039ca7a36109843550b4bbe2
111+08/139 0e6735e9400e34df32a24bc6
112+08/159 4cdce3dbee4325befd425faf
113+08/179 005758f8650f3cec82710cfe
114+08/199 52482d634ba2b06bf34693dd
115+08/219 1b3e53ed97faccd2061b9e26
116+08/239 c4ea1ea89908641b93e41266
117+08 replay-mismatches=0
118+09/019 4c119ea37ab5f5aaf7ab50f3
119+09/039 a1efea7a141d9b5481317d21
120+09/059 a0cce6141ccb877fe5e71f53
121+09/079 614e9de8720ed8292a34242d
122+09/099 fb69e78cf4ccaeba35c42a8a
123+09/119 52f16f33c2b608d3d6b59394
124+09/139 b9f02f6852f5de3b13640508
125+09/159 72b12cd005097c0ba898111c
126+09/179 06937b4778221356408d2ed2
127+09/199 369339d7370f38fcc22a12b9
128+09/219 b9eec1e382e0500d5f1edde7
129+09/239 c99a2183f2174d1e39502356
130+09 replay-mismatches=0
131+10/019 f1b345ef33635fe6028f81b4
132+10/039 bb0ee5ddb6ba0461e8747c48
133+10/059 7b0a1d3c4fcef2b20317695f
134+10/079 9d002061162fa4fab0050bcd
135+10/099 3878530870a8fb56b8328017
136+10/119 620d17d70aa0ae092bfe7fe0
137+10/139 5d8a36f80fa538d941383259
138+10/159 4f464ea644caecec04f35afc
139+10/179 c60434e2aa9d7a64724ca147
140+10/199 b9f0bdae9c7a31f6ca8bcbe6
141+10/219 14e56199cd1442fc6793d65b
142+10/239 8c43ab2d5bde3b2fda977ddd
143+10 replay-mismatches=0
144+11/019 e3eb02d62746d50757d63d18
145+11/039 070fa14e017eb9ba3056672d
146+11/059 8b016d3845f5d47e91b1826b
147+11/079 75ce3f04d3082bc4788d6c96
148+11/099 2b7cf678237f6988de3396a3
149+11/119 230ebcd9a41d473aaed1896e
150+11/139 a0473a1fc49d7c12f20b41be
151+11/159 b9ef5849eaeefbe486770b76
152+11/179 b3c43da50034693c385b844b
153+11/199 7145d045e0724186ce73059a
154+11/219 ee6f5fc91e84ee8214060439
155+11/239 f4307d6e12b1cd2db37a8032
156+11 replay-mismatches=0
157+12/019 4fade3921f5735064355eec1
158+12/039 e2aab8e88f7241073e73eb05
159+12/059 2b1492eeb901a30dddcecfea
160+12/079 809ab3f1339fe2c10da81dc6
161+12/099 09759c1960ee25243130cec7
162+12/119 d40b61390be6b1ade0fc9798
163+12/139 dd23c4487248a1236597baf8
164+12/159 6ccc55f91c960571ebc0f2dc
165+12/179 2165c2c3b2a0fd7a597c1251
166+12/199 679db69d3aee6356dafac627
167+12/219 eff11bcec70127843617c651
168+12/239 af6d8b16a1eb290229dd6000
169+12 replay-mismatches=1
170+13/019 ef8a3432b61607340356a764
171+13/039 4e823b712c6aab07bcd5764e
172+13/059 90b7e616d2c528faeee0f72b
173+13/079 b1330a7079b795346ee9e317
174+13/099 9f0d10089131edf34cceb0a1
175+13/119 069d5dc52bcfba2eaa2211bd
176+13/139 08b97ef26faddb637a424bef
177+13/159 c29b028480ce1c097e8c3066
178+13/179 85d6cc0d0267ce57c4f438d7
179+13/199 84a14ed35e312f14e0c4d220
180+13/219 8382c4d6025f22cddd967535
181+13/239 4e3ac44cff6dfa7d7a843378
182+13 replay-mismatches=0
183+14/019 747d54cdf75a492242809318
184+14/039 e6530a9282bd6095e15a9677
185+14/059 715de56023e1afc30c1feea1
186+14/079 f5c6253e95d471ccf7892526
187+14/099 09063bb10e3e19d067c9c85c
188+14/119 f8753c5d5010cd747e5dcc31
189+14/139 8a726baa028acc349f76fcb5
190+14/159 6707de5fd29ec469ba8913b9
191+14/179 3914c9045ccb2d16f6dfb386
192+14/199 f5992df3d18c885bcaab4496
193+14/219 38e1a9fe90723b701c438464
194+14/239 c90fb86719f7e4613e3720c2
195+14 replay-mismatches=0
196+15/019 95e07748c709fa34a80ceff7
197+15/039 4186ba13b89b2952cb8407b1
198+15/059 e113b092eeaeb7ba23f36f0b
199+15/079 c9ad53bbc38faf68af448dbe
200+15/099 6207b08ba91d6e0c93f27540
201+15/119 edffb952f657ba998d07de73
202+15/139 5c874878a9a518b08627f52d
203+15/159 e7c63b49c6f3ebb122ab821b
204+15/179 edb533ed1dcf2d238f7db6a9
205+15/199 80fa50afb7b21ddcc958e13e
206+15/219 f602f7cb0510d54ab8c94ff9
207+15/239 3ac7cd4affba36e038a2254b
208+15 replay-mismatches=2
209+16/019 4314babccb7675abd6328c32
210+16/039 3e6ee1b339dab4ff5e0c8b34
211+16/059 1afd8504d6a0be4d6cc9c3c3
212+16/079 50f71b7a9d7c444810cf1ba6
213+16/099 65a1c2e6e41adcddbc510efc
214+16/119 3f7c33fe85ccde2210b37f8f
215+16/139 1b47397842763e30313e66de
216+16/159 3d9b4904b96244df64051cc7
217+16/179 60ae20b4cabf7c1a02192010
218+16/199 210f0c4ad73131d2b465b362
219+16/219 4b62676c69410e9b6f9d8f26
220+16/239 72f70f009e426f88b33ebdae
221+16 replay-mismatches=0
222+17/019 1a06602479d8e795ee95c965
223+17/039 c7e7eb54ee63d9e7e23ceaca
224+17/059 51c34ba1a80d55b1f9ae42fa
225+17/079 d5f10d70e10e0e87e54ac6f3
226+17/099 0a3784787eb2a97cce6c8309
227+17/119 7ccd8bedfbf26111e264baa5
228+17/139 f028c427efd1ce9c0b9828d1
229+17/159 fb8b3eb57ac4186345d52918
230+17/179 1c7da7757137c15d7a05d3d4
231+17/199 55b35801df62b3250963613d
232+17/219 1cb11bbb2c5dd8995094be53
233+17/239 6a146a19d7c534c4766e98f8
234+17 replay-mismatches=0
235+18/019 a1d2576637f54892892ea0b4
236+18/039 4415a2923a28b75a5aa0dbfb
237+18/059 2bac49c23f1c09f170c68dc2
238+18/079 eeebf8f57f35ee0b110fc200
239+18/099 e3673a14664b853fa9613c64
240+18/119 57aa1250c9bb46869c8fc1a7
241+18/139 abaca7067643db7db4dfe105
242+18/159 6e878b08b5635eac7ab9816c
243+18/179 031a000b34a4ba3bc6cedc53
244+18/199 c523b55eb720c065fc6134ac
245+18/219 0f367277325b630ce5bc634f
246+18/239 e04b873c6820a46bfd2f4873
247+18 replay-mismatches=0
248+19/019 2db955914f2e068e72cf8dfa
249+19/039 ec344560f2bb7de5f25fdb78
250+19/059 d1a41ae321f1449d256cf19d
251+19/079 c0d8a7ed784bd9d278c61bfa
252+19/099 f91e7a807eea1aac3f05da1f
253+19/119 36d47e23f93682f8f8bd720b
254+19/139 b227a962815dfd349d06722f
255+19/159 5552419605856137ee5e3384
256+19/179 0c7cf6585dcbaa42d05758c1
257+19/199 ebcee2469216bd045f79d9a7
258+19/219 d5bdec7e45121f8f757c12df
259+19/239 ae4abbf3362cbcba26878acd
260+19 replay-mismatches=1
261+20/019 3c776bd6d460be5f6bcd060e
262+20/039 ef65b757bd137499a352d5ae
263+20/059 1cfcab4ffbc08c619e97bcd5
264+20/079 8879e2856d1a555f07aa60d7
265+20/099 e16919d7e8c9f20f7dfa5956
266+20/119 cb7f9cf61ef176f01706ac77
267+20/139 f9edefe5603a815455b563ff
268+20/159 cf8862d058a774e0fdb18628
269+20/179 82daa9b9d7f4edc82a424a4a
270+20/199 fe78853df2a403308904f8e2
271+20/219 0895c29599a30db493020dd2
272+20/239 0e2310234476bea5dbb44cf0
273+20 replay-mismatches=1
274+21/019 357ece3eab75324d3823a6a4
275+21/039 fe4501bedc7017bc40ff0509
276+21/059 c97dc13dbcd996b58361777e
277+21/079 478d3efcfcca35aac31016e5
278+21/099 3311dde300dcf85626edb8b7
279+21/119 67ff6fbebbb9976f9f5191d8
280+21/139 034216a3e7d38b221d5a8811
281+21/159 adc62b539970ad3123cb4175
282+21/179 0323505afc602891ba38d7ac
283+21/199 577472eed259f3abc0aaa526
284+21/219 5d1dc3faa423707ac2ca4ba7
285+21/239 068abdef9483e1760da84d41
286+21 replay-mismatches=1
287+22/019 6fc626ac7796049f3457a9a7
288+22/039 e9214d74742e6ab1a1c8baf0
289+22/059 72e63b2a0100e4153851a23d
290+22/079 9b1a8ebe1c432db2fedb68e3
291+22/099 924e3f255fb856ff7b7dc8ed
292+22/119 53fcd1df652d602fe412c5a0
293+22/139 7414a5f8fdbd506ab15f698d
294+22/159 bc2dd2281c12435224135073
295+22/179 dfeef83df0e865e875132be1
296+22/199 9fe1387f10258c696fd9e783
297+22/219 3009560abe632e04450f7f41
298+22/239 fe8d129db62a73ae9b9650bf
299+22 replay-mismatches=0
300+23/019 20cb94d03695ad09763e03d2
301+23/039 6c18ee020345a7699fd75494
302+23/059 f85c5ecebab82f1126f3c964
303+23/079 2fbeda367e9d56072d5b0edc
304+23/099 307896059fd55149f9d7c7e3
305+23/119 73bafc1697a5e247b2d3c75f
306+23/139 214cbbca9ce4b1d0c98ea569
307+23/159 82259d749685c0f294be858f
308+23/179 8d7b14f89c5f5d3113fbb3d2
309+23/199 ddee2d89e0897e90f9a6df78
310+23/219 65e4440830b9f98e48fe3a11
311+23/239 e8ca9cf0699b2c6333e0cb3e
312+23 replay-mismatches=0
313+24/019 b7fdbacf57cb0af4d47225ba
314+24/039 2a75c514d5b6525aaae38b3c
315+24/059 25cfe1b7ea38dca8b287dfcb
316+24/079 f26ad4606c3aa14fdd999af4
317+24/099 3e2f4c94e77ee245d7b1feb2
318+24/119 17ea49879ca006a1a9cc6f7a
319+24/139 90243c3b758308f0dd7982d3
320+24/159 c734adf4c419ccaccb7b344b
321+24/179 f4f0b4818a87d2d735361f5b
322+24/199 16acbd765f02caece9941217
323+24/219 a4605530cb75474769714cf4
324+24/239 ea3ffdb6ec8b58fe625b928a
325+24 replay-mismatches=1
326+25/019 eb73f8f3bb46422a6a12fc8f
327+25/039 823c96945f71196e67f10f65
328+25/059 c04f006b9f600ecd075cec6a
329+25/079 a21fe1e939c9fa2ddbc2d385
330+25/099 c20fca1d42014ae0701d6d0b
331+25/119 3b6b3af160572b8ebc24277f
332+25/139 e6151bf8cfffc442059ea129
333+25/159 5209600fc448f453bd687cb2
334+25/179 043175bc5eea52c09fb8f884
335+25/199 4b7c1cd9781ec41bf596b764
336+25/219 ad62688c776574ab5c442de7
337+25/239 22e025d483651540c55002c6
338+25 replay-mismatches=0
339+26/019 122c0cce3cc0ece13ce6f89a
340+26/039 0999ef9b5da72bc75cef9d49
341+26/059 2efdbf6da4b726f10f2878d1
342+26/079 f899906f635ddb86d5c73984
343+26/099 46367bf24897cad56dd5235c
344+26/119 a97b04bc18332773595951da
345+26/139 c5ae15a2b57146ef155e27b9
346+26/159 51ab1eaae8be298680367404
347+26/179 e35bdd23c19b6295280181ed
348+26/199 5f21c27026ba98b0a9d5d51c
349+26/219 2fcb984adf0b347e0da88d33
350+26/239 200f40ed44f86ce04857c1ef
351+26 replay-mismatches=0
352+27/019 ee2c010c41b9f0e5f70ce2b3
353+27/039 91ab08fb270466abbfb06c54
354+27/059 2e73fe488420a5de3866e7e3
355+27/079 a72a80db0e8ee9bffb891b38
356+27/099 917cde1321cfce583a63d9f2
357+27/119 b8056ec6e08df1e2f7033b6a
358+27/139 22214c1a34de027d97f18ad9
359+27/159 18debaf4cd5dfd2bb665bf34
360+27/179 b8c41633f2ea27468edc411a
361+27/199 85bc90436e42fca40bd64d3c
362+27/219 f0dbe3da68c8dd144c3dd9b7
363+27/239 214002b3d936e2fe396aae01
364+27 replay-mismatches=0
365+28/019 3ee1c2f031af3ad67ae50ebc
366+28/039 07508b3af21ec20fadeefe55
367+28/059 dfa63544d3d6a817a9212067
368+28/079 1855612e92d140c47b8abd1d
369+28/099 bcb387773cfd286bf68b7b7d
370+28/119 1f742786b7a0328e4e513984
371+28/139 491e0249e093af7d1fa703e3
372+28/159 7e72e1e6ffbd2f8191ff7116
373+28/179 daa22f39fc629726df0fd80c
374+28/199 ad54c46914aa19908c0a9880
375+28/219 54e180efdd3796a13c1c7670
376+28/239 8f0e0e94c590745fc2dc7678
377+28 replay-mismatches=1
378+29/019 72e981fc506bfdd3bbf15c37
379+29/039 8c46bcf213fe0e9cf159a7d2
380+29/059 213c8dd3d5bcda3abb11ab05
381+29/079 5c42ca0bf2bdde3f4e8beb0e
382+29/099 9da53eccca7406686de96ec0
383+29/119 c91f989577473d295e74d700
384+29/139 6d5ff6bc1b1b73cae023806b
385+29/159 a1e1bfef4df06dc65e727a5e
386+29/179 afb819e69c5c53d51d49d446
387+29/199 78df663b24d8507a469a7959
388+29/219 e40592fde61b5e14bf73242a
389+29/239 9500a168ffd9142d6bdbde5a
390+29 replay-mismatches=0
391+30/019 ea74787caee42ed3665575ed
392+30/039 91b5e5afa5d3bb06b087f6ad
393+30/059 ab536abe78f0cf27cdf94a75
394+30/079 cb0e90a99459de9ed8493a87
395+30/099 24cda2b6890297d9254603d1
396+30/119 dc93cdf4c2014d22ac4d1ef2
397+30/139 66e5417af5336b68e70cfa14
398+30/159 c10f3ab9c58aa3f43caa4f94
399+30/179 a7d6695e7add8ed6e7912161
400+30/199 f1fe2d3081c9ccfc7413e3a9
401+30/219 5c1dad4fffc15226a5eebe51
402+30/239 966ff4c469452f7951326c5e
403+30 replay-mismatches=0
404+31/019 869af9273a068555a7c65e06
405+31/039 af9002bdf228a2e1f034df74
406+31/059 375fd592eea7fdb3ce4a7563
407+31/079 8d6f8e9a242c0da7a8707d06
408+31/099 f4e05fd0482cb95b1141b933
409+31/119 0499dd21ead9843ec3668589
410+31/139 8fab9f1300b2d8c6757a9587
411+31/159 e5ea9bef6d0ff097b0b0f00f
412+31/179 2d21760a2b468c1f8e101b86
413+31/199 1c76b93d064c496485782148
414+31/219 8a23244f3e0b25b774aaabbb
415+31/239 6b47e6b3998cf340d47f2dc9
416+31 replay-mismatches=0
417+32/019 ed11b0c6e02dfce865d49934
418+32/039 bd3bbe698e10adf1f5fbc490
419+32/059 fb530e83cb69ec498ccd32a7
420+32/079 f54210cbb277563ac36c611d
421+32/099 9f2557fef0b33c2da3b1e22b
422+32/119 4b02e8f56434d83e8f5234f6
423+32/139 39feab897a9120d585c20a8e
424+32/159 d0953ad716ea690de47dfc3d
425+32/179 f6a7b77c5991d37449e779b1
426+32/199 2c188342e677e30c8ffe7f9e
427+32/219 7fc717e4fbfd57748ab342e3
428+32/239 506a3d22b3e2ff760873e0fb
429+32 replay-mismatches=0
430+33/019 75e34d5589c8f836a06d090e
431+33/039 372c230667e378656b2af4ff
432+33/059 938b86084dbb06afa4454b27
433+33/079 7fa7265281d43e73b797d780
434+33/099 ac83290b711b8dec3003df47
435+33/119 021ac2bab909abdd6e5c77b5
436+33/139 ed69b8d8881a85100d32c4cd
437+33/159 c1566ad91ec422c65923fe34
438+33/179 9275113b9df0b001112ad573
439+33/199 a269fac1fd59ac746cdcc3ed
440+33/219 8efc32f7b432b011f6862afb
441+33/239 360719a27cebdd0863b2e775
442+33 replay-mismatches=0
443+34/019 93dcae1fc2bfd6779aff1aff
444+34/039 e4c849c2eb75db26ab3e4a69
445+34/059 f44f95efd2e73205143c656c
446+34/079 500ac8d59cbff22ad8cedf95
447+34/099 8cde09d8fb843f0771e27c68
448+34/119 8888149eafa436929ba9002f
449+34/139 9c9180251a5d41fb6bb84e0e
450+34/159 a8c9c943771f1bac64729951
451+34/179 147dc1d5ce1aa53965747a76
452+34/199 d60cdab9e926b1a975ab7c68
453+34/219 70311db1c213b30d8622a677
454+34/239 16c8a61f803eef432762550b
455+34 replay-mismatches=0
456+35/019 6ce6075f205b75f864a11ca6
457+35/039 8375248e74a353fc4ec62d73
458+35/059 794e546c0d3346dc3965043f
459+35/079 f4a4a12dd0c59cfa3a7036b4
460+35/099 a424a229392a35da8d937062
461+35/119 28415d77a2772ddc9ff46361
462+35/139 b142770cfec4946c445f1925
463+35/159 a2c6d28bb5442fa733cf8ea3
464+35/179 0c4f1446420ee3280f18090c
465+35/199 d8ab4d4487ea3bb519574059
466+35/219 f3f1d026b78975accc285f57
467+35/239 a734eb4db0f69306dc7a5630
468+35 replay-mismatches=1
@@ -0,0 +1,468 @@
1+00/019 55a72c4c8d1c1a0e00550a15
2+00/039 4beaddfc77e4110f80524b38
3+00/059 a5c2085be8baec4f551edd92
4+00/079 e0ab87b780d0639fe8b96d12
5+00/099 b079ef26a0bb72ced471f499
6+00/119 f1ab642fcb4bfd4b3a7a03e6
7+00/139 27629663856dfa65244e102d
8+00/159 b3aaf605acd5327d41968195
9+00/179 9bb85c80ee152ab540d7ddab
10+00/199 6b4469404f496d823ad9e125
11+00/219 729e470dc8268f158fd3fe65
12+00/239 4c7ace24a712cd597c20d2e4
13+00 replay-mismatches=1
14+01/019 92d98f04143705d463e00d5a
15+01/039 0c887b0db21e564acbeb140b
16+01/059 4a1d21d32fb2e2dbb8a0a048
17+01/079 8c494398433b3f3292105b68
18+01/099 878ad5680b0b65efb2788c78
19+01/119 eba0201db6d4a0a544c7bc26
20+01/139 c366adc9bda774bbe629aa7f
21+01/159 82c5bf666042fdf83dea91dc
22+01/179 6c8e00e2c5851df5fc4d4531
23+01/199 e02a755cc1869e5c2135b076
24+01/219 d641dd26646f5cd455575cb9
25+01/239 7b59cd0114a7b8a5d044dd5b
26+01 replay-mismatches=1
27+02/019 3e82372b6cb2ef730fbd402c
28+02/039 398deed5e521d0788101775d
29+02/059 21401dc4b8bee3c8c6dce2ff
30+02/079 22edd985876e2267fd4a6896
31+02/099 e6c6c10fcee0f3be94cdc63e
32+02/119 4cc31105a1fd3951dbc19e78
33+02/139 ff2da75e170b01030791f703
34+02/159 ee1b1a0baa2e690f004bac7c
35+02/179 70044ab48fec67d54592058a
36+02/199 73e6310d3e4c2a02a0aac70f
37+02/219 38ffab48e284151fed6243a1
38+02/239 dee6444cff6f5bdda9f190a1
39+02 replay-mismatches=0
40+03/019 23e37ad503fc5be744f00967
41+03/039 ec34adbb74097df59cc75e32
42+03/059 85c664d4a3ad1d484c5118b3
43+03/079 902369e3cf92b7cada2e9f20
44+03/099 008d777b9692c94433ca31e0
45+03/119 83d72044067f5276a4f75a4b
46+03/139 0cdea02f941f932699118d8e
47+03/159 dfd6d2abbad3a1be1e3d6ecc
48+03/179 99e59e60677d6d5ec88631a0
49+03/199 8fe8fc36ce8901c3bf214f23
50+03/219 20b7f922c656a2e197d4589e
51+03/239 045ae146ae096692a82f7211
52+03 replay-mismatches=1
53+04/019 4faa96c672855d38d5f009b9
54+04/039 45daf6da341e0be752176143
55+04/059 b210cfeb355ca92703eb5a7d
56+04/079 567e6224445b47f639d67406
57+04/099 36dfe5e3817a756a77610280
58+04/119 28f266be513510780e5298a0
59+04/139 77159aaa349724f6a3468083
60+04/159 454932ee14333813f68147b2
61+04/179 5dc2da5983a46d4d8c045463
62+04/199 33b567344c5ba4cbb11073b7
63+04/219 98d41d64e511dc4f992f9ddd
64+04/239 a873589f7cbf01ab97712424
65+04 replay-mismatches=0
66+05/019 68d508ff98ffe9c97d491512
67+05/039 22833aa6ac5590fd5c51a10c
68+05/059 f37316a5530d28ea4d386599
69+05/079 72697ed3d723a5e4e2bf127e
70+05/099 b8128a9baa2e24f9cc2b10d0
71+05/119 4f7d25dbe1b50ecf1ff67901
72+05/139 c269b4f1d67c5238d12fabc0
73+05/159 1ebdd92e38aeadf77e3d6c72
74+05/179 ffc342703ae6c3a8c4cd9f57
75+05/199 cb964d13d6ed459f435eca69
76+05/219 945a552afa3fc038c0d0d49d
77+05/239 bb39be7fce9c2c1f3a7dd6a3
78+05 replay-mismatches=0
79+06/019 9f70eb84e027a0da12cf789e
80+06/039 fd1b4d60360d797d01dcf980
81+06/059 e957cf36ed0da231cc781732
82+06/079 ef5a1e45228ec8d02b4339fb
83+06/099 8aee61a050bf383a74a62a8b
84+06/119 b63b5ac60fb59413621ca130
85+06/139 08d32c2a7d6a3d47faf8ad7e
86+06/159 ae180c731417adc39a3ac92e
87+06/179 50ecc3cc8de94af4fb6256e6
88+06/199 bcad1567aa6b13757337ca65
89+06/219 6d9e62599ea93901fbb98bda
90+06/239 12ad1caedd5346ab013bd936
91+06 replay-mismatches=0
92+07/019 771e7f8f79ce9d77f0bb56ff
93+07/039 031ed4ce8ac59a36ac7c1365
94+07/059 2516663f39582c63aa218218
95+07/079 3cd9f1f67cef906806422bdd
96+07/099 c3709b3f1a08717fc304d159
97+07/119 9ca36d78bfa961ac711edb07
98+07/139 fea50cc9d78951ce47ab7b5c
99+07/159 ac33e3c1365cc8c449a0d77b
100+07/179 92874af432d8106e7d39dd73
101+07/199 6a9cdfd7f92ce4e2fc62bc5d
102+07/219 b3f6403504ede15a9d1f283b
103+07/239 a38515a32d34854dc6e2f4f7
104+07 replay-mismatches=1
105+08/019 29e2816bc5d015ce2d23c05c
106+08/039 2a86fe7ff64d840cd8569737
107+08/059 51239ba60dc3b8f1b5078c45
108+08/079 abb48c671fe1388ddd3dbe91
109+08/099 bcfe3784a4f3cbe24d417126
110+08/119 039ca7a36109843550b4bbe2
111+08/139 0e6735e9400e34df32a24bc6
112+08/159 4cdce3dbee4325befd425faf
113+08/179 005758f8650f3cec82710cfe
114+08/199 52482d634ba2b06bf34693dd
115+08/219 1b3e53ed97faccd2061b9e26
116+08/239 c4ea1ea89908641b93e41266
117+08 replay-mismatches=0
118+09/019 4c119ea37ab5f5aaf7ab50f3
119+09/039 a1efea7a141d9b5481317d21
120+09/059 a0cce6141ccb877fe5e71f53
121+09/079 614e9de8720ed8292a34242d
122+09/099 fb69e78cf4ccaeba35c42a8a
123+09/119 52f16f33c2b608d3d6b59394
124+09/139 b9f02f6852f5de3b13640508
125+09/159 72b12cd005097c0ba898111c
126+09/179 06937b4778221356408d2ed2
127+09/199 369339d7370f38fcc22a12b9
128+09/219 b9eec1e382e0500d5f1edde7
129+09/239 c99a2183f2174d1e39502356
130+09 replay-mismatches=0
131+10/019 f1b345ef33635fe6028f81b4
132+10/039 bb0ee5ddb6ba0461e8747c48
133+10/059 7b0a1d3c4fcef2b20317695f
134+10/079 9d002061162fa4fab0050bcd
135+10/099 3878530870a8fb56b8328017
136+10/119 620d17d70aa0ae092bfe7fe0
137+10/139 5d8a36f80fa538d941383259
138+10/159 4f464ea644caecec04f35afc
139+10/179 c60434e2aa9d7a64724ca147
140+10/199 b9f0bdae9c7a31f6ca8bcbe6
141+10/219 14e56199cd1442fc6793d65b
142+10/239 8c43ab2d5bde3b2fda977ddd
143+10 replay-mismatches=0
144+11/019 e3eb02d62746d50757d63d18
145+11/039 070fa14e017eb9ba3056672d
146+11/059 8b016d3845f5d47e91b1826b
147+11/079 75ce3f04d3082bc4788d6c96
148+11/099 2b7cf678237f6988de3396a3
149+11/119 230ebcd9a41d473aaed1896e
150+11/139 a0473a1fc49d7c12f20b41be
151+11/159 b9ef5849eaeefbe486770b76
152+11/179 b3c43da50034693c385b844b
153+11/199 7145d045e0724186ce73059a
154+11/219 ee6f5fc91e84ee8214060439
155+11/239 f4307d6e12b1cd2db37a8032
156+11 replay-mismatches=0
157+12/019 4fade3921f5735064355eec1
158+12/039 e2aab8e88f7241073e73eb05
159+12/059 2b1492eeb901a30dddcecfea
160+12/079 809ab3f1339fe2c10da81dc6
161+12/099 09759c1960ee25243130cec7
162+12/119 d40b61390be6b1ade0fc9798
163+12/139 dd23c4487248a1236597baf8
164+12/159 6ccc55f91c960571ebc0f2dc
165+12/179 2165c2c3b2a0fd7a597c1251
166+12/199 679db69d3aee6356dafac627
167+12/219 eff11bcec70127843617c651
168+12/239 af6d8b16a1eb290229dd6000
169+12 replay-mismatches=1
170+13/019 ef8a3432b61607340356a764
171+13/039 4e823b712c6aab07bcd5764e
172+13/059 90b7e616d2c528faeee0f72b
173+13/079 b1330a7079b795346ee9e317
174+13/099 9f0d10089131edf34cceb0a1
175+13/119 069d5dc52bcfba2eaa2211bd
176+13/139 08b97ef26faddb637a424bef
177+13/159 c29b028480ce1c097e8c3066
178+13/179 85d6cc0d0267ce57c4f438d7
179+13/199 84a14ed35e312f14e0c4d220
180+13/219 8382c4d6025f22cddd967535
181+13/239 4e3ac44cff6dfa7d7a843378
182+13 replay-mismatches=0
183+14/019 747d54cdf75a492242809318
184+14/039 e6530a9282bd6095e15a9677
185+14/059 715de56023e1afc30c1feea1
186+14/079 f5c6253e95d471ccf7892526
187+14/099 09063bb10e3e19d067c9c85c
188+14/119 f8753c5d5010cd747e5dcc31
189+14/139 8a726baa028acc349f76fcb5
190+14/159 6707de5fd29ec469ba8913b9
191+14/179 3914c9045ccb2d16f6dfb386
192+14/199 f5992df3d18c885bcaab4496
193+14/219 38e1a9fe90723b701c438464
194+14/239 c90fb86719f7e4613e3720c2
195+14 replay-mismatches=0
196+15/019 95e07748c709fa34a80ceff7
197+15/039 4186ba13b89b2952cb8407b1
198+15/059 e113b092eeaeb7ba23f36f0b
199+15/079 c9ad53bbc38faf68af448dbe
200+15/099 6207b08ba91d6e0c93f27540
201+15/119 edffb952f657ba998d07de73
202+15/139 5c874878a9a518b08627f52d
203+15/159 e7c63b49c6f3ebb122ab821b
204+15/179 edb533ed1dcf2d238f7db6a9
205+15/199 80fa50afb7b21ddcc958e13e
206+15/219 f602f7cb0510d54ab8c94ff9
207+15/239 3ac7cd4affba36e038a2254b
208+15 replay-mismatches=2
209+16/019 4314babccb7675abd6328c32
210+16/039 3e6ee1b339dab4ff5e0c8b34
211+16/059 1afd8504d6a0be4d6cc9c3c3
212+16/079 50f71b7a9d7c444810cf1ba6
213+16/099 65a1c2e6e41adcddbc510efc
214+16/119 3f7c33fe85ccde2210b37f8f
215+16/139 1b47397842763e30313e66de
216+16/159 3d9b4904b96244df64051cc7
217+16/179 60ae20b4cabf7c1a02192010
218+16/199 210f0c4ad73131d2b465b362
219+16/219 4b62676c69410e9b6f9d8f26
220+16/239 72f70f009e426f88b33ebdae
221+16 replay-mismatches=0
222+17/019 1a06602479d8e795ee95c965
223+17/039 c7e7eb54ee63d9e7e23ceaca
224+17/059 51c34ba1a80d55b1f9ae42fa
225+17/079 d5f10d70e10e0e87e54ac6f3
226+17/099 0a3784787eb2a97cce6c8309
227+17/119 7ccd8bedfbf26111e264baa5
228+17/139 f028c427efd1ce9c0b9828d1
229+17/159 fb8b3eb57ac4186345d52918
230+17/179 1c7da7757137c15d7a05d3d4
231+17/199 55b35801df62b3250963613d
232+17/219 1cb11bbb2c5dd8995094be53
233+17/239 6a146a19d7c534c4766e98f8
234+17 replay-mismatches=0
235+18/019 a1d2576637f54892892ea0b4
236+18/039 4415a2923a28b75a5aa0dbfb
237+18/059 2bac49c23f1c09f170c68dc2
238+18/079 eeebf8f57f35ee0b110fc200
239+18/099 e3673a14664b853fa9613c64
240+18/119 57aa1250c9bb46869c8fc1a7
241+18/139 abaca7067643db7db4dfe105
242+18/159 6e878b08b5635eac7ab9816c
243+18/179 031a000b34a4ba3bc6cedc53
244+18/199 c523b55eb720c065fc6134ac
245+18/219 0f367277325b630ce5bc634f
246+18/239 e04b873c6820a46bfd2f4873
247+18 replay-mismatches=0
248+19/019 2db955914f2e068e72cf8dfa
249+19/039 ec344560f2bb7de5f25fdb78
250+19/059 d1a41ae321f1449d256cf19d
251+19/079 c0d8a7ed784bd9d278c61bfa
252+19/099 f91e7a807eea1aac3f05da1f
253+19/119 36d47e23f93682f8f8bd720b
254+19/139 b227a962815dfd349d06722f
255+19/159 5552419605856137ee5e3384
256+19/179 0c7cf6585dcbaa42d05758c1
257+19/199 ebcee2469216bd045f79d9a7
258+19/219 d5bdec7e45121f8f757c12df
259+19/239 ae4abbf3362cbcba26878acd
260+19 replay-mismatches=1
261+20/019 3c776bd6d460be5f6bcd060e
262+20/039 ef65b757bd137499a352d5ae
263+20/059 1cfcab4ffbc08c619e97bcd5
264+20/079 8879e2856d1a555f07aa60d7
265+20/099 e16919d7e8c9f20f7dfa5956
266+20/119 cb7f9cf61ef176f01706ac77
267+20/139 f9edefe5603a815455b563ff
268+20/159 cf8862d058a774e0fdb18628
269+20/179 82daa9b9d7f4edc82a424a4a
270+20/199 fe78853df2a403308904f8e2
271+20/219 0895c29599a30db493020dd2
272+20/239 0e2310234476bea5dbb44cf0
273+20 replay-mismatches=1
274+21/019 357ece3eab75324d3823a6a4
275+21/039 fe4501bedc7017bc40ff0509
276+21/059 c97dc13dbcd996b58361777e
277+21/079 478d3efcfcca35aac31016e5
278+21/099 3311dde300dcf85626edb8b7
279+21/119 67ff6fbebbb9976f9f5191d8
280+21/139 034216a3e7d38b221d5a8811
281+21/159 adc62b539970ad3123cb4175
282+21/179 0323505afc602891ba38d7ac
283+21/199 577472eed259f3abc0aaa526
284+21/219 5d1dc3faa423707ac2ca4ba7
285+21/239 068abdef9483e1760da84d41
286+21 replay-mismatches=1
287+22/019 6fc626ac7796049f3457a9a7
288+22/039 e9214d74742e6ab1a1c8baf0
289+22/059 72e63b2a0100e4153851a23d
290+22/079 9b1a8ebe1c432db2fedb68e3
291+22/099 924e3f255fb856ff7b7dc8ed
292+22/119 53fcd1df652d602fe412c5a0
293+22/139 7414a5f8fdbd506ab15f698d
294+22/159 bc2dd2281c12435224135073
295+22/179 dfeef83df0e865e875132be1
296+22/199 9fe1387f10258c696fd9e783
297+22/219 3009560abe632e04450f7f41
298+22/239 fe8d129db62a73ae9b9650bf
299+22 replay-mismatches=0
300+23/019 20cb94d03695ad09763e03d2
301+23/039 6c18ee020345a7699fd75494
302+23/059 f85c5ecebab82f1126f3c964
303+23/079 2fbeda367e9d56072d5b0edc
304+23/099 307896059fd55149f9d7c7e3
305+23/119 73bafc1697a5e247b2d3c75f
306+23/139 214cbbca9ce4b1d0c98ea569
307+23/159 82259d749685c0f294be858f
308+23/179 8d7b14f89c5f5d3113fbb3d2
309+23/199 ddee2d89e0897e90f9a6df78
310+23/219 65e4440830b9f98e48fe3a11
311+23/239 e8ca9cf0699b2c6333e0cb3e
312+23 replay-mismatches=0
313+24/019 b7fdbacf57cb0af4d47225ba
314+24/039 2a75c514d5b6525aaae38b3c
315+24/059 25cfe1b7ea38dca8b287dfcb
316+24/079 f26ad4606c3aa14fdd999af4
317+24/099 3e2f4c94e77ee245d7b1feb2
318+24/119 17ea49879ca006a1a9cc6f7a
319+24/139 90243c3b758308f0dd7982d3
320+24/159 c734adf4c419ccaccb7b344b
321+24/179 f4f0b4818a87d2d735361f5b
322+24/199 16acbd765f02caece9941217
323+24/219 a4605530cb75474769714cf4
324+24/239 ea3ffdb6ec8b58fe625b928a
325+24 replay-mismatches=1
326+25/019 eb73f8f3bb46422a6a12fc8f
327+25/039 823c96945f71196e67f10f65
328+25/059 c04f006b9f600ecd075cec6a
329+25/079 a21fe1e939c9fa2ddbc2d385
330+25/099 c20fca1d42014ae0701d6d0b
331+25/119 3b6b3af160572b8ebc24277f
332+25/139 e6151bf8cfffc442059ea129
333+25/159 5209600fc448f453bd687cb2
334+25/179 043175bc5eea52c09fb8f884
335+25/199 4b7c1cd9781ec41bf596b764
336+25/219 ad62688c776574ab5c442de7
337+25/239 22e025d483651540c55002c6
338+25 replay-mismatches=0
339+26/019 122c0cce3cc0ece13ce6f89a
340+26/039 0999ef9b5da72bc75cef9d49
341+26/059 2efdbf6da4b726f10f2878d1
342+26/079 f899906f635ddb86d5c73984
343+26/099 46367bf24897cad56dd5235c
344+26/119 a97b04bc18332773595951da
345+26/139 c5ae15a2b57146ef155e27b9
346+26/159 51ab1eaae8be298680367404
347+26/179 e35bdd23c19b6295280181ed
348+26/199 5f21c27026ba98b0a9d5d51c
349+26/219 2fcb984adf0b347e0da88d33
350+26/239 200f40ed44f86ce04857c1ef
351+26 replay-mismatches=0
352+27/019 ee2c010c41b9f0e5f70ce2b3
353+27/039 91ab08fb270466abbfb06c54
354+27/059 2e73fe488420a5de3866e7e3
355+27/079 a72a80db0e8ee9bffb891b38
356+27/099 917cde1321cfce583a63d9f2
357+27/119 b8056ec6e08df1e2f7033b6a
358+27/139 22214c1a34de027d97f18ad9
359+27/159 18debaf4cd5dfd2bb665bf34
360+27/179 b8c41633f2ea27468edc411a
361+27/199 85bc90436e42fca40bd64d3c
362+27/219 f0dbe3da68c8dd144c3dd9b7
363+27/239 214002b3d936e2fe396aae01
364+27 replay-mismatches=0
365+28/019 3ee1c2f031af3ad67ae50ebc
366+28/039 07508b3af21ec20fadeefe55
367+28/059 dfa63544d3d6a817a9212067
368+28/079 1855612e92d140c47b8abd1d
369+28/099 bcb387773cfd286bf68b7b7d
370+28/119 1f742786b7a0328e4e513984
371+28/139 491e0249e093af7d1fa703e3
372+28/159 7e72e1e6ffbd2f8191ff7116
373+28/179 daa22f39fc629726df0fd80c
374+28/199 ad54c46914aa19908c0a9880
375+28/219 54e180efdd3796a13c1c7670
376+28/239 8f0e0e94c590745fc2dc7678
377+28 replay-mismatches=1
378+29/019 72e981fc506bfdd3bbf15c37
379+29/039 8c46bcf213fe0e9cf159a7d2
380+29/059 213c8dd3d5bcda3abb11ab05
381+29/079 5c42ca0bf2bdde3f4e8beb0e
382+29/099 9da53eccca7406686de96ec0
383+29/119 c91f989577473d295e74d700
384+29/139 6d5ff6bc1b1b73cae023806b
385+29/159 a1e1bfef4df06dc65e727a5e
386+29/179 afb819e69c5c53d51d49d446
387+29/199 78df663b24d8507a469a7959
388+29/219 e40592fde61b5e14bf73242a
389+29/239 9500a168ffd9142d6bdbde5a
390+29 replay-mismatches=0
391+30/019 ea74787caee42ed3665575ed
392+30/039 91b5e5afa5d3bb06b087f6ad
393+30/059 ab536abe78f0cf27cdf94a75
394+30/079 cb0e90a99459de9ed8493a87
395+30/099 24cda2b6890297d9254603d1
396+30/119 dc93cdf4c2014d22ac4d1ef2
397+30/139 66e5417af5336b68e70cfa14
398+30/159 c10f3ab9c58aa3f43caa4f94
399+30/179 a7d6695e7add8ed6e7912161
400+30/199 f1fe2d3081c9ccfc7413e3a9
401+30/219 5c1dad4fffc15226a5eebe51
402+30/239 966ff4c469452f7951326c5e
403+30 replay-mismatches=0
404+31/019 869af9273a068555a7c65e06
405+31/039 af9002bdf228a2e1f034df74
406+31/059 375fd592eea7fdb3ce4a7563
407+31/079 8d6f8e9a242c0da7a8707d06
408+31/099 f4e05fd0482cb95b1141b933
409+31/119 0499dd21ead9843ec3668589
410+31/139 8fab9f1300b2d8c6757a9587
411+31/159 e5ea9bef6d0ff097b0b0f00f
412+31/179 2d21760a2b468c1f8e101b86
413+31/199 1c76b93d064c496485782148
414+31/219 8a23244f3e0b25b774aaabbb
415+31/239 6b47e6b3998cf340d47f2dc9
416+31 replay-mismatches=0
417+32/019 ed11b0c6e02dfce865d49934
418+32/039 bd3bbe698e10adf1f5fbc490
419+32/059 fb530e83cb69ec498ccd32a7
420+32/079 f54210cbb277563ac36c611d
421+32/099 9f2557fef0b33c2da3b1e22b
422+32/119 4b02e8f56434d83e8f5234f6
423+32/139 39feab897a9120d585c20a8e
424+32/159 d0953ad716ea690de47dfc3d
425+32/179 f6a7b77c5991d37449e779b1
426+32/199 2c188342e677e30c8ffe7f9e
427+32/219 7fc717e4fbfd57748ab342e3
428+32/239 506a3d22b3e2ff760873e0fb
429+32 replay-mismatches=0
430+33/019 75e34d5589c8f836a06d090e
431+33/039 372c230667e378656b2af4ff
432+33/059 938b86084dbb06afa4454b27
433+33/079 7fa7265281d43e73b797d780
434+33/099 ac83290b711b8dec3003df47
435+33/119 021ac2bab909abdd6e5c77b5
436+33/139 ed69b8d8881a85100d32c4cd
437+33/159 c1566ad91ec422c65923fe34
438+33/179 9275113b9df0b001112ad573
439+33/199 a269fac1fd59ac746cdcc3ed
440+33/219 8efc32f7b432b011f6862afb
441+33/239 360719a27cebdd0863b2e775
442+33 replay-mismatches=0
443+34/019 93dcae1fc2bfd6779aff1aff
444+34/039 e4c849c2eb75db26ab3e4a69
445+34/059 f44f95efd2e73205143c656c
446+34/079 500ac8d59cbff22ad8cedf95
447+34/099 8cde09d8fb843f0771e27c68
448+34/119 8888149eafa436929ba9002f
449+34/139 9c9180251a5d41fb6bb84e0e
450+34/159 a8c9c943771f1bac64729951
451+34/179 147dc1d5ce1aa53965747a76
452+34/199 d60cdab9e926b1a975ab7c68
453+34/219 70311db1c213b30d8622a677
454+34/239 16c8a61f803eef432762550b
455+34 replay-mismatches=0
456+35/019 6ce6075f205b75f864a11ca6
457+35/039 8375248e74a353fc4ec62d73
458+35/059 794e546c0d3346dc3965043f
459+35/079 f4a4a12dd0c59cfa3a7036b4
460+35/099 a424a229392a35da8d937062
461+35/119 28415d77a2772ddc9ff46361
462+35/139 b142770cfec4946c445f1925
463+35/159 a2c6d28bb5442fa733cf8ea3
464+35/179 0c4f1446420ee3280f18090c
465+35/199 d8ab4d4487ea3bb519574059
466+35/219 f3f1d026b78975accc285f57
467+35/239 a734eb4db0f69306dc7a5630
468+35 replay-mismatches=1

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close