Signed-off-by: Mattias Andrée <[email protected]>
---
 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
@@ -1,7 +1,7 @@
 include config.mk
 
 .SUFFIXES:
-.SUFFIXES: .o .c
+.SUFFIXES: .test .test.o .o .c
 
 HDR =\
        arg.h\
@@ -19,7 +19,8 @@ HDR =\
        sha512-256.h\
        text.h\
        utf.h\
-       util.h
+       util.h\
+       test-common.h
 
 LIBUTF = libutf.a
 LIBUTFSRC =\
@@ -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)
 
@@ -193,12 +197,17 @@ $(BIN): $(LIB) $(@:=.o)
 
 $(OBJ): $(HDR) config.mk
 
+$(TEST): $(@:=.o) test-common.o
+
 .o:
        $(CC) $(LDFLAGS) -o $@ $< $(LIB)
 
 .c.o:
        $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<
 
+.test.o.test:
+       $(CC) $(LDFLAGS) -o $@ $< test-common.o
+
 $(LIBUTF): $(LIBUTFOBJ)
        $(AR) rc $@ $?
        $(RANLIB) $@
@@ -212,6 +221,9 @@ getconf.o: getconf.h
 getconf.h: getconf.sh
        ./getconf.sh > $@
 
+check: $(TEST) $(BIN)
+       @set -e; for f in $(TEST); do echo ./$$f; ./$$f; done
+
 install: all
        mkdir -p $(DESTDIR)$(PREFIX)/bin
        cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
@@ -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
@@ -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
@@ -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
@@ -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


Reply via email to