Browse Source

Add animation detection support to test runner

Feoramund 3 months ago
parent
commit
1b407ef207
2 changed files with 81 additions and 62 deletions
  1. 76 59
      core/testing/runner.odin
  2. 5 3
      core/testing/signal_handler_libc.odin

+ 76 - 59
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.

+ 5 - 3
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