diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -852,6 +852,8 @@ .. pipe .. + tty + .. .. kqueue libkqueue diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -127,6 +127,7 @@ TESTS_SUBDIRS+= acct TESTS_SUBDIRS+= execve TESTS_SUBDIRS+= pipe +TESTS_SUBDIRS+= tty .include diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/Makefile @@ -0,0 +1,12 @@ +TESTSDIR= ${TESTSBASE}/sys/kern/tty +BINDIR= ${TESTSDIR} + +PLAIN_TESTS_PORCH+= test_canon +PLAIN_TESTS_PORCH+= test_canon_fullbuf +PLAIN_TESTS_PORCH+= test_ncanon +PLAIN_TESTS_PORCH+= test_recanon + +PROGS+= fionread +PROGS+= readsz + +.include diff --git a/tests/sys/kern/tty/fionread.c b/tests/sys/kern/tty/fionread.c new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/fionread.c @@ -0,0 +1,21 @@ +/*- + * Copyright (c) 2024 Kyle Evans + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include +#include + +int +main(void) +{ + int nb; + + assert(ioctl(STDIN_FILENO, FIONREAD, &nb) == 0); + printf("%d", nb); + return (0); +} diff --git a/tests/sys/kern/tty/readsz.c b/tests/sys/kern/tty/readsz.c new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/readsz.c @@ -0,0 +1,130 @@ +/*- + * Copyright (c) 2024 Kyle Evans + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static void +usage(void) +{ + + fprintf(stderr, "usage: %s [-b bytes | -c lines | -e] [-s buffer-size]\n", + getprogname()); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + char *buf; + const char *errstr; + size_t bufsz = 0, reps; + ssize_t ret; + enum { MODE_BYTES, MODE_COUNT, MODE_EOF } mode; + int ch; + + /* + * -b specifies number of bytes. + * -c specifies number of read() calls. + * -e specifies eof (default) + * -s to pass a buffer size + * + * Reading N lines is the same as -c with a high buffer size. + */ + mode = MODE_EOF; + while ((ch = getopt(argc, argv, "b:c:es:")) != -1) { + switch (ch) { + case 'b': + mode = MODE_BYTES; + reps = strtonum(optarg, 0, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + case 'c': + mode = MODE_COUNT; + reps = strtonum(optarg, 1, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + case 'e': + mode = MODE_EOF; + break; + case 's': + bufsz = strtonum(optarg, 1, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + default: + usage(); + } + } + + if (bufsz == 0) { + if (mode == MODE_BYTES) + bufsz = reps; + else + bufsz = LINE_MAX; + } + + buf = malloc(bufsz); + if (buf == NULL) + err(1, "malloc"); + + for (;;) { + size_t readsz; + + /* + * Be careful not to over-read if we're in byte-mode. In every other + * mode, we'll read as much as we can. + */ + if (mode == MODE_BYTES) + readsz = MIN(bufsz, reps); + else + readsz = bufsz; + + ret = read(STDIN_FILENO, buf, readsz); + if (ret == -1 && errno == EINTR) + continue; + if (ret == -1) + err(1, "read"); + if (ret == 0) { + if (mode == MODE_EOF) + return (0); + errx(1, "premature EOF"); + } + + /* Write out what we've got */ + write(STDOUT_FILENO, buf, ret); + + /* + * Bail out if we've hit our metric (byte mode / count mode). + */ + switch (mode) { + case MODE_BYTES: + reps -= ret; + if (reps == 0) + return (0); + break; + case MODE_COUNT: + reps--; + if (reps == 0) + return (0); + break; + default: + break; + } + } + + return (0); +} diff --git a/tests/sys/kern/tty/test_canon.orch b/tests/sys/kern/tty/test_canon.orch new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/test_canon.orch @@ -0,0 +1,102 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +spawn("cat") + +write "Complete\r" +match "Complete\r" + +write "Basic\rIncomplete" +match "Basic\r" + +-- We shouldn't see any of the "Incomplete" line +fail(function() +end) + +match "Incomp" { + callback = function() + exit(1) + end +} + +fail(nil) + +-- Pushing a ^D along should force a flush of the tty, cat(1) will write the +-- result without a trailing newline. +write " line^D" +match "Incomplete line$" + +-- Erase! +write "Dog^H^D" +match "Do$" + +-- More erase! +write "Cat Dog^W^D" +match "Cat $" + +write "^D" +eof() + +local function fionread_test(str, expected) + spawn("fionread") + + write(str) + match(expected) +end + +-- Incomplete line +fionread_test("Hello", "0") +-- VEOF does not count +fionread_test("Hello^D", "5") +-- VEOF still doesn't count, even if the next line is an extra VEOF later +fionread_test("Hello^D^D", "5") +-- read(2) definitely won't return the second incomplete line +fionread_test("Hello^Dther", "5") +-- read(2) also won't return a second complete line at once +fionread_test("Hello^Dthere^D", "5") +-- Finally, send a VEOF to terminate a blank line and signal EOF in read(2) +fionread_test("^D", "0") + +-- \r will instead show up in the input stream to the application, so we must +-- make sure those are counted where VEOF generally wouldn't be. +fionread_test("Hello\r", "6") +fionread_test("Hello\rther", "6") +fionread_test("Hello\rthere\r", "6") +fionread_test("\r", "1") + +local function readsz_test(str, arg, expected) + spawn("readsz", table.unpack(arg)) + + if type(str) == "table" then + assert(#str == 2) + write(str[1]) + release() + + -- Give readsz a chance to consume the partial input before we send more + -- along. + sleep(1) + write(str[2]) + else + write(str) + end + match(expected) +end + +readsz_test("partial", {"-b", 3}, "^$") +readsz_test("partial^D", {"-b", 3}, "^par$") +readsz_test("partial^D", {"-c", 1}, "^partial$") +for s = 1, #"partial" do + readsz_test("partial^D", {"-s", s}, "^partial$") +end +-- Send part of the line, release and pause, then finish it. +readsz_test({"par", "tial^D"}, {"-c", 1}, "^partial$") +-- line is incomplete, so we'll just see the "partial" even if we want two +readsz_test("partial^Dline", {"-c", 2}, "^partial$") +readsz_test("partial^Dline^D", {"-c", 1}, "^partial$") +readsz_test("partial^Dline^D", {"-c", 2}, "^partialline$") diff --git a/tests/sys/kern/tty/test_canon_fullbuf.orch b/tests/sys/kern/tty/test_canon_fullbuf.orch new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/test_canon_fullbuf.orch @@ -0,0 +1,23 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local TTYINQ_DATASIZE = 128 +local scream = string.rep("A", TTYINQ_DATASIZE - 1) + +spawn("cat") + +-- Fill up a whole block with screaming + VEOF +write(scream .. "^D") +match(scream .. "$") + +scream = scream .. "A" + +-- Now fill up the next block, but spill the VEOF over to a third block. +write(scream .. "^D") +match(scream .. "$") diff --git a/tests/sys/kern/tty/test_ncanon.orch b/tests/sys/kern/tty/test_ncanon.orch new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/test_ncanon.orch @@ -0,0 +1,39 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local function spawn_one(...) + spawn(...) + + stty("lflag", 0, tty.lflag.ICANON) +end + +-- We can send one byte... +spawn_one("readsz", "-c", 1) +write "H" +match "^H$" + +-- or many. +spawn_one("readsz", "-c", 1) +write "Hello" +match "^Hello$" + +-- VEOF is a normal character here, passed through as-is. +spawn_one("readsz", "-c", 1) +write "Hello^D" +match "^Hello\x04$" +spawn_one("readsz", "-c", 1) +write "^D" +match "^\x04$" + +-- Confirm that FIONREAD agrees that VEOF will be returned, even if it was sent +-- while the tty was still in canonical mode. +spawn("fionread") +write "^D" +stty("lflag", 0, tty.lflag.ICANON) +match "^1$" diff --git a/tests/sys/kern/tty/test_recanon.orch b/tests/sys/kern/tty/test_recanon.orch new file mode 100644 --- /dev/null +++ b/tests/sys/kern/tty/test_recanon.orch @@ -0,0 +1,90 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local TTYINQ_DATASIZE = 128 +local scream = string.rep("A", TTYINQ_DATASIZE - 1) + +local function ncanon() + stty("lflag", nil, tty.lflag.ICANON) +end + +local function canon() + stty("lflag", tty.lflag.ICANON) +end + +spawn("readsz", "-e") +ncanon() + +-- Fill up a whole block with screaming + VEOF; when it gets recanonicalized, +-- the next line should be pointing to the beginning of the next block. +write(scream .. "^D") + +canon() +match(scream .. "$") + +-- The same as above, but spilling VEOF over to the next block. +spawn("readsz", "-e") +ncanon() + +write(scream .. "A^D") + +canon() +match(scream .. "A$") + +-- We'll do it again, except with one character spilled over to the next block +-- before we recanonicalize. We should then have the scream, followed by a +-- partial line containing the spill over. +spawn("cat") +ncanon() + +write(scream .. "^DZ") + +canon() +match(scream .. "$") + +-- Sending "B^D" should give us "ZB" to make sure that we didn't lose anything +-- at the beginning of the next block. + +write("B^D") +match("^ZB$") + +-- Next we'll VEOF at the beginning. +spawn("readsz", "-e") +ncanon() + +write("^D") +match("^$") + +-- Finally, we'll trigger recanonicalization with an empty buffer. This one is +-- just about avoiding a panic. +spawn("true") + +ncanon() +canon() +release() +eof() + +spawn("readsz", "-c", "1") + +write("Test^Dfoo") +ncanon() + +match("^Test\x04foo$") + +-- Finally, swap VEOF out with ^F; before recent changes, we would remain +-- canonicalized at Test^D and the kernel would block on it unless a short +-- buffer was used since VEOF would not appear within the canonicalized bit. +spawn("readsz", "-c", 1) + +write("Test^DLine^F") +stty("cc", { + VEOF = "^F" +}) + +match("^Test\x04Line$")