26static void require(
int condition,
const char* operation) {
28 printf(
"PTY-CONTRACT: FAIL %s errno=%d\n", operation, errno);
33static void make_raw(
int descriptor) {
34 struct termios attributes;
35 require(tcgetattr(descriptor, &attributes) == 0,
"tcgetattr");
36 cfmakeraw(&attributes);
37 require(tcsetattr(descriptor, TCSANOW, &attributes) == 0,
"tcsetattr");
40static void exchange_byte(
int writer,
int reader,
char expected,
const char* operation) {
43 require(ioctl(
reader, FIONREAD, &available) == 0 && available == 0,
"empty PTY FIONREAD");
44 require(write(writer, &expected, 1) == 1, operation);
45 require(ioctl(
reader, FIONREAD, &available) == 0 && available == 1,
"queued PTY FIONREAD");
46 require(ioctl(
reader, FIONREAD, &available) == 0 && available == 1,
47 "PTY FIONREAD preserves input");
49 ssize_t amount = read(
reader, &received, 1);
50 require(amount == 1 && received == expected, operation);
51 require(ioctl(
reader, FIONREAD, &available) == 0 && available == 0,
"drained PTY FIONREAD");
54static void read_exact(
int descriptor,
char* buffer,
size_t length,
const char* operation) {
56 while (total < length) {
57 ssize_t amount = read(descriptor, buffer + total, length - total);
58 require(amount > 0, operation);
59 total += (size_t)amount;
63static void test_posix_openpt_contract(
void) {
64 int master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
65 require(master >= 0,
"posix_openpt");
66 require(grantpt(master) == 0,
"grantpt");
69 require(ptsname_r(master, path,
sizeof(path)) == 0,
"ptsname_r");
72 require(ioctl(master, TIOCSPTLCK, &locked) == 0,
"lock PTY slave");
74 int slave = open(path, O_RDWR | O_NOCTTY | O_CLOEXEC);
75 require(slave < 0 && errno == EIO,
"locked PTY slave rejection");
78 require(unlockpt(master) == 0,
"unlockpt");
79 slave = open(path, O_RDWR | O_NOCTTY | O_CLOEXEC);
80 require(slave >= 0,
"unlocked PTY slave");
82 exchange_byte(master, slave,
'm',
"master-to-slave");
83 exchange_byte(slave, master,
's',
"slave-to-master");
89static void test_openpty_contract(
void) {
93 struct winsize size = {.ws_row = 25, .ws_col = 80};
94 require(openpty(&master, &slave, path, NULL, &size) == 0,
"openpty");
95 require(master >= 0 && slave >= 0 && path[0] ==
'/',
"openpty outputs");
97 exchange_byte(master, slave,
'o',
"openpty master-to-slave");
98 exchange_byte(slave, master,
'p',
"openpty slave-to-master");
103static void test_ioctl_contracts(
void) {
106 require(openpty(&master, &slave, NULL, NULL, NULL) == 0,
"ioctl openpty");
110 require(ioctl(slave, TIOCGSID, &session) == -1 && errno == ENOTTY,
"unattached slave TIOCGSID");
112 require(ioctl(master, TIOCGSID, &session) == -1 && errno == ENOTTY,
"unattached master TIOCGSID");
113 require(write(master,
"f", 1) == 1,
"invalid flush input");
115 require(ioctl(slave, TCFLSH, 99) == -1 && errno == EINVAL,
"TCFLSH invalid selector");
117 require(tcflush(slave, -1) == -1 && errno == EINVAL,
"tcflush negative selector");
119 require(read(slave, &received, 1) == 1 && received ==
'f',
"invalid flush preserves input");
123 require(pipe(ready) == 0 && pipe(release) == 0,
"session query pipes");
124 pid_t child = fork();
125 require(child >= 0,
"session query fork");
130 require(setsid() == getpid(),
"session query setsid");
131 require(ioctl(slave, TIOCSCTTY, 0) == 0,
"session query TIOCSCTTY");
132 require(ioctl(slave, TIOCGSID, &session) == 0 && session == getsid(0),
133 "controlling slave TIOCGSID");
135 require(ioctl(slave, TIOCGSID, (
void*)1) == -1 && errno == EFAULT,
"TIOCGSID bad address");
136 require(write(ready[1],
"r", 1) == 1,
"session query ready");
137 read_exact(release[0], &received, 1,
"session query release");
142 read_exact(ready[0], &received, 1,
"session query child ready");
143 require(ioctl(master, TIOCGSID, &session) == 0 && session == child,
144 "master queries slave session");
146 require(ioctl(slave, TIOCGSID, &session) == -1 && errno == ENOTTY,
147 "noncontrolling slave TIOCGSID");
148 require(write(release[1],
"r", 1) == 1,
"session query release child");
150 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
151 "session query child status");
153 require(ioctl(master, TIOCGSID, &session) == -1 && errno == ENOTTY,
154 "TIOCGSID after session exit");
161static void test_bulk_transfer(
int from_master,
int blocking,
int signals) {
164 require(openpty(&master, &slave, NULL, NULL, NULL) == 0,
"bulk openpty");
167 struct termios attributes;
168 require(tcgetattr(slave, &attributes) == 0,
"bulk tcgetattr");
169 attributes.c_lflag |= ISIG;
170 require(tcsetattr(slave, TCSANOW, &attributes) == 0,
"bulk ISIG");
172 int writer = from_master ? master : slave;
173 int reader = from_master ? slave : master;
174 require(fcntl(
reader, F_SETFL, O_NONBLOCK) == 0,
"bulk nonblocking reader");
177 const size_t length = 128 * 1024 + 13;
178 char* payload = malloc(length);
179 require(payload != NULL,
"bulk allocation");
180 for (
size_t i = 0; i < length; ++i) {
181 payload[i] =
' ' + (i % 95);
183 memcpy(payload,
"\033[200~", 6);
184 memcpy(payload + length - 6,
"\033[201~", 6);
189 require(child >= 0,
"bulk fork");
193 while (sent < length) {
194 ssize_t amount = write(writer, payload + sent, length - sent);
198 sent += (size_t)amount;
204 require(fcntl(writer, F_SETFL, O_NONBLOCK) == 0,
"bulk nonblocking writer");
209 while (received < length) {
211 ssize_t amount = write(writer, payload + sent, length - sent);
212 require(amount > 0,
"bulk write progress");
213 sent += (size_t)amount;
215 size_t target = blocking ? length : sent;
216 while (received < target) {
217 struct pollfd descriptor = {.fd =
reader, .events = POLLIN};
218 if (poll(&descriptor, 1, 2000) != 1 || !(descriptor.revents & POLLIN)) {
220 kill(child, SIGKILL);
221 waitpid(child, NULL, 0);
223 printf(
"\nPTY bulk: master=%d blocking=%d signals=%d sent=%zu received=%zu\n", from_master,
224 blocking, signals, sent, received);
225 require(0,
"bulk read readiness / acknowledged bytes missing");
228 size_t wanted = target - received;
229 if (wanted >
sizeof(output)) {
230 wanted =
sizeof(output);
232 ssize_t amount = read(
reader, output, wanted);
233 if (amount < 0 && (errno == EAGAIN || errno == EINTR)) {
236 require(amount > 0,
"bulk read progress");
237 require(!memcmp(output, payload + received, (
size_t)amount),
"bulk byte preservation");
238 received += (size_t)amount;
243 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
244 "bulk child status");
252static void test_fork_openpty_contract(
void) {
255 require(openpty(&master, &slave, NULL, NULL, NULL) == 0,
"fork openpty");
257 pid_t child = fork();
258 require(child >= 0,
"fork openpty fork");
261 const char message[] =
"fork-openpty-ok";
268 char output[
sizeof(
"fork-openpty-ok") - 1];
269 read_exact(master, output,
sizeof(output),
"fork openpty output");
270 require(!memcmp(output,
"fork-openpty-ok",
sizeof(output)),
"fork openpty output contents");
273 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
274 "fork openpty child status");
278static void test_forkpty_contract(
void) {
281 pid_t child = forkpty(&master, path, NULL, NULL);
282 require(child >= 0,
"forkpty");
284 const char message[] =
"forkpty-ok";
291 char output[
sizeof(
"forkpty-ok") - 1];
293 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
294 "forkpty child status");
295 read_exact(master, output,
sizeof(output),
"forkpty output");
296 require(!memcmp(output,
"forkpty-ok",
sizeof(output)),
"forkpty output contents");
300void test_pty_contracts(
void) {
301 printf(
"Testing PTY contracts...");
303 test_posix_openpt_contract();
304 test_openpty_contract();
305 test_ioctl_contracts();
306 test_bulk_transfer(1, 0, 0);
307 test_bulk_transfer(1, 0, 1);
308 test_bulk_transfer(0, 0, 0);
309 test_bulk_transfer(1, 1, 0);
310 test_bulk_transfer(0, 1, 0);
311 test_fork_openpty_contract();
312 test_forkpty_contract();