1//go:build linux
2
3package term
4
5import (
6 "io"
7 "os"
8 "syscall"
9 "testing"
10 "time"
11
12 "github.com/bnema/vev/pkg/rawterm"
13)
14
15// openPTYPair opens a real Linux PTY master/slave pair directly via
16// /dev/ptmx, self-contained (no import of the vev pty adapter package,
17// which is being built in parallel).
18func openPTYPair(t *testing.T) (master, slave *os.File) {
19 t.Helper()
20
21 m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
22 if err != nil {
23 t.Fatalf("open /dev/ptmx: %v", err)
24 }
25
26 s, err := rawterm.PreparePty(int(m.Fd()))
27 if err != nil {
28 _ = m.Close()
29 t.Fatalf("prepare pty: %v", err)
30 }
31
32 return m, s
33}
34
35// TestTerminal_RealPTY_RawModeSizeAndEscapes is the ONE integration test
36// exercising the real-tty paths (MakeRaw/Restore, GetSize) against an
37// actual PTY, since those are pure-syscall lines that unit tests with
38// pipes can't cover.
39func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) {
40 master, slave := openPTYPair(t)
41 defer func() { _ = master.Close() }()
42 defer func() { _ = slave.Close() }()
43
44 if err := rawterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
45 t.Fatalf("set winsize: %v", err)
46 }
47
48 // Drain everything written to the slave (escape sequences) so the
49 // batched writer's Flush never blocks on the PTY's internal buffer.
50 captured := make(chan []byte, 1)
51 go func() {
52 buf, _ := io.ReadAll(master)
53 captured <- buf
54 }()
55
56 tm := NewWithFiles(slave, slave)
57
58 sz, err := tm.Size()
59 if err != nil {
60 t.Fatalf("Size: %v", err)
61 }
62 if sz.Cols != 80 || sz.Rows != 24 {
63 t.Fatalf("Size = %+v, want {Cols:80 Rows:24}", sz)
64 }
65
66 restore, err := tm.EnterRaw()
67 if err != nil {
68 t.Fatalf("EnterRaw: %v", err)
69 }
70 if tm.rawSkipped {
71 t.Fatalf("expected rawSkipped=false for a real PTY")
72 }
73
74 if err := restore(); err != nil {
75 t.Fatalf("restore: %v", err)
76 }
77 // Idempotent.
78 if err := restore(); err != nil {
79 t.Fatalf("second restore: %v", err)
80 }
81
82 // Closing the slave lets the reader goroutine observe EOF on the
83 // master naturally. Don't also close master here: that would race
84 // the reader's in-flight Read against this goroutine's Close on the
85 // same fd and could discard already-buffered-but-unread data.
86 if err := slave.Close(); err != nil {
87 t.Fatalf("close slave: %v", err)
88 }
89
90 select {
91 case got := <-captured:
92 want := altScreenEnter + autowrapDisable + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + autowrapEnable + altScreenExit
93 if string(got) != want {
94 t.Fatalf("captured escapes = %q, want %q", got, want)
95 }
96 case <-time.After(5 * time.Second):
97 t.Fatalf("timed out waiting for captured PTY output")
98 }
99}