1package client
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/mock"
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/vev/internal/domain"
11 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12 "github.com/bnema/vev/internal/protocol"
13)
14
15func newTestAttachmentEcho(t *testing.T, mode domain.EchoPredictMode) (*attachmentEcho, *mockattachmentPredictionForeground, *portsmocks.MockClock) {
16 t.Helper()
17 out := newMockattachmentPredictionForeground(t)
18 clock := portsmocks.NewMockClock(t)
19 clock.EXPECT().Now().Return(echoTestStart).Maybe()
20 echo := &attachmentEcho{predictor: newEchoPredictor(mode), out: out, clock: clock}
21 echo.seed(protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 20, Rows: 3}, Data: []byte("\x1b[H\x1b[2J$ ")})
22 // One acknowledged input gives the engine its first round trip.
23 echo.predictor.input(1, nil, echoTestStart)
24 echo.predictor.applyOutput(protocol.Output{Epoch: 1, Echo: 1}, echoTestStart.Add(echoServerDelay))
25 echo.predictor.confEpoch = echo.predictor.predEpoch
26 return echo, out, clock
27}
28
29func TestNewAttachmentEchoSkipsNonPredictingAttachments(t *testing.T) {
30 t.Parallel()
31 fg := newMockattachmentPredictionForeground(t)
32 tests := []struct {
33 name string
34 local bool
35 mode domain.EchoPredictMode
36 fg AttachmentForeground
37 }{
38 {name: "local attachment", local: true, mode: domain.EchoPredictAdaptive},
39 {name: "prediction off", mode: domain.EchoPredictNever},
40 {name: "foreground without prediction seam", mode: domain.EchoPredictAdaptive},
41 }
42 for _, tt := range tests {
43 t.Run(tt.name, func(t *testing.T) {
44 t.Parallel()
45 w := &sessionAttachmentWorker{cfg: sessionAttachmentConfig{EchoPredict: tt.mode}}
46 w.cfg.Request.Local = tt.local
47 require.Nil(t, newAttachmentEcho(w, tt.fg))
48 })
49 }
50 require.NotNil(t, newAttachmentEcho(&sessionAttachmentWorker{cfg: sessionAttachmentConfig{EchoPredict: domain.EchoPredictAdaptive, Clock: systemClock{}}}, struct {
51 AttachmentForeground
52 attachmentPredictionForeground
53 }{attachmentPredictionForeground: fg}), "a remote attachment with the seam predicts")
54}
55
56func TestAttachmentEchoDrawsGuessAndArmsTick(t *testing.T) {
57 t.Parallel()
58 echo, out, clock := newTestAttachmentEcho(t, domain.EchoPredictAdaptive)
59 // A slow link: guesses show, and the tick waits on the glitch trigger.
60 echo.predictor.srtt = 100 * time.Millisecond
61 tick := portsmocks.NewMockTimer(t)
62 clock.EXPECT().NewTimer(echoTickInterval).Return(tick).Once()
63 var drawn []byte
64 out.EXPECT().writePrediction(mock.Anything).RunAndReturn(func(data []byte) (bool, error) {
65 drawn = append([]byte(nil), data...)
66 return true, nil
67 }).Once()
68
69 require.NoError(t, echo.input(2, []byte("b")))
70 require.Equal(t, "\x1b[1;3H\x1b[0mb\x1b[0m\x1b[1;4H", string(drawn))
71
72 fired := make(chan time.Time)
73 tick.EXPECT().C().Return(fired).Once()
74 require.Equal(t, (<-chan time.Time)(fired), echo.tickC())
75 tick.EXPECT().Stop().Return(true).Once()
76 echo.close()
77 require.Nil(t, echo.tickC())
78}
79
80func TestAttachmentEchoForgetsGuessesWhileOverlayOwnsTerminal(t *testing.T) {
81 t.Parallel()
82 echo, out, _ := newTestAttachmentEcho(t, domain.EchoPredictAlways)
83 out.EXPECT().writePrediction(mock.Anything).Return(false, nil).Once()
84
85 require.NoError(t, echo.input(2, []byte("b")))
86 require.False(t, echo.predictor.active(), "guesses survived an overlay")
87 require.Empty(t, echo.predictor.drawn, "an overlay-covered guess must not be repainted later")
88}
89
90func TestAttachmentEchoSameSizeResizeRepaintsDrawnGuesses(t *testing.T) {
91 t.Parallel()
92 echo, out, _ := newTestAttachmentEcho(t, domain.EchoPredictAlways)
93 out.EXPECT().writePrediction(mock.Anything).Return(true, nil).Once()
94 require.NoError(t, echo.input(2, []byte("b")))
95
96 var repaint []byte
97 out.EXPECT().writePrediction(mock.Anything).RunAndReturn(func(data []byte) (bool, error) {
98 repaint = append([]byte(nil), data...)
99 return true, nil
100 }).Once()
101 require.NoError(t, echo.resized(domain.Size{Cols: 20, Rows: 3}))
102 require.Equal(t, "\x1b[1;3H\x1b[0m \x1b[0m\x1b[1;3H", string(repaint))
103
104 // A new size brings a full repaint from the daemon: nothing is written.
105 require.NoError(t, echo.resized(domain.Size{Cols: 30, Rows: 3}))
106}