[hackers] [sbase][PATCH] Add test framework with a test for tty(1)

From: Mattias Andrée <maandree_AT_kth.se>
Date: Sat, 7 Jul 2018 23:26:29 +0200

Signed-off-by: Mattias Andrée <maandree_AT_kth.se>
---
 Makefile      |  20 +-
 test-common.c | 823 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test-common.h | 190 ++++++++++++++
 tty.test.c    |  26 ++
 4 files changed, 1055 insertions(+), 4 deletions(-)
 create mode 100644 test-common.c
 create mode 100644 test-common.h
 create mode 100644 tty.test.c
diff --git a/Makefile b/Makefile
index 0e421e7..005cf13 100644
--- a/Makefile
+++ b/Makefile
_AT_@ -1,7 +1,7 @@
 include config.mk
 
 .SUFFIXES:
-.SUFFIXES: .o .c
+.SUFFIXES: .test .test.o .o .c
 
 HDR =\
 	arg.h\
_AT_@ -19,7 +19,8 @@ HDR =\
 	sha512-256.h\
 	text.h\
 	utf.h\
-	util.h
+	util.h\
+	test-common.h
 
 LIBUTF = libutf.a
 LIBUTFSRC =\
_AT_@ -181,9 +182,12 @@ BIN =\
 	xinstall\
 	yes
 
+TEST =\
+	tty.test
+
 LIBUTFOBJ = $(LIBUTFSRC:.c=.o)
 LIBUTILOBJ = $(LIBUTILSRC:.c=.o)
-OBJ = $(BIN:=.o) $(LIBUTFOBJ) $(LIBUTILOBJ)
+OBJ = $(BIN:=.o) $(TEST:=.o) test-common.o $(LIBUTFOBJ) $(LIBUTILOBJ)
 SRC = $(BIN:=.c)
 MAN = $(BIN:=.1)
 
_AT_@ -193,12 +197,17 @@ $(BIN): $(LIB) $(@:=.o)
 
 $(OBJ): $(HDR) config.mk
 
+$(TEST): $(_AT_:=.o) test-common.o
+
 .o:
 	$(CC) $(LDFLAGS) -o $_AT_ $< $(LIB)
 
 .c.o:
 	$(CC) $(CFLAGS) $(CPPFLAGS) -o $_AT_ -c $<
 
+.test.o.test:
+	$(CC) $(LDFLAGS) -o $_AT_ $< test-common.o
+
 $(LIBUTF): $(LIBUTFOBJ)
 	$(AR) rc $_AT_ $?
 	$(RANLIB) $_AT_
_AT_@ -212,6 +221,9 @@ getconf.o: getconf.h
 getconf.h: getconf.sh
 	./getconf.sh > $_AT_
 
+check: $(TEST) $(BIN)
+	_AT_set -e; for f in $(TEST); do echo ./$$f; ./$$f; done
+
 install: all
 	mkdir -p $(DESTDIR)$(PREFIX)/bin
 	cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
_AT_@ -271,7 +283,7 @@ sbase-box-uninstall: uninstall
 	cd $(DESTDIR)$(PREFIX)/bin && rm -f sbase-box
 
 clean:
-	rm -f $(BIN) $(OBJ) $(LIB) sbase-box sbase-$(VERSION).tar.gz
+	rm -f $(BIN) $(TEST) $(OBJ) $(LIB) sbase-box sbase-$(VERSION).tar.gz
 	rm -f getconf.h
 
 .PHONY: all install uninstall dist sbase-box sbase-box-install sbase-box-uninstall clean
