diff --git a/Makefile b/Makefile
index cc8585f2..b52fbf48 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,7 @@ ext \
games \
mbr \
mkinitrd \
+regress \
utils \
kernel
diff --git a/doc/user-guide b/doc/user-guide
index 089645ea..8141abd5 100644
--- a/doc/user-guide
+++ b/doc/user-guide
@@ -177,6 +177,7 @@ Sortix comes with a number of home-made programs. Here is an overview:
* `pager` - display file page by page
* `pong` - remake of the classic pong game
* `pwd` - print current directory path
+* `regress` - run system tests
* `rm` - remove file
* `rmdir` - remove empty directory
* `sh` - alias for the shell
@@ -532,6 +533,15 @@ initial filesystem used to bootstrap the real root filesystem.
make
make install
+### Building regress ###
+
+This is a collection of operating system test cases run using the `regress`
+driver program.
+
+ cd /src/regress
+ make
+ make install
+
### Building the Sortix Kernel ###
The Sortix kernel is the core of the Sortix operating system. It provides all
diff --git a/regress/.gitignore b/regress/.gitignore
new file mode 100644
index 00000000..3504b478
--- /dev/null
+++ b/regress/.gitignore
@@ -0,0 +1,3 @@
+regress
+test-*
+!test-*.c++
diff --git a/regress/Makefile b/regress/Makefile
new file mode 100644
index 00000000..dbcdac87
--- /dev/null
+++ b/regress/Makefile
@@ -0,0 +1,35 @@
+SOFTWARE_MEANT_FOR_SORTIX=1
+include ../compiler.mak
+include ../version.mak
+include ../dirs.mak
+
+OPTLEVEL?=-O2 -g
+CXXFLAGS?=$(OPTLEVEL)
+TESTDIR?=$(LIBEXECDIR)/test
+
+CPPFLAGS:=$(CPPFLAGS) -DVERSIONSTR=\"$(VERSION)\" -DTESTDIR=\"$(TESTDIR)\"
+CXXFLAGS:=$(CXXFLAGS) -Wall -Wextra -fno-exceptions -fno-rtti
+
+BINARIES:=\
+regress \
+
+TESTS:=\
+
+all: $(BINARIES) $(TESTS)
+
+.PHONY: all install uninstall clean
+
+install: all
+ mkdir -p $(DESTDIR)$(BINDIR)
+ install $(BINARIES) $(DESTDIR)$(BINDIR)
+ mkdir -p $(DESTDIR)$(TESTDIR)
+ifneq ($(TESTS),)
+ install $(TESTS) $(DESTDIR)$(TESTDIR)
+endif
+
+%: %.c++
+ $(CXX) -std=gnu++11 $(CPPFLAGS) $(CXXFLAGS) $< -o $@
+
+clean:
+ rm -f $(BINARIES) $(TESTS) *.o
+
diff --git a/regress/regress.c++ b/regress/regress.c++
new file mode 100644
index 00000000..4baf6c07
--- /dev/null
+++ b/regress/regress.c++
@@ -0,0 +1,336 @@
+/*******************************************************************************
+
+ Copyright(C) Jonas 'Sortie' Termansen 2014.
+
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option)
+ any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ more details.
+
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see .
+
+ regress.c++
+ Automatically invokes system tests.
+
+*******************************************************************************/
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static const int VERBOSITY_SILENT = 0;
+static const int VERBOSITY_NO_OUTPUT = 1;
+static const int VERBOSITY_QUIET = 2;
+static const int VERBOSITY_NORMAL = 3;
+static const int VERBOSITY_VERBOSE = 4;
+
+bool is_usable_terminal(int fd)
+{
+ struct wincurpos wcp;
+ struct winsize ws;
+ return isatty(fd) &&
+ tcgetwinsize(fd, &ws) == 0 &&
+ tcgetwincurpos(fd, &wcp) == 0 &&
+ 10 <= ws.ws_col;
+}
+
+void tenth_last_column()
+{
+ struct wincurpos wcp;
+ struct winsize ws;
+ fflush(stdout);
+ tcgetwinsize(1, &ws);
+ tcgetwincurpos(1, &wcp);
+ if ( wcp.wcp_col - ws.ws_col < 10 )
+ printf("\n");
+ printf("\e[%zuG", ws.ws_col - 10);
+ fflush(stdout);
+}
+
+static void help(FILE* fp, const char* argv0)
+{
+ fprintf(fp, "Usage: %s [OPTION]...\n", argv0);
+ fprintf(fp, "Automatically invoke system test cases.\n");
+ fprintf(fp, "\n");
+ fprintf(fp, "Mandatory arguments to long options are mandatory for short options too.\n");
+ fprintf(fp, " -b, --buffered buffer up test output in a pipe\n");
+ fprintf(fp, " -q --quiet output only failed tests\n");
+ fprintf(fp, " -s --silent don't output test results\n");
+ fprintf(fp, " --testdir=DIR use test programs in DIR\n");
+ fprintf(fp, " -u, --unbuffered send test output immediately to the terminal\n");
+ fprintf(fp, " -v --verbose verbose output\n");
+ fprintf(fp, " --help display this help and exit\n");
+ fprintf(fp, " --version output version information and exit\n");
+}
+
+static void version(FILE* fp, const char* argv0)
+{
+ fprintf(fp, "%s (Sortix) %s\n", argv0, VERSIONSTR);
+ fprintf(fp, "License GPLv3+: GNU GPL version 3 or later .\n");
+ fprintf(fp, "This is free software: you are free to change and redistribute it.\n");
+ fprintf(fp, "There is NO WARRANTY, to the extent permitted by law.\n");
+}
+
+static void compact_arguments(int* argc, char*** argv)
+{
+ for ( int i = 0; i < *argc; i++ )
+ {
+ while ( i < *argc && !(*argv)[i] )
+ {
+ for ( int n = i; n < *argc; n++ )
+ (*argv)[n] = (*argv)[n+1];
+ (*argc)--;
+ }
+ }
+}
+
+bool get_option_variable(const char* option, char** varptr,
+ const char* arg, int argc, char** argv, int* ip,
+ const char* argv0)
+{
+ size_t option_len = strlen(option);
+ if ( strncmp(option, arg, option_len) != 0 )
+ return false;
+ if ( arg[option_len] == '=' )
+ {
+ *varptr = strdup(arg + option_len + 1);
+ return true;
+ }
+ if ( arg[option_len] != '\0' )
+ return false;
+ if ( *ip + 1 == argc )
+ {
+ fprintf(stderr, "%s: expected operand after `%s'\n", argv0, option);
+ exit(1);
+ }
+ *varptr = strdup(argv[++*ip]), argv[*ip] = NULL;
+ return true;
+}
+
+static int no_dot_files(const struct dirent* entry)
+{
+ return entry->d_name[0] != '.' ? 1 : 0;
+}
+
+#define GET_OPTION_VARIABLE(str, varptr) \
+ get_option_variable(str, varptr, arg, argc, argv, &i, argv0)
+
+#define PIPE_BUFFER_SIZE 65536
+static unsigned char pipe_buffer[PIPE_BUFFER_SIZE];
+
+int main(int argc, char* argv[])
+{
+ bool buffered = true;
+#ifdef TESTDIR
+ char* testdir_path = strdup(TESTDIR);
+#else
+ char* testdir_path = NULL;
+#endif
+ int verbosity = VERBOSITY_NORMAL;
+
+ const char* argv0 = argv[0];
+ for ( int i = 1; i < argc; i++ )
+ {
+ const char* arg = argv[i];
+ if ( arg[0] != '-' || !arg[1] )
+ continue;
+ argv[i] = NULL;
+ if ( !strcmp(arg, "--") )
+ break;
+ if ( arg[1] != '-' )
+ {
+ while ( char c = *++arg ) switch ( c )
+ {
+ case 'b': buffered = true; break;
+ case 'q': verbosity--; break;
+ case 's': verbosity = VERBOSITY_SILENT; break;
+ case 'u': buffered = false; break;
+ case 'v': verbosity++; break;
+ default:
+ fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c);
+ help(stderr, argv0);
+ exit(1);
+ }
+ }
+ else if ( !strcmp(arg, "--help") )
+ help(stdout, argv0), exit(0);
+ else if ( !strcmp(arg, "--version") )
+ version(stdout, argv0), exit(0);
+ else if ( !strcmp(arg, "--buffered") )
+ buffered = true;
+ else if ( !strcmp(arg, "--quiet") )
+ verbosity--;
+ else if ( !strcmp(arg, "--silent") )
+ verbosity = VERBOSITY_SILENT;
+ else if ( GET_OPTION_VARIABLE("--testdir", &testdir_path) ) { }
+ else if ( !strcmp(arg, "--unbuffered") )
+ buffered = false;
+ else if ( !strcmp(arg, "--verbose") )
+ verbosity++;
+ else
+ {
+ fprintf(stderr, "%s: unknown option: %s\n", argv0, arg);
+ help(stderr, argv0);
+ exit(1);
+ }
+ }
+
+ compact_arguments(&argc, &argv);
+
+ if ( !testdir_path )
+ error(1, 0, "No test directory was specified and no default available");
+
+ struct dirent** entries;
+ int num_entries = scandir(testdir_path, &entries, no_dot_files, alphasort);
+ if ( num_entries < 0 )
+ error(2, errno, "scandir: `%s'", testdir_path);
+
+ int exit_status = 0;
+ bool use_terminal = is_usable_terminal(1);
+
+ for ( int i = 0; i < num_entries; i++ )
+ {
+ const struct dirent* entry = entries[i];
+
+ const char* test_name = entry->d_name;
+ char* test_path;
+ asprintf(&test_path, "%s/%s", testdir_path, test_name);
+
+ bool partial_begun_line = false;
+ if ( VERBOSITY_NORMAL <= verbosity )
+ {
+ if ( use_terminal )
+ {
+ printf("%s ", test_path);
+ tenth_last_column();
+ printf("[ ]");
+ if ( buffered )
+ partial_begun_line = true;
+ else
+ printf("\n");
+ fflush(stdout);
+ }
+ else if ( !buffered )
+ {
+ printf("%s [ ]\n", test_path);
+ fflush(stdout);
+ }
+ }
+
+ int pipe_fds[2];
+ if ( buffered && pipe(pipe_fds) < 0 )
+ error(1, errno, "pipe");
+
+ pid_t child_pid = fork();
+ if ( child_pid < 0 )
+ error(1, errno, "fork");
+
+ if ( !child_pid )
+ {
+ int dev_null_fd = open("/dev/null", O_RDWR);
+
+ if ( 0 < dev_null_fd )
+ dup2(dev_null_fd, 0);
+
+ if ( verbosity <= VERBOSITY_NO_OUTPUT )
+ {
+ dup2(dev_null_fd, 1);
+ dup2(dev_null_fd, 2);
+ }
+ else if ( buffered )
+ {
+ dup2(pipe_fds[1], 1);
+ dup2(pipe_fds[1], 2);
+ }
+
+ if ( buffered )
+ {
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ }
+
+ if ( 0 < dev_null_fd )
+ close(dev_null_fd);
+
+ char* child_argv[] = { test_path, NULL };
+ execv(child_argv[0], child_argv);
+ error(127, errno, "`%s'", test_path);
+ }
+
+ size_t bytes_read = 0;
+ if ( VERBOSITY_NO_OUTPUT < verbosity && buffered )
+ {
+ close(pipe_fds[1]);
+
+ while ( bytes_read < PIPE_BUFFER_SIZE )
+ {
+ ssize_t amount = read(pipe_fds[0],
+ pipe_buffer + bytes_read,
+ PIPE_BUFFER_SIZE - bytes_read);
+ if ( !amount )
+ break;
+ if ( amount < 0 )
+ break;
+ bytes_read += amount;
+ }
+
+ close(pipe_fds[0]);
+ }
+
+ int child_exit_code;
+ if ( waitpid(child_pid, &child_exit_code, 0) < 0 )
+ error(1, errno, "waitpid(%ji)", (intmax_t) child_pid);
+
+ bool success = WIFEXITED(child_exit_code) &&
+ WEXITSTATUS(child_exit_code) == 0;
+
+ if ( !success )
+ exit_status = 1;
+
+ if ( (VERBOSITY_NORMAL <= verbosity && success) ||
+ (VERBOSITY_NO_OUTPUT <= verbosity && !success ) )
+ {
+ if ( use_terminal )
+ {
+ if ( !partial_begun_line )
+ printf("%s ", test_path);
+ tenth_last_column();
+ printf("%s\n", success ?
+ "[\e[32mPASSED\e[m]" :
+ "[\e[31mFAILED\e[m]");
+ }
+ else
+ {
+ if ( success )
+ printf("%s [PASSED]\n", test_path);
+ else
+ printf("%s [FAILED]\n", test_path);
+ }
+
+ if ( VERBOSITY_NO_OUTPUT < verbosity && buffered )
+ fwrite(pipe_buffer, 1, bytes_read, stdout);
+
+ fflush(stdout);
+ }
+ }
+
+ for ( int i = 0; i < num_entries; i++ )
+ free(entries[i]);
+ free(entries);
+
+ return exit_status;
+}
diff --git a/regress/test.h b/regress/test.h
new file mode 100644
index 00000000..3c039e28
--- /dev/null
+++ b/regress/test.h
@@ -0,0 +1,53 @@
+/*******************************************************************************
+
+ Copyright(C) Jonas 'Sortie' Termansen 2014.
+
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option)
+ any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ more details.
+
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see .
+
+ test.h
+ Shared test utility functions.
+
+*******************************************************************************/
+
+#ifndef TEST_H
+#define TEST_H
+
+#undef NDEBUG
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define test_assert(x) assert(x)
+
+__attribute__((noreturn))
+void test_error(int errnum, const char* format, ...)
+{
+ fprintf(stderr, "%s: ", program_invocation_name);
+
+ va_list list;
+ va_start(list, format);
+ vfprintf(stderr, format, list);
+ va_end(list);
+
+ if ( errnum )
+ fprintf(stderr, ": %s", strerror(errnum));
+ fprintf(stderr, "\n");
+
+ exit(1);
+}
+
+#endif
diff --git a/system/Makefile b/system/Makefile
index f60b9212..6581a19a 100644
--- a/system/Makefile
+++ b/system/Makefile
@@ -27,6 +27,7 @@ clean:
$(MAKE) -B -C $(SRCDIR)/utils clean
$(MAKE) -B -C $(SRCDIR)/ext clean
$(MAKE) -B -C $(SRCDIR)/mbr clean
+ $(MAKE) -B -C $(SRCDIR)/systest clean
$(MAKE) -B -C $(SRCDIR)/kernel clean
# Set up the base filesystem.
@@ -76,6 +77,7 @@ system:
$(MAKE) -B -C $(SRCDIR)/bench install DESTDIR=$(ROOT) C_INCLUDE_PATH=$(ROOT)/include CPLUS_INCLUDE_PATH=$(ROOT)/include LIBRARY_PATH=$(ROOT)/$(cputype)/lib
$(MAKE) -B -C $(SRCDIR)/ext install DESTDIR=$(ROOT) C_INCLUDE_PATH=$(ROOT)/include CPLUS_INCLUDE_PATH=$(ROOT)/include LIBRARY_PATH=$(ROOT)/$(cputype)/lib
$(MAKE) -B -C $(SRCDIR)/mbr install DESTDIR=$(ROOT) C_INCLUDE_PATH=$(ROOT)/include CPLUS_INCLUDE_PATH=$(ROOT)/include LIBRARY_PATH=$(ROOT)/$(cputype)/lib
+ $(MAKE) -B -C $(SRCDIR)/systest install DESTDIR=$(ROOT) C_INCLUDE_PATH=$(ROOT)/include CPLUS_INCLUDE_PATH=$(ROOT)/include LIBRARY_PATH=$(ROOT)/$(cputype)/lib
$(MAKE) -B -C $(SRCDIR)/kernel install DESTDIR=$(ROOT) C_INCLUDE_PATH=$(ROOT)/include CPLUS_INCLUDE_PATH=$(ROOT)/include LIBRARY_PATH=$(ROOT)/$(cputype)/libs
# Rebuild and install the kernel