summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTavian Barnes <tavianator@tavianator.com>2025-02-27 16:11:40 -0500
committerTavian Barnes <tavianator@tavianator.com>2025-02-27 16:11:40 -0500
commit1aefb830360e43b6e5ddee96791eb83cdad766cc (patch)
treea1ab767d7cacb576de370ba6112d5ea75a232db7
parenta36774be636c3429c6e73de33bf65a1bdbdcfb4b (diff)
downloadbfs-1aefb830360e43b6e5ddee96791eb83cdad766cc.tar.xz
tests/units: Run each test in a separate process
-rw-r--r--tests/main.c188
1 files changed, 161 insertions, 27 deletions
diff --git a/tests/main.c b/tests/main.c
index 81c2311..d118bd7 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -7,24 +7,39 @@
#include "tests.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "color.h"
+#include "list.h"
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
+#include <stdint.h>
#include <string.h>
#include <time.h>
+#include <unistd.h>
/** Result of the current test. */
-static thread_local bool pass;
+static bool pass;
bool bfs_check_impl(bool result) {
pass &= result;
return result;
}
-/** Unit test function type. */
-typedef void test_fn(void);
+/**
+ * A running test.
+ */
+struct test_proc {
+ /** Linked list links. */
+ struct test_proc *prev, *next;
+
+ /** The PID of this test. */
+ pid_t pid;
+ /** The name of this test. */
+ const char *name;
+};
/**
* Global test context.
@@ -35,6 +50,17 @@ struct test_ctx {
/** The arguments themselves. */
char **argv;
+ /** Maximum jobs (-j). */
+ int jobs;
+ /** Current jobs. */
+ int running;
+ /** Completed jobs. */
+ int done;
+ /** List of running tests. */
+ struct {
+ struct test_proc *head, *tail;
+ } procs;
+
/** Parsed colors. */
struct colors *colors;
/** Colorized output stream. */
@@ -45,10 +71,15 @@ struct test_ctx {
};
/** Initialize the test context. */
-static int test_init(struct test_ctx *ctx, int argc, char **argv) {
+static int test_init(struct test_ctx *ctx, int jobs, int argc, char **argv) {
ctx->argc = argc;
ctx->argv = argv;
+ ctx->jobs = jobs;
+ ctx->running = 0;
+ ctx->done = 0;
+ LIST_INIT(&ctx->procs);
+
ctx->colors = parse_colors();
ctx->cout = cfwrap(stdout, ctx->colors, false);
if (!ctx->cout) {
@@ -60,26 +91,15 @@ static int test_init(struct test_ctx *ctx, int argc, char **argv) {
return 0;
}
-/** Finalize the test context. */
-static int test_fini(struct test_ctx *ctx) {
- if (ctx->cout) {
- cfclose(ctx->cout);
- }
-
- free_colors(ctx->colors);
-
- return ctx->ret;
-}
-
/** Check if a test case is enabled for this run. */
static bool should_run(const struct test_ctx *ctx, const char *test) {
// Run all tests by default
- if (ctx->argc < 2) {
+ if (ctx->argc == 0) {
return true;
}
// With args, run only specified tests
- for (int i = 1; i < ctx->argc; ++i) {
+ for (int i = 0; i < ctx->argc; ++i) {
if (strcmp(test, ctx->argv[i]) == 0) {
return true;
}
@@ -88,19 +108,104 @@ static bool should_run(const struct test_ctx *ctx, const char *test) {
return false;
}
-/** Run a test if it's enabled. */
-static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
- if (should_run(ctx, test)) {
- pass = true;
- fn();
+/** Wait for a test to finish. */
+static void wait_test(struct test_ctx *ctx) {
+ int wstatus;
+ pid_t pid = xwaitpid(0, &wstatus, 0);
+ bfs_everify(pid > 0, "xwaitpid()");
- if (pass) {
- cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test);
+ struct test_proc *proc = NULL;
+ for_list (struct test_proc, i, &ctx->procs) {
+ if (i->pid == pid) {
+ proc = i;
+ break;
+ }
+ }
+
+ bfs_verify(proc, "No test_proc for PID %ju", (intmax_t)pid);
+
+ bool passed = false;
+
+ if (WIFEXITED(wstatus)) {
+ int status = WEXITSTATUS(wstatus);
+ if (status == EXIT_SUCCESS) {
+ cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", proc->name);
+ passed = true;
+ } else if (status == EXIT_FAILURE) {
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", proc->name);
} else {
- cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test);
- ctx->ret = EXIT_FAILURE;
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (Exit %d)\n", proc->name, status);
+ }
+ } else {
+ const char *str = NULL;
+ if (WIFSIGNALED(wstatus)) {
+ str = strsignal(WTERMSIG(wstatus));
}
+ if (!str) {
+ str = "Unknown";
+ }
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (%s)\n", proc->name, str);
+ }
+
+ if (!passed) {
+ ctx->ret = EXIT_FAILURE;
}
+
+ --ctx->running;
+ ++ctx->done;
+ LIST_REMOVE(&ctx->procs, proc);
+ free(proc);
+}
+
+/** Unit test function type. */
+typedef void test_fn(void);
+
+/** Run a test if it's enabled. */
+static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
+ if (!should_run(ctx, test)) {
+ return;
+ }
+
+ while (ctx->running >= ctx->jobs) {
+ wait_test(ctx);
+ }
+
+ struct test_proc *proc = ALLOC(struct test_proc);
+ bfs_everify(proc, "alloc()");
+
+ LIST_ITEM_INIT(proc);
+ proc->name = test;
+
+ fflush(NULL);
+ proc->pid = fork();
+ bfs_everify(proc->pid >= 0, "fork()");
+
+ if (proc->pid > 0) {
+ // Parent
+ ++ctx->running;
+ LIST_APPEND(&ctx->procs, proc);
+ return;
+ }
+
+ // Child
+ pass = true;
+ fn();
+ exit(pass ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+/** Finalize the test context. */
+static int test_fini(struct test_ctx *ctx) {
+ while (ctx->running > 0) {
+ wait_test(ctx);
+ }
+
+ if (ctx->cout) {
+ cfclose(ctx->cout);
+ }
+
+ free_colors(ctx->colors);
+
+ return ctx->ret;
}
int main(int argc, char *argv[]) {
@@ -116,8 +221,37 @@ int main(int argc, char *argv[]) {
}
tzset();
+ long jobs = 0;
+
+ const char *cmd = argc > 0 ? argv[0] : "units";
+ int c;
+ while (c = getopt(argc, argv, ":j:"), c != -1) {
+ switch (c) {
+ case 'j':
+ if (xstrtol(optarg, NULL, 10, &jobs) != 0 || jobs <= 0) {
+ fprintf(stderr, "%s: Bad job count '%s'\n", cmd, optarg);
+ return EXIT_FAILURE;
+ }
+ break;
+ case ':':
+ fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ case '?':
+ fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (jobs == 0) {
+ jobs = nproc();
+ }
+
+ if (optind > argc) {
+ optind = argc;
+ }
+
struct test_ctx ctx;
- if (test_init(&ctx, argc, argv) != 0) {
+ if (test_init(&ctx, jobs, argc - optind, argv + optind) != 0) {
goto done;
}