git: 096c39fae4ad - main - tests: kern: add some porch(1)-based tty tests

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Mon, 21 Oct 2024 01:32:43 UTC
The branch main has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=096c39fae4ad5135a317925d8749b7d83f65ebf8

commit 096c39fae4ad5135a317925d8749b7d83f65ebf8
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2024-10-21 01:31:59 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2024-10-21 01:32:31 +0000

    tests: kern: add some porch(1)-based tty tests
    
    If sysutils/porch is installed, we'll do some basic testing of tty
    behavior.  The existing tests primarily cover ICANON-related processing
    and corner cases that have been fixed somewhat recently, but I
    anticipate growing this out a bit in due time.
    
    Reviewed by:    ngie
    Differential Revision:  https://reviews.freebsd.org/D46806
---
 etc/mtree/BSD.tests.dist                   |   2 +
 tests/sys/kern/Makefile                    |   1 +
 tests/sys/kern/tty/Makefile                |  12 +++
 tests/sys/kern/tty/fionread.c              |  21 +++++
 tests/sys/kern/tty/readsz.c                | 130 +++++++++++++++++++++++++++++
 tests/sys/kern/tty/test_canon.orch         | 102 ++++++++++++++++++++++
 tests/sys/kern/tty/test_canon_fullbuf.orch |  23 +++++
 tests/sys/kern/tty/test_ncanon.orch        |  39 +++++++++
 tests/sys/kern/tty/test_recanon.orch       |  90 ++++++++++++++++++++
 9 files changed, 420 insertions(+)

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 8cac5e8d55e2..221e4b32a89b 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -854,6 +854,8 @@
             ..
             pipe
             ..
+            tty
+            ..
         ..
         kqueue
             libkqueue
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index e334ff64da41..933c1c9aa10e 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -127,6 +127,7 @@ WARNS?=	3
 TESTS_SUBDIRS+=	acct
 TESTS_SUBDIRS+=	execve
 TESTS_SUBDIRS+=	pipe
+TESTS_SUBDIRS+=	tty
 
 .include <netbsd-tests.test.mk>
 
diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile
new file mode 100644
index 000000000000..c362793a8b64
--- /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 <bsd.test.mk>
diff --git a/tests/sys/kern/tty/fionread.c b/tests/sys/kern/tty/fionread.c
new file mode 100644
index 000000000000..929d613f883b
--- /dev/null
+++ b/tests/sys/kern/tty/fionread.c
@@ -0,0 +1,21 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/ioctl.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <unistd.h>
+
+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
index 000000000000..95dafa02472f
--- /dev/null
+++ b/tests/sys/kern/tty/readsz.c
@@ -0,0 +1,130 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+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
index 000000000000..28018edfdcd6
--- /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 <kevans@FreeBSD.org>
+--
+-- 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
index 000000000000..1833703e4f45
--- /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 <kevans@FreeBSD.org>
+--
+-- 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
index 000000000000..14a34d82fa9a
--- /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 <kevans@FreeBSD.org>
+--
+-- 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
index 000000000000..e3943495ca5d
--- /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 <kevans@FreeBSD.org>
+--
+-- 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$")