[hackers] [sbase][PATCH v2] Add tests for some utilities

From: Mattias Andrée <maandree_AT_kth.se>
Date: Wed, 11 Jul 2018 21:39:23 +0200

The following utilities are tested:
- basename(1)
- dirname(1)
- echo(1)
- false(1)
- link(1)
- printenv(1)
- sleep(1)
- test(1)
- time(1)
- true(1)
- tty(1)
- uname(1)
- unexpand(1)
- unlink(1)
- whoami(1)
- yes(1)

Some tests contain "#ifdef TODO", these tests current
fail, but there are patches submitted for most of them.
There are not patches submitted for fixing the
"#ifdef TODO"s in expand.test.c and unexpand.test.c.

Signed-off-by: Mattias Andrée <maandree_AT_kth.se>
---
 Makefile        |  45 ++++-
 basename.test.c |  68 +++++++
 dirname.test.c  |  55 ++++++
 echo.test.c     |  51 ++++++
 expand.test.c   |  92 ++++++++++
 false.test.c    |  32 ++++
 link.test.c     |  58 ++++++
 printenv.test.c |  79 ++++++++
 sleep.test.c    |  53 ++++++
 test-common.c   | 560 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test-common.h   | 219 ++++++++++++++++++++++
 test.test.c     | 408 +++++++++++++++++++++++++++++++++++++++++
 time.test.c     | 218 ++++++++++++++++++++++
 true.test.c     |  31 ++++
 tty.test.c      |  44 +++++
 uname.test.c    | 283 ++++++++++++++++++++++++++++
 unexpand.test.c |  97 ++++++++++
 unlink.test.c   |  56 ++++++
 whoami.test.c   |  38 ++++
 yes.test.c      | 131 +++++++++++++
 20 files changed, 2614 insertions(+), 4 deletions(-)
 create mode 100644 basename.test.c
 create mode 100644 dirname.test.c
 create mode 100644 echo.test.c
 create mode 100644 expand.test.c
 create mode 100644 false.test.c
 create mode 100644 link.test.c
 create mode 100644 printenv.test.c
 create mode 100644 sleep.test.c
 create mode 100644 test-common.c
 create mode 100644 test-common.h
 create mode 100644 test.test.c
 create mode 100644 time.test.c
 create mode 100644 true.test.c
 create mode 100644 tty.test.c
 create mode 100644 uname.test.c
 create mode 100644 unexpand.test.c
 create mode 100644 unlink.test.c
 create mode 100644 whoami.test.c
 create mode 100644 yes.test.c
diff --git a/Makefile b/Makefile
index 0e421e7..b83058f 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,28 @@ BIN =\
 	xinstall\
 	yes
 
+TEST =\
+	basename.test\
+	dirname.test\
+	echo.test\
+	expand.test\
+	false.test\
+	link.test\
+	printenv.test\
+	sleep.test\
+	test.test\
+	time.test\
+	true.test\
+	tty.test\
+	uname.test\
+	unexpand.test\
+	unlink.test\
+	whoami.test\
+	yes.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 +213,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 +237,17 @@ getconf.o: getconf.h
 getconf.h: getconf.sh
 	./getconf.sh > $_AT_
 
+check: $(TEST) $(BIN)
+	_AT_set -e;\
+	echo './sleep.test &' ; ./sleep.test & sleep_pid=$$!;\
+	for f in $(TEST); do\
+		if test "$$f" != sleep.test; then\
+			echo ./$$f; ./$$f;\
+		fi;\
+	done;\
+	echo 'wait';\
+	wait $$sleep_pid
+
 install: all
 	mkdir -p $(DESTDIR)$(PREFIX)/bin
 	cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
_AT_@ -271,7 +307,8 @@ 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
+	rm -rf testdir-*/
 
 .PHONY: all install uninstall dist sbase-box sbase-box-install sbase-box-uninstall clean
