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.SetWinsizeFull(int(slave.Fd()), rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384}); 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 geometry, err := tm.Geometry()
59 if err != nil {
60 t.Fatalf("Geometry: %v", err)
61 }
62 if geometry.Cols != 80 || geometry.Rows != 24 {
63 t.Fatalf("Geometry = %+v, want {Cols:80 Rows:24}", geometry)
64 }
65 if geometry.PixelWidth != 640 || geometry.PixelHeight != 384 {
66 t.Fatalf("Geometry = %+v, want 640x384 pixels", geometry)
67 }
68
69 restore, err := tm.EnterRaw()
70 if err != nil {
71 t.Fatalf("EnterRaw: %v", err)
72 }
73 if tm.rawSkipped {
74 t.Fatalf("expected rawSkipped=false for a real PTY")
75 }
76
77 if err := restore(); err != nil {
78 t.Fatalf("restore: %v", err)
79 }
80 // Idempotent.
81 if err := restore(); err != nil {
82 t.Fatalf("second restore: %v", err)
83 }
84
85 // Closing the slave lets the reader goroutine observe EOF on the
86 // master naturally. Don't also close master here: that would race
87 // the reader's in-flight Read against this goroutine's Close on the
88 // same fd and could discard already-buffered-but-unread data.
89 if err := slave.Close(); err != nil {
90 t.Fatalf("close slave: %v", err)
91 }
92
93 select {
94 case got := <-captured:
95 want := altScreenEnter + autowrapDisable + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + focusReportEnable + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + focusReportDisable + autowrapEnable + altScreenExit
96 if string(got) != want {
97 t.Fatalf("captured escapes = %q, want %q", got, want)
98 }
99 case <-time.After(5 * time.Second):
100 t.Fatalf("timed out waiting for captured PTY output")
101 }
102}