diff --git a/test-common.c b/test-common.c
new file mode 100644
index 0000000..458b094
--- /dev/null
+++ b/test-common.c
_AT_@ -0,0 +1,823 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+struct Counter {
+	const char *name;
+	size_t value;
+};
+
+const char *test_file = NULL;
+int test_line = 0;
+int main_ret = 0;
+int timeout = 10;
+int pdeath_sig = SIGINT;
+void (*atfork)(void) = NULL;
+
+static struct Counter counters[16];
+static size_t ncounters = 0;
+static pid_t async_pids[1024];
+static size_t async_npids = 0;
+
+static void
+eperror(const char *prefix)
+{
+	perror(prefix);
+	fflush(stderr);
+	exit(64);
+}
+
+struct Process *
+stdin_text(struct Process *proc, char *s)
+{
+	proc->input[0].data = s;
+	proc->input[0].flags &= ~IO_STREAM_BINARY;
+	return proc;
+}
+
+struct Process *
+stdin_bin(struct Process *proc, char *s, size_t n)
+{
+	proc->input[0].data = s;
+	proc->input[0].len = n;
+	proc->input[0].flags |= IO_STREAM_BINARY;
+	return proc;
+}
+
+struct Process *
+stdin_fds(struct Process *proc, int input_fd, int output_fd)
+{
+	proc->input[0].flags &= ~IO_STREAM_DATA;
+	proc->input[0].input_fd = input_fd;
+	proc->input[0].output_fd = output_fd;
+	return proc;
+}
+
+struct Process *
+stdin_type(struct Process *proc, int type)
+{
+	proc->input[0].flags &= ~IO_STREAM_CREATE_MASK;
+	proc->input[0].flags |= type;
+	return proc;
+}
+
+struct Process *
+stdout_fds(struct Process *proc, int input_fd, int output_fd)
+{
+	proc->output[0].flags &= ~IO_STREAM_DATA;
+	proc->output[0].input_fd = input_fd;
+	proc->output[0].output_fd = output_fd;
+	return proc;
+}
+
+struct Process *
+stdout_type(struct Process *proc, int type)
+{
+	proc->output[0].flags &= ~IO_STREAM_CREATE_MASK;
+	proc->output[0].flags |= type;
+	return proc;
+}
+
+struct Process *
+stderr_fds(struct Process *proc, int input_fd, int output_fd)
+{
+	proc->output[1].flags &= ~IO_STREAM_DATA;
+	proc->output[1].input_fd = input_fd;
+	proc->output[1].output_fd = output_fd;
+	return proc;
+}
+
+struct Process *
+stderr_type(struct Process *proc, int type)
+{
+	proc->output[1].flags &= ~IO_STREAM_CREATE_MASK;
+	proc->output[1].flags |= type;
+	return proc;
+}
+
+struct Process *
+set_preexec(struct Process *proc, void (*preexec)(struct Process *))
+{
+	proc->preexec = preexec;
+	return proc;
+}
+
+struct Process *
+set_async(struct Process *proc)
+{
+	proc->flags |= PROCESS_ASYNC;
+	return proc;
+}
+
+struct Process *
+set_setsid(struct Process *proc)
+{
+	proc->flags |= PROCESS_SETSID;
+	return proc;
+}
+
+void
+push_counter(const char *name)
+{
+	if (ncounters == ELEMSOF(counters)) {
+		fprintf(stderr, "too many counters\n");
+		fflush(stderr);
+		exit(64);
+	}
+	counters[ncounters++].name = name;
+}
+
+void
+set_counter(size_t value)
+{
+	if (!ncounters) {
+		fprintf(stderr, "no counters have been pushed\n");
+		fflush(stderr);
+		exit(64);
+	}
+	counters[ncounters - 1].value = value;
+}
+
+void
+pop_counter(void)
+{
+	if (!ncounters) {
+		fprintf(stderr, "all counters have already been popped\n");
+		fflush(stderr);
+		exit(64);
+	}
+	ncounters--;
+}
+
+pid_t
+async_fork(int *retp)
+{
+	pid_t pid;
+
+	if (!retp)
+		retp = &(int){0};
+
+	if (async_npids == ELEMSOF(async_pids))
+		*retp = async_join();
+
+	switch ((pid = fork())) {
+	case -1:
+		eperror("fork");
+	case 0:
+		if (atfork)
+			atfork();
+		async_npids = 0;
+#ifdef PR_SET_PDEATHSIG
+		prctl(PR_SET_PDEATHSIG, pdeath_sig);
+#endif
+		alarm(timeout);
+		break;
+	default:
+		async_pids[async_npids++] = pid;
+		break;
+	}
+
+	return pid;
+}
+
+int
+async_join(void)
+{
+	int ret = 0, status;
+
+	while (async_npids--) {
+		if (waitpid(async_pids[async_npids], &status, 0) != async_pids[async_npids])
+			eperror("waitpid");
+		ret |= WIFEXITED(status) ? WEXITSTATUS(status) : 64;
+	}
+
+	main_ret |= status;
+	async_npids = 0;
+	return ret;
+}
+
+void
+start_process(struct Process *proc)
+{
+	struct InputStream *in;
+	struct OutputStream *out;
+	int exec_sig_pipe[2];
+	int fd, minfd = 3, fds[2], status;
+	size_t i;
+	ssize_t r;
+	pid_t pid;
+
+	if (!proc->file)
+		proc->file = proc->argv[0];
+
+	for (i = 0; i < proc->ninput; i++)
+		if (minfd <= proc->input[i].fd)
+			minfd = proc->input[i].fd + 1;
+	for (i = 0; i < proc->noutput; i++)
+		if (minfd <= proc->output[i].fd)
+			minfd = proc->output[i].fd + 1;
+
+	if (pipe(exec_sig_pipe))
+		eperror("pipe");
+
+	for (i = 0; i < 2; i++) {
+		fd = fcntl(exec_sig_pipe[i], F_DUPFD_CLOEXEC, minfd);
+		if (fd < 0)
+			eperror("fcntl F_DUPFD_CLOEXEC");
+		close(exec_sig_pipe[i]);
+		exec_sig_pipe[i] = fd;
+	}
+
+	for (i = 0; i < proc->ninput; i++) {
+		in = &proc->input[i];
+		in->name = NULL;
+		if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_PIPE) {
+			if (pipe(fds))
+				eperror("pipe");
+		} else if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_STREAM) {
+			if (socketpair(PF_LOCAL, SOCK_STREAM, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_STREAM");
+		} else if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_DGRAM) {
+			if (socketpair(PF_LOCAL, SOCK_DGRAM, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_DGRAM");
+		} else if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_SEQPACKET) {
+			if (socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_SEQPACKET");
+		} else if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_TTY_NOCTTY) {
+			in->name = strdup(openpt(0, fds));
+			if (!in->name)
+				eperror("strdup");
+			if (!(in->flags & IO_STREAM_MASTER))
+				fd = fds[0], fds[0] = fds[1], fds[1] = fd;
+		} else if ((in->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_TTY_CTTY) {
+			in->name = strdup(openpt(1, fds));
+			if (!in->name)
+				eperror("strdup");
+			if (!(in->flags & IO_STREAM_MASTER))
+				fd = fds[0], fds[0] = fds[1], fds[1] = fd;
+		} else {
+			goto precreated_input;
+		}
+		in->input_fd = fds[0];
+		in->output_fd = fds[1];
+	precreated_input:
+		if (in->flags & IO_STREAM_DATA) {
+			switch (pid = fork()) {
+			case -1:
+				eperror("fork");
+			case 0:
+				break;
+			default:
+				if (waitpid(pid, &status, 0) != pid)
+					eperror("waitpid");
+				if (status)
+					exit(64);
+				if (!(in->flags & IO_STREAM_KEEP_OPEN))
+					close(in->output_fd);
+				continue;
+			}
+			if (atfork)
+				atfork();
+			dealloc_process(proc);
+			close(in->input_fd);
+			close(exec_sig_pipe[0]);
+			close(exec_sig_pipe[1]);
+			while (i--) {
+				if (proc->input[i].input_fd > 0)
+					close(proc->input[i].input_fd);
+				if (proc->input[i].output_fd > 0)
+					close(proc->input[i].output_fd);
+			}
+			switch (pid = fork()) {
+			case -1:
+				eperror("fork");
+			case 0:
+				exit(0);
+			default:
+				break;
+			}
+			while (in->len) {
+				r = write(in->output_fd, in->data, in->len);
+				if (r < 0)
+					eperror("write");
+				in->data += r;
+				in->len -= (size_t)r;
+			}
+			close(in->output_fd);
+			exit(0);
+		}
+	}
+
+	for (i = 0; i < proc->noutput; i++) {
+		out = &proc->output[i];
+		out->name = NULL;
+		if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_PIPE) {
+			if (pipe(fds))
+				eperror("pipe");
+		} else if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_STREAM) {
+			if (socketpair(PF_LOCAL, SOCK_STREAM, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_STREAM");
+		} else if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_DGRAM) {
+			if (socketpair(PF_LOCAL, SOCK_DGRAM, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_DGRAM");
+		} else if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_SOCK_SEQPACKET) {
+			if (socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, fds))
+				eperror("socketpair PF_LOCAL SOCK_SEQPACKET");
+		} else if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_TTY_NOCTTY) {
+			out->name = strdup(openpt(0, fds));
+			if (!out->name)
+				eperror("strdup");
+			if (out->flags & IO_STREAM_MASTER)
+				fd = fds[0], fds[0] = fds[1], fds[1] = fd;
+		} else if ((out->flags & IO_STREAM_CREATE_MASK) == IO_STREAM_TTY_CTTY) {
+			out->name = strdup(openpt(1, fds));
+			if (!out->name)
+				eperror("strdup");
+			if (out->flags & IO_STREAM_MASTER)
+				fd = fds[0], fds[0] = fds[1], fds[1] = fd;
+		} else {
+			continue;
+		}
+		out->input_fd = fds[0];
+		out->output_fd = fds[1];
+	}
+
+	switch ((proc->pid = fork())) {
+	case -1:
+		eperror("fork");
+	case 0:
+		break;
+	default:
+		if (atfork)
+			atfork();
+		for (i = 0; i < proc->ninput; i++)
+			close(proc->input[i].input_fd);
+		for (i = 0; i < proc->noutput; i++)
+			close(proc->output[i].output_fd);
+		close(exec_sig_pipe[1]);
+		read(exec_sig_pipe[0], &(char){0}, 1);
+		if (clock_gettime(CLOCK_MONOTONIC, &proc->start_time))
+			eperror("clock_gettime CLOCK_MONOTONIC");
+		close(exec_sig_pipe[0]);
+		return;
+	}
+
+#ifdef PR_SET_PDEATHSIG
+	prctl(PR_SET_PDEATHSIG, proc->pdeath_sig);
+#endif
+
+	for (i = 0; i < proc->ninput; i++) {
+		fd = fcntl(proc->input[i].input_fd, F_DUPFD, minfd);
+		if (fd < 0)
+			eperror("fcntl F_DUPFD");
+		close(proc->input[i].input_fd);
+		proc->input[i].input_fd = fd;
+	}
+
+	for (i = 0; i < proc->noutput; i++) {
+		fd = fcntl(proc->output[i].output_fd, F_DUPFD, minfd);
+		if (fd < 0)
+			eperror("fcntl F_DUPFD");
+		close(proc->output[i].output_fd);
+		proc->output[i].output_fd = fd;
+	}
+
+	for (i = 0; i < proc->ninput; i++) {
+		if (dup2(proc->input[i].input_fd, proc->input[i].fd) != proc->input[i].fd)
+			eperror("dup2");
+		close(proc->input[i].input_fd);
+	}
+
+	for (i = 0; i < proc->noutput; i++) {
+		if (dup2(proc->output[i].output_fd, proc->output[i].fd) != proc->output[i].fd)
+			eperror("dup2");
+		close(proc->output[i].output_fd);
+	}
+
+	if (proc->flags & PROCESS_SETSID)
+		if (setsid() < 0)
+			eperror("setsid");
+
+	alarm(proc->timeout);
+
+	if (proc->preexec)
+		proc->preexec(proc);
+
+	dealloc_process(proc);
+	execvp(proc->file, (void *)proc->argv);
+	fprintf(stderr, "exec %s: %s\n", proc->file, strerror(errno));
+	fflush(stderr);
+	exit(64);
+}
+
+void
+wait_process(struct Process *proc)
+{
+	struct pollfd *pfds = NULL;
+	struct OutputStream *out;
+	size_t i, j, p, n;
+	ssize_t r;
+
+	if (!proc->noutput)
+		goto no_output;
+
+	n = proc->noutput;
+	pfds = calloc(n, sizeof(*pfds));
+	if (!pfds)
+		eperror("calloc");
+
+	for (i = j = 0; i < n; j++) {
+		if (proc->output[j].flags & IO_STREAM_DATA) {
+			pfds[i].fd = proc->output[j].input_fd;
+			pfds[i].events = POLLIN;
+			proc->output[j].data = NULL;
+			proc->output[j].len = 0;
+			proc->output[j].size = 0;
+			proc->output[j].error = 0;
+			i++;
+		} else {
+			n--;
+		}
+	}
+
+	while (n) {
+		r = (ssize_t)poll(pfds, n, -1);
+		if (r < 0)
+			eperror("poll");
+		for (j = 0; j < n;) {
+			for (i = 0; proc->output[i].input_fd != pfds[j].fd; i++);
+			out = &proc->output[i];
+			if (out->len + 512 >= out->size) {
+				out->data = realloc(out->data, out->size += 8096);
+				if (!out->data)
+					eperror("realloc");
+			}
+			errno = 0;
+			r = read(out->input_fd, &out->data[out->len], out->size - out->len);
+			if (r <= 0)
+				goto close_output;
+			p = out->len;
+			out->len += (size_t)r;
+			if (proc->output[i].head) {
+				for (; p < out->len; p++) {
+					if (proc->output[i].data[p] == '\n') {
+						if (!--proc->output[i].head) {
+							out->len = p + 1;
+							goto close_output;
+						}
+					}
+				}
+			}
+			j++;
+			continue;
+		close_output:
+			out->error = errno;
+			close(out->input_fd);
+			if (memchr(out->data, '\0', out->len))
+				proc->output[i].flags |= IO_STREAM_BINARY;
+			out->size = out->len + 1;
+			out->data = realloc(out->data, out->size);
+			if (!out->data)
+				eperror("realloc");
+			out->data[out->len] = '\0';
+			memmove(&pfds[j], &pfds[j + 1], (--n - j) * sizeof(*pfds));
+		}
+	}
+
+no_output:
+	if (waitpid(proc->pid, &proc->status, 0) != proc->pid)
+		eperror("waitpid");
+
+	if (clock_gettime(CLOCK_MONOTONIC, &proc->exit_time))
+		eperror("clock_gettime CLOCK_MONOTONIC");
+	proc->runtime.tv_sec = proc->exit_time.tv_sec - proc->start_time.tv_sec;
+	proc->runtime.tv_nsec = proc->exit_time.tv_nsec - proc->start_time.tv_nsec;
+	if (proc->runtime.tv_nsec < 0) {
+		proc->runtime.tv_sec -= 1;
+		proc->runtime.tv_nsec += 1000000000L;
+	}
+
+	for (i = 0; i < proc->ninput; i++)
+		if (proc->input[i].flags & IO_STREAM_KEEP_OPEN)
+			close(proc->input[i].output_fd);
+
+	free(pfds);
+}
+
+void
+dealloc_process(struct Process *proc)
+{
+	size_t i;
+	for (i = 0; i < proc->ninput; i++)
+		free(proc->input[i].name);
+	for (i = 0; i < proc->noutput; i++) {
+		if (proc->output[i].flags & IO_STREAM_DATA)
+			free(proc->output[i].data);
+		free(proc->output[i].name);
+	}
+}
+
+const char *
+openpt(int ctty, int master_slave[2])
+{
+	const char *slave;
+	master_slave[0] = posix_openpt(O_RDWR | O_NOCTTY);
+	if (master_slave[0] < 0)
+		eperror("posix_openpt");
+	if (grantpt(master_slave[0]) < 0)
+		eperror("grantpt");
+	if (unlockpt(master_slave[0]) < 0)
+		eperror("unlockpt");
+	slave = ptsname(master_slave[0]);
+	if (!slave)
+		eperror("ptsname");
+	master_slave[1] = open(slave, O_RDWR | (ctty ? 0 : O_NOCTTY));
+	if (master_slave[1] < 0)
+		eperror("open");
+	return slave;
+}
+
+static const char *
+test_str(void)
+{
+	static char buf[8096];
+	char *p = buf;
+	size_t i;
+
+	p += sprintf(buf, "test at %s:%i", test_file, test_line);
+	switch (ncounters) {
+	case 0:
+		break;
+	case 1:
+		p += sprintf(p, ", with %s=%zu, ", counters[0].name, counters[0].value);
+		break;
+	case 2:
+		p += sprintf(p, ", with %s=%zu and %s=%zu, ",
+		             counters[0].name, counters[0].value, counters[1].name, counters[1].value);
+		break;
+	default:
+		p = stpcpy(p, ", with");
+		for (i = 0; i < ncounters; i++) {
+			if (i == ncounters - 1)
+				p = stpcpy(p, "and ");
+			p += sprintf(p, "%s=%zu, ", counters[i].name, counters[i].value);
+		}
+		break;
+	}
+
+	return buf;
+}
+
+int
+check_runtime(struct Process *proc, double min, double max)
+{
+	double dur;
+	int ret;
+
+	dur = proc->runtime.tv_nsec;
+	dur /= 1000000000.;
+	dur += proc->runtime.tv_sec;
+
+	main_ret |= ret = (min <= dur && dur <= max);
+	if (ret) {
+		fprintf(stderr, "%s failed: runtime was %lfs, expected between %lf and %lf\n",
+		        test_str(), dur, min, dur);
+	}
+
+	return ret;
+}
+
+static int
+check_exit_(struct Process *proc, unsigned int *testp, va_list ap)
+{
+	int ok = 0, ret, min, max, value, status = proc->status;
+	size_t alts = 0;
+	char expected[8096];
+	char *p = expected;
+
+	goto beginning;
+	for (;; alts += 1) {
+		*testp = va_arg(ap, unsigned int);
+	beginning:
+		switch (*testp) {
+		case EXIT_VALUE:
+			value = va_arg(ap, int);
+			ok = WIFEXITED(status) && WEXITSTATUS(status) == value;
+			p += sprintf(p, ", exitstatus=%i", value);
+			break;
+		case EXIT_RANGE:
+			min = va_arg(ap, int);
+			max = va_arg(ap, int);
+			ok = WIFEXITED(status) && min <= WEXITSTATUS(status) && WEXITSTATUS(status) <= max;
+			p += sprintf(p, ", exitstatus=%i..%i", min, max);
+			break;
+		case SIGNAL_VALUE:
+			value = va_arg(ap, int);
+			ok = WIFSTOPPED(status) && WTERMSIG(status) == value;
+			p += sprintf(p, ", termsig=%i", value);
+			break;
+		case SIGNAL_RANGE:
+			min = va_arg(ap, int);
+			max = va_arg(ap, int);
+			ok = WIFSTOPPED(status) && min <= WTERMSIG(status) && WTERMSIG(status) <= max;
+			p += sprintf(p, ", termsig=%i..%i", min, max);
+			break;
+		default:
+			goto stop;
+		}
+	}
+stop:
+
+	if (!ok) {
+		fprintf(stderr, "%s failed: exit status was %s=%i, expected %s%s\n",
+		        test_str(), WIFEXITED(status) ? "exitstatus" : WIFSTOPPED(status) ? "termsig" : "unknown",
+		        WIFEXITED(status) ? WEXITSTATUS(status) : WIFSTOPPED(status) ? WTERMSIG(status) : status,
+		        alts == 1 ? "" : "either of: ", &expected[2]);
+	}
+
+	main_ret |= ret = !ok;
+	return ret;
+}
+
+int
+check_exit(struct Process *proc, ...)
+{
+	unsigned int test;
+	va_list ap;
+	va_start(ap, proc);
+	test = va_arg(ap, unsigned int);
+	return check_exit_(proc, &test, ap);
+	va_end(ap);
+}
+
+static int
+check_output_test(const char *data, size_t len, struct OutputStream *s, int flags)
+{
+	int beginning = (flags & OUTPUT_BEGINNING) == OUTPUT_BEGINNING;
+	int contains = (flags & OUTPUT_CONTAINS) == OUTPUT_CONTAINS;
+	int anycase = (flags & OUTPUT_ANYCASE) == OUTPUT_ANYCASE;
+	int at_beginning = 1;
+	size_t i, off, off_end;
+
+	if (len > s->len)
+		return 0;
+
+	off_end = contains ? s->len - len : 1;
+	for (off = 0; off < off_end; at_beginning = (s->data[off] == '\n'), off++) {
+		if (beginning && !at_beginning)
+			continue;
+		if (!anycase) {
+			for (i = 0; i < len; i++)
+				if (data[i] != s->data[i + off])
+					break;
+		} else {
+			for (i = 0; i < len; i++)
+				if (tolower(data[i]) != tolower(s->data[i + off]))
+					break;
+		}
+		if (i != len)
+			continue;
+		if (beginning || i + off == s->len)
+			return 1;
+	}
+
+	return 0;
+}
+
+static int
+check_output_(struct OutputStream *s, unsigned int *testp, va_list ap)
+{
+	char fd_str[1024];
+	int ok = 0, ret;
+	const char *data;
+	size_t len;
+
+	goto beginning;
+	for (;;) {
+		*testp = va_arg(ap, unsigned int);
+	beginning:
+		if ((*testp & CHECK_OP_MASK) != CHECK_OP_SOUT)
+			break;
+		if (*testp == OUTPUT_ERROR) {
+			ok = s->error == va_arg(ap, int);
+			continue;
+		}
+		do {
+			data = va_arg(ap, const char *);
+			if (!data)
+				break;
+			if ((*testp & OUTPUT_BINARY) == OUTPUT_BINARY)
+				len = va_arg(ap, size_t);
+			else
+				len = strlen(data);
+			if ((*testp & OUTPUT_TEXT_ONLY) == OUTPUT_TEXT_ONLY)
+				if (s->flags & IO_STREAM_BINARY)
+					continue;
+			ok |= check_output_test(data, len, s, *testp);
+		} while ((*testp & OUTPUT_MULTIPLE) == OUTPUT_MULTIPLE);
+	}
+
+	if (!ok) {
+		switch (s->fd) {
+		case STDIN_FILENO:
+			stpcpy(fd_str, "stdin");
+			break;
+		case STDOUT_FILENO:
+			stpcpy(fd_str, "stdout");
+			break;
+		case STDERR_FILENO:
+			stpcpy(fd_str, "stderr");
+			break;
+		default:
+			sprintf(fd_str, "fd %i", s->fd);
+			break;
+		}
+
+		fprintf(stderr, "%s failed: output to %s did not match expectation, was ", test_str(), fd_str);
+		if (s->flags & IO_STREAM_BINARY)
+			fprintf(stderr, "binary of length %zu\n", s->len);
+		else
+			fprintf(stderr, "“%s”\n", s->data);
+	}
+
+	main_ret |= ret = !ok;
+	return ret;
+}
+
+int
+check_output(struct OutputStream *s, ...)
+{
+	unsigned int test;
+	va_list ap;
+	va_start(ap, s);
+	test = va_arg(ap, unsigned int);
+	return check_output_(s, &test, ap);
+	va_end(ap);
+}
+
+int
+check(struct Process *proc, ...)
+{
+	unsigned int test;
+	double min, max;
+	va_list ap;
+	int fd, r, ret = 0;
+	struct OutputStream *out;
+	int (*callback)(struct Process *);
+
+	if (!(proc->flags & PROCESS_ASYNC))
+		start_process(proc);
+	wait_process(proc);
+
+	va_start(ap, proc);
+
+	for (;;) {
+		test = va_arg(ap, unsigned int);
+	again:
+		switch (test & CHECK_OP_MASK) {
+		case CHECK_OP_EXIT:
+			ret |= check_exit_(proc, &test, ap);
+			goto again;
+		case CHECK_OP_TIME:
+			min = va_arg(ap, double);
+			max = va_arg(ap, double);
+			ret |= check_runtime(proc, min, max);
+			break;
+		case CHECK_OP_SOUT:
+		case CHECK_OP_OUTF:
+			if ((test & CHECK_OP_MASK) == CHECK_OP_SOUT) {
+				fd = STDOUT_FILENO;
+			} else {
+				switch (test) {
+				case CHECK_STDOUT:
+					fd = STDOUT_FILENO;
+					break;
+				case CHECK_STDERR:
+					fd = STDERR_FILENO;
+					break;
+				default:
+					fd = va_arg(ap, int);
+					break;
+				}
+				test = va_arg(ap, unsigned int);
+			}
+			for (out = proc->output; out->fd != fd; out++);
+			fflush(stderr);
+			ret |= check_output_(out, &test, ap);
+			goto again;
+		case CHECK_OP_CALL:
+			callback = va_arg(ap, int (*)(struct Process *));
+			ret |= r = callback(proc);
+			main_ret |= r;
+			if (r < 0)
+				goto stop;
+			break;
+		default:
+			goto stop;
+		}
+	}
+stop:
+
+	va_end(ap);
+	dealloc_process(proc);
+	return ret;
+}
diff --git a/test-common.h b/test-common.h
new file mode 100644
index 0000000..a76d1aa
--- /dev/null
+++ b/test-common.h
_AT_@ -0,0 +1,190 @@
+/* See LICENSE file for copyright and license details. */
+#ifdef __linux__
+# include <sys/prctl.h>
+#endif
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+enum {
+	SUCCESS       =   0,
+	SUCCESS_TRUE  =   0, /* e.g. true(1), diff(1) with identical files */
+	SUCCESS_FALSE =   1, /* e.g. false(1), diff(1) with differing files */
+	REGULAR_ERROR =   1,
+	BOOLEAN_ERROR =   2, /* e.g. error in diff(1) */
+	PARTIAL_ERROR =  64,
+	SETUP_ERROR   = 125, /* e.g. error before exec in env(1)  */
+	EXEC_ERROR    = 126, /* e.g. error in exec in env(1), but not ENOENT */
+	NOENT_ERROR   = 127  /* e.g. error in exec in env(1), but only ENOENT */
+};
+
+#define IO_STREAM_PIPE           0x0001U
+#define IO_STREAM_SOCK_STREAM    0x0002U
+#define IO_STREAM_SOCK_DGRAM     0x0003U
+#define IO_STREAM_SOCK_SEQPACKET 0x0004U
+#define IO_STREAM_TTY_NOCTTY     0x0005U
+#define IO_STREAM_TTY_CTTY       0x0006U
+#define IO_STREAM_CREATE_MASK    0x0007U
+
+#define IO_STREAM_DATA      0x0008U
+#define IO_STREAM_BINARY    0x0010U
+#define IO_STREAM_MASTER    0x0020U
+#define IO_STREAM_KEEP_OPEN 0x0040U
+
+#define END              0x0000U
+#define EXIT_VALUE       0x1000U
+#define EXIT_RANGE       0x1001U
+#define SIGNAL_VALUE     0x1002U
+#define SIGNAL_RANGE     0x1003U
+#define RUNTIME          0x2000U
+#define OUTPUT_ERROR     0x30FFU
+#define OUTPUT_EQUALS    0x3000U
+#define OUTPUT_ANYCASE   0x3001U
+#define OUTPUT_BEGINNING 0x3002U
+#define OUTPUT_CONTAINS  0x3004U
+#define OUTPUT_LINE      (OUTPUT_BEGINNING | OUTPUT_CONTAINS)
+#define OUTPUT_BINARY    0x3008U
+#define OUTPUT_MULTIPLE  0x3010U
+#define OUTPUT_TEXT_ONLY 0x3020U
+#define CHECK_STDOUT     0x4001U
+#define CHECK_STDERR     0x4002U
+#define CHECK_FD         0x4003U
+#define CALLBACK         0x5000U
+
+#define CHECK_OP_MASK 0xF000U
+#define CHECK_OP_EXIT 0x1000U
+#define CHECK_OP_TIME 0x2000U
+#define CHECK_OP_SOUT 0x3000U
+#define CHECK_OP_OUTF 0x4000U
+#define CHECK_OP_CALL 0x5000U
+
+struct InputStream {
+	int fd;
+	int input_fd;
+	int output_fd;
+	int flags;
+	const char *data;
+	size_t len;
+	char *name;
+};
+
+struct OutputStream {
+	int fd;
+	int input_fd;
+	int output_fd;
+	int flags;
+	int error;
+	size_t head;
+	char *data;
+	size_t len;
+	size_t size;
+	char *name;
+};
+
+#define PROCESS_ASYNC  0x0001
+#define PROCESS_SETSID 0x0002
+
+struct Process {
+	const char *file;
+	const char *const *argv;
+	pid_t pid;
+	int status;
+	int timeout;
+	int pdeath_sig;
+	int flags;
+	struct InputStream input[8];
+	size_t ninput;
+	struct OutputStream output[8];
+	size_t noutput;
+	struct timespec start_time;
+	struct timespec exit_time;
+	struct timespec runtime;
+	void (*preexec)(struct Process *);
+};
+
+extern const char *test_file;
+extern int test_line;
+extern int main_ret;
+extern int timeout;
+extern int pdeath_sig;
+extern void (*atfork)(void);
+
+#define ELEMSOF(A) (sizeof(A) / sizeof(*(A)))
+
+#define COUNTER(VAR, START, WHILE) push_counter(#VAR), VAR = 0; (WHILE) ? (set_counter(VAR), 1) : (pop_counter(), 0); (VAR)++
+
+#define CMD(...)\
+	(test_line = __LINE__,\
+	 test_file = __FILE__,\
+	 &(struct Process){\
+		.file = NULL,\
+		.argv = (const char *[]){__VA_ARGS__, NULL},\
+		.timeout = timeout,\
+		.pdeath_sig = pdeath_sig,\
+		.flags = 0,\
+		.input = {{\
+			.fd = STDIN_FILENO,\
+			.flags = IO_STREAM_PIPE | IO_STREAM_DATA,\
+			.data = NULL,\
+			.len = 0\
+		}},\
+		.ninput = 1,\
+		.output = {{\
+			.fd = STDOUT_FILENO,\
+			.flags = IO_STREAM_PIPE | IO_STREAM_DATA,\
+			.head = 0\
+		}, {\
+			.fd = STDERR_FILENO,\
+			.flags = IO_STREAM_PIPE | IO_STREAM_DATA,\
+			.head = 0\
+		}},\
+		.noutput = 2,\
+		.preexec = NULL\
+	})
+
+#define STDIN_BIN(PROC, S) stdin_bin(PROC, S, sizeof(S) - 1)
+
+#define CHECK(PROC, ...) check(PROC, __VA_ARGS__, END)
+
+#define ASYNC(...) (async_fork(NULL) ? async_join() : (__VA_ARGS__, exit(main_ret), 64))
+
+struct Process *stdin_text(struct Process *proc, char *s);
+struct Process *stdin_bin(struct Process *proc, char *s, size_t n);
+struct Process *stdin_fds(struct Process *proc, int input_fd, int output_fd);
+struct Process *stdin_type(struct Process *proc, int type);
+struct Process *stdout_fds(struct Process *proc, int input_fd, int output_fd);
+struct Process *stdout_type(struct Process *proc, int type);
+struct Process *stderr_fds(struct Process *proc, int input_fd, int output_fd);
+struct Process *stderr_type(struct Process *proc, int type);
+struct Process *set_preexec(struct Process *proc, void (*preexec)(struct Process *));
+struct Process *set_async(struct Process *proc);
+struct Process *set_setsid(struct Process *proc);
+
+void push_counter(const char *name);
+void set_counter(size_t value);
+void pop_counter(void);
+
+pid_t async_fork(int *retp);
+int async_join(void);
+
+void start_process(struct Process *proc);
+void wait_process(struct Process *proc);
+void dealloc_process(struct Process *proc);
+
+const char *openpt(int ctty, int master_slave[2]);
+
+int check_runtime(struct Process *proc, double min, double max);
+int check_exit(struct Process *proc, ...);
+int check_output(struct OutputStream *s, ...);
+int check(struct Process *proc, ...);
diff --git a/tty.test.c b/tty.test.c
new file mode 100644
index 0000000..ba93870
--- /dev/null
+++ b/tty.test.c
_AT_@ -0,0 +1,26 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+static char buf[1024];
+
+static int
+get_tty_name(struct Process *proc)
+{
+	stpcpy(stpcpy(buf, proc->input[0].name), "\n");
+	return 0;
+}
+
+int
+main(void)
+{
+	alarm(timeout);
+
+	CHECK(CMD("./tty", "-"), EXIT_VALUE, 2, CHECK_STDOUT, OUTPUT_EQUALS, "");
+	CHECK(CMD("./tty", "x"), EXIT_VALUE, 2, CHECK_STDOUT, OUTPUT_EQUALS, "");
+	CHECK(CMD("./tty", "---"), EXIT_VALUE, 2, CHECK_STDOUT, OUTPUT_EQUALS, "");
+	CHECK(CMD("./tty"), EXIT_VALUE, 1, OUTPUT_EQUALS, "not a tty\n", CHECK_STDERR, OUTPUT_EQUALS, "");
+	CHECK(stdin_type(CMD("./tty"), IO_STREAM_TTY_NOCTTY | IO_STREAM_KEEP_OPEN), CALLBACK, get_tty_name, EXIT_VALUE, 0,
+	      CHECK_STDOUT, OUTPUT_EQUALS, buf, CHECK_STDOUT, OUTPUT_BEGINNING, "/dev/pts/", CHECK_STDERR, OUTPUT_EQUALS, "");
+
+	return main_ret;
+}
-- 
2.11.1
Received on Sat Jul 07 2018 - 23:26:29 CEST

This archive was generated by hypermail 2.3.0 : Sat Jul 07 2018 - 23:36:25 CEST