From 6454d4d215802341610a765ad7e772f961179b40 Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Thu, 20 Feb 2014 15:48:20 +0100 Subject: [PATCH] Add regress(1). --- Makefile | 1 + doc/user-guide | 10 ++ regress/.gitignore | 3 + regress/Makefile | 35 +++++ regress/regress.c++ | 336 ++++++++++++++++++++++++++++++++++++++++++++ regress/test.h | 53 +++++++ system/Makefile | 2 + 7 files changed, 440 insertions(+) create mode 100644 regress/.gitignore create mode 100644 regress/Makefile create mode 100644 regress/regress.c++ create mode 100644 regress/test.h 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