diff --git a/basename.test.c b/basename.test.c
new file mode 100644
index 0000000..bb22153
--- /dev/null
+++ b/basename.test.c
_AT_@ -0,0 +1,68 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+static struct Case {
+	const char *path;
+	const char *suffix;
+	const char *basename;
+} cases[] = {
+	{"/",      NULL,      "/\n"},
+	{"///",    NULL,      "/\n"},
+	{"x/",     NULL,      "x\n"},
+	{"x//",    NULL,      "x\n"},
+	{"a/b/c",  NULL,      "c\n"},
+	{"/a",     NULL,      "a\n"},
+	{"a/b/c/", NULL,      "c\n"},
+	{"/a/",    NULL,      "a\n"},
+	{"a.b",    "b",       "a.\n"},
+	{"a.b/",   "b",       "a.\n"},
+	{"a.b/",   ".b",      "a\n"},
+	{"a.b",    "a.b",     "a.b\n"},
+	{"a.b",    "c",       "a.b\n"},
+	{"a.b",    "longer!", "a.b\n"},
+	{NULL, NULL, NULL}
+};
+
+int
+main(void)
+{
+	size_t i;
+
+	alarm(timeout);
+
+	proc = CMD("./basename");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./basename", "a", "b", "c");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./basename", "");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "\n") || check_stdout(EQUALS, ".\n")));
+
+	proc = CMD("./basename", "--", "");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "\n") || check_stdout(EQUALS, ".\n")));
+
+	proc = CMD("./basename", "//");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	proc = CMD("./basename", "--", "//");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	for (COUNTER(i, 0, cases[i].path)) {
+		proc = CMD("./basename", cases[i].path, cases[i].suffix);
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].basename));
+
+		proc = CMD("./basename", "--", cases[i].path, cases[i].suffix);
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].basename));
+
+		if (!cases[i].suffix) {
+			proc = CMD("./basename", cases[i].path, "");
+			CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].basename));
+
+			proc = CMD("./basename", "--", cases[i].path, "");
+			CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].basename));
+		}
+	}
+
+	return main_ret;
+}
diff --git a/dirname.test.c b/dirname.test.c
new file mode 100644
index 0000000..e2e08d3
--- /dev/null
+++ b/dirname.test.c
_AT_@ -0,0 +1,55 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+static struct Case {
+	const char *path;
+	const char *dirname;
+} cases[] = {
+	{"",        ".\n"},
+	{"/",       "/\n"},
+	{"///",     "/\n"},
+	{"a/b",     "a\n"},
+	{"a/b/",    "a\n"},
+	{"a/b//",   "a\n"},
+	{"a",       ".\n"},
+	{"a/",      ".\n"},
+	{"/a/b/c",  "/a/b\n"},
+	{"//a/b/c", "//a/b\n"},
+	{NULL, NULL}
+};
+
+int
+main(void)
+{
+	size_t i;
+
+	alarm(timeout);
+
+	proc = CMD("./dirname");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./dirname", "a", "b", "c");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./dirname", "//");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	proc = CMD("./dirname", "--", "//");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	proc = CMD("./dirname", "//a");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	proc = CMD("./dirname", "--", "//a");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && (check_stdout(EQUALS, "/\n") || check_stdout(EQUALS, "//\n")));
+
+	for (COUNTER(i, 0, cases[i].path)) {
+		proc = CMD("./dirname", cases[i].path);
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].dirname));
+
+		proc = CMD("./dirname", "--", cases[i].path);
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].dirname));
+	}
+
+	return main_ret;
+}
diff --git a/echo.test.c b/echo.test.c
new file mode 100644
index 0000000..168130c
--- /dev/null
+++ b/echo.test.c
_AT_@ -0,0 +1,51 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+static struct Case {
+	const char *argv[4];
+	const char *output;
+} cases[] = {
+	{{NULL}, "\n"},
+	{{"a"}, "a\n"},
+	{{"a b"}, "a b\n"},
+	{{"a  b"}, "a  b\n"},
+	{{"a", "b"}, "a b\n"},
+	{{"--"}, "--\n"},
+	{{"a", "--", "c"}, "a -- c\n"},
+	{{"a", "-n", "b"}, "a -n b\n"},
+	{{"-qwertyiopasdfghjklzxcvbnm"}, "-qwertyiopasdfghjklzxcvbnm\n"},
+	{{"--help"}, "--help\n"},
+	{{"--version"}, "--version\n"},
+	{{"-e", "1"}, "-e 1\n"},
+
+	{{"-n", "a"}, "-n a\n"},
+	{{"-n", "a b"}, "-n a b\n"},
+	{{"-n", "a  b"}, "-n a  b\n"},
+	{{"-n", "a", "b"}, "-n a b\n"},
+	{{"-n", "--"}, "-n --\n"},
+	{{"-n", "a", "--", "c"}, "-n a -- c\n"},
+	{{"-n", "a", "-n", "b"}, "-n a -n b\n"},
+	{{"-n", "-n -qwertyiopasdfghjklzxcvbnm\n"}, "-n -qwertyiopasdfghjklzxcvbnm\n"},
+	{{"-n", "--help"}, "-n --help\n"},
+	{{"-n", "--version"}, "-n --version\n"},
+	{{"-n", "-e", "1"}, "-n -e 1\n"},
+	{{"-n", "-n"}, "-n -n\n"},
+	{{"-nn"}, "-nn\n"},
+
+	{{NULL}, NULL},
+};
+
+int
+main(void)
+{
+	size_t i;
+
+	alarm(timeout);
+
+	for (COUNTER(i, 0, cases[i].argv[0])) {
+		proc = CMD("./echo", cases[i].argv[0], cases[i].argv[1], cases[i].argv[2], cases[i].argv[3]);
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, cases[i].output));
+	}
+
+	return main_ret;
+}
diff --git a/expand.test.c b/expand.test.c
new file mode 100644
index 0000000..07f9978
--- /dev/null
+++ b/expand.test.c
_AT_@ -0,0 +1,92 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+struct Case {
+	const char *input;
+	size_t input_len;
+	const char *argv[2];
+	const char *output;
+	size_t output_len;
+};
+
+static struct Case failure_cases[] = {
+	{"",   0, {"---"},    "",   0},
+	{"",   0, {"-t2,1"},  "",   0},
+	{"",   0, {"-t0"},    "",   0},
+	{"",   0, {"-t0,1"},  "",   0},
+	{"",   0, {"-t"},     "",   0},
+	{"",   0, {"-t"},     "",   0},
+	{"",   0, {"-t", ""}, "",   0},
+	{NULL, 0, {NULL},     NULL, 0}
+};
+
+static struct Case success_cases[] = {
+	{BIN("x"),               {NULL},        BIN("x")},
+	{BIN("\tx"),             {NULL},        BIN("        x")},
+	{BIN("        x"),       {NULL},        BIN("        x")},
+	{BIN("1\t2"),            {NULL},        BIN("1       2")},
+	{BIN("1\t2"),            {"-t8"},       BIN("1       2")},
+	{BIN("1\t2"),            {"-t", "8"},   BIN("1       2")},
+	{BIN("1\t2"),            {NULL},        BIN("1       2")},
+	{BIN("1\t2"),            {"-t8"},       BIN("1       2")},
+	{BIN("1\t2"),            {"-t", "8"},   BIN("1       2")},
+	{BIN("1\t\t2\t\t3"),     {"-t4"},       BIN("1       2       3")},
+	{BIN("1\t\t\b\t2\t\t3"), {"-t4"},       BIN("1       \b 2       3")},
+	{BIN("1\t\t\b2\t\t3"),   {"-t4"},       BIN("1       \b2        3")},
+	{BIN("1\t\t 2\t\t3"),    {"-t4"},       BIN("1        2      3")},
+	{BIN("\t\n\t\n"),        {NULL},        BIN("        \n        \n")},
+	{BIN("åäö\tx\n"),        {NULL},        BIN("åäö     x\n")},
+	{BIN("åäö\b\tx\n"),      {NULL},        BIN("åäö\b      x\n")},
+#ifdef TODO
+	{BIN("〇\tx\n"),         {NULL},        BIN("〇      x\n")},
+	{BIN("〇\b\tx\n"),       {NULL},        BIN("〇\b       x\n")},
+#endif
+	{BIN("\tx\ty"),          {"-t2,8"},     BIN("  x     y")},
+	{BIN("\tx\t\ty\n"),      {"-t2,7"},     BIN("  x     y\n")},
+	{BIN("\tx    y\n"),      {"-t2,8"},     BIN("  x    y\n")},
+	{BIN("\tx\ty\n"),        {"-t2 8"},     BIN("  x     y\n")},
+	{BIN("\tx\t\ty\n"),      {"-t2 7"},     BIN("  x     y\n")},
+	{BIN("\tx    y\n"),      {"-t2 8"},     BIN("  x    y\n")},
+	{BIN(" \tx\n"),          {NULL},        BIN("        x\n")},
+	{BIN("1\t2"),            {"--"},        BIN("1       2")},
+	{BIN("1\t2"),            {"--", "-"},   BIN("1       2")},
+	{BIN("1\t2"),            {"-t8", "--"}, BIN("1       2")},
+	{BIN("\0\t\0"),          {NULL},        BIN("\0       \0")},
+	{NULL, 0,                {NULL},        NULL, 0}
+};
+
+int
+main(void)
+{
+	size_t i;
+	char f1[200], dir[100];
+
+	alarm(timeout);
+
+	sprintf(dir, "testdir-%ju", (uintmax_t)getpid());
+	if (mkdir(dir, 0700))
+		eperror(dir);
+	sprintf(f1, "%s/1", dir);
+	write_file(f1, "1\t2", 0);
+
+	proc = CMD("./expand", f1, f1);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "1       21       2"));
+
+	for (COUNTER(i, 0, failure_cases[i].input)) {
+		proc = CMD("./expand", failure_cases[i].argv[0], failure_cases[i].argv[1]);
+		set_input(IN_NBIN(STDIN_FILENO, failure_cases[i].input, failure_cases[i].input_len));
+		CHECK(check_exit(EXIT, REGULAR_ERROR) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+	}
+
+	for (COUNTER(i, 0, success_cases[i].input)) {
+		proc = CMD("./expand", success_cases[i].argv[0], success_cases[i].argv[1]);
+		set_input(IN_NBIN(STDIN_FILENO, success_cases[i].input, success_cases[i].input_len));
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") &&
+		      check_stdout(EQUALS | BINARY, success_cases[i].output, success_cases[i].output_len));
+	}
+
+	unlink(f1);
+	rmdir(dir);
+
+	return main_ret;
+}
diff --git a/false.test.c b/false.test.c
new file mode 100644
index 0000000..bdfec2b
--- /dev/null
+++ b/false.test.c
_AT_@ -0,0 +1,32 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+#define TEST(...)\
+	proc = CMD(__VA_ARGS__);\
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""))
+/* POSIX only specified !check_exit(EXIT, SUCCESS) */
+
+int
+main(void)
+{
+	alarm(timeout);
+
+	TEST("./false");
+	TEST("./false", "1");
+	TEST("./false", "1", "2");
+	TEST("./false", "1", "2", "3");
+	TEST("./false", "-");
+	TEST("./false", "-h");
+	TEST("./false", "-H");
+	TEST("./false", "-v");
+	TEST("./false", "-V");
+	TEST("./false", "-vVhH");
+	TEST("./false", "--");
+	TEST("./false", "--", "1");
+	TEST("./false", "--hello");
+	TEST("./false", "--help");
+	TEST("./false", "--version");
+	TEST("./false", "---");
+
+	return main_ret;
+}
diff --git a/link.test.c b/link.test.c
new file mode 100644
index 0000000..8307d3c
--- /dev/null
+++ b/link.test.c
_AT_@ -0,0 +1,58 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+int
+main(void)
+{
+	char path1[200], path2[200], path3[200], dir[100];
+
+	alarm(timeout);
+
+	sprintf(dir, "testdir-%ju", (uintmax_t)getpid());
+	if (mkdir(dir, 0700))
+		eperror(dir);
+	sprintf(path1, "%s/a", dir);
+	sprintf(path2, "%s/b", dir);
+	sprintf(path3, "%s/c", dir);
+	if (mkdir(path3, 0700))
+		eperror(path3);
+	close(open(path1, O_CREAT | O_WRONLY | O_EXCL, 0600));
+
+	proc = CMD("./link");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./link", "---");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./link", path1);
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./link", path2, path1);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(ENOENT)));
+
+	proc = CMD("./link", path1, path2);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));
+
+	proc = CMD("./link", path1, path2);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(EEXIST)));
+
+	unlink(path2);
+
+#ifdef TODO
+	proc = CMD("./link", "--", path1, path2);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));
+
+	proc = CMD("./link", "--", path1, path2);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(EEXIST)));
+#endif
+
+	proc = CMD("./link", path1, path3);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(EEXIST)));
+
+	unlink(path1);
+	unlink(path2);
+	rmdir(path3);
+	rmdir(dir);
+
+	return main_ret;
+}
diff --git a/printenv.test.c b/printenv.test.c
new file mode 100644
index 0000000..c1168e6
--- /dev/null
+++ b/printenv.test.c
_AT_@ -0,0 +1,79 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+extern char **environ;
+
+int
+main(void)
+{
+	alarm(timeout);
+
+#ifdef TODO
+	proc = CMD("./printenv", "-0-");
+	CHECK(check_usage_error(BOOLEAN_ERROR));
+
+	proc = CMD("./printenv", "---");
+	CHECK(check_usage_error(BOOLEAN_ERROR));
+#endif
+
+	environ = (char *[]){NULL};
+
+	proc = CMD("./printenv");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+#ifdef TODO
+	proc = CMD("./printenv", "--");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+#endif
+
+	environ = (char *[]){"X=Y", "1=2", NULL};
+
+	proc = CMD("./printenv");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "X=Y\n1=2\n"));
+
+#ifdef TODO
+	proc = CMD("./printenv", "--");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "X=Y\n1=2\n"));
+#endif
+
+	proc = CMD("./printenv", "X", "1");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "Y\n2\n"));
+
+	proc = CMD("./printenv", "1", "X");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "2\nY\n"));
+
+	proc = CMD("./printenv", "X");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "Y\n"));
+
+	proc = CMD("./printenv", "1");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "2\n"));
+
+#ifdef TODO
+	proc = CMD("./printenv", "--", "X", "1");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "Y\n2\n"));
+
+	proc = CMD("./printenv", "--", "1", "X");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "2\nY\n"));
+
+	proc = CMD("./printenv", "--", "X");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "Y\n"));
+
+	proc = CMD("./printenv", "--", "1");
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "2\n"));
+#endif
+
+	proc = CMD("./printenv", " non-existent ");
+	CHECK(check_exit(EXIT, SUCCESS_FALSE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./printenv", "X", " non-existent ", "1");
+	CHECK(check_exit(EXIT, SUCCESS_FALSE) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "Y\n2\n"));
+
+	/*
+	 * Do not test e.g. `env A=B=C printenv A=B`, failure of such test
+	 * would most likely indicate that getenv(3) does not verify that
+	 * the variable name does not contain a '='. Calling printenv with
+	 * a '=' would also be user error.
+	 */
+
+	return main_ret;
+}
diff --git a/sleep.test.c b/sleep.test.c
new file mode 100644
index 0000000..1f4de12
--- /dev/null
+++ b/sleep.test.c
_AT_@ -0,0 +1,53 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+int
+main(void)
+{
+	alarm(timeout);
+
+	ASYNC_BEGIN {
+		proc = CMD("./sleep", "2");
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(1.99, 2.03));
+	} ASYNC_END;
+
+#ifdef TODO
+	ASYNC_BEGIN {
+		proc = CMD("./sleep", "--", "2");
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(1.99, 2.03));
+	} ASYNC_END;
+
+	ASYNC_BEGIN {
+		proc = CMD("./sleep", "--", "1");
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(0.99, 1.03));
+	} ASYNC_END;
+#endif
+
+	ASYNC_BEGIN {
+		proc = CMD("./sleep", "1");
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(0.99, 1.03));
+	} ASYNC_END;
+
+	proc = CMD("./sleep");
+	CHECK(check_usage_error(REGULAR_ERROR) && check_runtime(0.00, 0.03));
+
+	proc = CMD("./sleep", "-");
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(0.00, 0.03));
+
+#ifdef TODO
+	proc = CMD("./sleep", "--");
+	CHECK(check_usage_error(REGULAR_ERROR) && check_runtime(0.00, 0.03));
+
+	proc = CMD("./sleep", "---");
+	CHECK(check_usage_error(REGULAR_ERROR) && check_runtime(0.00, 0.03));
+#endif
+
+	proc = CMD("./sleep", "--", "-1");
+	CHECK(check_usage_error(REGULAR_ERROR) && check_runtime(0.00, 0.03));
+
+	proc = CMD("./sleep", "0");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "") && check_runtime(0.00, 0.03));
+
+	async_join();
+	return main_ret;
+}
diff --git a/test-common.c b/test-common.c
new file mode 100644
index 0000000..8989755
--- /dev/null
+++ b/test-common.c
_AT_@ -0,0 +1,560 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+struct Counter {
+	const char *name;
+	size_t value;
+};
+
+struct Process *proc = NULL;
+const char *test_file = NULL;
+int test_line = 0;
+int timeout = 10;
+int pdeath_sig = SIGINT;
+int main_ret = 0;
+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;
+
+void
+eperror(const char *prefix)
+{
+	perror(prefix);
+	fflush(stderr);
+	exit(99);
+}
+
+void
+push_counter(const char *name)
+{
+	counters[ncounters++].name = name;
+}
+
+void
+set_counter(size_t value)
+{
+	counters[ncounters - 1].value = value;
+}
+
+void
+pop_counter(void)
+{
+	ncounters--;
+}
+
+pid_t
+async_fork(void)
+{
+	pid_t pid;
+
+	if (async_npids == ELEMSOF(async_pids))
+		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;
+}
+
+void
+async_join(void)
+{
+	int status;
+
+	while (async_npids--) {
+		if (waitpid(async_pids[async_npids], &status, 0) != async_pids[async_npids])
+			eperror("waitpid");
+		if (status)
+			exit(1);
+	}
+
+	async_npids = 0;
+}
+
+int
+safe_fd(int fd, int cloexec)
+{
+	int new_fd = fcntl(fd, cloexec ? F_DUPFD_CLOEXEC : F_DUPFD, FD_SAFE_FROM);
+	if (new_fd < 0)
+		eperror(cloexec ? "fcntl F_DUPFD_CLOEXEC" : "fcntl F_DUPFD");
+	close(fd);
+	return new_fd;
+}
+
+void
+xpipe(int fds[2], int cloexec)
+{
+	if (pipe(fds))
+		eperror("pipe");
+	fds[0] = safe_fd(fds[0], cloexec);
+	fds[1] = safe_fd(fds[1], cloexec);
+}
+
+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");
+	master_slave[0] = safe_fd(master_slave[0], 0);
+	master_slave[1] = safe_fd(master_slave[1], 0);
+	return slave;
+}
+
+void
+write_file(const char *path, const char *data, size_t n)
+{
+	int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
+	size_t p = 0;
+	ssize_t r;
+	n = n ? n : strlen(data);
+	if (fd < 0)
+		eperror("open");
+	for (; p < n; p += (size_t)r)
+		if ((r = write(fd, &data[p], n - p)) < 0)
+			eperror("write");
+	close(fd);
+}
+
+void
+set_input(struct InputStream *in)
+{
+	size_t i;
+	for (i = 0; i < proc->ninput && in->fd != proc->input[i].fd; i++);
+	proc->ninput += (i == proc->ninput);
+	proc->input[i] = *in;
+}
+
+void
+set_output(struct OutputStream *out)
+{
+	size_t i;
+	for (i = 0; i < proc->noutput && out->fd != proc->output[i].fd; i++);
+	proc->noutput += (i == proc->noutput);
+	proc->output[i] = *out;
+}
+
+
+void
+start_process(void)
+{
+	struct InputStream *in;
+	struct OutputStream *out;
+	int exec_sig_pipe[2];
+	size_t i;
+	ssize_t r;
+	int fd, fds[2];
+
+	proc->started = 1;
+
+	if (!proc->file)
+		proc->file = proc->argv[0];
+
+	for (i = 0; i < proc->ninput; i++) {
+		in = &proc->input[i];
+		in->pid = -1;
+
+		if ((in->flags & CREATE_MASK) == PIPE) {
+			xpipe(fds, 0);
+			in->input_fd = fds[0];
+			in->output_fd = fds[1];
+		}
+
+		if (in->flags & DATA) {
+			switch ((in->pid = fork())) {
+			case -1:
+				eperror("fork");
+			case 0:
+				break;
+			default:
+				close(in->output_fd);
+				continue;
+			}
+			if (atfork)
+				atfork();
+			for (fd = 0; fd < FD_MAX; fd++)
+				if (fd != STDERR_FILENO && fd != in->output_fd)
+					close(fd);
+			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;
+			}
+			if (close(in->output_fd))
+				eperror("close");
+			exit(0);
+		}
+	}
+
+	for (i = 0; i < proc->noutput; i++) {
+		out = &proc->output[i];
+		out->error = 0;
+		out->data = NULL;
+		out->len = 0;
+		out->size = 0;
+
+		if ((out->flags & CREATE_MASK) == PIPE) {
+			xpipe(fds, 0);
+			out->input_fd = fds[0];
+			out->output_fd = fds[1];
+		}
+	}
+
+	xpipe(exec_sig_pipe, 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++) {
+		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);
+	}
+
+	alarm(proc->timeout);
+
+	execvp(proc->file, (void *)proc->argv);
+	fprintf(stderr, "exec %s: %s\n", proc->file, strerror(errno));
+	fflush(stderr);
+	exit(99);
+}
+
+void
+wait_process(void)
+{
+	struct pollfd *pfds = NULL;
+	struct OutputStream *out;
+	size_t i, j, n;
+	ssize_t r;
+	int status;
+
+	proc->waited = 1;
+
+	n = proc->noutput;
+	pfds = calloc(n + 1, sizeof(*pfds));
+	if (!pfds)
+		eperror("calloc");
+
+	for (i = j = 0; i < n; j++) {
+		if (proc->output[j].flags & DATA) {
+			pfds[i].fd = proc->output[j].input_fd;
+			pfds[i].events = POLLIN;
+			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) {
+				out->len += (size_t)r;
+				j++;
+			} else {
+				out->error = errno;
+				close(out->input_fd);
+				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));
+			}
+		}
+	}
+
+	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 & DATA) {
+			if (waitpid(proc->input[i].pid, &status, 0) != proc->input[i].pid)
+				eperror("waitpid");
+			if (status)
+				exit(99);
+		}
+	}
+
+	free(pfds);
+}
+
+void
+dealloc_process(void)
+{
+	size_t i;
+	for (i = 0; i < proc->noutput; i++)
+		if (proc->output[i].flags & DATA)
+			free(proc->output[i].data);
+}
+
+int
+check_runtime(double min, double max)
+{
+	double dur;
+	dur = proc->runtime.tv_nsec;
+	dur /= 1000000000.;
+	dur += proc->runtime.tv_sec;
+	return min <= dur && dur <= max;
+}
+
+int
+check_exit(enum ExitStatus type, int min, ...)
+{
+	int max = min, status = proc->status;
+	va_list ap;
+
+	if (type & RANGE) {
+		va_start(ap, min);
+		max = va_arg(ap, int);
+		va_end(ap);
+	}
+
+	if ((type & 0x0F) == EXIT)
+		return WIFEXITED(status) && min <= WEXITSTATUS(status) && WEXITSTATUS(status) <= max;
+	else
+		return WIFSTOPPED(status) && min <= WTERMSIG(status) && WTERMSIG(status) <= max;
+}
+
+static int
+check_output_test(const char *data, size_t len, struct OutputStream *s, enum OutputTest test)
+{
+	int beginning = (test & BEGINNING) == BEGINNING;
+	int contains = (test & CONTAINS) == CONTAINS;
+	int anycase = (test & ANYCASE) == ANYCASE;
+	int at_beginning = 1;
+	size_t i, off, off_end;
+
+	if (len > s->len)
+		return 0;
+
+	off_end = contains ? s->len - len + 1 : 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 || contains || i + off == s->len)
+			return 1;
+	}
+
+	return 0;
+}
+
+int
+check_output(int fd, enum OutputTest test, ...)
+{
+	struct OutputStream *s;
+	const char *data;
+	size_t len;
+	va_list ap;
+
+	for (s = proc->output; s->fd != fd; s++);
+
+	va_start(ap, test);
+
+	if (test == ERROR) {
+		return s->error == va_arg(ap, int);
+	} else if (test & PARTIAL) {
+		if (s->error && s->error != va_arg(ap, int))
+			return 0;
+	} else {
+		if (s->error)
+			return 0;
+	}
+
+	data = va_arg(ap, const char *);
+	if (test & BINARY)
+		len = va_arg(ap, size_t);
+	else
+		len = strlen(data);
+
+	if (test & TEXT_ONLY)
+		if (memchr(s->data, '\0', s->len))
+			 return 0;
+
+	va_end(ap);
+
+	return check_output_test(data, len, s, test);
+}
+
+void
+print_failure(void)
+{
+	/* Write to memory first to avoid lines from different tests getting mixed
+	 * when multiple tests tested concurrently. */
+
+	struct OutputStream *out;
+	FILE *fp;
+	char *buf = NULL;
+	size_t size = 0;
+	size_t i;
+	char *p, *q;
+
+	fp = open_memstream(&buf, &size);
+	if (!fp)
+		eperror("open_memstream");
+
+	fprintf(fp, "test at %s:%i", test_file, test_line);
+	switch (ncounters) {
+	case 0:
+		break;
+	case 1:
+		fprintf(fp, ", with %s=%zu,", counters[0].name, counters[0].value);
+		break;
+	case 2:
+		fprintf(fp, ", with %s=%zu and %s=%zu,",
+		        counters[0].name, counters[0].value, counters[1].name, counters[1].value);
+		break;
+	default:
+		fprintf(fp, ", with");
+		for (i = 0; i < ncounters; i++) {
+			if (i == ncounters - 1)
+				fprintf(fp, " and");
+			fprintf(fp, " %s=%zu,", counters[i].name, counters[i].value);
+		}
+		break;
+	}
+	fprintf(fp, " failed:\n");
+
+	if (WIFEXITED(proc->status))
+		fprintf(fp, "\tnormal exit with value %i\n", WEXITSTATUS(proc->status));
+	else if (WIFSTOPPED(proc->status))
+		fprintf(fp, "\tkilled by signal %i (%s)\n", WTERMSIG(proc->status), strsignal(WTERMSIG(proc->status)));
+	else
+		fprintf(fp, "\tunrecognised exit status %i\n", proc->status);
+
+	fprintf(fp, "\tran for %lli.%09li seconds\n", (unsigned long long int)proc->runtime.tv_sec, proc->runtime.tv_nsec);
+
+	for (i = 0; i < proc->noutput; i++) {
+		out = &proc->output[i];
+		if (!(out->flags & DATA))
+			continue;
+		fprintf(fp, "\toutput to ");
+		if (out->fd == 0)
+			fprintf(fp, "stdin");
+		else if (out->fd == 1)
+			fprintf(fp, "stdout");
+		else if (out->fd == 2)
+			fprintf(fp, "stderr");
+		else
+			fprintf(fp, "fd %i", out->fd);
+		fprintf(fp, " was %zu bytes long and in %s, ", out->len,
+		        memchr(out->data, '\0', out->len) ? "binary" :
+			(out->len && out->data[out->len - 1] == '\n') ? "text" :
+		        "text without newline at the end");
+		if (out->error)
+			fprintf(fp, "error %i (%s) encountered", out->error, strerror(out->error));
+		else
+			fprintf(fp, "no error encountered");
+		if (!out->len || memchr(out->data, '\0', out->len)) {
+			fprintf(fp, "\n");
+		} else {
+			fprintf(fp, ":\n");
+			for (p = out->data; p && *p; p = q) {
+				q = strchr(p, '\n');
+				if (q) {
+					q += 1;
+					fprintf(fp, "\t\t%.*s", (int)(q - p), p);
+				} else {
+					fprintf(fp, "\t\t%s\n", p);
+				}
+			}
+		}
+	}
+
+	fprintf(fp, "\n");
+
+	if (fflush(fp))
+		eperror("fflush");
+	if (fclose(fp))
+		eperror("fclose");
+	fwrite(buf, 1, size, stderr);
+	free(buf);
+}
diff --git a/test-common.h b/test-common.h
new file mode 100644
index 0000000..abd4796
--- /dev/null
+++ b/test-common.h
_AT_@ -0,0 +1,219 @@
+/* See LICENSE file for copyright and license details. */
+#ifdef __linux__
+# include <sys/prctl.h>
+#endif
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#define FD_SAFE_FROM 128
+#define FD_MAX       256
+
+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 SIGNAL_EXIT(SIGNUM) (128 + (SIGNUM))
+};
+
+enum StreamSpec {
+	PIPE        = 0x01,
+	CREATE_MASK = 0x0F,
+	DATA        = 0x10
+};
+
+enum ExitStatus {
+	EXIT   = 0,
+	SIGNAL = 1,
+	RANGE  = 0x10
+};
+
+enum OutputTest {
+	ERROR     = 0xFF,
+	PARTIAL   = 0x01,
+	EQUALS    = 0,
+	ANYCASE   = 0x02,
+	BEGINNING = 0x04,
+	CONTAINS  = 0x08,
+	BINARY    = 0x10,
+	TEXT_ONLY = 0x20,
+#define LINE (BEGINNING | CONTAINS)
+};
+
+struct InputStream {
+	int fd;
+	int input_fd;
+	int output_fd;
+	enum StreamSpec flags;
+	const char *data;
+	size_t len;
+	pid_t pid;
+};
+
+struct OutputStream {
+	int fd;
+	int input_fd;
+	int output_fd;
+	enum StreamSpec flags;
+	int error;
+	char *data;
+	size_t len;
+	size_t size;
+};
+
+struct Process {
+	const char *file;
+	const char **argv;
+	pid_t pid;
+	int status;
+	int timeout;
+	int pdeath_sig;
+	int started;
+	int waited;
+	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;
+};
+
+extern struct Process *proc;
+extern const char *test_file;
+extern int test_line;
+extern int timeout;
+extern int pdeath_sig;
+extern int main_ret;
+extern void (*atfork)(void);
+
+#define IN_TEXT(FD, TEXT) IN_NBIN(FD, TEXT, strlen(TEXT))
+
+#define IN_BIN(FD, TEXT) IN_NBIN(FD, TEXT, sizeof(TEXT) - 1)
+
+#define IN_NBIN(FD, TEXT, N)\
+	(&(struct InputStream){\
+		.fd = (FD),\
+		.flags = PIPE | DATA,\
+		.data = (TEXT),\
+		.len = (N)\
+	})
+
+#define IN_FDS(FD, IN, OUT)\
+	(&(struct InputStream){\
+		.fd = (FD),\
+		.input_fd = (IN),\
+		.output_fd = (OUT),\
+		.flags = 0\
+	})
+
+#define OUT_FDS(FD, IN, OUT)\
+	(&(struct OutputStream){\
+		.fd = (FD),\
+		.input_fd = (IN),\
+		.output_fd = (OUT),\
+		.flags = 0\
+	})
+
+#define OUT_PIPE(FD)\
+	(&(struct OutputStream){\
+		.fd = (FD),\
+		.flags = PIPE | DATA\
+	})
+
+#define CMD(...)\
+	(&(struct Process){\
+		.file = NULL,\
+		.argv = (const char *[]){__VA_ARGS__, NULL},\
+		.timeout = timeout,\
+		.pdeath_sig = pdeath_sig,\
+		.started = 0,\
+		.input = {{\
+			.fd = STDIN_FILENO,\
+			.flags = PIPE | DATA,\
+			.data = NULL,\
+			.len = 0\
+		}},\
+		.ninput = 1,\
+		.output = {{\
+			.fd = STDOUT_FILENO,\
+			.flags = PIPE | DATA\
+		}, {\
+			.fd = STDERR_FILENO,\
+			.flags = PIPE | DATA\
+		}},\
+		.noutput = 2\
+	})
+
+#define CHECK(OK_COND)\
+	do {\
+		test_line = __LINE__;\
+		test_file = __FILE__;\
+		if (!proc->started)\
+			start_process();\
+		if (!proc->waited)\
+			wait_process();\
+		if (!(OK_COND)) {\
+			print_failure();\
+			main_ret = 1;\
+		}\
+		dealloc_process();\
+	} while (0)
+
+#define ELEMSOF(A) (sizeof(A) / sizeof(*(A)))
+
+#define COUNTER(VAR, START, WHILE) VAR = START, push_counter(#VAR); (WHILE) ? (set_counter(VAR), 1) : (pop_counter(), 0); (VAR)++
+
+#define BIN(TEXT) TEXT, sizeof(TEXT) - 1
+
+#define ASYNC_BEGIN do { if (!async_fork()) {
+#define ASYNC_END   exit(main_ret); }} while (0)
+
+void eperror(const char *prefix);
+
+void push_counter(const char *name);
+void set_counter(size_t value);
+void pop_counter(void);
+
+pid_t async_fork(void);
+void async_join(void);
+
+int safe_fd(int fd, int cloexec);
+void xpipe(int fds[2], int cloexec);
+const char *openpt(int ctty, int master_slave[2]);
+void write_file(const char *path, const char *data, size_t n);
+
+void set_input(struct InputStream *in);
+void set_output(struct OutputStream *out);
+
+void start_process(void);
+void wait_process(void);
+void dealloc_process(void);
+
+int check_runtime(double min, double max);
+int check_exit(enum ExitStatus type, int min, ...);
+int check_output(int fd, enum OutputTest test, ...);
+#define check_stdout(...) check_output(STDOUT_FILENO, __VA_ARGS__)
+#define check_stderr(...) check_output(STDERR_FILENO, __VA_ARGS__)
+#define check_usage_error(EXIT_VALUE) (check_exit(EXIT, EXIT_VALUE) && check_stderr(BEGINNING, "usage: ") && check_stdout(EQUALS, ""))
+
+void print_failure(void);
diff --git a/test.test.c b/test.test.c
new file mode 100644
index 0000000..8e6080f
--- /dev/null
+++ b/test.test.c
_AT_@ -0,0 +1,408 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#define FILES\
+	"/dev/loop0", /* -bea */\
+	"/dev/null",  /* -cea */\
+	"/",          /* -dea */\
+	"h",          /* -dheaL */\
+	SAFE_FILES
+
+#define SAFE_FILES\
+	" does not exist ",\
+	"r",          /* -frea */\
+	"w",          /* -fwea */\
+	"x",          /* -fxea */\
+	"u",          /* -fuea */\
+	"g",          /* -fgea */\
+	"k",          /* -fkea */\
+	"+e",         /* -hL */\
+	"s",          /* -fsea */\
+	"S",          /* -Sea */\
+	"p"           /* -pea */
+
+static struct TestInt {
+	const char *s;
+	int cmpval;
+} testints[] = {
+	{"0", 0},
+	{"00", 0},
+	{"1", 1},
+	{"01", 1},
+	{"10", 2},
+	{"11", 3},
+	{"20", 4},
+	{"21", 5},
+	{"100", 6},
+	{"1000", 7},
+	{"100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 8},
+	{"100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 9},
+	{"200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 10},
+	{"+0", 0},
+	{"+00", 0},
+	{"+1", 1},
+	{"+01", 1},
+	{"+10", 2},
+	{"+11", 3},
+	{"+20", 4},
+	{"+21", 5},
+	{"+100", 6},
+	{"+1000", 7},
+	{"+100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 8},
+	{"+100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 9},
+	{"+200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 10},
+	{"-0", 0},
+	{"-00", 0},
+	{"-1", -1},
+	{"-01", -1},
+	{"-10", -2},
+	{"-11", -3},
+	{"-20", -4},
+	{"-21", -5},
+	{"-100", -6},
+	{"-1000", -7},
+	{"-100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", -8},
+	{"-100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", -9},
+	{"-200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", -10},
+	{NULL, 0}
+};
+
+#define test_check_usage_error() (check_exit(EXIT, BOOLEAN_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, ": "))
+
+#define SYNTAX_TEST(...)\
+	do {\
+		push_counter("syntax-test-part");\
+		\
+		set_counter(0);\
+		proc = CMD("../test", __VA_ARGS__);\
+		CHECK(test_check_usage_error());\
+		\
+		set_counter(1);\
+		proc = CMD("[", __VA_ARGS__, "]");\
+		proc->file = "../test";\
+		CHECK(test_check_usage_error());\
+		\
+		set_counter(2);\
+		proc = CMD("[", __VA_ARGS__);\
+		proc->file = "../test";\
+		CHECK(test_check_usage_error());\
+		\
+		pop_counter();\
+	} while (0)
+
+#define SINGLE_TEST(OK, ...)\
+	do {\
+		push_counter("single-test-part");\
+		\
+		set_counter(0);\
+		proc = CMD("../test", __VA_ARGS__);\
+		CHECK(check_exit(EXIT, (OK)) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));\
+		\
+		set_counter(1);\
+		proc = CMD("../test", "!", __VA_ARGS__);\
+		CHECK(check_exit(EXIT, !(OK)) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));\
+		\
+		set_counter(2);\
+		proc = CMD("../[", __VA_ARGS__, "]");\
+		proc->file = "../test";\
+		CHECK(check_exit(EXIT, (OK)) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));\
+		\
+		set_counter(3);\
+		proc = CMD("[", "!", __VA_ARGS__, "]"); \
+		proc->file = "../test";\
+		CHECK(check_exit(EXIT, !(OK)) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));\
+		\
+		set_counter(4);\
+		proc = CMD("[", "!", __VA_ARGS__);\
+		proc->file = "../test";\
+		CHECK(test_check_usage_error());\
+		\
+		pop_counter();\
+	} while (0)
+
+#define CMP_SINGLE_TEST(I1, I2, CMP, FLAG)\
+	do {\
+		proc = CMD("../test", (I1)->s, FLAG, (I2)->s);\
+		CHECK(check_exit(EXIT, !((I1)->cmpval CMP (I2)->cmpval)) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));\
+	} while (0)
+
+#define MULTI_TEST(FLAG, TEST_FILES, ...)\
+	do {\
+		const char *good_files[] = {__VA_ARGS__, NULL};\
+		for (COUNTER(i, 0, (TEST_FILES)[i])) {\
+			ok = 1;\
+			for (j = 0; good_files[j]; j++) {\
+				if (!strcmp((TEST_FILES)[i], good_files[j])) {\
+					ok = 0;\
+					break;\
+				}\
+			}\
+			SINGLE_TEST(ok, (FLAG), (TEST_FILES)[i]);\
+		}\
+	} while (0)
+
+int
+main(void)
+{
+	const char *files[] = {FILES, NULL};
+	const char *safe_files[] = {SAFE_FILES, NULL};
+	int fd, pfds[2], tfds[2];
+	char dir[100], pfd[50], tfd[50], pfdn[51], tfdn[51];
+	struct sockaddr_un un;
+	struct timespec times[2] = {{.tv_nsec = 0}, {.tv_nsec = 0}};
+	int is_root = !geteuid(), ok;
+	size_t i, j;
+
+	alarm(timeout = 20);
+
+	sprintf(dir, "testdir-%ju", (uintmax_t)getpid());
+	if (mkdir(dir, 0700) || chdir(dir))
+		eperror(dir);
+	if (symlink(".", "h"))
+		eperror("h");
+	if (symlink(" non existent file ", "+e"))
+		eperror("+e");
+	close(open("r", O_WRONLY | O_CREAT, 0400));
+	close(open("w", O_WRONLY | O_CREAT, 0200));
+	close(open("x", O_WRONLY | O_CREAT, 0100));
+	close(open("u", O_WRONLY | O_CREAT, 04000));
+	close(open("g", O_WRONLY | O_CREAT, 02000));
+	close(open("k", O_WRONLY | O_CREAT, 01000));
+	link("x", "x-hardlink");
+	fd = open("s", O_WRONLY | O_CREAT, 0);
+	if (fd < 0)
+		eperror("s");
+	write(fd, &(char){0}, 1);
+	close(fd);
+	fd = socket(PF_LOCAL, SOCK_STREAM, 0);
+	if (fd < 0)
+		eperror("socket");
+	memset(&un, 0, sizeof(un));
+	un.sun_family = AF_LOCAL;
+	stpcpy(un.sun_path, "S");
+	bind(fd, (void *)&un, (socklen_t)sizeof(un));
+	if (fd < 0)
+		eperror("S");
+	close(fd);
+	if (chmod("S", 0))
+		eperror("S");
+	if (mkfifo("p", 0))
+		eperror("p");
+	xpipe(pfds, 0);
+	openpt(0, tfds);
+	sprintf(pfd, "%i", pfds[1]);
+	sprintf(tfd, "%i", tfds[1]);
+	sprintf(pfdn, "%i\n", pfds[1]);
+	sprintf(tfdn, "%i\n", tfds[1]);
+	close(open("-eq", O_WRONLY | O_CREAT, 0400));
+	close(open("also-old", O_WRONLY | O_CREAT, 06000));
+	close(open("old", O_WRONLY | O_CREAT, 06000));
+	close(open("new", O_WRONLY | O_CREAT, 06000));
+	times[0].tv_sec = 1;
+	times[1].tv_sec = 10000;
+	utimensat(AT_FDCWD, "also-old", times, 0);
+	times[0].tv_sec = 1;
+	times[1].tv_sec = 10000;
+	utimensat(AT_FDCWD, "old", times, 0);
+	times[0].tv_sec = 1;
+	times[1].tv_sec = 40000;
+	utimensat(AT_FDCWD, "new", times, 0);
+
+	ASYNC_BEGIN {
+		proc = CMD("[");
+		proc->file = "../test";
+		CHECK(test_check_usage_error());
+
+		for (COUNTER(i, 0, i < 4)) {
+			proc = CMD("../test", NULL, NULL, NULL, NULL, NULL, NULL);
+			for (j = 1; j < i + 1; j++)
+				proc->argv[j] = "!";
+			proc->argv[j] = NULL;
+			CHECK(check_exit(EXIT, i % 2 == 0 ? SUCCESS_FALSE : SUCCESS_TRUE) &&
+			      check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+			proc = CMD("[", NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+			proc->file = "../test";
+			for (j = 1; j < i + 1; j++)
+				proc->argv[j] = "!";
+			proc->argv[j++] = "]";
+			proc->argv[j] = NULL;
+			CHECK(check_exit(EXIT, i % 2 == 0 ? SUCCESS_FALSE : SUCCESS_TRUE) &&
+			      check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+		}
+
+		SINGLE_TEST(0, "---");
+		SINGLE_TEST(1, "");
+		SINGLE_TEST(0, "--");
+		SINGLE_TEST(0, "x");
+		SINGLE_TEST(0, "abc");
+		SINGLE_TEST(0, "-");
+		SINGLE_TEST(0, " -");
+		SINGLE_TEST(0, " ");
+		SYNTAX_TEST("--", "x");
+		SYNTAX_TEST("-z", "--", "x");
+		SYNTAX_TEST("-n", "--", "x");
+		SINGLE_TEST(1, "-z", "--");
+		SINGLE_TEST(1, "-z", "x");
+		SINGLE_TEST(0, "-z", "");
+		SINGLE_TEST(0, "-z");
+		SINGLE_TEST(0, "-n", "--");
+		SINGLE_TEST(0, "-n", "x");
+		SINGLE_TEST(1, "-n", "");
+		SINGLE_TEST(0, "-n");
+		SINGLE_TEST(0, "-1");
+		SINGLE_TEST(0, "--help");
+		SINGLE_TEST(0, "--version");
+	} ASYNC_END;
+	ASYNC_BEGIN {
+		MULTI_TEST("-b", files, "/dev/loop0");
+		MULTI_TEST("-c", files, "/dev/null");
+		MULTI_TEST("-d", files, "/", "h");
+		MULTI_TEST("-e", files, "/dev/loop0", "/dev/null", "/", "h", "r", "w", "x", "u", "g", "k", "s", "S", "p");
+		MULTI_TEST("-f", files, "r", "w", "x", "u", "g", "k", "s");
+		MULTI_TEST("-g", files, "g");
+		MULTI_TEST("-h", files, "h", "+e");
+		MULTI_TEST("-L", files, "h", "+e");
+		MULTI_TEST("-p", files, "p");
+		if (is_root)
+			MULTI_TEST("-r", safe_files, "r", "w", "x", "u", "g", "k", "s", "S", "p");
+		else
+			MULTI_TEST("-r", safe_files, "r");
+		MULTI_TEST("-S", files, "S");
+		MULTI_TEST("-s", safe_files, "s");
+		MULTI_TEST("-u", files, "u");
+		if (is_root)
+			MULTI_TEST("-w", safe_files, "r", "w", "x", "u", "g", "k", "s", "S", "p");
+		else
+			MULTI_TEST("-w", safe_files, "w");
+		MULTI_TEST("-x", safe_files, "x");
+		SINGLE_TEST(1, "-t", pfd);
+		SINGLE_TEST(0, "-t", tfd);
+		SINGLE_TEST(0, "", "=", "");
+		SINGLE_TEST(0, "a", "=", "a");
+		SINGLE_TEST(1, "a", "=", "");
+		SINGLE_TEST(1, "", "=", "a");
+		SINGLE_TEST(1, "a", "=", "b");
+		SINGLE_TEST(1, "1", "=", "01");
+		SINGLE_TEST(0, "a=b");
+		SINGLE_TEST(1, "", "!=", "");
+		SINGLE_TEST(1, "a", "!=", "a");
+		SINGLE_TEST(0, "a", "!=", "");
+		SINGLE_TEST(0, "", "!=", "a");
+		SINGLE_TEST(0, "a", "!=", "b");
+		SINGLE_TEST(0, "1", "!=", "01");
+		SINGLE_TEST(0, "=");
+		SINGLE_TEST(0, "!=");
+		SINGLE_TEST(0, "==");
+		SINGLE_TEST(0, "a!=a");
+		SINGLE_TEST(0, "a==b");
+		SYNTAX_TEST("-", "-eq", "-");
+		SYNTAX_TEST("-", "-eq", "+");
+		SINGLE_TEST(0, "!", "=", "!");
+		SINGLE_TEST(1, "(", "=", ")");
+		SINGLE_TEST(0, "-f", "=", "-f");
+		SINGLE_TEST(0, "-f", "-eq");
+	} ASYNC_END;
+	ASYNC_BEGIN {
+		for (COUNTER(i, 0, testints[i].s)) {
+			SYNTAX_TEST("-", "-eq", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-eq", "-");
+			SYNTAX_TEST("-", "-ne", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-ne", "-");
+			SYNTAX_TEST("-", "-lt", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-lt", "-");
+			SYNTAX_TEST("-", "-le", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-le", "-");
+			SYNTAX_TEST("-", "-gt", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-gt", "-");
+			SYNTAX_TEST("-", "-ge", testints[i].s);
+			SYNTAX_TEST(testints[i].s, "-ge", "-");
+		}
+	} ASYNC_END;
+	for (COUNTER(i, 0, testints[i].s)) {
+		ASYNC_BEGIN {
+			for (COUNTER(j, 0, testints[j].s)) {
+				CMP_SINGLE_TEST(&testints[i], &testints[j], ==, "-eq");
+				CMP_SINGLE_TEST(&testints[i], &testints[j], !=, "-ne");
+				CMP_SINGLE_TEST(&testints[i], &testints[j], <,  "-lt");
+				CMP_SINGLE_TEST(&testints[i], &testints[j], <=, "-le");
+				CMP_SINGLE_TEST(&testints[i], &testints[j], >,  "-gt");
+				CMP_SINGLE_TEST(&testints[i], &testints[j], >=, "-ge");
+			}
+		} ASYNC_END;
+	}
+
+	ASYNC_BEGIN {
+		/* Check non-standard features */
+		MULTI_TEST("-k", files, "k");
+		SINGLE_TEST(0, "x", "-ef", "x");
+		SINGLE_TEST(0, "x", "-ef", "x-hardlink");
+		SINGLE_TEST(1, "x", "-ef", "z");
+		SINGLE_TEST(1, "x", "-ef", "r");
+		SINGLE_TEST(0, "new", "-nt", "old");
+		SINGLE_TEST(1, "old", "-nt", "new");
+		SINGLE_TEST(1, "old", "-nt", "also-old");
+		SINGLE_TEST(1, "new", "-ot", "old");
+		SINGLE_TEST(0, "old", "-ot", "new");
+		SINGLE_TEST(1, "old", "-ot", "also-old");
+	} ASYNC_END;
+	ASYNC_BEGIN {
+		/* Check non-support of harmful features */
+		SYNTAX_TEST("a", "-ab");
+		SYNTAX_TEST("a", "-ob");
+		SYNTAX_TEST("a", "-a");
+		SYNTAX_TEST("a", "-o");
+		SYNTAX_TEST("1", "-a", "1");
+		SYNTAX_TEST("1", "-o", "1");
+		SYNTAX_TEST("1", "-o", "1", "-a", "1");
+		SYNTAX_TEST("1", "-a", "1", "-o", "1");
+		SYNTAX_TEST("(", "-e", "/", ")");
+		SYNTAX_TEST("!", "1", "-o", "1");
+		SYNTAX_TEST("!", "1", "-a", "1");
+		SYNTAX_TEST("!", "1", "-o", "1", "-a", "1");
+		SYNTAX_TEST("!", "1", "-a", "1", "-o", "1");
+		SYNTAX_TEST("!", "(", "-e", "/", ")");
+	} ASYNC_END;
+	ASYNC_BEGIN {
+		/* Check non-support of harmful features */
+		SYNTAX_TEST("1", "<", "1");
+		SYNTAX_TEST("1", "<=", "1");
+		SYNTAX_TEST("1", ">", "1");
+		SYNTAX_TEST("1", ">=", "1");
+		SYNTAX_TEST("!", "1", "<", "1");
+		SYNTAX_TEST("!", "1", "<=", "1");
+		SYNTAX_TEST("!", "1", ">", "1");
+		SYNTAX_TEST("!", "1", ">=", "1");
+	} ASYNC_END;
+
+	async_join();
+
+	close(tfds[0]);
+	close(tfds[1]);
+	close(pfds[0]);
+	close(pfds[1]);
+	unlink("h");
+	unlink("r");
+	unlink("w");
+	unlink("x");
+	unlink("x-hardlink");
+	unlink("u");
+	unlink("g");
+	unlink("k");
+	unlink("+e");
+	unlink("s");
+	unlink("S");
+	unlink("p");
+	unlink("-N");
+	unlink("+N");
+	unlink("also-old");
+	unlink("old");
+	unlink("new");
+	unlink("-eq");
+	chdir("..");
+	rmdir(dir);
+
+	return main_ret;
+}
diff --git a/time.test.c b/time.test.c
new file mode 100644
index 0000000..37f7414
--- /dev/null
+++ b/time.test.c
_AT_@ -0,0 +1,218 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+#include <sys/time.h>
+
+static int
+time_trunc(int argc, char *argv[])
+{
+	int fds[2];
+	pid_t pid;
+	FILE *fp;
+	char *lines[4] = {NULL, NULL, NULL, NULL}, *tmp_line, *p;
+	size_t sizes[4] = {0, 0, 0, 0}, tmp_size;
+	ssize_t lens[4] = {0, 0, 0, 0}, tmp_len;
+	size_t i;
+
+	if (pipe(fds))
+		return perror("pipe"), -1;
+
+	switch ((pid = fork())) {
+	case -1:
+		return perror("fork"), -1;
+	case 0:
+#ifdef SET_PDEATHSIG
+		if (prctl(PR_SET_PDEATHSIG, SIGINT))
+			return perror("PR_SET_PDEATHSIG"), -1;
+#endif
+		close(fds[0]);
+		if (fds[1] != 2) {
+			if (dup2(fds[1], 2) != 2)
+				return perror("dup2"), -1;
+			close(fds[1]);
+		}
+		execvp(argv[2], &argv[2]);
+		return perror("exec"), -1;
+	default:
+		close(fds[1]);
+		break;
+	}
+
+	fp = fdopen(fds[0], "rb");
+	if (!fp)
+		return perror("fdopen"), -1;
+
+	while ((lens[3] = getline(&lines[3], &sizes[3], fp)) > 0) {
+		tmp_line = lines[0];
+		tmp_size = sizes[0];
+		tmp_len = lens[0];
+		memmove(lines, &lines[1], 3 * sizeof(*lines));
+		memmove(sizes, &sizes[1], 3 * sizeof(*sizes));
+		memmove(lens, &lens[1], 3 * sizeof(*lens));
+		lines[3] = tmp_line;
+		sizes[3] = tmp_size;
+		lens[3] = tmp_len;
+		if (tmp_line)
+			fwrite(tmp_line, 1, tmp_len, stderr);
+	}
+
+	if (ferror(fp))
+		return perror("ferror"), -1;
+	if (fclose(fp))
+		return perror("fclose"), -1;
+
+	for (i = 0; i < 3; i++) {
+		if (!lines[i])
+			continue;
+		if (memchr(lines[i], '\0', lens[i]) || !(p = strchr(lines[i], ' '))) {
+			fwrite(lines[i], 1, lens[i], stderr);
+			continue;
+		}
+		p = strchr(p, '.');
+		if (!p || strspn(&p[1], "0123456789")[&p[1]] != '\n' || strlen(p) < 3) {
+			fwrite(lines[i], 1, lens[i], stderr);
+			continue;
+		}
+		p[2] = '\0';
+		fprintf(stderr, "%s\n", lines[i]);
+	}
+
+	for (i = 0; i < 4; i++)
+		free(lines[i]);
+
+	if (fflush(stderr))
+		return perror("fflush"), -1;
+	if (ferror(stderr))
+		return perror("ferror"), -1;
+	if (fclose(stderr))
+		return perror("fclose"), -1;
+
+	(void) argc;
+	return 0;
+}
+
+volatile uintmax_t b = 1;
+volatile uintmax_t x;
+volatile uintmax_t *bs = &b;
+
+int
+main(int argc, char *argv[])
+{
+	struct itimerval timerval = {{0, 120000UL}, {0, 120000UL}};
+
+	if (argc > 1) {
+		if (!strcmp(argv[1], "trunc")) {
+			if (!time_trunc(argc, argv))
+				return 0;
+		} else if (!strcmp(argv[1], "sleep")) {
+			setitimer(ITIMER_REAL, &timerval, NULL);
+			pause();
+		} else if (!strcmp(argv[1], "user")) {
+			setitimer(ITIMER_REAL, &timerval, NULL);
+			for (x = 0; x != UINTMAX_MAX;) x += *bs;
+		} else if (!strcmp(argv[1], "signal")) {
+			timerval.it_interval.tv_usec = timerval.it_value.tv_usec = 100UL;
+			setitimer(ITIMER_REAL, &timerval, NULL);
+			pause();
+		}
+		return 200;
+	}
+
+	alarm(timeout);
+
+	ASYNC_BEGIN {
+		proc = CMD(argv[0], "trunc", "./time", "-p", "--", argv[0], "sleep");
+		CHECK(check_exit(EXIT, 0) && check_runtime(0.10, 0.15) && check_stdout(EQUALS, "") &&
+		      check_stderr(LINE, "real 0.1\nuser 0.0\nsys 0.0\n"));
+	} ASYNC_END;
+
+	ASYNC_BEGIN {
+		proc = CMD(argv[0], "trunc", "./time", "--", argv[0], "sleep"); /* non-portable test */
+		CHECK(check_exit(EXIT, 0) && check_runtime(0.10, 0.15) && check_stdout(EQUALS, "") &&
+		      check_stderr(LINE, "real 0.1\nuser 0.0\nsys 0.0\n"));
+	} ASYNC_END;
+
+	proc = CMD("./time", "---");
+	CHECK(check_exit(EXIT | RANGE, 1, SETUP_ERROR) && check_stderr(BEGINNING, "usage: ") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time");
+	CHECK(check_exit(EXIT | RANGE, 1, SETUP_ERROR) && check_stderr(BEGINNING, "usage: ") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "--");
+	CHECK(check_exit(EXIT | RANGE, 1, SETUP_ERROR) && check_stderr(BEGINNING, "usage: ") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "true");
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "false");
+	CHECK(!check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "--", "true");
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "--", "false");
+	CHECK(!check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "-p", "true");
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "-p", "false");
+	CHECK(!check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "cat");
+	set_input(IN_TEXT(STDIN_FILENO, "hello world\n"));
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, "hello world\n"));
+
+	proc = CMD("./time", "printf", "hello world\\n");
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, "hello world\n"));
+
+	proc = CMD("./time", "-p", "printf", "hello world\\n");
+	CHECK(check_exit(EXIT, SUCCESS) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, "hello world\n"));
+
+	proc = CMD("./time", "sh", "-c", "printf 'hello world\\n' >&2");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") &&
+	      check_stderr(BEGINNING, "hello world\n") && !check_stderr(EQUALS, "hello world\n"));
+
+	proc = CMD("./time", "-p", "sh", "-c", "printf 'hello world\\n' >&2");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") &&
+	      check_stderr(BEGINNING, "hello world\n") && !check_stderr(EQUALS, "hello world\n"));
+
+	proc = CMD("./time", "sh", "-c", "exit 4");
+	CHECK(check_exit(EXIT, 4) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "-p", "sh", "-c", "exit 4");
+	CHECK(check_exit(EXIT, 4) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "/");
+	CHECK(check_exit(EXIT, EXEC_ERROR) && check_stdout(EQUALS, "") &&
+	      (check_stderr(CONTAINS, strerror(EISDIR)) || check_stderr(CONTAINS, strerror(EACCES))));
+
+	proc = CMD("./time", "-p", "/");
+	CHECK(check_exit(EXIT, EXEC_ERROR) && check_stdout(EQUALS, "") &&
+	      (check_stderr(CONTAINS, strerror(EISDIR)) || check_stderr(CONTAINS, strerror(EACCES))));
+
+	proc = CMD("./time", "./ file that does not exist ");
+	CHECK(check_exit(EXIT, NOENT_ERROR) && check_stderr(CONTAINS, strerror(ENOENT)) && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "-p", "./ file that does not exist ");
+	CHECK(check_exit(EXIT, NOENT_ERROR) && check_stderr(CONTAINS, strerror(ENOENT)) && check_stdout(EQUALS, ""));
+
+	proc = CMD("./time", "-p", "--", argv[0], "signal");
+	CHECK(check_exit(EXIT | RANGE, 1, SETUP_ERROR) || check_exit(EXIT, SIGNAL_EXIT(SIGALRM)));
+
+	proc = CMD(argv[0], "trunc", "./time", "-p", "--", argv[0], "signal");
+	CHECK(check_exit(EXIT, 0) && check_stdout(EQUALS, "") && check_runtime(0.00, 0.05) &&
+	      check_stderr(LINE, "real 0.0\n"));
+
+	async_join();
+
+
+	proc = CMD(argv[0], "trunc", "./time", "-p", "--", argv[0], "user");
+	CHECK(check_exit(EXIT, 0) && check_runtime(0.10, 0.15) && check_stdout(EQUALS, "") &&
+	      check_stderr(LINE, "real 0.1\nuser 0.1\nsys 0.0\n"));
+
+	proc = CMD(argv[0], "trunc", "./time", "--", argv[0], "user"); /* non-portable test */
+	CHECK(check_exit(EXIT, 0) && check_runtime(0.10, 0.15) && check_stdout(EQUALS, "") &&
+	      check_stderr(LINE, "real 0.1\nuser 0.1\nsys 0.0\n"));
+
+	return main_ret;
+}
diff --git a/true.test.c b/true.test.c
new file mode 100644
index 0000000..0479f23
--- /dev/null
+++ b/true.test.c
_AT_@ -0,0 +1,31 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+#define TEST(...)\
+	proc = CMD(__VA_ARGS__);\
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""))
+
+int
+main(void)
+{
+	alarm(timeout);
+
+	TEST("./true");
+	TEST("./true", "1");
+	TEST("./true", "1", "2");
+	TEST("./true", "1", "2", "3");
+	TEST("./true", "-");
+	TEST("./true", "-h");
+	TEST("./true", "-H");
+	TEST("./true", "-v");
+	TEST("./true", "-V");
+	TEST("./true", "-vVhH");
+	TEST("./true", "--");
+	TEST("./true", "--", "1");
+	TEST("./true", "--hello");
+	TEST("./true", "--help");
+	TEST("./true", "--version");
+	TEST("./true", "---");
+
+	return main_ret;
+}
diff --git a/tty.test.c b/tty.test.c
new file mode 100644
index 0000000..ff4e511
--- /dev/null
+++ b/tty.test.c
_AT_@ -0,0 +1,44 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+int
+main(void)
+{
+	char buf[1024];
+	int fds[2];
+
+	alarm(timeout);
+
+	proc = CMD("./tty", "-");
+	CHECK(check_usage_error(BOOLEAN_ERROR));
+
+	proc = CMD("./tty", "x");
+	CHECK(check_usage_error(BOOLEAN_ERROR));
+
+	proc = CMD("./tty", "---");
+	CHECK(check_usage_error(BOOLEAN_ERROR));
+
+	proc = CMD("./tty");
+	CHECK(check_exit(EXIT, SUCCESS_FALSE) && check_stdout(EQUALS, "not a tty\n") && check_stderr(EQUALS, ""));
+
+	stpcpy(stpcpy(buf, openpt(0, fds)), "\n");
+	proc = CMD("./tty");
+	set_input(IN_FDS(STDIN_FILENO, fds[1], fds[0]));
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") &&
+	      check_stdout(EQUALS, buf) && check_stdout(BEGINNING, "/dev/pts/"));
+	close(fds[0]);
+
+#ifdef TODO
+	proc = CMD("./tty",  "--");
+	CHECK(check_exit(EXIT, SUCCESS_FALSE) && check_stdout(EQUALS, "not a tty\n") && check_stderr(EQUALS, ""));
+
+	stpcpy(stpcpy(buf, openpt(0, fds)), "\n");
+	proc = CMD("./tty", "--");
+	set_input(IN_FDS(STDIN_FILENO, fds[1], fds[0]));
+	CHECK(check_exit(EXIT, SUCCESS_TRUE) && check_stderr(EQUALS, "") &&
+	      check_stdout(EQUALS, buf) && check_stdout(BEGINNING, "/dev/pts/"));
+	close(fds[0]);
+#endif
+
+	return main_ret;
+}
diff --git a/uname.test.c b/uname.test.c
new file mode 100644
index 0000000..c2a825f
--- /dev/null
+++ b/uname.test.c
_AT_@ -0,0 +1,283 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+#include <sys/utsname.h>
+
+#define TEST(...)\
+	proc = CMD(__VA_ARGS__);\
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, buf))
+
+#define TEST1(OUT, ...)\
+	sprintf(buf, "%s\n", OUT);\
+	TEST(__VA_ARGS__)
+
+static const char *values[8];
+static char chars[8];
+
+static void
+test2(size_t f1, size_t f2)
+{
+	char buf[1000], flag1[10], flag2[10];
+	char c1 = chars[f1];
+	char c2 = chars[f2];
+
+	sprintf(buf, "%s %s\n", values[f1], values[f2]);
+	sprintf(flag1, "-%c%c", c1, c2);
+	TEST("./uname", flag1);
+
+	sprintf(flag1, "-%c%c", c2, c1);
+	TEST("./uname", flag1);
+
+	sprintf(flag1, "-%c", c1);
+	sprintf(flag2, "-%c", c2);
+	TEST("./uname", flag1, flag2);
+	TEST("./uname", flag2, flag1);
+}
+
+static void
+test3(size_t f1, size_t f2, size_t f3)
+{
+	char buf[1000], flag1[10], flag2[10], flag3[10];
+
+	sprintf(buf, "%s %s %s\n", values[f1], values[f2], values[f3]);
+	sprintf(flag1, "-%c", chars[f1]);
+	sprintf(flag2, "-%c", chars[f2]);
+	sprintf(flag3, "-%c", chars[f3]);
+
+	TEST("./uname", flag1, flag2, flag3);
+	TEST("./uname", flag1, flag3, flag2);
+	TEST("./uname", flag2, flag1, flag3);
+	TEST("./uname", flag3, flag1, flag2);
+	TEST("./uname", flag2, flag3, flag1);
+	TEST("./uname", flag3, flag2, flag1);
+}
+
+static void
+test4(size_t f1, size_t f2, size_t f3, size_t f4)
+{
+	char buf[1000], flag1[10], flag2[10], flag3[10], flag4[10];
+
+	sprintf(buf, "%s %s %s %s\n", values[f1], values[f2], values[f3], values[f4]);
+	sprintf(flag1, "-%c", chars[f1]);
+	sprintf(flag2, "-%c", chars[f2]);
+	sprintf(flag3, "-%c", chars[f3]);
+	sprintf(flag4, "-%c", chars[f4]);
+
+	TEST("./uname", flag1, flag2, flag3, flag4);
+	TEST("./uname", flag1, flag2, flag4, flag3);
+	TEST("./uname", flag1, flag3, flag2, flag4);
+	TEST("./uname", flag1, flag3, flag4, flag2);
+	TEST("./uname", flag1, flag4, flag2, flag3);
+	TEST("./uname", flag1, flag4, flag3, flag2);
+	TEST("./uname", flag2, flag1, flag3, flag4);
+	TEST("./uname", flag2, flag1, flag4, flag3);
+	TEST("./uname", flag2, flag3, flag1, flag4);
+	TEST("./uname", flag2, flag3, flag4, flag1);
+	TEST("./uname", flag2, flag4, flag1, flag3);
+	TEST("./uname", flag2, flag4, flag3, flag1);
+	TEST("./uname", flag3, flag1, flag2, flag4);
+	TEST("./uname", flag3, flag1, flag4, flag2);
+	TEST("./uname", flag3, flag2, flag1, flag4);
+	TEST("./uname", flag3, flag2, flag4, flag1);
+	TEST("./uname", flag3, flag4, flag1, flag2);
+	TEST("./uname", flag3, flag4, flag2, flag1);
+	TEST("./uname", flag4, flag1, flag2, flag3);
+	TEST("./uname", flag4, flag1, flag3, flag2);
+	TEST("./uname", flag4, flag2, flag1, flag3);
+	TEST("./uname", flag4, flag2, flag3, flag1);
+	TEST("./uname", flag4, flag3, flag1, flag2);
+	TEST("./uname", flag4, flag3, flag2, flag1);
+}
+
+static void
+test5(size_t f1, size_t f2, size_t f3, size_t f4, size_t f5)
+{
+	char buf[1000], flag1[10], flag2[10], flag3[10], flag4[10], flag5[10];
+
+	sprintf(buf, "%s %s %s %s %s\n", values[f1], values[f2], values[f3], values[f4], values[f5]);
+	sprintf(flag1, "-%c", chars[f1]);
+	sprintf(flag2, "-%c", chars[f2]);
+	sprintf(flag3, "-%c", chars[f3]);
+	sprintf(flag4, "-%c", chars[f4]);
+	sprintf(flag5, "-%c", chars[f5]);
+
+	TEST("./uname", flag1, flag2, flag3, flag4, flag5);
+	TEST("./uname", flag1, flag2, flag4, flag3, flag5);
+	TEST("./uname", flag1, flag3, flag2, flag4, flag5);
+	TEST("./uname", flag1, flag3, flag4, flag2, flag5);
+	TEST("./uname", flag1, flag4, flag2, flag3, flag5);
+	TEST("./uname", flag1, flag4, flag3, flag2, flag5);
+	TEST("./uname", flag2, flag1, flag3, flag4, flag5);
+	TEST("./uname", flag2, flag1, flag4, flag3, flag5);
+	TEST("./uname", flag2, flag3, flag1, flag4, flag5);
+	TEST("./uname", flag2, flag3, flag4, flag1, flag5);
+	TEST("./uname", flag2, flag4, flag1, flag3, flag5);
+	TEST("./uname", flag2, flag4, flag3, flag1, flag5);
+	TEST("./uname", flag3, flag1, flag2, flag4, flag5);
+	TEST("./uname", flag3, flag1, flag4, flag2, flag5);
+	TEST("./uname", flag3, flag2, flag1, flag4, flag5);
+	TEST("./uname", flag3, flag2, flag4, flag1, flag5);
+	TEST("./uname", flag3, flag4, flag1, flag2, flag5);
+	TEST("./uname", flag3, flag4, flag2, flag1, flag5);
+	TEST("./uname", flag4, flag1, flag2, flag3, flag5);
+	TEST("./uname", flag4, flag1, flag3, flag2, flag5);
+	TEST("./uname", flag4, flag2, flag1, flag3, flag5);
+	TEST("./uname", flag4, flag2, flag3, flag1, flag5);
+	TEST("./uname", flag4, flag3, flag1, flag2, flag5);
+	TEST("./uname", flag4, flag3, flag2, flag1, flag5);
+	TEST("./uname", flag1, flag2, flag3, flag5, flag4);
+	TEST("./uname", flag1, flag2, flag4, flag5, flag3);
+	TEST("./uname", flag1, flag3, flag2, flag5, flag4);
+	TEST("./uname", flag1, flag3, flag4, flag5, flag2);
+	TEST("./uname", flag1, flag4, flag2, flag5, flag3);
+	TEST("./uname", flag1, flag4, flag3, flag5, flag2);
+	TEST("./uname", flag2, flag1, flag3, flag5, flag4);
+	TEST("./uname", flag2, flag1, flag4, flag5, flag3);
+	TEST("./uname", flag2, flag3, flag1, flag5, flag4);
+	TEST("./uname", flag2, flag3, flag4, flag5, flag1);
+	TEST("./uname", flag2, flag4, flag1, flag5, flag3);
+	TEST("./uname", flag2, flag4, flag3, flag5, flag1);
+	TEST("./uname", flag3, flag1, flag2, flag5, flag4);
+	TEST("./uname", flag3, flag1, flag4, flag5, flag2);
+	TEST("./uname", flag3, flag2, flag1, flag5, flag4);
+	TEST("./uname", flag3, flag2, flag4, flag5, flag1);
+	TEST("./uname", flag3, flag4, flag1, flag5, flag2);
+	TEST("./uname", flag3, flag4, flag2, flag5, flag1);
+	TEST("./uname", flag4, flag1, flag2, flag5, flag3);
+	TEST("./uname", flag4, flag1, flag3, flag5, flag2);
+	TEST("./uname", flag4, flag2, flag1, flag5, flag3);
+	TEST("./uname", flag4, flag2, flag3, flag5, flag1);
+	TEST("./uname", flag4, flag3, flag1, flag5, flag2);
+	TEST("./uname", flag4, flag3, flag2, flag5, flag1);
+	TEST("./uname", flag1, flag2, flag5, flag3, flag4);
+	TEST("./uname", flag1, flag2, flag5, flag4, flag3);
+	TEST("./uname", flag1, flag3, flag5, flag2, flag4);
+	TEST("./uname", flag1, flag3, flag5, flag4, flag2);
+	TEST("./uname", flag1, flag4, flag5, flag2, flag3);
+	TEST("./uname", flag1, flag4, flag5, flag3, flag2);
+	TEST("./uname", flag2, flag1, flag5, flag3, flag4);
+	TEST("./uname", flag2, flag1, flag5, flag4, flag3);
+	TEST("./uname", flag2, flag3, flag5, flag1, flag4);
+	TEST("./uname", flag2, flag3, flag5, flag4, flag1);
+	TEST("./uname", flag2, flag4, flag5, flag1, flag3);
+	TEST("./uname", flag2, flag4, flag5, flag3, flag1);
+	TEST("./uname", flag3, flag1, flag5, flag2, flag4);
+	TEST("./uname", flag3, flag1, flag5, flag4, flag2);
+	TEST("./uname", flag3, flag2, flag5, flag1, flag4);
+	TEST("./uname", flag3, flag2, flag5, flag4, flag1);
+	TEST("./uname", flag3, flag4, flag5, flag1, flag2);
+	TEST("./uname", flag3, flag4, flag5, flag2, flag1);
+	TEST("./uname", flag4, flag1, flag5, flag2, flag3);
+	TEST("./uname", flag4, flag1, flag5, flag3, flag2);
+	TEST("./uname", flag4, flag2, flag5, flag1, flag3);
+	TEST("./uname", flag4, flag2, flag5, flag3, flag1);
+	TEST("./uname", flag4, flag3, flag5, flag1, flag2);
+	TEST("./uname", flag4, flag3, flag5, flag2, flag1);
+	TEST("./uname", flag1, flag5, flag2, flag3, flag4);
+	TEST("./uname", flag1, flag5, flag2, flag4, flag3);
+	TEST("./uname", flag1, flag5, flag3, flag2, flag4);
+	TEST("./uname", flag1, flag5, flag3, flag4, flag2);
+	TEST("./uname", flag1, flag5, flag4, flag2, flag3);
+	TEST("./uname", flag1, flag5, flag4, flag3, flag2);
+	TEST("./uname", flag2, flag5, flag1, flag3, flag4);
+	TEST("./uname", flag2, flag5, flag1, flag4, flag3);
+	TEST("./uname", flag2, flag5, flag3, flag1, flag4);
+	TEST("./uname", flag2, flag5, flag3, flag4, flag1);
+	TEST("./uname", flag2, flag5, flag4, flag1, flag3);
+	TEST("./uname", flag2, flag5, flag4, flag3, flag1);
+	TEST("./uname", flag3, flag5, flag1, flag2, flag4);
+	TEST("./uname", flag3, flag5, flag1, flag4, flag2);
+	TEST("./uname", flag3, flag5, flag2, flag1, flag4);
+	TEST("./uname", flag3, flag5, flag2, flag4, flag1);
+	TEST("./uname", flag3, flag5, flag4, flag1, flag2);
+	TEST("./uname", flag3, flag5, flag4, flag2, flag1);
+	TEST("./uname", flag4, flag5, flag1, flag2, flag3);
+	TEST("./uname", flag4, flag5, flag1, flag3, flag2);
+	TEST("./uname", flag4, flag5, flag2, flag1, flag3);
+	TEST("./uname", flag4, flag5, flag2, flag3, flag1);
+	TEST("./uname", flag4, flag5, flag3, flag1, flag2);
+	TEST("./uname", flag4, flag5, flag3, flag2, flag1);
+	TEST("./uname", flag5, flag1, flag2, flag3, flag4);
+	TEST("./uname", flag5, flag1, flag2, flag4, flag3);
+	TEST("./uname", flag5, flag1, flag3, flag2, flag4);
+	TEST("./uname", flag5, flag1, flag3, flag4, flag2);
+	TEST("./uname", flag5, flag1, flag4, flag2, flag3);
+	TEST("./uname", flag5, flag1, flag4, flag3, flag2);
+	TEST("./uname", flag5, flag2, flag1, flag3, flag4);
+	TEST("./uname", flag5, flag2, flag1, flag4, flag3);
+	TEST("./uname", flag5, flag2, flag3, flag1, flag4);
+	TEST("./uname", flag5, flag2, flag3, flag4, flag1);
+	TEST("./uname", flag5, flag2, flag4, flag1, flag3);
+	TEST("./uname", flag5, flag2, flag4, flag3, flag1);
+	TEST("./uname", flag5, flag3, flag1, flag2, flag4);
+	TEST("./uname", flag5, flag3, flag1, flag4, flag2);
+	TEST("./uname", flag5, flag3, flag2, flag1, flag4);
+	TEST("./uname", flag5, flag3, flag2, flag4, flag1);
+	TEST("./uname", flag5, flag3, flag4, flag1, flag2);
+	TEST("./uname", flag5, flag3, flag4, flag2, flag1);
+	TEST("./uname", flag5, flag4, flag1, flag2, flag3);
+	TEST("./uname", flag5, flag4, flag1, flag3, flag2);
+	TEST("./uname", flag5, flag4, flag2, flag1, flag3);
+	TEST("./uname", flag5, flag4, flag2, flag3, flag1);
+	TEST("./uname", flag5, flag4, flag3, flag1, flag2);
+	TEST("./uname", flag5, flag4, flag3, flag2, flag1);
+}
+
+int
+main(void)
+{
+	struct utsname name;
+	const char *sflag, *nflag, *rflag, *vflag, *mflag;
+	char buf[1000];
+	size_t a, b, c, d, e;
+
+	alarm(timeout);
+
+	if (uname(&name) < 0)
+		eperror("uname");
+	sflag = values[0] = name.sysname,  chars[0] = 's';
+	nflag = values[1] = name.nodename, chars[1] = 'n';
+	rflag = values[2] = name.release,  chars[2] = 'r';
+	vflag = values[3] = name.version,  chars[3] = 'v';
+	mflag = values[4] = name.machine,  chars[4] = 'm';
+
+#ifdef TODO
+	proc = CMD("./uname", "-");
+	CHECK(check_usage_error(REGULAR_ERROR));
+#endif
+
+	proc = CMD("./uname", "---");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	TEST1(sflag, "./uname");
+	TEST1(sflag, "./uname", "--");
+	TEST1(sflag, "./uname", "-s");
+	TEST1(sflag, "./uname", "-ss");
+	TEST1(sflag, "./uname", "-s", "-s");
+	TEST1(mflag, "./uname", "-m");
+	TEST1(nflag, "./uname", "-n");
+	TEST1(rflag, "./uname", "-r");
+	TEST1(vflag, "./uname", "-v");
+
+	for (COUNTER(a, 0, a < 5)) {
+		ASYNC_BEGIN {
+			for (COUNTER(b, a + 1, b < 5)) {
+				test2(a, b);
+				for (COUNTER(c, b + 1, c < 5)) {
+					test3(a, b, c);
+					for (COUNTER(d, c + 1, d < 5)) {
+						test4(a, b, c, d);
+						for (COUNTER(e, d + 1, e < 5))
+							test5(a, b, c, d, e);
+					}
+				}
+			}
+		} ASYNC_END;
+	}
+
+	sprintf(buf, "%s %s %s %s %s\n", values[0], values[1], values[2], values[3], values[4]);
+	TEST("./uname", "-a");
+	TEST("./uname", "-anv");
+	TEST("./uname", "-mras");
+
+	return main_ret;
+}
diff --git a/unexpand.test.c b/unexpand.test.c
new file mode 100644
index 0000000..2666dbf
--- /dev/null
+++ b/unexpand.test.c
_AT_@ -0,0 +1,97 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+struct Case {
+	const char *input;
+	size_t input_len;
+	const char *argv[2];
+	const char *output;
+	size_t output_len;
+};
+
+static struct Case failure_cases[] = {
+	{"",   0, {"---"},    "",   0},
+	{"",   0, {"-t2,1"},  "",   0},
+	{"",   0, {"-t0"},    "",   0},
+	{"",   0, {"-t0,1"},  "",   0},
+	{"",   0, {"-t"},     "",   0},
+	{"",   0, {"-t"},     "",   0},
+	{"",   0, {"-t", ""}, "",   0},
+	{NULL, 0, {NULL},     NULL, 0}
+};
+
+static struct Case success_cases[] = {
+	{BIN("x"),                    {NULL},      BIN("x")},
+	{BIN("        x"),            {NULL},      BIN("\tx")},
+	{BIN("\tx"),                  {NULL},      BIN("\tx")},
+	{BIN("1\t2"),                 {NULL},      BIN("1\t2")},
+	{BIN("1\t2"),                 {"-a"},      BIN("1\t2")},
+	{BIN("1\t2"),                 {"-t8"},     BIN("1\t2")},
+	{BIN("1\t2"),                 {"-t", "8"}, BIN("1\t2")},
+	{BIN("1       2"),            {"-"},       BIN("1       2")},
+	{BIN("1       2"),            {"--"},      BIN("1       2")},
+	{BIN("1       2"),            {"--", "-"}, BIN("1       2")},
+	{BIN("1       2"),            {NULL},      BIN("1       2")},
+	{BIN("1       2"),            {"-a"},      BIN("1\t2")},
+	{BIN("1       2"),            {"-t8"},     BIN("1\t2")},
+	{BIN("1       2"),            {"-t", "8"}, BIN("1\t2")},
+	{BIN("1       2       3"),    {"-a"},      BIN("1\t2\t3")},
+	{BIN("1       2       3"),    {"-t4"},     BIN("1\t\t2\t\t3")},
+	{BIN("1       \b 2       3"), {"-t4"},     BIN("1\t\t\b\t2\t\t3")},
+	{BIN("1       \b2        3"), {"-t4"},     BIN("1\t\t\b2\t\t3")},
+	{BIN("1        2      3"),    {"-t4"},     BIN("1\t\t 2\t\t3")},
+	{BIN("1    \n  8"),           {"-a"},      BIN("1    \n  8")},
+	{BIN("        \n        \n"), {NULL},      BIN("\t\n\t\n")},
+	{BIN("åäö     x\n"),          {"-a"},      BIN("åäö\tx\n")},
+	{BIN("åäö\b      x\n"),       {"-a"},      BIN("åäö\b\tx\n")},
+#ifdef TODO
+	{BIN("〇      x\n"),          {"-a"},      BIN("〇\tx\n")},
+	{BIN("〇\b       x\n"),       {"-a"},      BIN("〇\b\tx\n")},
+#endif
+	{BIN("  x     y\n"),          {"-t2,8"},   BIN("\tx\ty\n")},
+	{BIN("  x     y\n"),          {"-t2,7"},   BIN("\tx\t y\n")},
+	{BIN("  x    y\n"),           {"-t2,8"},   BIN("\tx    y\n")},
+	{BIN("  x     y\n"),          {"-t2 8"},   BIN("\tx\ty\n")},
+	{BIN("  x     y\n"),          {"-t2 7"},   BIN("\tx\t y\n")},
+	{BIN("  x    y\n"),           {"-t2 8"},   BIN("\tx    y\n")},
+	{BIN(" \tx\n"),               {NULL},      BIN("\tx\n")},
+	{BIN("1       2       3"),    {"-at8"},    BIN("1\t2\t3")},
+	{BIN("\0       \0       \0"), {"-at8"}, BIN("\0\t\0\t\0")},
+	{NULL, 0,                     {NULL},      NULL, 0}
+};
+
+int
+main(void)
+{
+	size_t i;
+	char f1[200], dir[100];
+
+	alarm(timeout);
+
+	sprintf(dir, "testdir-%ju", (uintmax_t)getpid());
+	if (mkdir(dir, 0700))
+		eperror(dir);
+	sprintf(f1, "%s/1", dir);
+	write_file(f1, "1       2", 0);
+
+	proc = CMD("./unexpand", "-a", f1, f1);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "1\t21\t2"));
+
+	for (COUNTER(i, 0, failure_cases[i].input)) {
+		proc = CMD("./unexpand", failure_cases[i].argv[0], failure_cases[i].argv[1]);
+		set_input(IN_NBIN(STDIN_FILENO, failure_cases[i].input, failure_cases[i].input_len));
+		CHECK(check_exit(EXIT, REGULAR_ERROR) && !check_stderr(EQUALS, "") && check_stdout(EQUALS, ""));
+	}
+
+	for (COUNTER(i, 0, success_cases[i].input)) {
+		proc = CMD("./unexpand", success_cases[i].argv[0], success_cases[i].argv[1]);
+		set_input(IN_NBIN(STDIN_FILENO, success_cases[i].input, success_cases[i].input_len));
+		CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") &&
+		      check_stdout(EQUALS | BINARY, success_cases[i].output, success_cases[i].output_len));
+	}
+
+	unlink(f1);
+	rmdir(dir);
+
+	return main_ret;
+}
diff --git a/unlink.test.c b/unlink.test.c
new file mode 100644
index 0000000..27cc5c6
--- /dev/null
+++ b/unlink.test.c
_AT_@ -0,0 +1,56 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+int
+main(void)
+{
+	char path[200], dir[100];
+
+	alarm(timeout);
+
+	sprintf(dir, "testdir-%ju", (uintmax_t)getpid());
+	if (mkdir(dir, 0700))
+		eperror(dir);
+	sprintf(path, "%s/f", dir);
+	close(open(path, O_CREAT | O_WRONLY | O_EXCL, 0600));
+
+#ifdef TODO
+	proc = CMD("./unlink", "--");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./unlink", "---");
+	CHECK(check_usage_error(REGULAR_ERROR));
+#endif
+
+	proc = CMD("./unlink", dir);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(EISDIR)));
+
+#ifdef TODO
+	proc = CMD("./unlink", "--", dir);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(EISDIR)));
+#endif
+
+	proc = CMD("./unlink", path, path);
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./unlink", path);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));
+
+	proc = CMD("./unlink", path);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(ENOENT)));
+
+#ifdef TODO
+	close(open(path, O_CREAT | O_WRONLY | O_EXCL, 0600));
+
+	proc = CMD("./unlink", "--", path);
+	CHECK(check_exit(EXIT, SUCCESS) && check_stdout(EQUALS, "") && check_stderr(EQUALS, ""));
+
+	proc = CMD("./unlink", "--", path);
+	CHECK(check_exit(EXIT, REGULAR_ERROR) && check_stdout(EQUALS, "") && check_stderr(CONTAINS, strerror(ENOENT)));
+#endif
+
+	unlink(path);
+	rmdir(dir);
+
+	return main_ret;
+}
diff --git a/whoami.test.c b/whoami.test.c
new file mode 100644
index 0000000..213da22
--- /dev/null
+++ b/whoami.test.c
_AT_@ -0,0 +1,38 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+#include <pwd.h>
+
+int
+main(void)
+{
+	char buf[100];
+
+	alarm(timeout);
+
+	stpcpy(stpcpy(buf, getpwuid(geteuid())->pw_name), "\n");
+
+	proc = CMD("./whoami");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, buf));
+
+#ifdef TODO
+	proc = CMD("./whoami", "--");
+	CHECK(check_exit(EXIT, SUCCESS) && check_stderr(EQUALS, "") && check_stdout(EQUALS, buf));
+#endif
+
+	proc = CMD("./whoami", "-");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./whoami", "x");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./whoami", "--", "-");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./whoami", "--", "x");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	proc = CMD("./whoami", "---");
+	CHECK(check_usage_error(REGULAR_ERROR));
+
+	return main_ret;
+}
diff --git a/yes.test.c b/yes.test.c
new file mode 100644
index 0000000..d5c41d4
--- /dev/null
+++ b/yes.test.c
_AT_@ -0,0 +1,131 @@
+/* See LICENSE file for copyright and license details. */
+#include "test-common.h"
+
+int
+head(size_t lines, char *argv[])
+{
+	int fds[2], status;
+	pid_t pid;
+	char buf[BUFSIZ], *p;
+	size_t n;
+	ssize_t r;
+
+	if (pipe(fds))
+		eperror("pipe");
+
+	switch ((pid = fork())) {
+	case -1:
+		eperror("fork");
+	case 0:
+		close(fds[0]);
+		if (fds[1] != STDOUT_FILENO) {
+			if (dup2(fds[1], STDOUT_FILENO) != STDOUT_FILENO)
+				eperror("dup2");
+			close(fds[1]);
+		}
+		execv(*argv, argv);
+		eperror("execv");
+	default:
+		close(fds[1]);
+		break;
+	}
+
+	while (lines) {
+		r = read(fds[0], buf, sizeof(buf));
+		if (r < 0)
+			perror("read");
+		if (!r)
+			break;
+		for (n = 0; n < (size_t)r && lines; n++) {
+			if (buf[n] == '\n' && !--lines) {
+				n += 1;
+				break;
+			}
+		}
+		for (p = buf; n; p = &p[n], n -= (size_t)r)
+			if ((r = write(STDOUT_FILENO, p, n)) < 0)
+				eperror("write");
+	}
+
+	close(fds[0]);
+	if (waitpid(pid, &status, 0) != pid)
+		perror("waitpid");
+
+	if (WIFSIGNALED(status))
+		return 128 + WTERMSIG(status);
+	if (WIFEXITED(status))
+		return WEXITSTATUS(status) % 128;
+	return 128;
+}
+
+int
+main(int argc, char *argv[])
+{
+	char *long1, *long2, *long_out1, *long_out2;
+
+	if (argc > 2)
+		return head((size_t)atoi(argv[1]), &argv[2]);
+
+	alarm(timeout);
+
+	long1 = malloc(10000);
+	if (!long1)
+		eperror("malloc");
+	long2 = malloc(10000);
+	if (!long2)
+		eperror("malloc");
+	long_out1 = malloc(20001UL);
+	if (!long_out1)
+		eperror("malloc");
+	long_out2 = malloc(40001UL);
+	if (!long_out2)
+		eperror("malloc");
+	memset(long1, 'x', 9999), long1[9999] = '\0';
+	memset(long2, 'y', 9999), long2[9999] = '\0';
+	stpcpy(stpcpy(stpcpy(stpcpy(long_out1, long1), "\n"), long1), "\n");
+	stpcpy(stpcpy(stpcpy(stpcpy(stpcpy(stpcpy(stpcpy(stpcpy(
+		long_out2,
+		long1), " "), long2), "\n"),
+		long1), " "), long2), "\n");
+
+	timeout = 1;
+
+#ifdef TODO
+	proc = CMD("./yes", "---");
+	CHECK(check_usage_error(REGULAR_ERROR));
+#endif
+
+	/* check support for 0 operands */
+
+	proc = CMD(argv[0], "5", "./yes");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "y\ny\ny\ny\ny\n"));
+
+#ifdef TODO
+	proc = CMD(argv[0], "5", "./yes", "--");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "y\ny\ny\ny\ny\n"));
+#endif
+
+	/* check support for 1 operand */
+
+	proc = CMD(argv[0], "5", "./yes", "x");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "x\nx\nx\nx\nx\n"));
+
+	proc = CMD(argv[0], "4", "./yes", "åäö𝔘");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "åäö𝔘\nåäö𝔘\nåäö𝔘\nåäö𝔘\n"));
+
+	proc = CMD(argv[0], "2", "./yes", long1);
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, long_out1));
+
+	/* check support for multiple operands */
+
+	proc = CMD(argv[0], "5", "./yes", "a", "b");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "a b\na b\na b\na b\na b\n"));
+
+	proc = CMD(argv[0], "3", "./yes", "a", "b", "c");
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, "a b c\na b c\na b c\n"));
+
+	proc = CMD(argv[0], "2", "./yes", long1, long2);
+	CHECK(check_exit(EXIT, SIGNAL_EXIT(SIGPIPE)) && check_stderr(EQUALS, "") && check_stdout(EQUALS, long_out2));
+
+	return main_ret;
+}
-- 
2.11.1
Received on Wed Jul 11 2018 - 21:39:23 CEST

This archive was generated by hypermail 2.3.0 : Wed Jul 11 2018 - 21:48:25 CEST