1package daemon
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 vt "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"
10 "github.com/stretchr/testify/require"
11)
12
13func TestCopyWheelReplayMatchesFullComposition(t *testing.T) {
14 for _, panes := range []int{1, 2, 4} {
15 t.Run(fmt.Sprint(panes), func(t *testing.T) {
16 f := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, panes: panes, historyRows: 200})
17 f.d.enterCopyMode(f.sess, f.ac)
18 base := f.ac.render.cache.frame
19 terminal := vt.NewScreen(base.Width, base.Height)
20 replay := func() []byte {
21 output := unmarshalTestOutput(t, f.output.lastPayload())
22 terminal.Write(output.Data)
23 f.ac.ackOutputState(f.ac.output.currentEpoch(), f.ac.output.next)
24 return output.Data
25 }
26 replay()
27 for i, delta := range []int{-30, -3, 3, -1, 1, -60, 3, 1} {
28 previous := f.ac.render.cache.copyViewport.frame
29 var before vt.Frame
30 if previous.Width > 0 {
31 before = captureTestFrame(previous)
32 }
33 // Concurrent live output must update the unadorned base without
34 // contaminating retained immutable copy rows.
35 f.activePane.mu.Lock()
36 f.activePane.screen.Write([]byte(fmt.Sprintf("\x1b[1;1HLIVE-%d", i)))
37 f.activePane.mu.Unlock()
38 f.d.copyWheel(f.sess, f.ac, delta)
39 data := replay()
40 if before.Width > 0 {
41 require.Equal(t, before, captureTestFrame(previous))
42 }
43 cache := f.ac.render.cache
44 mode := f.ac.overlays.copyMode
45 require.NotNil(t, mode)
46 target := cache.copyViewport.target
47 require.NotNil(t, cache.copyViewport.document)
48 want, _ := composeCopyClientFrame(mode, target, cache.frame.Clone(), resolveStyles(nil))
49 require.Equal(t, captureTestFrame(want), captureTestFrame(cache.copyViewport.frame), "cached composition, step %d", i)
50 for y := range want.Height {
51 for x := range want.Width {
52 require.True(t, want.Cell(x, y).Equal(terminal.Cell(x, y)), "replay step %d cell %d,%d", i, x, y)
53 }
54 }
55 require.NoError(t, cache.copyViewport.frame.CheckInvariants())
56 if panes == 1 && i == 1 {
57 require.Contains(t, string(data), "\x1b[3T")
58 }
59 if panes == 1 && i == 2 {
60 require.Contains(t, string(data), "\x1b[3S")
61 }
62 if target.Width != want.Width {
63 require.False(t, strings.Contains(string(data), "\x1b[3T"))
64 }
65 }
66 })
67 }
68}
69
70// TestCopyWheelFailedScrollKeepsCommittedFrames proves a scroll that fails to
71// publish writes only the spare buffers: the committed base and viewport
72// frames are byte-for-byte unchanged.
73func TestCopyWheelFailedScrollKeepsCommittedFrames(t *testing.T) {
74 f := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 80, Rows: 24}, panes: 1, historyRows: 200})
75 f.d.enterCopyMode(f.sess, f.ac)
76 for _, delta := range []int{-10, -3} {
77 f.d.copyWheel(f.sess, f.ac, delta)
78 f.ac.ackOutputState(f.ac.output.currentEpoch(), f.ac.output.next)
79 }
80 require.NotZero(t, f.ac.render.spare.copyViewport.frame.Width, "both buffers must hold a viewport page")
81 before := cloneComposeCache(f.ac.render.cache)
82 var compositions int
83 f.ac.renderStages = renderStageHooks{compose: func() { compositions++ }}
84 f.ac.replaceTransport(cacheFailTransport{})
85 f.d.copyWheel(f.sess, f.ac, -3)
86 f.d.paint(f.sess, f.ac, false, nil)
87 require.Positive(t, compositions, "the failed scroll must be composed")
88 require.Equal(t, before, cloneComposeCache(f.ac.render.cache))
89}