1//go:build linux
2
3package rawterm
4
5import (
6 "errors"
7 "os"
8 "syscall"
9 "testing"
10 "unsafe"
11)
12
13// openPTYPair opens a real Linux PTY master/slave pair directly via
14// /dev/ptmx using the package under test, so these tests double as an
15// integration check of PreparePty against the kernel.
16func openPTYPair(t *testing.T) (master, slave *os.File) {
17 t.Helper()
18
19 masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
20 if err != nil {
21 t.Skipf("open /dev/ptmx: %v", err)
22 }
23 master = os.NewFile(uintptr(masterFD), "pty-master")
24
25 slave, err = PreparePty(masterFD)
26 if err != nil {
27 _ = master.Close()
28 t.Fatalf("PreparePty: %v", err)
29 }
30
31 return master, slave
32}
33
34func TestIsTerminal(t *testing.T) {
35 master, slave := openPTYPair(t)
36 defer func() { _ = master.Close() }()
37 defer func() { _ = slave.Close() }()
38
39 if !IsTerminal(int(slave.Fd())) {
40 t.Errorf("IsTerminal(pty slave) = false, want true")
41 }
42
43 r, w, err := os.Pipe()
44 if err != nil {
45 t.Fatalf("os.Pipe: %v", err)
46 }
47 defer func() { _ = r.Close() }()
48 defer func() { _ = w.Close() }()
49
50 if IsTerminal(int(r.Fd())) {
51 t.Errorf("IsTerminal(pipe) = true, want false")
52 }
53}
54
55func TestSetWinsizeGetSizeRoundtrip(t *testing.T) {
56 master, slave := openPTYPair(t)
57 defer func() { _ = master.Close() }()
58 defer func() { _ = slave.Close() }()
59
60 tests := []struct {
61 name string
62 cols uint16
63 rows uint16
64 }{
65 {name: "typical", cols: 80, rows: 24},
66 {name: "wide", cols: 220, rows: 60},
67 {name: "minimal", cols: 1, rows: 1},
68 }
69
70 for _, tt := range tests {
71 t.Run(tt.name, func(t *testing.T) {
72 if err := SetWinsize(int(slave.Fd()), tt.cols, tt.rows); err != nil {
73 t.Fatalf("SetWinsize: %v", err)
74 }
75 gotCols, gotRows, err := GetSize(int(slave.Fd()))
76 if err != nil {
77 t.Fatalf("GetSize: %v", err)
78 }
79 if gotCols != int(tt.cols) || gotRows != int(tt.rows) {
80 t.Fatalf("GetSize = (%d, %d), want (%d, %d)", gotCols, gotRows, tt.cols, tt.rows)
81 }
82 })
83 }
84}
85
86func TestWinsizeFullRoundtrip(t *testing.T) {
87 master, slave := openPTYPair(t)
88 defer func() { _ = master.Close() }()
89 defer func() { _ = slave.Close() }()
90
91 want := Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384}
92 if err := SetWinsizeFull(int(slave.Fd()), want); err != nil {
93 t.Fatalf("SetWinsizeFull: %v", err)
94 }
95 got, err := GetWinsize(int(slave.Fd()))
96 if err != nil {
97 t.Fatalf("GetWinsize: %v", err)
98 }
99 if got != want {
100 t.Fatalf("GetWinsize = %+v, want %+v", got, want)
101 }
102}
103
104func TestMakeRawRestore(t *testing.T) {
105 master, slave := openPTYPair(t)
106 defer func() { _ = master.Close() }()
107 defer func() { _ = slave.Close() }()
108
109 fd := int(slave.Fd())
110
111 var original syscall.Termios
112 if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&original)); err != nil {
113 t.Fatalf("get termios before MakeRaw: %v", err)
114 }
115
116 old, err := MakeRaw(fd)
117 if err != nil {
118 t.Fatalf("MakeRaw: %v", err)
119 }
120 if old == nil {
121 t.Fatalf("MakeRaw returned nil state")
122 }
123
124 var current syscall.Termios
125 if err := ioctl(fd, reqGetTermios, unsafe.Pointer(¤t)); err != nil {
126 t.Fatalf("get termios after MakeRaw: %v", err)
127 }
128 if current.Lflag&syscall.ECHO != 0 {
129 t.Errorf("ECHO still set after MakeRaw")
130 }
131 if current.Lflag&syscall.ICANON != 0 {
132 t.Errorf("ICANON still set after MakeRaw")
133 }
134 if current.Cc[syscall.VMIN] != 1 {
135 t.Errorf("Cc[VMIN] = %d, want 1", current.Cc[syscall.VMIN])
136 }
137
138 if err := Restore(fd, old); err != nil {
139 t.Fatalf("Restore: %v", err)
140 }
141
142 var restored syscall.Termios
143 if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&restored)); err != nil {
144 t.Fatalf("get termios after Restore: %v", err)
145 }
146 if restored != original {
147 t.Errorf("termios after Restore = %#v, want original %#v", restored, original)
148 }
149}
150
151func TestForegroundProcessGroup(t *testing.T) {
152 master, slave := openPTYPair(t)
153 defer func() { _ = master.Close() }()
154 defer func() { _ = slave.Close() }()
155
156 // No session has been established on this pty (no process ever made it
157 // a controlling terminal via Setsid+Setctty), so the kernel legitimately
158 // has no foreground process group to report and returns ENOTTY. That
159 // still exercises the ioctl path; only an unexpected error is a failure.
160 if _, err := ForegroundProcessGroup(int(slave.Fd())); err != nil && !errors.Is(err, syscall.ENOTTY) {
161 t.Fatalf("ForegroundProcessGroup: %v", err)
162 }
163}