aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorRuss Allbery <rra@stanford.edu>2006-08-23 21:50:29 +0000
committerRuss Allbery <rra@stanford.edu>2006-08-23 21:50:29 +0000
commit06f652577d54e4a2b7d2724a1f9201e220d78159 (patch)
tree3861fb3c601ff240d3819112c37a77e2225b71d2 /tests
parent4718fc31896a0cc73ce93647b02bca4fb37754bd (diff)
Add a test infrastructure and a very basic test for the client
functionality so far.
Diffstat (limited to 'tests')
-rw-r--r--tests/TESTS1
-rw-r--r--tests/client/basic-t.in124
-rw-r--r--tests/data/README17
-rwxr-xr-xtests/data/cmd-fake39
-rw-r--r--tests/data/fake-keytabbin0 -> 62 bytes
-rw-r--r--tests/data/wallet.conf4
-rw-r--r--tests/runtests.c758
7 files changed, 943 insertions, 0 deletions
diff --git a/tests/TESTS b/tests/TESTS
new file mode 100644
index 0000000..e89e3fe
--- /dev/null
+++ b/tests/TESTS
@@ -0,0 +1 @@
+client/basic
diff --git a/tests/client/basic-t.in b/tests/client/basic-t.in
new file mode 100644
index 0000000..cb7619f
--- /dev/null
+++ b/tests/client/basic-t.in
@@ -0,0 +1,124 @@
+#! /bin/sh
+# $Id$
+#
+# Test suite for the remctl command-line client.
+#
+# Written by Russ Allbery <rra@stanford.edu>
+# Copyright 2006 Board of Trustees, Leland Stanford Jr. University
+# See README for licensing terms.
+
+# The count starts at 1 and is updated each time ok is printed. printcount
+# takes "ok" or "not ok".
+count=1
+printcount () {
+ echo "$1 $count $2"
+ count=`expr $count + 1`
+}
+
+# Run a program expected to succeed, and print ok if it does and produces
+# the correct output.
+runsuccess () {
+ w_output="$1"
+ shift
+ principal=`cat data/test.principal`
+ output=`$wallet -k "$principal" -p 14444 -s localhost "$@" 2>&1`
+ status=$?
+ if [ $status = 0 ] && [ x"$output" = x"$w_output" ] ; then
+ printcount "ok"
+ else
+ printcount "not ok"
+ echo " saw: $output"
+ echo " not: $w_output"
+ fi
+}
+
+# Run a program expected to fail and make sure it fails with the correct
+# exit status and the correct failure message. Strip the second colon and
+# everything after it off the error message since it's system-specific.
+runfailure () {
+ w_status="$1"
+ shift
+ w_output="$1"
+ shift
+ principal=`cat data/test.principal`
+ output=`$wallet -k "$principal" -p 14444 -s localhost "$@" 2>&1`
+ status=$?
+ output=`echo "$output" | sed 's/\(:[^:]*\):.*/\1/'`
+ if [ $status = $w_status ] && [ x"$output" = x"$w_output" ] ; then
+ printcount "ok"
+ else
+ printcount "not ok"
+ echo " saw: ($status) $output"
+ echo " not: ($w_status) $w_output"
+ fi
+}
+
+# Print the number of tests.
+echo 6
+
+# Find the client program.
+if [ -f ../data/test.keytab ] ; then
+ cd ..
+else
+ if [ -f tests/data/test.keytab ] ; then
+ cd tests
+ fi
+fi
+if [ ! -f data/test.keytab ] || [ -z "@REMCTLD@" ] ; then
+ for n in 1 2 3 4 5 6 ; do
+ echo ok $n \# skip -- no Kerberos configuration
+ done
+ exit 0
+fi
+wallet=../client/wallet
+if [ ! -x "$wallet" ] ; then
+ echo 'Cannot locate wallet client binary' >&2
+ exit 1
+fi
+
+# Start the remctld daemon and wait for it to start.
+rm -f data/pid
+KRB5_KTNAME=data/test.keytab; export KRB5_KTNAME
+( @REMCTLD@ -m -p 14444 -s `cat data/test.principal` -P data/pid \
+ -f data/wallet.conf &)
+KRB5CCNAME=data/test.cache; export KRB5CCNAME
+kinit -t -k data/test.keytab `cat data/test.principal` > /dev/null 2>&1
+if [ $? != 0 ] ; then
+ kinit -t data/test.keytab `cat data/test.principal` > /dev/null 2>&1
+fi
+if [ $? != 0 ] ; then
+ kinit -k -K data/test.keytab `cat data/test.principal` > /dev/null 2>&1
+fi
+if [ $? != 0 ] ; then
+ echo 'Unable to obtain Kerberos tickets' >&2
+ exit 1
+fi
+[ -f data/pid ] || sleep 1
+if [ ! -f data/pid ] ; then
+ echo 'remctld did not start' >&2
+ exit 1
+fi
+
+# Now, we can finally run our tests.
+runsuccess "" -c fake-wallet get keytab service/fake-test
+if cmp keytab data/fake-keytab >/dev/null 2>&1 ; then
+ printcount "ok"
+ rm keytab
+else
+ printcount "not ok"
+fi
+runsuccess "Some stuff about service/fake-test" \
+ -c fake-wallet show keytab service/fake-test
+runfailure 1 "Unknown object type srvtab" \
+ -c fake-wallet get srvtab service/fake-test
+runfailure 1 "Unknown keytab service/unknown" \
+ -c fake-wallet show keytab service/unknown
+runfailure 1 "Unknown keytab service/unknown" \
+ -c fake-wallet get keytab service/unknown
+
+# Clean up.
+rm -f data/test.cache
+if [ -f data/pid ] ; then
+ kill -HUP `cat data/pid`
+ rm -f data/pid
+fi
diff --git a/tests/data/README b/tests/data/README
new file mode 100644
index 0000000..890c4dc
--- /dev/null
+++ b/tests/data/README
@@ -0,0 +1,17 @@
+This directory contains data used by wallet's test suite. To enable tests
+that require GSS-API authentication and a working end-to-end Kerberos
+environment, create the K5 keytab that will be used for both the server
+and the client and put it in this directory as test.keytab. Then, create
+a file named test.principal and in it put the principal name corresponding
+to the key in the keytab on a single line ending with a newline.
+
+The presence of these two files will enable the tests that actually do
+GSS-API authentication.
+
+If you are building in a different directory tree than the source tree,
+don't put the files in this directory. Instead, after running configure,
+you will have an empty tests/data directory in your build tree. Put the
+test.keytab and test.principal files in that directory instead.
+
+Note that to successfully run much of the test suite, you will need to have
+remctld installed on the system running the tests.
diff --git a/tests/data/cmd-fake b/tests/data/cmd-fake
new file mode 100755
index 0000000..4093320
--- /dev/null
+++ b/tests/data/cmd-fake
@@ -0,0 +1,39 @@
+#!/bin/sh
+# $Id$
+#
+# This is a fake wallet backend that returns bogus data for verification by
+# the client test suite. It doesn't test any of the wallet server code.
+
+command="$1"
+shift
+type="$1"
+if [ "$1" != "keytab" ] ; then
+ echo "Unknown object type $1" >&2
+ exit 1
+fi
+shift
+
+case "$command" in
+get)
+ if [ "$1" = "service/fake-test" ] ; then
+ cat data/fake-keytab
+ exit 0
+ else
+ echo "Unknown keytab $1" >&2
+ exit 1
+ fi
+ ;;
+show)
+ if [ "$1" = "service/fake-test" ] ; then
+ echo "Some stuff about $1"
+ exit 0
+ else
+ echo "Unknown keytab $1" >&2
+ exit 1
+ fi
+ ;;
+*)
+ echo "Unknown command $command" >&2
+ exit 1
+ ;;
+esac
diff --git a/tests/data/fake-keytab b/tests/data/fake-keytab
new file mode 100644
index 0000000..92e3caa
--- /dev/null
+++ b/tests/data/fake-keytab
Binary files differ
diff --git a/tests/data/wallet.conf b/tests/data/wallet.conf
new file mode 100644
index 0000000..7ad998f
--- /dev/null
+++ b/tests/data/wallet.conf
@@ -0,0 +1,4 @@
+# remctl configuration for wallet client tests.
+# $Id$
+
+fake-wallet ALL data/cmd-fake ANYUSER
diff --git a/tests/runtests.c b/tests/runtests.c
new file mode 100644
index 0000000..f9da690
--- /dev/null
+++ b/tests/runtests.c
@@ -0,0 +1,758 @@
+/* $Id$
+**
+** Run a set of tests, reporting results.
+**
+** Usage:
+**
+** runtests <test-list>
+**
+** Expects a list of executables located in the given file, one line per
+** executable. For each one, runs it as part of a test suite, reporting
+** results. Test output should start with a line containing the number of
+** tests (numbered from 1 to this number), and then each line should be in
+** the following format:
+**
+** ok <number>
+** not ok <number>
+** ok <number> # skip
+**
+** where <number> is the number of the test. ok indicates success, not ok
+** indicates failure, and "# skip" indicates the test was skipped for some
+** reason (maybe because it doesn't apply to this platform).
+**
+** Any bug reports, bug fixes, and improvements are very much welcome and
+** should be sent to the e-mail address below.
+**
+** Copyright 2000, 2001, 2004 Russ Allbery <rra@stanford.edu>
+**
+** Permission is hereby granted, free of charge, to any person obtaining a
+** copy of this software and associated documentation files (the
+** "Software"), to deal in the Software without restriction, including
+** without limitation the rights to use, copy, modify, merge, publish,
+** distribute, sublicense, and/or sell copies of the Software, and to
+** permit persons to whom the Software is furnished to do so, subject to
+** the following conditions:
+**
+** The above copyright notice and this permission notice shall be included
+** in all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#include <config.h>
+#include <system.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <time.h>
+
+/* sys/time.h must be included before sys/resource.h on some platforms. */
+#include <sys/resource.h>
+
+/* Test status codes. */
+enum test_status {
+ TEST_FAIL,
+ TEST_PASS,
+ TEST_SKIP,
+ TEST_INVALID
+};
+
+/* Error exit statuses for test processes. */
+#define CHILDERR_DUP 100 /* Couldn't redirect stderr or stdout. */
+#define CHILDERR_EXEC 101 /* Couldn't exec child process. */
+#define CHILDERR_STDERR 102 /* Couldn't open stderr file. */
+
+/* Structure to hold data for a set of tests. */
+struct testset {
+ const char *file; /* The file name of the test. */
+ int count; /* Expected count of tests. */
+ int current; /* The last seen test number. */
+ int length; /* The length of the last status message. */
+ int passed; /* Count of passing tests. */
+ int failed; /* Count of failing lists. */
+ int skipped; /* Count of skipped tests (passed). */
+ enum test_status *results; /* Table of results by test number. */
+ int aborted; /* Whether the set as aborted. */
+ int reported; /* Whether the results were reported. */
+ int status; /* The exit status of the test. */
+};
+
+/* Structure to hold a linked list of test sets. */
+struct testlist {
+ struct testset *ts;
+ struct testlist *next;
+};
+
+/* Header used for test output. %s is replaced by the file name of the list
+ of tests. */
+static const char banner[] = "\n\
+Running all tests listed in %s. If any tests fail, run the failing\n\
+test program by hand to see more details. The test program will have the\n\
+same name as the test set but with \"-t\" appended.\n\n";
+
+/* Header for reports of failed tests. */
+static const char header[] = "\n\
+Failed Set Fail/Total (%) Skip Stat Failing Tests\n\
+-------------------------- -------------- ---- ---- ------------------------";
+
+/* Include the file name and line number in malloc failures. */
+#define xmalloc(size) x_malloc((size), __FILE__, __LINE__)
+#define xstrdup(p) x_strdup((p), __FILE__, __LINE__)
+
+/* Internal prototypes. */
+static void sysdie(const char *format, ...);
+static void *x_malloc(size_t, const char *file, int line);
+static char *x_strdup(const char *, const char *file, int line);
+static int test_analyze(const struct testset *);
+static int test_batch(const char *testlist);
+static void test_checkline(const char *line, struct testset *);
+static void test_fail_summary(const struct testlist *);
+static int test_init(const char *line, struct testset *);
+static int test_print_range(int first, int last, int chars, int limit);
+static void test_summarize(const struct testset *, int status);
+static pid_t test_start(const char *path, int *fd);
+static double tv_diff(const struct timeval *, const struct timeval *);
+static double tv_seconds(const struct timeval *);
+static double tv_sum(const struct timeval *, const struct timeval *);
+
+
+/* Report a fatal error, including the results of strerror, and exit. */
+static void
+sysdie(const char *format, ...)
+{
+ int oerrno;
+ va_list args;
+
+ oerrno = errno;
+ fflush(stdout);
+ fprintf(stderr, "runtests: ");
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, ": %s\n", strerror(oerrno));
+ exit(1);
+}
+
+
+/* Allocate memory, reporting a fatal error and exiting on failure. */
+static void *
+x_malloc(size_t size, const char *file, int line)
+{
+ void *p;
+
+ p = malloc(size);
+ if (!p)
+ sysdie("failed to malloc %lu bytes at %s line %d",
+ (unsigned long) size, file, line);
+ return p;
+}
+
+
+/* Copy a string, reporting a fatal error and exiting on failure. */
+static char *
+x_strdup(const char *s, const char *file, int line)
+{
+ char *p;
+ size_t len;
+
+ len = strlen(s) + 1;
+ p = malloc(len);
+ if (!p)
+ sysdie("failed to strdup %lu bytes at %s line %d",
+ (unsigned long) len, file, line);
+ memcpy(p, s, len);
+ return p;
+}
+
+
+/* Given a struct timeval, return the number of seconds it represents as a
+ double. Use difftime() to convert a time_t to a double. */
+static double
+tv_seconds(const struct timeval *tv)
+{
+ return difftime(tv->tv_sec, 0) + tv->tv_usec * 1e-6;
+}
+
+/* Given two struct timevals, return the difference in seconds. */
+static double
+tv_diff(const struct timeval *tv1, const struct timeval *tv0)
+{
+ return tv_seconds(tv1) - tv_seconds(tv0);
+}
+
+/* Given two struct timevals, return the sum in seconds as a double. */
+static double
+tv_sum(const struct timeval *tv1, const struct timeval *tv2)
+{
+ return tv_seconds(tv1) + tv_seconds(tv2);
+}
+
+
+/* Read the first line of test output, which should contain the range of
+ test numbers, and initialize the testset structure. Assume it was zeroed
+ before being passed in. Return true if initialization succeeds, false
+ otherwise. */
+static int
+test_init(const char *line, struct testset *ts)
+{
+ int i;
+
+ /* Prefer a simple number of tests, but if the count is given as a range
+ such as 1..10, accept that too for compatibility with Perl's
+ Test::Harness. */
+ while (isspace((unsigned char)(*line)))
+ line++;
+ if (strncmp(line, "1..", 3) == 0)
+ line += 3;
+
+ /* Get the count, check it for validity, and initialize the struct. */
+ i = atoi(line);
+ if (i <= 0) {
+ puts("invalid test count");
+ ts->aborted = 1;
+ ts->reported = 1;
+ return 0;
+ }
+ ts->count = i;
+ ts->results = xmalloc(ts->count * sizeof(enum test_status));
+ for (i = 0; i < ts->count; i++)
+ ts->results[i] = TEST_INVALID;
+ return 1;
+}
+
+
+/* Start a program, connecting its stdout to a pipe on our end and its
+ stderr to /dev/null, and storing the file descriptor to read from in the
+ two argument. Returns the PID of the new process. Errors are fatal. */
+static pid_t
+test_start(const char *path, int *fd)
+{
+ int fds[2], errfd;
+ pid_t child;
+
+ if (pipe(fds) == -1) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't create pipe");
+ }
+ child = fork();
+ if (child == (pid_t) -1) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't fork");
+ } else if (child == 0) {
+ /* In child. Set up our stdout and stderr. */
+ errfd = open("/dev/null", O_WRONLY);
+ if (errfd < 0)
+ _exit(CHILDERR_STDERR);
+ if (dup2(errfd, 2) == -1)
+ _exit(CHILDERR_DUP);
+ close(fds[0]);
+ if (dup2(fds[1], 1) == -1)
+ _exit(CHILDERR_DUP);
+
+ /* Now, exec our process. */
+ if (execl(path, path, (char *) 0) == -1)
+ _exit(CHILDERR_EXEC);
+ } else {
+ /* In parent. Close the extra file descriptor. */
+ close(fds[1]);
+ }
+ *fd = fds[0];
+ return child;
+}
+
+
+/* Back up over the output saying what test we were executing. */
+static void
+test_backspace(struct testset *ts)
+{
+ int i;
+
+ if (!isatty(STDOUT_FILENO))
+ return;
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ for (i = 0; i < ts->length; i++)
+ putchar(' ');
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ ts->length = 0;
+}
+
+
+/* Given a single line of output from a test, parse it and return the
+ success status of that test. Anything printed to stdout not matching the
+ form /^(not )?ok \d+/ is ignored. Sets ts->current to the test number
+ that just reported status. */
+static void
+test_checkline(const char *line, struct testset *ts)
+{
+ enum test_status status = TEST_PASS;
+ int current;
+
+ /* If the given line isn't newline-terminated, it was too big for an
+ fgets(), which means ignore it. */
+ if (line[strlen(line) - 1] != '\n')
+ return;
+
+ /* Parse the line, ignoring something we can't parse. */
+ if (strncmp(line, "not ", 4) == 0) {
+ status = TEST_FAIL;
+ line += 4;
+ }
+ if (strncmp(line, "ok ", 3) != 0)
+ return;
+ line += 3;
+ current = atoi(line);
+ if (current == 0)
+ return;
+ if (current < 0 || current > ts->count) {
+ test_backspace(ts);
+ printf("invalid test number %d\n", current);
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+ while (isspace((unsigned char)(*line)))
+ line++;
+ while (isdigit((unsigned char)(*line)))
+ line++;
+ while (isspace((unsigned char)(*line)))
+ line++;
+ if (*line == '#') {
+ line++;
+ while (isspace((unsigned char)(*line)))
+ line++;
+ if (strncmp(line, "skip", 4) == 0)
+ status = TEST_SKIP;
+ }
+
+ /* Make sure that the test number is in range and not a duplicate. */
+ if (ts->results[current - 1] != TEST_INVALID) {
+ test_backspace(ts);
+ printf("duplicate test number %d\n", current);
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+
+ /* Good results. Increment our various counters. */
+ switch (status) {
+ case TEST_PASS: ts->passed++; break;
+ case TEST_FAIL: ts->failed++; break;
+ case TEST_SKIP: ts->skipped++; break;
+ default: break;
+ }
+ ts->current = current;
+ ts->results[current - 1] = status;
+ test_backspace(ts);
+ if (isatty(STDOUT_FILENO)) {
+ ts->length = printf("%d/%d", current, ts->count);
+ fflush(stdout);
+ }
+}
+
+
+/* Print out a range of test numbers, returning the number of characters it
+ took up. Add a comma and a space before the range if chars indicates
+ that something has already been printed on the line, and print
+ ... instead if chars plus the space needed would go over the limit (use a
+ limit of 0 to disable this. */
+static int
+test_print_range(int first, int last, int chars, int limit)
+{
+ int needed = 0;
+ int out = 0;
+ int n;
+
+ if (chars > 0) {
+ needed += 2;
+ if (!limit || chars <= limit) out += printf(", ");
+ }
+ for (n = first; n > 0; n /= 10)
+ needed++;
+ if (last > first) {
+ for (n = last; n > 0; n /= 10)
+ needed++;
+ needed++;
+ }
+ if (limit && chars + needed > limit) {
+ if (chars <= limit)
+ out += printf("...");
+ } else {
+ if (last > first)
+ out += printf("%d-", first);
+ out += printf("%d", last);
+ }
+ return out;
+}
+
+
+/* Summarize a single test set. The second argument is 0 if the set exited
+ cleanly, a positive integer representing the exit status if it exited
+ with a non-zero status, and a negative integer representing the signal
+ that terminated it if it was killed by a signal. */
+static void
+test_summarize(const struct testset *ts, int status)
+{
+ int i;
+ int missing = 0;
+ int failed = 0;
+ int first = 0;
+ int last = 0;
+
+ if (ts->aborted) {
+ fputs("aborted", stdout);
+ if (ts->count > 0)
+ printf(", passed %d/%d", ts->passed, ts->count - ts->skipped);
+ } else {
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ if (missing == 0)
+ fputs("MISSED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ missing++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (missing && !failed)
+ fputs("; ", stdout);
+ if (failed == 0)
+ fputs("FAILED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ failed++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ if (!missing && !failed) {
+ fputs(!status ? "ok" : "dubious", stdout);
+ if (ts->skipped > 0) {
+ if (ts->skipped == 1)
+ printf(" (skipped %d test)", ts->skipped);
+ else
+ printf(" (skipped %d tests)", ts->skipped);
+ }
+ }
+ }
+ if (status > 0)
+ printf(" (exit status %d)", status);
+ else if (status < 0)
+ printf(" (killed by signal %d%s)", -status,
+ WCOREDUMP(ts->status) ? ", core dumped" : "");
+ putchar('\n');
+}
+
+
+/* Given a test set, analyze the results, classify the exit status, handle a
+ few special error messages, and then pass it along to test_summarize()
+ for the regular output. */
+static int
+test_analyze(const struct testset *ts)
+{
+ if (ts->reported)
+ return 0;
+ if (WIFEXITED(ts->status) && WEXITSTATUS(ts->status) != 0) {
+ switch (WEXITSTATUS(ts->status)) {
+ case CHILDERR_DUP:
+ if (!ts->reported)
+ puts("can't dup file descriptors");
+ break;
+ case CHILDERR_EXEC:
+ if (!ts->reported)
+ puts("execution failed (not found?)");
+ break;
+ case CHILDERR_STDERR:
+ if (!ts->reported)
+ puts("can't open /dev/null");
+ break;
+ default:
+ test_summarize(ts, WEXITSTATUS(ts->status));
+ break;
+ }
+ return 0;
+ } else if (WIFSIGNALED(ts->status)) {
+ test_summarize(ts, -WTERMSIG(ts->status));
+ return 0;
+ } else {
+ test_summarize(ts, 0);
+ return (ts->failed == 0);
+ }
+}
+
+
+/* Runs a single test set, accumulating and then reporting the results.
+ Returns true if the test set was successfully run and all tests passed,
+ false otherwise. */
+static int
+test_run(struct testset *ts)
+{
+ pid_t testpid, child;
+ int outfd, i, status;
+ FILE *output;
+ char buffer[BUFSIZ];
+ char *file;
+
+ /* Initialize the test and our data structures, flagging this set in
+ error if the initialization fails. */
+ file = xmalloc(strlen(ts->file) + 3);
+ strcpy(file, ts->file);
+ strcat(file, "-t");
+ testpid = test_start(file, &outfd);
+ free(file);
+ output = fdopen(outfd, "r");
+ if (!output) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("fdopen failed");
+ }
+ if (!fgets(buffer, sizeof(buffer), output))
+ ts->aborted = 1;
+ if (!ts->aborted && !test_init(buffer, ts)) {
+ while (fgets(buffer, sizeof(buffer), output))
+ ;
+ ts->aborted = 1;
+ }
+
+ /* Pass each line of output to test_checkline(). */
+ while (!ts->aborted && fgets(buffer, sizeof(buffer), output))
+ test_checkline(buffer, ts);
+ if (ferror(output))
+ ts->aborted = 1;
+ test_backspace(ts);
+
+ /* Close the output descriptor, retrieve the exit status, and pass that
+ information to test_analyze() for eventual output. */
+ fclose(output);
+ child = waitpid(testpid, &ts->status, 0);
+ if (child == (pid_t) -1) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("waitpid for %u failed", (unsigned int) testpid);
+ }
+ status = test_analyze(ts);
+
+ /* Convert missing tests to failed tests. */
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ ts->failed++;
+ ts->results[i] = TEST_FAIL;
+ status = 0;
+ }
+ }
+ return status;
+}
+
+
+/* Summarize a list of test failures. */
+static void
+test_fail_summary(const struct testlist *fails)
+{
+ const struct testset *ts;
+ int i, chars, total, first, last;
+
+ puts(header);
+
+ /* Failed Set Fail/Total (%) Skip Stat Failing (25)
+ -------------------------- -------------- ---- ---- -------------- */
+ for (; fails; fails = fails->next) {
+ ts = fails->ts;
+ total = ts->count - ts->skipped;
+ printf("%-26.26s %4d/%-4d %3.0f%% %4d ", ts->file, ts->failed,
+ total, total ? (ts->failed * 100.0) / total : 0,
+ ts->skipped);
+ if (WIFEXITED(ts->status))
+ printf("%4d ", WEXITSTATUS(ts->status));
+ else
+ printf(" -- ");
+ if (ts->aborted) {
+ puts("aborted");
+ continue;
+ }
+ chars = 0;
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ chars += test_print_range(first, last, chars, 20);
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, chars, 20);
+ putchar('\n');
+ }
+}
+
+
+/* Run a batch of tests from a given file listing each test on a line by
+ itself. The file must be rewindable. Returns true iff all tests
+ passed. */
+static int
+test_batch(const char *testlist)
+{
+ FILE *tests;
+ size_t length, i;
+ size_t longest = 0;
+ char buffer[BUFSIZ];
+ int line;
+ struct testset ts, *tmp;
+ struct timeval start, end;
+ struct rusage stats;
+ struct testlist *failhead = 0;
+ struct testlist *failtail = 0;
+ int total = 0;
+ int passed = 0;
+ int skipped = 0;
+ int failed = 0;
+ int aborted = 0;
+
+ /* Open our file of tests to run and scan it, checking for lines that
+ are too long and searching for the longest line. */
+ tests = fopen(testlist, "r");
+ if (!tests)
+ sysdie("can't open %s", testlist);
+ line = 0;
+ while (fgets(buffer, sizeof(buffer), tests)) {
+ line++;
+ length = strlen(buffer) - 1;
+ if (buffer[length] != '\n') {
+ fprintf(stderr, "%s:%d: line too long\n", testlist, line);
+ exit(1);
+ }
+ if (length > longest)
+ longest = length;
+ }
+ if (fseek(tests, 0, SEEK_SET) == -1)
+ sysdie("can't rewind %s", testlist);
+
+ /* Add two to longest and round up to the nearest tab stop. This is how
+ wide the column for printing the current test name will be. */
+ longest += 2;
+ if (longest % 8)
+ longest += 8 - (longest % 8);
+
+ /* Start the wall clock timer. */
+ gettimeofday(&start, NULL);
+
+ /* Now, plow through our tests again, running each one. Check line
+ length again out of paranoia. */
+ line = 0;
+ while (fgets(buffer, sizeof(buffer), tests)) {
+ line++;
+ length = strlen(buffer) - 1;
+ if (buffer[length] != '\n') {
+ fprintf(stderr, "%s:%d: line too long\n", testlist, line);
+ exit(1);
+ }
+ buffer[length] = '\0';
+ fputs(buffer, stdout);
+ for (i = length; i < longest; i++)
+ putchar('.');
+ memset(&ts, 0, sizeof(ts));
+ ts.file = xstrdup(buffer);
+ if (!test_run(&ts)) {
+ tmp = xmalloc(sizeof(struct testset));
+ memcpy(tmp, &ts, sizeof(struct testset));
+ if (!failhead) {
+ failhead = xmalloc(sizeof(struct testset));
+ failhead->ts = tmp;
+ failhead->next = 0;
+ failtail = failhead;
+ } else {
+ failtail->next = xmalloc(sizeof(struct testset));
+ failtail = failtail->next;
+ failtail->ts = tmp;
+ failtail->next = 0;
+ }
+ }
+ aborted += ts.aborted;
+ total += ts.count;
+ passed += ts.passed;
+ skipped += ts.skipped;
+ failed += ts.failed;
+ }
+ total -= skipped;
+
+ /* Stop the timer and get our child resource statistics. */
+ gettimeofday(&end, NULL);
+ getrusage(RUSAGE_CHILDREN, &stats);
+
+ /* Print out our final results. */
+ if (failhead) test_fail_summary(failhead);
+ putchar('\n');
+ if (aborted != 0) {
+ if (aborted == 1)
+ printf("Aborted %d test set", aborted);
+ else
+ printf("Aborted %d test sets", aborted);
+ printf(", passed %d/%d tests", passed, total);
+ }
+ else if (failed == 0)
+ fputs("All tests successful", stdout);
+ else
+ printf("Failed %d/%d tests, %.2f%% okay", failed, total,
+ (total - failed) * 100.0 / total);
+ if (skipped != 0) {
+ if (skipped == 1)
+ printf(", %d test skipped", skipped);
+ else
+ printf(", %d tests skipped", skipped);
+ }
+ puts(".");
+ printf("Files=%d, Tests=%d", line, total);
+ printf(", %.2f seconds", tv_diff(&end, &start));
+ printf(" (%.2f usr + %.2f sys = %.2f CPU)\n",
+ tv_seconds(&stats.ru_utime), tv_seconds(&stats.ru_stime),
+ tv_sum(&stats.ru_utime, &stats.ru_stime));
+ return (failed == 0 && aborted == 0);
+}
+
+
+/* Main routine. Given a file listing tests, run each test listed. */
+int
+main(int argc, char *argv[])
+{
+ if (argc != 2) {
+ fprintf(stderr, "Usage: runtests <test-list>\n");
+ exit(1);
+ }
+ printf(banner, argv[1]);
+ exit(test_batch(argv[1]) ? 0 : 1);
+}