From 90964b2510dadeaf39e978e67b2c942af3b74a38 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 19:16:31 +0200 Subject: [PATCH 01/11] it's not even used --- src/run.c | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/src/run.c b/src/run.c index 0f4b516..4c96638 100644 --- a/src/run.c +++ b/src/run.c @@ -128,54 +128,6 @@ void tdo_run_report_error(struct TdoTest test, FILE *file, char const *step, cha fprintf(file, "\t}"); } -void tdo_run_report_timeout(struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus status, double duration) { - fprintf(file, "\n"); - fprintf(file, "\t{\n"); - - fprintf(file, "\t\t\"file\": \""); - tdo_json_escaped(file, run->test->symbol.file->name); - fprintf(file, "\",\n"); - - fprintf(file, "\t\t\"name\": \""); - tdo_json_escaped(file, run->test->symbol.name); - fprintf(file, "\",\n"); - - fprintf(file, "\t\t\"duration\": %lf,\n", duration); - - fprintf(file, "\t\t\"status\": \""); - if (tdo_process_status_is_exit(status)) { - if (step[0] == 'f') { - fprintf(file, "complete"); - } else { - fprintf(file, "exit"); - } - } else if (tdo_process_status_is_signal(status)) { - fprintf(file, "signal"); - } else if (tdo_process_status_is_stop(status)) { - fprintf(file, "stop"); - } - fprintf(file, "\""); - - if (tdo_process_status_is_exit(status) && step[0] != 'f') { - fprintf(file, ",\n\t\t\"exit\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_exit(status)); - } else if (tdo_process_status_is_signal(status)) { - fprintf(file, ",\n\t\t\"signal\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_signal(status)); - } else if (tdo_process_status_is_stop(status)) { - fprintf(file, ",\n\t\t\"stop\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_stop(status)); - } - - if (step[0] != 'f') { - fprintf(file, ",\n\t\t\"step\": \""); - tdo_json_escaped(file, (struct TdoString) { .length=strlen(step), .bytes=(char*)step }); - fprintf(file, "\""); - } - - tdo_log_dump(run->out, file, "stdout"); - tdo_log_dump(run->err, file, "stderr"); - - fprintf(file, "\n\t}"); -} - enum TdoError tdo_string_previous_line(struct TdoString *line, struct TdoString string, size_t index) { if (string.bytes == NULL || string.length == 0) return TDO_ERROR_EOF; if (string.bytes[index] != '\n') return TDO_ERROR_NEWLINE; From 3947099951b3e0a4df5b38dd2671eb588a243546 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 17:44:38 +0200 Subject: [PATCH 02/11] always print where tests come from --- src/main.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.c b/src/main.c index d7ee594..7d47d61 100644 --- a/src/main.c +++ b/src/main.c @@ -110,6 +110,8 @@ int main(int argc, char **argv) { goto error_open_input; } file_name = args.test_file; + + if (args.internal_status == NULL) fprintf(stderr, "Reading tests from input file '%s'\n", args.test_file); } else if (args.single_test) { if (args.internal_status == NULL) fprintf(stderr, "Reading test from command line\n"); file_name = ""; From ba60e6ebad6e97cf33734936a7003214704f33d4 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 17:44:55 +0200 Subject: [PATCH 03/11] only print if not internal runner --- src/run.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.c b/src/run.c index 4c96638..248e674 100644 --- a/src/run.c +++ b/src/run.c @@ -425,7 +425,7 @@ void tdo_run_single(struct TdoTest *test, struct TdoArena *arena, FILE *status) enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoArena *arena, struct TdoArray tests) { enum TdoError result = TDO_ERROR_UNKNOWN; struct TdoArenaState state = tdo_arena_state_get(arena); - fprintf(stderr, "Running %zu tests\n", tests.length); + if (args.internal_status == NULL) fprintf(stderr, "Running %zu tests\n", tests.length); struct TdoRunStatus status; result = tdo_run_status_init(&status, arena, args); From 5a5ed5aba720e310beddfec41599fda6ac28407d Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 19:13:08 +0200 Subject: [PATCH 04/11] extract `time_between` function --- src/platform.h | 1 + src/platform/posix.c | 7 +++++++ src/platform/run_posix.c | 21 ++++----------------- src/platform/run_windows.c | 8 ++------ src/platform/windows.c | 6 ++++++ 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/platform.h b/src/platform.h index 112adf7..44f5cd5 100644 --- a/src/platform.h +++ b/src/platform.h @@ -37,6 +37,7 @@ #endif TdoMonotoneTime tdo_time_get(void); +double tdo_time_between(TdoMonotoneTime end, TdoMonotoneTime start); FILE *tdo_file_open_exclusive(char const *path, bool overwrite); diff --git a/src/platform/posix.c b/src/platform/posix.c index 7f1d325..26ccd08 100644 --- a/src/platform/posix.c +++ b/src/platform/posix.c @@ -17,6 +17,13 @@ TdoMonotoneTime tdo_time_get(void) { return time; } +double tdo_time_between(TdoMonotoneTime end, TdoMonotoneTime start) { + return ( + (double)(end.tv_sec - start.tv_sec) + + (double)(end.tv_nsec - start.tv_nsec) * 1e-9 + ); +} + FILE *tdo_file_open_exclusive(char const *path, bool overwrite) { int open_flags = O_WRONLY | O_CREAT; if (!overwrite) open_flags |= O_EXCL; diff --git a/src/platform/run_posix.c b/src/platform/run_posix.c index 9a89b8d..1cb299e 100644 --- a/src/platform/run_posix.c +++ b/src/platform/run_posix.c @@ -180,11 +180,7 @@ void tdo_run_poll_exit(struct TdoRun *run, struct TdoRunStatus *status, struct T enum TdoError status_err = tdo_log_drain(&run->status, arena); struct timespec end_time = tdo_time_get(); - - double duration = ( - (double)(end_time.tv_sec - run->start_time.tv_sec) - + (double)(end_time.tv_nsec - run->start_time.tv_nsec) * 1e-9 - ); + double duration = tdo_time_between(end_time, run->start_time); if (status->finished > 0) fprintf(output, ","); if (out_err == TDO_ERROR_OK && err_err == TDO_ERROR_OK && status_err == TDO_ERROR_OK) { @@ -225,10 +221,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str if (err != TDO_ERROR_OK) { TdoMonotoneTime end_time = tdo_time_get(); - double duration = ( - (double)(end_time.tv_sec - run->start_time.tv_sec) - + (double)(end_time.tv_nsec - run->start_time.tv_nsec) * 1e-9 - ); + double duration = tdo_time_between(end_time, run->start_time); if (status->finished > 0) fprintf(output, ","); if (err == TDO_ERROR_MEMORY) { @@ -255,10 +248,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str for (size_t i = 0; i < args.processes; i++) { struct TdoRun *run = &status->runs[i]; if (run->active) { - double duration = ( - (double)(end_time.tv_sec - run->start_time.tv_sec) - + (double)(end_time.tv_nsec - run->start_time.tv_nsec) * 1e-9 - ); + double duration = tdo_time_between(end_time, run->start_time); tdo_run_report_error(*run->test, output, NULL, "could not poll pipes", duration); status->finished += 1; run->active = false; @@ -280,10 +270,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str if (run->active) { tdo_run_poll_exit(run, status, arena, output); if (run->active) { - double duration = ( - (double)(end_time.tv_sec - run->start_time.tv_sec) - + (double)(end_time.tv_nsec - run->start_time.tv_nsec) * 1e-9 - ); + double duration = tdo_time_between(end_time, run->start_time); if (duration > args.time_limit) { // timeout if (status->finished > 0) fprintf(output, ","); diff --git a/src/platform/run_windows.c b/src/platform/run_windows.c index 862c0f0..dd7cf9d 100644 --- a/src/platform/run_windows.c +++ b/src/platform/run_windows.c @@ -66,7 +66,6 @@ struct TdoRunStatus { struct TdoRun *runs; HANDLE job; HANDLE iocp; - LARGE_INTEGER clock_frequency; DWORD pid; struct TdoString executable_name; size_t started; @@ -294,7 +293,7 @@ void tdo_run_maybe_report_exit(struct TdoArena *arena, struct TdoRun *run, struc if (run->process_handle != NULL || tdo_run_pipes_pending(run) > 0 || tdo_run_pipes_cancelling(run)) return; LARGE_INTEGER end_time = tdo_time_get(); - double duration = (double)(end_time.QuadPart - run->start_time.QuadPart) / status->clock_frequency.QuadPart; + double duration = tdo_time_between(end_time, run->start_time); if (status->finished > 0) fprintf(output, ","); if (run->read_too_much) { @@ -401,7 +400,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str enum TdoError err = tdo_log_append(log, arena, bytes_transferred, ov->buffer); if (err != TDO_ERROR_OK) { LARGE_INTEGER end_time = tdo_time_get(); - double duration = (double)(end_time.QuadPart - run->start_time.QuadPart) / status->clock_frequency.QuadPart; + double duration = tdo_time_between(end_time, run->start_time); run->read_too_much = true; TerminateProcess(run->process_handle, 1); // test produced more logs than we can read, why let it continue? @@ -517,7 +516,6 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .runs = NULL, .job = NULL, .iocp = NULL, - .clock_frequency = { .QuadPart = 1 }, .started = 0, .finished = 0, .running = 0, @@ -580,8 +578,6 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * goto error_job_settings; } - QueryPerformanceFrequency(&status->clock_frequency); - for (size_t i = 0; i < args.processes; i++) { struct TdoRun *run = &status->runs[i]; run->out.fd = INVALID_HANDLE_VALUE; diff --git a/src/platform/windows.c b/src/platform/windows.c index 7161686..9468073 100644 --- a/src/platform/windows.c +++ b/src/platform/windows.c @@ -11,6 +11,12 @@ TdoMonotoneTime tdo_time_get(void) { return time; } +double tdo_time_between(TdoMonotoneTime end, TdoMonotoneTime start) { + LARGE_INTEGER freq = { .QuadPart = 1 }; + QueryPerformanceFrequency(&freq); + return (double)(end.QuadPart - start.QuadPart) / freq.QuadPart; +} + FILE *tdo_file_open_exclusive(char const *path, bool overwrite) { HANDLE hFile = CreateFile( path, From 333637840116b14acb2c48e6ce70e25ced0e7883 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 19:15:41 +0200 Subject: [PATCH 05/11] keep track of each test status --- src/platform/run_posix.c | 26 +++++++++----- src/platform/run_windows.c | 14 ++++++-- src/run.c | 72 +++++++++++++++++++++----------------- src/run.h | 7 ++-- 4 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/platform/run_posix.c b/src/platform/run_posix.c index 1cb299e..03d67f4 100644 --- a/src/platform/run_posix.c +++ b/src/platform/run_posix.c @@ -38,6 +38,11 @@ struct TdoRunStatus { size_t started; size_t finished; size_t running; + size_t success; + size_t exit; + size_t timeout; + size_t signal; + size_t error; bool fork_failed; bool log_setup_failed; }; @@ -184,11 +189,11 @@ void tdo_run_poll_exit(struct TdoRun *run, struct TdoRunStatus *status, struct T if (status->finished > 0) fprintf(output, ","); if (out_err == TDO_ERROR_OK && err_err == TDO_ERROR_OK && status_err == TDO_ERROR_OK) { - tdo_run_report_status(run, arena, output, return_status, duration, false); + tdo_run_report_status(status, run, arena, output, return_status, duration, false); } else if (out_err == TDO_ERROR_MEMORY || err_err == TDO_ERROR_MEMORY || status_err == TDO_ERROR_MEMORY) { - tdo_run_report_error(*run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_error(*run->test, output, NULL, "could not read output", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not read output", duration); } run->active = false; @@ -225,9 +230,9 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str if (status->finished > 0) fprintf(output, ","); if (err == TDO_ERROR_MEMORY) { - tdo_run_report_error(*run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_error(*run->test, output, NULL, "could not read output", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not read output", duration); } kill(run->pid, SIGKILL); @@ -249,7 +254,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str struct TdoRun *run = &status->runs[i]; if (run->active) { double duration = tdo_time_between(end_time, run->start_time); - tdo_run_report_error(*run->test, output, NULL, "could not poll pipes", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not poll pipes", duration); status->finished += 1; run->active = false; } @@ -258,7 +263,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str struct TdoTest *ts = tests.data; for (size_t i = status->started; i < tests.length; i++) { if (status->finished > 0) fprintf(output, ","); - tdo_run_report_error(ts[i], output, NULL, "could not poll pipes", -1.0); + tdo_run_report_error(status, ts[i], output, NULL, "could not poll pipes", -1.0); status->finished += 1; } } @@ -274,7 +279,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str if (duration > args.time_limit) { // timeout if (status->finished > 0) fprintf(output, ","); - tdo_run_report_status(run, arena, output, 0, duration, true); + tdo_run_report_status(status, run, arena, output, 0, duration, true); kill(run->pid, SIGKILL); run->active = false; @@ -300,6 +305,11 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .started = 0, .finished = 0, .running = 0, + .success = 0, + .exit = 0, + .timeout = 0, + .signal = 0, + .error = 0, .fork_failed = false, .log_setup_failed = false, }; diff --git a/src/platform/run_windows.c b/src/platform/run_windows.c index dd7cf9d..01f344b 100644 --- a/src/platform/run_windows.c +++ b/src/platform/run_windows.c @@ -71,6 +71,11 @@ struct TdoRunStatus { size_t started; size_t finished; size_t running; + size_t success; + size_t exit; + size_t timeout; + size_t signal; + size_t error; bool fork_failed; bool log_setup_failed; }; @@ -297,9 +302,9 @@ void tdo_run_maybe_report_exit(struct TdoArena *arena, struct TdoRun *run, struc if (status->finished > 0) fprintf(output, ","); if (run->read_too_much) { - tdo_run_report_error(*run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_status(run, arena, output, run->exit_code, duration, run->timed_out); + tdo_run_report_status(status, run, arena, output, run->exit_code, duration, run->timed_out); } run->active = false; @@ -519,6 +524,11 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .started = 0, .finished = 0, .running = 0, + .success = 0, + .exit = 0, + .timeout = 0, + .signal = 0, + .error = 0, .fork_failed = false, .log_setup_failed = false, }; diff --git a/src/run.c b/src/run.c index 248e674..38f99c9 100644 --- a/src/run.c +++ b/src/run.c @@ -45,7 +45,7 @@ void tdo_log_dump(struct TdoLog log, FILE *file, char const *name) { fprintf(file, "\""); } -void tdo_run_report_exit(struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus status, double duration, bool timed_out) { +void tdo_run_report_exit(struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out) { fprintf(file, "\n"); fprintf(file, "\t{\n"); @@ -61,27 +61,33 @@ void tdo_run_report_exit(struct TdoRun *run, FILE *file, char const *step, TdoPr fprintf(file, "\t\t\"status\": \""); if (timed_out) { + status->timeout += 1; fprintf(file, "timeout"); - } else if (tdo_process_status_is_exit(status)) { + } else if (tdo_process_status_is_exit(process_status)) { if (step[0] == 'f') { + status->success += 1; fprintf(file, "complete"); } else { + status->exit += 1; fprintf(file, "exit"); } - } else if (tdo_process_status_is_signal(status)) { + } else if (tdo_process_status_is_signal(process_status)) { + status->signal += 1; fprintf(file, "signal"); - } else if (tdo_process_status_is_stop(status)) { + } else if (tdo_process_status_is_stop(process_status)) { + fprintf(stderr, "When can this happen anyway?\n"); + abort(); fprintf(file, "stop"); } fprintf(file, "\""); if (!timed_out) { - if (tdo_process_status_is_exit(status) && step[0] != 'f') { - fprintf(file, ",\n\t\t\"exit\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_exit(status)); - } else if (tdo_process_status_is_signal(status)) { - fprintf(file, ",\n\t\t\"signal\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_signal(status)); - } else if (tdo_process_status_is_stop(status)) { - fprintf(file, ",\n\t\t\"stop\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_stop(status)); + if (tdo_process_status_is_exit(process_status) && step[0] != 'f') { + fprintf(file, ",\n\t\t\"exit\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_exit(process_status)); + } else if (tdo_process_status_is_signal(process_status)) { + fprintf(file, ",\n\t\t\"signal\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_signal(process_status)); + } else if (tdo_process_status_is_stop(process_status)) { + fprintf(file, ",\n\t\t\"stop\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_stop(process_status)); } } @@ -97,7 +103,9 @@ void tdo_run_report_exit(struct TdoRun *run, FILE *file, char const *step, TdoPr fprintf(file, "\n\t}"); } -void tdo_run_report_error(struct TdoTest test, FILE *file, char const *step, char const *error, double duration) { +void tdo_run_report_error(struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration) { + status->error += 1; + fprintf(file, "\n"); fprintf(file, "\t{\n"); @@ -200,25 +208,25 @@ enum TdoError tdo_run_report_assemble_step(struct TdoString *step, struct TdoAre return TDO_ERROR_OK; } -void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *file, int status, double duration, bool timed_out) { +void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out) { struct TdoArenaState state = tdo_arena_state_get(arena); struct TdoString log_status = run->status.data; if (log_status.bytes == NULL || log_status.length == 0) { - tdo_run_report_error(*run->test, file, NULL, "no data in status pipe", duration); + tdo_run_report_error(status, *run->test, file, NULL, "no data in status pipe", duration); goto done; } else if (log_status.bytes[log_status.length - 1] != '\n') { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, does not end with newline", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, does not end with newline", duration); goto done; } else if (log_status.length <= 1) { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, only contains newline", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, only contains newline", duration); goto done; } struct TdoString last_line = tdo_string_init(); enum TdoError err = tdo_string_previous_line(&last_line, run->status.data, run->status.data.length - 1); if (err != TDO_ERROR_OK || last_line.length <= 0) { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, could not find last line", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not find last line", duration); goto done; } last_line.bytes[last_line.length] = '\0'; // replace newline with null terminator @@ -229,7 +237,7 @@ void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *fil struct TdoString step_name; err = tdo_string_previous_line(&step_name, run->status.data, run->status.data.length - last_line.length - 2); if (err != TDO_ERROR_OK || last_line.length <= 0) { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, could not find line before error", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not find line before error", duration); goto done; } step_name.bytes[step_name.length] = '\0'; // overwrite newline @@ -241,7 +249,7 @@ void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *fil size_t index; enum TdoError err_parse = tdo_parse_size_t(&index, step_name.bytes + 2); if (err_parse != TDO_ERROR_OK) { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); goto done; } @@ -258,28 +266,28 @@ void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *fil } if (current == NULL) { - tdo_run_report_error(*run->test, file, step_name.bytes, "invalid current fixture index", duration); + tdo_run_report_error(status, *run->test, file, step_name.bytes, "invalid current fixture index", duration); goto done; } } else { - tdo_run_report_error(*run->test, file, step_name.bytes, "unknown error", duration); + tdo_run_report_error(status, *run->test, file, step_name.bytes, "unknown error", duration); goto done; } struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, step_name, *current); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(*run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); goto done; } - tdo_run_report_error(*run->test, file, step.bytes, last_line.bytes + 1, duration); + tdo_run_report_error(status, *run->test, file, step.bytes, last_line.bytes + 1, duration); } else if (last_line.length >= 2 && (last_line.bytes[0] == 'b' || last_line.bytes[0] == 'a') && last_line.bytes[1] == '_') { // unexpected exit while running fixture size_t index; enum TdoError err_parse = tdo_parse_size_t(&index, last_line.bytes + 2); if (err_parse != TDO_ERROR_OK) { - tdo_run_report_error(*run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); + tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); goto done; } @@ -296,34 +304,34 @@ void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *fil } } if (current == NULL) { - tdo_run_report_error(*run->test, file, last_line.bytes, "invalid current fixture index", duration); + tdo_run_report_error(status, *run->test, file, last_line.bytes, "invalid current fixture index", duration); goto done; } struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, last_line, *current); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(*run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); goto done; } - tdo_run_report_exit(run, file, step.bytes, status, duration, timed_out); + tdo_run_report_exit(status, run, file, step.bytes, process_status, duration, timed_out); } else if (strncmp(last_line.bytes, "test", 4) == 0) { struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, last_line, run->test->symbol); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(*run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); goto done; } // unexpected exit while running test - tdo_run_report_exit(run, file, step.bytes, status, duration, timed_out); + tdo_run_report_exit(status, run, file, step.bytes, process_status, duration, timed_out); } else if (strncmp(last_line.bytes, "finished", 8) == 0) { // test finished normally - tdo_run_report_exit(run, file, last_line.bytes, status, duration, timed_out); + tdo_run_report_exit(status, run, file, last_line.bytes, process_status, duration, timed_out); } else { // unknown status - tdo_run_report_error(*run->test, file, NULL, "unknown error", duration); + tdo_run_report_error(status, *run->test, file, NULL, "unknown error", duration); } done: @@ -456,14 +464,14 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren struct TdoTest *ts = tests.data; for (size_t i = status.started; i < tests.length; i++) { if (status.finished > 0) fprintf(output, ","); - tdo_run_report_error(ts[i], output, NULL, "could not create child process", -1.0); + tdo_run_report_error(&status, ts[i], output, NULL, "could not create child process", -1.0); status.finished += 1; } } else if (status.running == 0 && status.log_setup_failed) { struct TdoTest *ts = tests.data; for (size_t i = status.started; i < tests.length; i++) { if (status.finished > 0) fprintf(output, ","); - tdo_run_report_error(ts[i], output, NULL, "could not setup log redirection", -1.0); + tdo_run_report_error(&status, ts[i], output, NULL, "could not setup log redirection", -1.0); status.finished += 1; } } diff --git a/src/run.h b/src/run.h index 48abe4d..e7011de 100644 --- a/src/run.h +++ b/src/run.h @@ -4,6 +4,7 @@ #include struct TdoRun; +struct TdoRunStatus; void tdo_json_escaped(FILE *file, struct TdoString string); void tdo_log_dump(struct TdoLog log, FILE *file, char const *name); @@ -12,9 +13,9 @@ enum TdoError tdo_parse_size_t(size_t *number, char const *string); enum TdoError tdo_run_report_assemble_step(struct TdoString *step, struct TdoArena *arena, struct TdoString step_name, struct TdoSymbol symbol); -void tdo_run_report_status(struct TdoRun *run, struct TdoArena *arena, FILE *file, int status, double duration, bool timed_out); -void tdo_run_report_exit(struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus status, double duration, bool timed_out); -void tdo_run_report_error(struct TdoTest test, FILE *file, char const *step, char const *error, double duration); +void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out); +void tdo_run_report_exit(struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out); +void tdo_run_report_error(struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration); void tdo_status_error(FILE *file, char const *fmt, ...); void tdo_assert_library_loaded(struct TdoFile *file, FILE *status); From 47c0260f57ca6bc9949af8bbd952072cbdb15a62 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 19:18:32 +0200 Subject: [PATCH 06/11] print summary at the end --- src/run.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/run.c b/src/run.c index 38f99c9..5c36f30 100644 --- a/src/run.c +++ b/src/run.c @@ -439,6 +439,8 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren result = tdo_run_status_init(&status, arena, args); if (result != TDO_ERROR_OK) goto error_setup; + TdoMonotoneTime time_start = tdo_time_get(); + fprintf(output, "["); while (status.finished < tests.length) { while (status.running < args.processes && status.started < tests.length && !status.fork_failed && !status.log_setup_failed) { @@ -480,6 +482,22 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren fprintf(output, "\n]\n"); result = TDO_ERROR_OK; + TdoMonotoneTime time_end = tdo_time_get(); + + char const *spacing = " "; + + fprintf(stderr, "Ran %zu tests in %.2lf seconds:\n", tests.length, tdo_time_between(time_end, time_start)); + fprintf(stderr, "%ssuccess: %3zu/%zu\n", spacing, status.success, tests.length); + + size_t total_fails = status.exit + status.timeout + status.signal + status.error; + if (total_fails) { + fprintf(stderr, "%sfailure: %3zu/%zu\n", spacing, total_fails, tests.length); + if (status.exit > 0) fprintf(stderr, "%s%sexit: %3zu/%zu\n", spacing, spacing, status.exit, total_fails); + if (status.timeout > 0) fprintf(stderr, "%s%stimeout: %3zu/%zu\n", spacing, spacing, status.timeout, total_fails); + if (status.signal > 0) fprintf(stderr, "%s%ssignal: %3zu/%zu\n", spacing, spacing, status.signal, total_fails); + if (status.error > 0) fprintf(stderr, "%s%serror: %3zu/%zu\n", spacing, spacing, status.error, total_fails); + } + tdo_run_status_deinit(status, args); error_setup: tdo_arena_state_set(arena, state); From 2019aaae7fbc64ac61aeb14654bdc450b59c54f2 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 20:07:44 +0200 Subject: [PATCH 07/11] let's not repeat ourselves --- src/arguments.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arguments.c b/src/arguments.c index 24c81ec..24d5961 100644 --- a/src/arguments.c +++ b/src/arguments.c @@ -46,7 +46,7 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar char const *job_str = NULL; if (strcmp(s, "-j") == 0) { if (argc <= 1) { - fprintf(stderr, "Missing job argument to '-j'\n"); + fprintf(stderr, "Missing argument to '-j'\n"); result = TDO_ERROR_ARG_PARSE; argc -= 1; argv += 1; continue; @@ -96,7 +96,7 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar } } else if (strcmp(s, "--timeout") == 0) { if (argc <= 1) { - fprintf(stderr, "Missing timeout argument to '--timeout'\n"); + fprintf(stderr, "Missing argument to '--timeout'\n"); result = TDO_ERROR_ARG_PARSE; } else { argc -= 1; argv += 1; From b1c3e79af696be5cfc97216db44b8934d5a4127f Mon Sep 17 00:00:00 2001 From: Pontus Date: Fri, 15 May 2026 07:50:26 +0200 Subject: [PATCH 08/11] print human friendly format by default --- src/arguments.c | 18 +++ src/arguments.h | 6 + src/main.c | 1 + src/platform/run_posix.c | 26 ++-- src/platform/run_windows.c | 23 +-- src/run.c | 291 ++++++++++++++++++++++++------------- src/run.h | 7 +- test/conftest.py | 2 +- 8 files changed, 247 insertions(+), 127 deletions(-) diff --git a/src/arguments.c b/src/arguments.c index 24d5961..41b3b26 100644 --- a/src/arguments.c +++ b/src/arguments.c @@ -16,6 +16,7 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar .output = NULL, .overwrite = false, .internal_status = NULL, + .format = TDO_FORMAT_HUMAN, }; if (argc < 1) return TDO_ERROR_ARG_FIRST; @@ -118,6 +119,23 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar args->time_limit = timeout; } } + } else if (strcmp(s, "--format") == 0) { + if (argc <= 1) { + fprintf(stderr, "Missing argument to '--format'\n"); + result = TDO_ERROR_ARG_PARSE; + } else { + argc -= 1; argv += 1; + char const *format_str = argv[0]; + + if (strcmp(format_str, "human") == 0) { + args->format = TDO_FORMAT_HUMAN; + } else if (strcmp(format_str, "json") == 0) { + args->format = TDO_FORMAT_JSON; + } else { + fprintf(stderr, "Unknown format argument '%s', see '-h' for options\n", format_str); + result = TDO_ERROR_ARG_PARSE; + } + } } else { fprintf(stderr, "Unrecognized argument: '%s'\n", s); result = TDO_ERROR_ARG_PARSE; diff --git a/src/arguments.h b/src/arguments.h index 2bc7e80..25f0bd1 100644 --- a/src/arguments.h +++ b/src/arguments.h @@ -6,6 +6,11 @@ #define TDO_PROCESS_MAX ((size_t) 2048) +enum TdoFormat { + TDO_FORMAT_HUMAN, + TDO_FORMAT_JSON, +}; + struct TdoArguments { size_t processes; float time_limit; @@ -13,6 +18,7 @@ struct TdoArguments { char const *test_file; char const *output; char const *internal_status; + enum TdoFormat format; bool overwrite; }; diff --git a/src/main.c b/src/main.c index 7d47d61..33832eb 100644 --- a/src/main.c +++ b/src/main.c @@ -37,6 +37,7 @@ static const char *tdo_help_text = "Options:\n" " -t \"TEST_DEFINITION\" Run a single test definition string directly.\n" " -j [N] Run tests in parallel using N processes (default: 1).\n" + " --format FMT Select output format: 'human' or 'json' (default: human).\n" " -o FILE Write results to the specified FILE.\n" " -f Force overwrite the output file if it already exists.\n" " --timeout SECONDS Set a maximum execution time per test (default: 5.0).\n" diff --git a/src/platform/run_posix.c b/src/platform/run_posix.c index 03d67f4..debc99e 100644 --- a/src/platform/run_posix.c +++ b/src/platform/run_posix.c @@ -43,6 +43,7 @@ struct TdoRunStatus { size_t timeout; size_t signal; size_t error; + size_t success_in_a_row; bool fork_failed; bool log_setup_failed; }; @@ -177,7 +178,7 @@ void tdo_run_start_new(struct TdoRunStatus *status, struct TdoArena *arena, stru status->running += 1; } -void tdo_run_poll_exit(struct TdoRun *run, struct TdoRunStatus *status, struct TdoArena *arena, FILE *output) { +void tdo_run_poll_exit(struct TdoArguments *args, struct TdoRun *run, struct TdoRunStatus *status, struct TdoArena *arena, FILE *output) { int return_status; if (waitpid(run->pid, &return_status, WNOHANG) > 0) { enum TdoError out_err = tdo_log_drain(&run->out, arena); @@ -187,13 +188,12 @@ void tdo_run_poll_exit(struct TdoRun *run, struct TdoRunStatus *status, struct T struct timespec end_time = tdo_time_get(); double duration = tdo_time_between(end_time, run->start_time); - if (status->finished > 0) fprintf(output, ","); if (out_err == TDO_ERROR_OK && err_err == TDO_ERROR_OK && status_err == TDO_ERROR_OK) { - tdo_run_report_status(status, run, arena, output, return_status, duration, false); + tdo_run_report_status(args, status, run, arena, output, return_status, duration, false); } else if (out_err == TDO_ERROR_MEMORY || err_err == TDO_ERROR_MEMORY || status_err == TDO_ERROR_MEMORY) { - tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(args, status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_error(status, *run->test, output, NULL, "could not read output", duration); + tdo_run_report_error(args, status, *run->test, output, NULL, "could not read output", duration); } run->active = false; @@ -228,11 +228,10 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str TdoMonotoneTime end_time = tdo_time_get(); double duration = tdo_time_between(end_time, run->start_time); - if (status->finished > 0) fprintf(output, ","); if (err == TDO_ERROR_MEMORY) { - tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(&args, status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_error(status, *run->test, output, NULL, "could not read output", duration); + tdo_run_report_error(&args, status, *run->test, output, NULL, "could not read output", duration); } kill(run->pid, SIGKILL); @@ -254,7 +253,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str struct TdoRun *run = &status->runs[i]; if (run->active) { double duration = tdo_time_between(end_time, run->start_time); - tdo_run_report_error(status, *run->test, output, NULL, "could not poll pipes", duration); + tdo_run_report_error(&args, status, *run->test, output, NULL, "could not poll pipes", duration); status->finished += 1; run->active = false; } @@ -262,8 +261,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str struct TdoTest *ts = tests.data; for (size_t i = status->started; i < tests.length; i++) { - if (status->finished > 0) fprintf(output, ","); - tdo_run_report_error(status, ts[i], output, NULL, "could not poll pipes", -1.0); + tdo_run_report_error(&args, status, ts[i], output, NULL, "could not poll pipes", -1.0); status->finished += 1; } } @@ -273,13 +271,12 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str for (size_t i = 0; i < args.processes; i++) { struct TdoRun *run = &status->runs[i]; if (run->active) { - tdo_run_poll_exit(run, status, arena, output); + tdo_run_poll_exit(&args, run, status, arena, output); if (run->active) { double duration = tdo_time_between(end_time, run->start_time); if (duration > args.time_limit) { // timeout - if (status->finished > 0) fprintf(output, ","); - tdo_run_report_status(status, run, arena, output, 0, duration, true); + tdo_run_report_status(&args, status, run, arena, output, 0, duration, true); kill(run->pid, SIGKILL); run->active = false; @@ -310,6 +307,7 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .timeout = 0, .signal = 0, .error = 0, + .success_in_a_row = 0, .fork_failed = false, .log_setup_failed = false, }; diff --git a/src/platform/run_windows.c b/src/platform/run_windows.c index 01f344b..4b28c50 100644 --- a/src/platform/run_windows.c +++ b/src/platform/run_windows.c @@ -76,6 +76,7 @@ struct TdoRunStatus { size_t timeout; size_t signal; size_t error; + size_t success_in_a_row; bool fork_failed; bool log_setup_failed; }; @@ -294,17 +295,16 @@ void tdo_run_start_new(struct TdoRunStatus *status, struct TdoArena *arena, stru return; } -void tdo_run_maybe_report_exit(struct TdoArena *arena, struct TdoRun *run, struct TdoRunStatus *status, FILE *output) { +void tdo_run_maybe_report_exit(struct TdoArguments *args, struct TdoArena *arena, struct TdoRun *run, struct TdoRunStatus *status, FILE *output) { if (run->process_handle != NULL || tdo_run_pipes_pending(run) > 0 || tdo_run_pipes_cancelling(run)) return; LARGE_INTEGER end_time = tdo_time_get(); double duration = tdo_time_between(end_time, run->start_time); - if (status->finished > 0) fprintf(output, ","); if (run->read_too_much) { - tdo_run_report_error(status, *run->test, output, NULL, "could not allocate space for output", duration); + tdo_run_report_error(args, status, *run->test, output, NULL, "could not allocate space for output", duration); } else { - tdo_run_report_status(status, run, arena, output, run->exit_code, duration, run->timed_out); + tdo_run_report_status(args, status, run, arena, output, run->exit_code, duration, run->timed_out); } run->active = false; @@ -312,7 +312,7 @@ void tdo_run_maybe_report_exit(struct TdoArena *arena, struct TdoRun *run, struc status->finished += 1; } -void tdo_run_handle_exit(struct TdoArena *arena, struct TdoRun *run, struct TdoRunStatus *status, FILE *output, DWORD pid, DWORD msg) { +void tdo_run_handle_exit(struct TdoArguments *args, struct TdoArena *arena, struct TdoRun *run, struct TdoRunStatus *status, FILE *output, DWORD pid, DWORD msg) { if (msg != JOB_OBJECT_MSG_END_OF_PROCESS_TIME && msg != JOB_OBJECT_MSG_EXIT_PROCESS) return; if (run == NULL) { @@ -339,7 +339,7 @@ void tdo_run_handle_exit(struct TdoArena *arena, struct TdoRun *run, struct TdoR CloseHandle(run->process_handle); run->process_handle = NULL; run->exit_code = return_status; - tdo_run_maybe_report_exit(arena, run, status, output); + tdo_run_maybe_report_exit(args, arena, run, status, output); } void tdo_run_handle_pipe_disconnect(struct TdoArena *arena, struct TdoRun *run, struct TdoLog *log, struct TdoOverlap *overlap, struct TdoRunStatus *status, FILE *output) { @@ -365,7 +365,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str break; } } - tdo_run_handle_exit(arena, run, status, output, pid, bytes_transferred); + tdo_run_handle_exit(&args, arena, run, status, output, pid, bytes_transferred); return; } @@ -392,7 +392,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str // next read started } else if (code == ERROR_BROKEN_PIPE || code == ERROR_PIPE_NOT_CONNECTED) { tdo_run_handle_pipe_disconnect(arena, run, log, ov, status, output); - tdo_run_maybe_report_exit(arena, run, status, output); + tdo_run_maybe_report_exit(&args, arena, run, status, output); } else { fprintf(stderr, "async ReadFile Failed: %lu\n", code); fflush(NULL); @@ -437,7 +437,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str // next read started } else if (code == ERROR_BROKEN_PIPE || code == ERROR_PIPE_NOT_CONNECTED) { tdo_run_handle_pipe_disconnect(arena, run, log, ov, status, output); - tdo_run_maybe_report_exit(arena, run, status, output); + tdo_run_maybe_report_exit(&args, arena, run, status, output); } else { fprintf(stderr, "async ReadFile Failed: %lu\n", code); fflush(NULL); @@ -447,7 +447,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str } else { // no bytes transferred, pipe closed tdo_run_handle_pipe_disconnect(arena, run, log, ov, status, output); - tdo_run_maybe_report_exit(arena, run, status, output); + tdo_run_maybe_report_exit(&args, arena, run, status, output); } break; case TDO_PIPE_IDLE: fprintf(stderr, "Idle pipe received completion packet\n"); fflush(NULL); abort(); @@ -478,7 +478,7 @@ void tdo_run_poll_event(struct TdoRunStatus *status, struct TdoArena *arena, str int s = ov->status; tdo_run_handle_pipe_disconnect(arena, run, log, ov, status, output); if (s != TDO_PIPE_CANCELLING) { - tdo_run_maybe_report_exit(arena, run, status, output); + tdo_run_maybe_report_exit(&args, arena, run, status, output); } } else { fprintf(stderr, "Read from pipe failed: %lu\n", GetLastError()); @@ -529,6 +529,7 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .timeout = 0, .signal = 0, .error = 0, + .success_in_a_row = 0, .fork_failed = false, .log_setup_failed = false, }; diff --git a/src/run.c b/src/run.c index 5c36f30..485fbbf 100644 --- a/src/run.c +++ b/src/run.c @@ -39,101 +39,188 @@ void tdo_json_escaped(FILE *file, struct TdoString string) { } } +void tdo_human_escaped(FILE *file, struct TdoString string) { + for (size_t i = 0; i < string.length; i++) { + unsigned char c = string.bytes[i]; + switch (string.bytes[i]) { + case '\"': fputs("\\\"", file); break; + case '\\': fputs("\\\\", file); break; + case '\b': fputs("\\b", file); break; + case '\f': fputs("\\f", file); break; + case '\n': fputs("\\n\n", file); break; + case '\r': fputs("\\r", file); break; + case '\t': fputs("\\t", file); break; + default: + if (c < 0x20 || c > 0x7E) { + // control character or "random data" + fprintf(file, "\\u%04x", (unsigned int) c); + } else { + // ascii + fputc(string.bytes[i], file); break; + } + } + } +} + void tdo_log_dump(struct TdoLog log, FILE *file, char const *name) { fprintf(file, ",\n\t\t\"%s\": \"", name); tdo_json_escaped(file, log.data); fprintf(file, "\""); } -void tdo_run_report_exit(struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out) { - fprintf(file, "\n"); - fprintf(file, "\t{\n"); - - fprintf(file, "\t\t\"file\": \""); - tdo_json_escaped(file, run->test->symbol.file->name); - fprintf(file, "\",\n"); - - fprintf(file, "\t\t\"name\": \""); - tdo_json_escaped(file, run->test->symbol.name); - fprintf(file, "\",\n"); +void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out) { + if (args->format == TDO_FORMAT_HUMAN) { + bool log_output = false; + if (!tdo_process_status_is_exit(process_status) || step[0] != 'f') { + status->success_in_a_row = 0; + if (status->finished > 0) fprintf(file, "\n"); + fprintf(file, "%s::%s ", run->test->symbol.file->name.bytes, run->test->symbol.name.bytes); + } - fprintf(file, "\t\t\"duration\": %lf,\n", duration); + if (timed_out) { + status->timeout += 1; + log_output = true; + fprintf(file, "TIMEOUT"); + } else if (tdo_process_status_is_exit(process_status)) { + if (step[0] == 'f') { + if (status->finished > 0 && status->success_in_a_row == 0) fprintf(file, "\n"); + + status->success += 1; + status->success_in_a_row += 1; + + fprintf(file, "."); + if (status->success_in_a_row % 80 == 0) fprintf(file, "\n"); + } else { + status->exit += 1; + log_output = true; + fprintf(file, "UNEXPECTED EXIT"); + } + } else if (tdo_process_status_is_signal(process_status)) { + status->signal += 1; + log_output = true; + fprintf(file, "SIGNAL"); + } else if (tdo_process_status_is_stop(process_status)) { + fprintf(stderr, "When can this happen anyway?\n"); + fflush(NULL); + abort(); + fprintf(file, "STOPPED\n"); + } - fprintf(file, "\t\t\"status\": \""); - if (timed_out) { - status->timeout += 1; - fprintf(file, "timeout"); - } else if (tdo_process_status_is_exit(process_status)) { - if (step[0] == 'f') { - status->success += 1; - fprintf(file, "complete"); - } else { - status->exit += 1; - fprintf(file, "exit"); + if (log_output) { + fprintf(file, "Captured stdout ----------------------------------------------------------------\n"); + tdo_human_escaped(file, run->out.data); + fprintf(file, "Captured stderr ----------------------------------------------------------------\n"); + tdo_human_escaped(file, run->err.data); + fprintf(file, "--------------------------------------------------------------------------------"); } - } else if (tdo_process_status_is_signal(process_status)) { - status->signal += 1; - fprintf(file, "signal"); - } else if (tdo_process_status_is_stop(process_status)) { - fprintf(stderr, "When can this happen anyway?\n"); - abort(); - fprintf(file, "stop"); - } - fprintf(file, "\""); - if (!timed_out) { - if (tdo_process_status_is_exit(process_status) && step[0] != 'f') { - fprintf(file, ",\n\t\t\"exit\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_exit(process_status)); + } else if (args->format == TDO_FORMAT_JSON) { + if (status->finished > 0) fprintf(file, ","); + fprintf(file, "\n"); + fprintf(file, "\t{\n"); + + fprintf(file, "\t\t\"file\": \""); + tdo_json_escaped(file, run->test->symbol.file->name); + fprintf(file, "\",\n"); + + fprintf(file, "\t\t\"name\": \""); + tdo_json_escaped(file, run->test->symbol.name); + fprintf(file, "\",\n"); + + fprintf(file, "\t\t\"duration\": %lf,\n", duration); + + fprintf(file, "\t\t\"status\": \""); + if (timed_out) { + status->timeout += 1; + fprintf(file, "timeout"); + } else if (tdo_process_status_is_exit(process_status)) { + if (step[0] == 'f') { + status->success += 1; + fprintf(file, "complete"); + } else { + status->exit += 1; + fprintf(file, "exit"); + } } else if (tdo_process_status_is_signal(process_status)) { - fprintf(file, ",\n\t\t\"signal\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_signal(process_status)); + status->signal += 1; + fprintf(file, "signal"); } else if (tdo_process_status_is_stop(process_status)) { - fprintf(file, ",\n\t\t\"stop\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_stop(process_status)); + fprintf(stderr, "When can this happen anyway?\n"); + fflush(NULL); + abort(); + fprintf(file, "stop"); } - } - - if (timed_out || step[0] != 'f') { - fprintf(file, ",\n\t\t\"step\": \""); - tdo_json_escaped(file, (struct TdoString) { .length=strlen(step), .bytes=(char*)step }); fprintf(file, "\""); - } - tdo_log_dump(run->out, file, "stdout"); - tdo_log_dump(run->err, file, "stderr"); - - fprintf(file, "\n\t}"); -} - -void tdo_run_report_error(struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration) { - status->error += 1; + if (!timed_out) { + if (tdo_process_status_is_exit(process_status) && step[0] != 'f') { + fprintf(file, ",\n\t\t\"exit\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_exit(process_status)); + } else if (tdo_process_status_is_signal(process_status)) { + fprintf(file, ",\n\t\t\"signal\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_signal(process_status)); + } else if (tdo_process_status_is_stop(process_status)) { + fprintf(file, ",\n\t\t\"stop\": " TDO_PROCESS_CODE_FORMAT, tdo_process_code_stop(process_status)); + } + } - fprintf(file, "\n"); - fprintf(file, "\t{\n"); + if (timed_out || step[0] != 'f') { + fprintf(file, ",\n\t\t\"step\": \""); + tdo_json_escaped(file, (struct TdoString) { .length=strlen(step), .bytes=(char*)step }); + fprintf(file, "\""); + } - fprintf(file, "\t\t\"file\": \""); - tdo_json_escaped(file, test.symbol.file->name); - fprintf(file, "\",\n"); + tdo_log_dump(run->out, file, "stdout"); + tdo_log_dump(run->err, file, "stderr"); - fprintf(file, "\t\t\"name\": \""); - tdo_json_escaped(file, test.symbol.name); - fprintf(file, "\",\n"); + fprintf(file, "\n\t}"); + } else { + fprintf(stderr, "Unknown format\n"); + fflush(NULL); + abort(); + } +} - fprintf(file, "\t\t\"duration\": %lf,\n", duration); +void tdo_run_report_error(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration) { + status->error += 1; - fprintf(file, "\t\t\"status\": \"error\",\n"); - fprintf(file, "\t\t\"error\": \""); - tdo_json_escaped(file, (struct TdoString) { .length=strlen(error), .bytes=(char*)error }); - fprintf(file, "\",\n"); + if (args->format == TDO_FORMAT_HUMAN) { + if (status->finished > 0) fprintf(file, "\n"); + fprintf(file, "%s::%s ERROR\n", test.symbol.file->name.bytes, test.symbol.name.bytes); + fprintf(file, " %s", error); + } else if (args->format == TDO_FORMAT_JSON) { + if (status->finished > 0) fprintf(file, ","); + fprintf(file, "\n"); + fprintf(file, "\t{\n"); + + fprintf(file, "\t\t\"file\": \""); + tdo_json_escaped(file, test.symbol.file->name); + fprintf(file, "\",\n"); + + fprintf(file, "\t\t\"name\": \""); + tdo_json_escaped(file, test.symbol.name); + fprintf(file, "\",\n"); + + fprintf(file, "\t\t\"duration\": %lf,\n", duration); + + fprintf(file, "\t\t\"status\": \"error\",\n"); + fprintf(file, "\t\t\"error\": \""); + tdo_json_escaped(file, (struct TdoString) { .length=strlen(error), .bytes=(char*)error }); + fprintf(file, "\",\n"); + + fprintf(file, "\t\t\"step\": "); + if (step != NULL) { + fprintf(file, "\""); + tdo_json_escaped(file, (struct TdoString) { .length=strlen(step), .bytes=(char*)step }); + fprintf(file, "\"\n"); + } else { + fprintf(file, "null\n"); + } - fprintf(file, "\t\t\"step\": "); - if (step != NULL) { - fprintf(file, "\""); - tdo_json_escaped(file, (struct TdoString) { .length=strlen(step), .bytes=(char*)step }); - fprintf(file, "\"\n"); + fprintf(file, "\t}"); } else { - fprintf(file, "null\n"); + fprintf(stderr, "Unknown format\n"); + fflush(NULL); + abort(); } - - fprintf(file, "\t}"); } enum TdoError tdo_string_previous_line(struct TdoString *line, struct TdoString string, size_t index) { @@ -208,25 +295,25 @@ enum TdoError tdo_run_report_assemble_step(struct TdoString *step, struct TdoAre return TDO_ERROR_OK; } -void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out) { +void tdo_run_report_status(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out) { struct TdoArenaState state = tdo_arena_state_get(arena); struct TdoString log_status = run->status.data; if (log_status.bytes == NULL || log_status.length == 0) { - tdo_run_report_error(status, *run->test, file, NULL, "no data in status pipe", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "no data in status pipe", duration); goto done; } else if (log_status.bytes[log_status.length - 1] != '\n') { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, does not end with newline", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, does not end with newline", duration); goto done; } else if (log_status.length <= 1) { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, only contains newline", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, only contains newline", duration); goto done; } struct TdoString last_line = tdo_string_init(); enum TdoError err = tdo_string_previous_line(&last_line, run->status.data, run->status.data.length - 1); if (err != TDO_ERROR_OK || last_line.length <= 0) { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not find last line", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, could not find last line", duration); goto done; } last_line.bytes[last_line.length] = '\0'; // replace newline with null terminator @@ -237,7 +324,7 @@ void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, stru struct TdoString step_name; err = tdo_string_previous_line(&step_name, run->status.data, run->status.data.length - last_line.length - 2); if (err != TDO_ERROR_OK || last_line.length <= 0) { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not find line before error", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, could not find line before error", duration); goto done; } step_name.bytes[step_name.length] = '\0'; // overwrite newline @@ -249,7 +336,7 @@ void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, stru size_t index; enum TdoError err_parse = tdo_parse_size_t(&index, step_name.bytes + 2); if (err_parse != TDO_ERROR_OK) { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); goto done; } @@ -266,28 +353,28 @@ void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, stru } if (current == NULL) { - tdo_run_report_error(status, *run->test, file, step_name.bytes, "invalid current fixture index", duration); + tdo_run_report_error(args, status, *run->test, file, step_name.bytes, "invalid current fixture index", duration); goto done; } } else { - tdo_run_report_error(status, *run->test, file, step_name.bytes, "unknown error", duration); + tdo_run_report_error(args, status, *run->test, file, step_name.bytes, "unknown error", duration); goto done; } struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, step_name, *current); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "could not build step", duration); goto done; } - tdo_run_report_error(status, *run->test, file, step.bytes, last_line.bytes + 1, duration); + tdo_run_report_error(args, status, *run->test, file, step.bytes, last_line.bytes + 1, duration); } else if (last_line.length >= 2 && (last_line.bytes[0] == 'b' || last_line.bytes[0] == 'a') && last_line.bytes[1] == '_') { // unexpected exit while running fixture size_t index; enum TdoError err_parse = tdo_parse_size_t(&index, last_line.bytes + 2); if (err_parse != TDO_ERROR_OK) { - tdo_run_report_error(status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "malformed status pipe, could not parse fixture index", duration); goto done; } @@ -304,34 +391,34 @@ void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, stru } } if (current == NULL) { - tdo_run_report_error(status, *run->test, file, last_line.bytes, "invalid current fixture index", duration); + tdo_run_report_error(args, status, *run->test, file, last_line.bytes, "invalid current fixture index", duration); goto done; } struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, last_line, *current); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "could not build step", duration); goto done; } - tdo_run_report_exit(status, run, file, step.bytes, process_status, duration, timed_out); + tdo_run_report_exit(args, status, run, file, step.bytes, process_status, duration, timed_out); } else if (strncmp(last_line.bytes, "test", 4) == 0) { struct TdoString step; enum TdoError err_step = tdo_run_report_assemble_step(&step, arena, last_line, run->test->symbol); if (err_step != TDO_ERROR_OK) { - tdo_run_report_error(status, *run->test, file, NULL, "could not build step", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "could not build step", duration); goto done; } // unexpected exit while running test - tdo_run_report_exit(status, run, file, step.bytes, process_status, duration, timed_out); + tdo_run_report_exit(args, status, run, file, step.bytes, process_status, duration, timed_out); } else if (strncmp(last_line.bytes, "finished", 8) == 0) { // test finished normally - tdo_run_report_exit(status, run, file, last_line.bytes, process_status, duration, timed_out); + tdo_run_report_exit(args, status, run, file, last_line.bytes, process_status, duration, timed_out); } else { // unknown status - tdo_run_report_error(status, *run->test, file, NULL, "unknown error", duration); + tdo_run_report_error(args, status, *run->test, file, NULL, "unknown error", duration); } done: @@ -441,7 +528,8 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren TdoMonotoneTime time_start = tdo_time_get(); - fprintf(output, "["); + if (args.format == TDO_FORMAT_JSON) fprintf(output, "["); + while (status.finished < tests.length) { while (status.running < args.processes && status.started < tests.length && !status.fork_failed && !status.log_setup_failed) { fflush(stdout); @@ -465,21 +553,28 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren if (status.running == 0 && status.fork_failed) { struct TdoTest *ts = tests.data; for (size_t i = status.started; i < tests.length; i++) { - if (status.finished > 0) fprintf(output, ","); - tdo_run_report_error(&status, ts[i], output, NULL, "could not create child process", -1.0); + tdo_run_report_error(&args, &status, ts[i], output, NULL, "could not create child process", -1.0); status.finished += 1; } } else if (status.running == 0 && status.log_setup_failed) { struct TdoTest *ts = tests.data; for (size_t i = status.started; i < tests.length; i++) { - if (status.finished > 0) fprintf(output, ","); - tdo_run_report_error(&status, ts[i], output, NULL, "could not setup log redirection", -1.0); + tdo_run_report_error(&args, &status, ts[i], output, NULL, "could not setup log redirection", -1.0); status.finished += 1; } } } - fprintf(output, "\n]\n"); + if (args.format == TDO_FORMAT_HUMAN) { + fprintf(output, "\n"); + } else if (args.format == TDO_FORMAT_JSON) { + fprintf(output, "\n]\n"); + } else { + fprintf(stderr, "Unknown format\n"); + fflush(NULL); + abort(); + } + fflush(output); result = TDO_ERROR_OK; TdoMonotoneTime time_end = tdo_time_get(); @@ -493,8 +588,8 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren if (total_fails) { fprintf(stderr, "%sfailure: %3zu/%zu\n", spacing, total_fails, tests.length); if (status.exit > 0) fprintf(stderr, "%s%sexit: %3zu/%zu\n", spacing, spacing, status.exit, total_fails); - if (status.timeout > 0) fprintf(stderr, "%s%stimeout: %3zu/%zu\n", spacing, spacing, status.timeout, total_fails); if (status.signal > 0) fprintf(stderr, "%s%ssignal: %3zu/%zu\n", spacing, spacing, status.signal, total_fails); + if (status.timeout > 0) fprintf(stderr, "%s%stimeout: %3zu/%zu\n", spacing, spacing, status.timeout, total_fails); if (status.error > 0) fprintf(stderr, "%s%serror: %3zu/%zu\n", spacing, spacing, status.error, total_fails); } diff --git a/src/run.h b/src/run.h index e7011de..977cd78 100644 --- a/src/run.h +++ b/src/run.h @@ -1,6 +1,7 @@ #ifndef TDO_RUN_H #define TDO_RUN_H #include "test.h" +#include "arguments.h" #include struct TdoRun; @@ -13,9 +14,9 @@ enum TdoError tdo_parse_size_t(size_t *number, char const *string); enum TdoError tdo_run_report_assemble_step(struct TdoString *step, struct TdoArena *arena, struct TdoString step_name, struct TdoSymbol symbol); -void tdo_run_report_status(struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out); -void tdo_run_report_exit(struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out); -void tdo_run_report_error(struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration); +void tdo_run_report_status(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoRun *run, struct TdoArena *arena, FILE *file, TdoProcessStatus process_status, double duration, bool timed_out); +void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out); +void tdo_run_report_error(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration); void tdo_status_error(FILE *file, char const *fmt, ...); void tdo_assert_library_loaded(struct TdoFile *file, FILE *status); diff --git a/test/conftest.py b/test/conftest.py index 7099535..e121d8b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -477,7 +477,7 @@ def strip_asan_noise(text: str) -> str: def run_tests(runner: Runner): def run(tests: str, executable: Optional[str] = None, args: Optional[List[Any]] = None): p = subprocess.Popen( - [executable or runner(), *[str(a) for a in args or []]], + [executable or runner(), '--format', 'json', *[str(a) for a in args or []]], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, From a0ef399f67c66a400d8b6a4812af3503f8495e47 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 21:58:34 +0200 Subject: [PATCH 09/11] allow setting verbosity --- src/arguments.c | 5 +++++ src/arguments.h | 7 +++++++ src/main.c | 4 ++++ src/run.c | 15 ++++++++++----- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/arguments.c b/src/arguments.c index 41b3b26..c068c71 100644 --- a/src/arguments.c +++ b/src/arguments.c @@ -17,6 +17,7 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar .overwrite = false, .internal_status = NULL, .format = TDO_FORMAT_HUMAN, + .verbosity = TDO_VERBOSITY_NONE, }; if (argc < 1) return TDO_ERROR_ARG_FIRST; @@ -136,6 +137,10 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar result = TDO_ERROR_ARG_PARSE; } } + } else if (strcmp(s, "-v") == 0) { + args->verbosity = TDO_VERBOSITY_MINOR; + } else if (strcmp(s, "-vv") == 0) { + args->verbosity = TDO_VERBOSITY_MAJOR; } else { fprintf(stderr, "Unrecognized argument: '%s'\n", s); result = TDO_ERROR_ARG_PARSE; diff --git a/src/arguments.h b/src/arguments.h index 25f0bd1..f8907a0 100644 --- a/src/arguments.h +++ b/src/arguments.h @@ -11,6 +11,12 @@ enum TdoFormat { TDO_FORMAT_JSON, }; +enum TdoVerbosity { + TDO_VERBOSITY_NONE, + TDO_VERBOSITY_MINOR, + TDO_VERBOSITY_MAJOR, +}; + struct TdoArguments { size_t processes; float time_limit; @@ -19,6 +25,7 @@ struct TdoArguments { char const *output; char const *internal_status; enum TdoFormat format; + enum TdoVerbosity verbosity; bool overwrite; }; diff --git a/src/main.c b/src/main.c index 33832eb..2b6cbd2 100644 --- a/src/main.c +++ b/src/main.c @@ -38,6 +38,10 @@ static const char *tdo_help_text = " -t \"TEST_DEFINITION\" Run a single test definition string directly.\n" " -j [N] Run tests in parallel using N processes (default: 1).\n" " --format FMT Select output format: 'human' or 'json' (default: human).\n" + " -v, -vv Set output verbosity level:\n" + " -v: Minor verbosity\n" + " -vv: Major verbosity\n" + " Note: Verbosity only applies to 'human' format.\n" " -o FILE Write results to the specified FILE.\n" " -f Force overwrite the output file if it already exists.\n" " --timeout SECONDS Set a maximum execution time per test (default: 5.0).\n" diff --git a/src/run.c b/src/run.c index 485fbbf..8208346 100644 --- a/src/run.c +++ b/src/run.c @@ -71,7 +71,7 @@ void tdo_log_dump(struct TdoLog log, FILE *file, char const *name) { void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoRun *run, FILE *file, char const *step, TdoProcessStatus process_status, double duration, bool timed_out) { if (args->format == TDO_FORMAT_HUMAN) { bool log_output = false; - if (!tdo_process_status_is_exit(process_status) || step[0] != 'f') { + if (!tdo_process_status_is_exit(process_status) || step[0] != 'f' || args->verbosity > TDO_VERBOSITY_NONE) { status->success_in_a_row = 0; if (status->finished > 0) fprintf(file, "\n"); fprintf(file, "%s::%s ", run->test->symbol.file->name.bytes, run->test->symbol.name.bytes); @@ -83,13 +83,18 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, fprintf(file, "TIMEOUT"); } else if (tdo_process_status_is_exit(process_status)) { if (step[0] == 'f') { - if (status->finished > 0 && status->success_in_a_row == 0) fprintf(file, "\n"); + if (status->finished > 0 && status->success_in_a_row == 0 && args->verbosity == TDO_VERBOSITY_NONE) fprintf(file, "\n"); status->success += 1; status->success_in_a_row += 1; - fprintf(file, "."); - if (status->success_in_a_row % 80 == 0) fprintf(file, "\n"); + if (args->verbosity == TDO_VERBOSITY_NONE) { + fprintf(file, "."); + if (status->success_in_a_row % 80 == 0) fprintf(file, "\n"); + } else { + fprintf(file, "SUCCESS"); + if (args->verbosity == TDO_VERBOSITY_MAJOR) fprintf(file, "\n"); + } } else { status->exit += 1; log_output = true; @@ -106,7 +111,7 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, fprintf(file, "STOPPED\n"); } - if (log_output) { + if (log_output || args->verbosity == TDO_VERBOSITY_MAJOR) { fprintf(file, "Captured stdout ----------------------------------------------------------------\n"); tdo_human_escaped(file, run->out.data); fprintf(file, "Captured stderr ----------------------------------------------------------------\n"); From 4a790fb439bedb6a60a1161cc4e726ea057db306 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 22:08:25 +0200 Subject: [PATCH 10/11] stop on first error --- src/arguments.c | 3 +++ src/arguments.h | 1 + src/main.c | 1 + src/platform/run_posix.c | 2 ++ src/platform/run_windows.c | 2 ++ src/run.c | 17 ++++++++++++++++- 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/arguments.c b/src/arguments.c index c068c71..fb5e3e1 100644 --- a/src/arguments.c +++ b/src/arguments.c @@ -18,6 +18,7 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar .internal_status = NULL, .format = TDO_FORMAT_HUMAN, .verbosity = TDO_VERBOSITY_NONE, + .stop_on_first_error = false, }; if (argc < 1) return TDO_ERROR_ARG_FIRST; @@ -141,6 +142,8 @@ enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **ar args->verbosity = TDO_VERBOSITY_MINOR; } else if (strcmp(s, "-vv") == 0) { args->verbosity = TDO_VERBOSITY_MAJOR; + } else if (strcmp(s, "-x") == 0) { + args->stop_on_first_error = true; } else { fprintf(stderr, "Unrecognized argument: '%s'\n", s); result = TDO_ERROR_ARG_PARSE; diff --git a/src/arguments.h b/src/arguments.h index f8907a0..259b1fd 100644 --- a/src/arguments.h +++ b/src/arguments.h @@ -27,6 +27,7 @@ struct TdoArguments { enum TdoFormat format; enum TdoVerbosity verbosity; bool overwrite; + bool stop_on_first_error; }; enum TdoError tdo_arguments_parse(struct TdoArguments *args, int argc, char **argv); diff --git a/src/main.c b/src/main.c index 2b6cbd2..d98941b 100644 --- a/src/main.c +++ b/src/main.c @@ -37,6 +37,7 @@ static const char *tdo_help_text = "Options:\n" " -t \"TEST_DEFINITION\" Run a single test definition string directly.\n" " -j [N] Run tests in parallel using N processes (default: 1).\n" + " -x Stop execution immediately on the first test failure.\n" " --format FMT Select output format: 'human' or 'json' (default: human).\n" " -v, -vv Set output verbosity level:\n" " -v: Minor verbosity\n" diff --git a/src/platform/run_posix.c b/src/platform/run_posix.c index debc99e..3e593e3 100644 --- a/src/platform/run_posix.c +++ b/src/platform/run_posix.c @@ -46,6 +46,7 @@ struct TdoRunStatus { size_t success_in_a_row; bool fork_failed; bool log_setup_failed; + bool any_failed; }; enum TdoError tdo_log_drain(struct TdoLog *log, struct TdoArena *arena) { @@ -310,6 +311,7 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .success_in_a_row = 0, .fork_failed = false, .log_setup_failed = false, + .any_failed = false, }; status->runs = tdo_arena_alloc(arena, sizeof(struct TdoRun), args.processes); diff --git a/src/platform/run_windows.c b/src/platform/run_windows.c index 4b28c50..f9da66a 100644 --- a/src/platform/run_windows.c +++ b/src/platform/run_windows.c @@ -79,6 +79,7 @@ struct TdoRunStatus { size_t success_in_a_row; bool fork_failed; bool log_setup_failed; + bool any_failed; }; enum TdoError tdo_pipe_connect(struct TdoRun *run, struct TdoOverlap *overlap) { @@ -532,6 +533,7 @@ enum TdoError tdo_run_status_init(struct TdoRunStatus *status, struct TdoArena * .success_in_a_row = 0, .fork_failed = false, .log_setup_failed = false, + .any_failed = false, }; status->runs = tdo_arena_alloc(arena, sizeof(struct TdoRun), args.processes); diff --git a/src/run.c b/src/run.c index 8208346..fcb7c3e 100644 --- a/src/run.c +++ b/src/run.c @@ -79,6 +79,7 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, if (timed_out) { status->timeout += 1; + status->any_failed = true; log_output = true; fprintf(file, "TIMEOUT"); } else if (tdo_process_status_is_exit(process_status)) { @@ -97,11 +98,13 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, } } else { status->exit += 1; + status->any_failed = true; log_output = true; fprintf(file, "UNEXPECTED EXIT"); } } else if (tdo_process_status_is_signal(process_status)) { status->signal += 1; + status->any_failed = true; log_output = true; fprintf(file, "SIGNAL"); } else if (tdo_process_status_is_stop(process_status)) { @@ -137,6 +140,7 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, fprintf(file, "\t\t\"status\": \""); if (timed_out) { status->timeout += 1; + status->any_failed = true; fprintf(file, "timeout"); } else if (tdo_process_status_is_exit(process_status)) { if (step[0] == 'f') { @@ -144,10 +148,12 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, fprintf(file, "complete"); } else { status->exit += 1; + status->any_failed = true; fprintf(file, "exit"); } } else if (tdo_process_status_is_signal(process_status)) { status->signal += 1; + status->any_failed = true; fprintf(file, "signal"); } else if (tdo_process_status_is_stop(process_status)) { fprintf(stderr, "When can this happen anyway?\n"); @@ -186,6 +192,7 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, void tdo_run_report_error(struct TdoArguments *args, struct TdoRunStatus *status, struct TdoTest test, FILE *file, char const *step, char const *error, double duration) { status->error += 1; + status->any_failed = true; if (args->format == TDO_FORMAT_HUMAN) { if (status->finished > 0) fprintf(file, "\n"); @@ -568,6 +575,10 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren status.finished += 1; } } + + if (args.stop_on_first_error && status.any_failed) { + break; + } } if (args.format == TDO_FORMAT_HUMAN) { @@ -586,7 +597,11 @@ enum TdoError tdo_run_all(struct TdoArguments args, FILE *output, struct TdoAren char const *spacing = " "; - fprintf(stderr, "Ran %zu tests in %.2lf seconds:\n", tests.length, tdo_time_between(time_end, time_start)); + if (status.finished < tests.length) { + fprintf(stderr, "Stopped after running %zu/%zu tests in %.2lf seconds:\n", status.finished, tests.length, tdo_time_between(time_end, time_start)); + } else { + fprintf(stderr, "Ran %zu tests in %.2lf seconds:\n", tests.length, tdo_time_between(time_end, time_start)); + } fprintf(stderr, "%ssuccess: %3zu/%zu\n", spacing, status.success, tests.length); size_t total_fails = status.exit + status.timeout + status.signal + status.error; From c0b5504b3311c31b20d4ebaf452073bead8a92e7 Mon Sep 17 00:00:00 2001 From: p-rosit Date: Thu, 14 May 2026 22:20:26 +0200 Subject: [PATCH 11/11] print the exit code and signal --- src/run.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/run.c b/src/run.c index fcb7c3e..1e8b0d1 100644 --- a/src/run.c +++ b/src/run.c @@ -100,13 +100,13 @@ void tdo_run_report_exit(struct TdoArguments *args, struct TdoRunStatus *status, status->exit += 1; status->any_failed = true; log_output = true; - fprintf(file, "UNEXPECTED EXIT"); + fprintf(file, "UNEXPECTED EXIT (" TDO_PROCESS_CODE_FORMAT ")\n", tdo_process_code_exit(process_status)); } } else if (tdo_process_status_is_signal(process_status)) { status->signal += 1; status->any_failed = true; log_output = true; - fprintf(file, "SIGNAL"); + fprintf(file, "SIGNAL (" TDO_PROCESS_CODE_FORMAT ")\n", tdo_process_code_signal(process_status)); } else if (tdo_process_status_is_stop(process_status)) { fprintf(stderr, "When can this happen anyway?\n"); fflush(NULL);