diff options
| author | Feoramund <161657516+Feoramund@users.noreply.github.com> | 2025-05-20 16:17:14 -0400 |
|---|---|---|
| committer | Feoramund <161657516+Feoramund@users.noreply.github.com> | 2025-05-20 19:28:07 -0400 |
| commit | 1b407ef20789b1d04d61b0a53ca8c98fe6621db2 (patch) | |
| tree | 88b663421eb12b5a0af7f1025ccd0343f2a4e3ca /core/testing | |
| parent | 3c40a54dcd9ff8e1b608fac5c91ed0f1d0ed7d00 (diff) | |
Add animation detection support to test runner
Diffstat (limited to 'core/testing')
| -rw-r--r-- | core/testing/runner.odin | 135 | ||||
| -rw-r--r-- | core/testing/signal_handler_libc.odin | 8 |
2 files changed, 81 insertions, 62 deletions
diff --git a/core/testing/runner.odin b/core/testing/runner.odin index ff8ca00b9..a184eb28c 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -45,6 +45,7 @@ PER_THREAD_MEMORY : int : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_S // The format is: `package.test_name,test_name_only,...` TEST_NAMES : string : #config(ODIN_TEST_NAMES, "") // Show the fancy animated progress report. +// This requires terminal color support, as well as STDOUT to not be redirected to a file. FANCY_OUTPUT : bool : #config(ODIN_TEST_FANCY, true) // Copy failed tests to the clipboard when done. USE_CLIPBOARD : bool : #config(ODIN_TEST_CLIPBOARD, false) @@ -72,6 +73,7 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level { } @(private) global_log_colors_disabled: bool +@(private) global_ansi_disabled: bool JSON :: struct { total: int, @@ -219,7 +221,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { stdout := io.to_writer(os.stream_from_handle(os.stdout)) stderr := io.to_writer(os.stream_from_handle(os.stderr)) + // The animations are only ever shown through STDOUT; + // STDERR is used exclusively for logging regardless of error level. global_log_colors_disabled = !terminal.color_enabled || !terminal.is_terminal(os.stderr) + global_ansi_disabled = !terminal.is_terminal(os.stdout) + + should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled // -- Prepare test data. @@ -278,12 +285,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { total_done_count := 0 total_test_count := len(internal_tests) - when !FANCY_OUTPUT { - // This is strictly for updating the window title when the progress - // report is disabled. We're otherwise able to depend on the call to - // `needs_to_redraw`. - last_done_count := -1 - } + + // This is strictly for updating the window title when the progress + // report is disabled. We're otherwise able to depend on the call to + // `needs_to_redraw`. + last_done_count := -1 + if total_test_count == 0 { // Exit early. @@ -352,31 +359,31 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error) defer destroy_report(&report) - when FANCY_OUTPUT { - // We cannot make use of the ANSI save/restore cursor codes, because they - // work by absolute screen coordinates. This will cause unnecessary - // scrollback if we print at the bottom of someone's terminal. - ansi_redraw_string := fmt.aprintf( - // ANSI for "go up N lines then erase the screen from the cursor forward." - ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED + - // We'll combine this with the window title format string, since it - // can be printed at the same time. - "%s", - // 1 extra line for the status bar. - 1 + len(report.packages), OSC_WINDOW_TITLE) - assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.") - defer delete(ansi_redraw_string) - - thread_count_status_string: string = --- - { - PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH - unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s") - thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING) - assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.") - } - defer delete(thread_count_status_string) + // We cannot make use of the ANSI save/restore cursor codes, because they + // work by absolute screen coordinates. This will cause unnecessary + // scrollback if we print at the bottom of someone's terminal. + ansi_redraw_string := fmt.aprintf( + // ANSI for "go up N lines then erase the screen from the cursor forward." + ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED + + // We'll combine this with the window title format string, since it + // can be printed at the same time. + "%s", + // 1 extra line for the status bar. + 1 + len(report.packages), OSC_WINDOW_TITLE) + assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.") + defer delete(ansi_redraw_string) + + thread_count_status_string: string = --- + { + PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH + + unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s") + thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING) + assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.") } + defer delete(thread_count_status_string) + task_data_slots: []Task_Data = --- task_data_slots, alloc_error = make([]Task_Data, thread_count) @@ -496,11 +503,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { setup_signal_handler() - fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE) + if !global_ansi_disabled { + fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE) + } - when FANCY_OUTPUT { - signals_were_raised := false + signals_were_raised := false + if should_show_animations { redraw_report(stdout, report) draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count) } @@ -718,22 +727,22 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { break main_loop } - when FANCY_OUTPUT { - // Because the bounds checking procs send directly to STDERR with - // no way to redirect or handle them, we need to at least try to - // let the user see those messages when using the animated progress - // report. This flag may be set by the block of code below if a - // signal is raised. - // - // It'll be purely by luck if the output is interleaved properly, - // given the nature of non-thread-safe printing. - // - // At worst, if Odin did not print any error for this signal, we'll - // just re-display the progress report. The fatal log error message - // should be enough to clue the user in that something dire has - // occurred. - bypass_progress_overwrite := false - } + + // Because the bounds checking procs send directly to STDERR with + // no way to redirect or handle them, we need to at least try to + // let the user see those messages when using the animated progress + // report. This flag may be set by the block of code below if a + // signal is raised. + // + // It'll be purely by luck if the output is interleaved properly, + // given the nature of non-thread-safe printing. + // + // At worst, if Odin did not print any error for this signal, we'll + // just re-display the progress report. The fatal log error message + // should be enough to clue the user in that something dire has + // occurred. + bypass_progress_overwrite := false + if test_index, reason, ok := should_stop_test(); ok { #no_bounds_check report.all_test_states[test_index] = .Failed @@ -767,7 +776,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason) } - when FANCY_OUTPUT { + if should_show_animations { bypass_progress_overwrite = true signals_were_raised = true } @@ -781,7 +790,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { // -- Redraw. - when FANCY_OUTPUT { + if should_show_animations { if len(log_messages) == 0 && !needs_to_redraw(report) { continue main_loop } @@ -791,7 +800,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } else { if total_done_count != last_done_count { - fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count) + if !global_ansi_disabled { + fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count) + } last_done_count = total_done_count } @@ -816,7 +827,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { clear(&log_messages) bytes.buffer_reset(&batch_buffer) - when FANCY_OUTPUT { + if should_show_animations { redraw_report(batch_writer, report) draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count) fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer)) @@ -837,7 +848,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { finished_in := time.since(start_time) - when !FANCY_OUTPUT { + if !should_show_animations || !terminal.is_terminal(os.stderr) { // One line to space out the results, since we don't have the status // bar in plain mode. fmt.wprintln(batch_writer) @@ -851,24 +862,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { if total_done_count != total_test_count { not_run_count := total_test_count - total_done_count + message := " %i %s left undone." if global_log_colors_disabled else " " + SGR_READY + "%i" + SGR_RESET + " %s left undone." fmt.wprintf(batch_writer, - " " + SGR_READY + "%i" + SGR_RESET + " %s left undone.", + message, not_run_count, "test was" if not_run_count == 1 else "tests were") } if total_success_count == total_test_count { + message := " %s successful." if global_log_colors_disabled else " %s " + SGR_SUCCESS + "successful." + SGR_RESET fmt.wprintfln(batch_writer, - " %s " + SGR_SUCCESS + "successful." + SGR_RESET, + message, "The test was" if total_test_count == 1 else "All tests were") } else if total_failure_count > 0 { if total_failure_count == total_test_count { + message := " %s failed." if global_log_colors_disabled else " %s " + SGR_FAILED + "failed." + SGR_RESET fmt.wprintfln(batch_writer, - " %s " + SGR_FAILED + "failed." + SGR_RESET, + message, "The test" if total_test_count == 1 else "All tests") } else { + message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed." fmt.wprintfln(batch_writer, - " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.", + message, total_failure_count, "" if total_failure_count == 1 else "s") } @@ -922,9 +937,11 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } - fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW) + if !global_ansi_disabled { + fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW) + } - when FANCY_OUTPUT { + if should_show_animations { if signals_were_raised { fmt.wprintln(batch_writer, ` Signals were raised during this test run. Log messages are likely to have collided with each other. diff --git a/core/testing/signal_handler_libc.odin b/core/testing/signal_handler_libc.odin index d17a6d6dc..f9527e22f 100644 --- a/core/testing/signal_handler_libc.odin +++ b/core/testing/signal_handler_libc.odin @@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) { // NOTE(Feoramund): Using these write calls in a signal handler is // undefined behavior in C99 but possibly tolerated in POSIX 2008. // Either way, we may as well try to salvage what we can. - show_cursor := ansi.CSI + ansi.DECTCEM_SHOW - libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout) - libc.fflush(libc.stdout) + if !global_ansi_disabled { + show_cursor := ansi.CSI + ansi.DECTCEM_SHOW + libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout) + libc.fflush(libc.stdout) + } // This is an attempt at being compliant by avoiding printf. sigbuf: [8]byte |