1package client
2
3import (
4 "bytes"
5 "context"
6 "io"
7 "testing"
8 "time"
9
10 "github.com/bnema/vev-vt/protocol/terminalquery"
11 "github.com/stretchr/testify/require"
12
13 portsmocks "github.com/bnema/vev/internal/ports/mocks"
14)
15
16func TestProbeTerminalCapabilities(t *testing.T) {
17 const query = terminalquery.KittyGraphicsQuery + terminalquery.KittyKeyboardQuery + terminalquery.DeviceAttributesQuery
18 tests := []struct {
19 name string
20 reply string
21 timeout bool
22 want terminalCapabilities
23 wantRest string
24 }{
25 {name: "graphics and keyboard", reply: "\x1b_Gi=31;OK\x1b\\\x1b[?0u\x1b[?62;c", want: terminalCapabilities{KittyGraphics: true, KittyKeyboard: true}},
26 {name: "keyboard only", reply: "\x1b[?0u\x1b[?62;22c", want: terminalCapabilities{KittyKeyboard: true}},
27 {name: "neither", reply: "\x1b[?62;22c"},
28 {name: "typed input is kept in order", reply: "ab\x1b[?1u\x1b[?62;22ccd", want: terminalCapabilities{KittyKeyboard: true}, wantRest: "abcd"},
29 {name: "no DA1 means nothing", reply: "\x1b[?0u", timeout: true, wantRest: ""},
30 {name: "silent terminal times out", timeout: true},
31 }
32 for _, tt := range tests {
33 t.Run(tt.name, func(t *testing.T) {
34 inR, inW := io.Pipe()
35 defer func() { _ = inW.Close() }()
36 pump := newTerminalInputPump(inR)
37 pump.start()
38 defer pump.stop()
39
40 var out bytes.Buffer
41 term := portsmocks.NewMockTerminal(t)
42 term.EXPECT().Out().Return(&out).Once()
43 written := make(chan struct{})
44 term.EXPECT().Flush().RunAndReturn(func() error {
45 go func() {
46 defer close(written)
47 if tt.reply != "" {
48 _, _ = inW.Write([]byte(tt.reply))
49 }
50 }()
51 return nil
52 }).Once()
53 clk := &pcFakeClock{}
54
55 got := make(chan terminalCapabilities, 1)
56 go func() { got <- probeTerminalCapabilities(context.Background(), term, clk, pump) }()
57 if tt.timeout {
58 require.Eventually(t, func() bool {
59 clk.mu.Lock()
60 defer clk.mu.Unlock()
61 return len(clk.timers) == 1
62 }, timeoutForTest, pollForTest)
63 // A pipe write returns once the pump read it, so a partial
64 // reply is in before the deadline fires.
65 <-written
66 require.Eventually(t, func() bool { return pumpDrained(pump) }, timeoutForTest, pollForTest)
67 clk.fireLast()
68 }
69 caps := <-got
70 require.Equal(t, query, out.String())
71 require.Equal(t, tt.want, caps)
72
73 consumer, ok := pump.tryClaim()
74 require.True(t, ok)
75 defer pump.revoke(consumer)
76 if tt.wantRest == "" {
77 _, ok := pump.take(context.Background(), consumer)
78 require.False(t, ok, "no byte may be left for the next consumer")
79 return
80 }
81 result, ok := pump.take(context.Background(), consumer)
82 require.True(t, ok)
83 require.Equal(t, tt.wantRest, string(result.data))
84 })
85 }
86}
87
88const (
89 timeoutForTest = 2 * time.Second
90 pollForTest = time.Millisecond
91)
92
93// pumpDrained reports that the probe consumed every read the pump delivered.
94func pumpDrained(p *terminalInputPump) bool {
95 p.mu.Lock()
96 defer p.mu.Unlock()
97 return p.pending == nil && p.delivering == 0
98}