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,...`
 // The format is: `package.test_name,test_name_only,...`
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 // Show the fancy animated progress report.
 // 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)
 FANCY_OUTPUT          : bool   : #config(ODIN_TEST_FANCY, true)
 // Copy failed tests to the clipboard when done.
 // Copy failed tests to the clipboard when done.
 USE_CLIPBOARD         : bool   : #config(ODIN_TEST_CLIPBOARD, false)
 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_log_colors_disabled: bool
+@(private) global_ansi_disabled: bool
 
 
 JSON :: struct {
 JSON :: struct {
 	total:    int,
 	total:    int,
@@ -219,7 +221,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	stdout := io.to_writer(os.stream_from_handle(os.stdout))
 	stdout := io.to_writer(os.stream_from_handle(os.stdout))
 	stderr := io.to_writer(os.stream_from_handle(os.stderr))
 	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_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.
 	// -- Prepare test data.
 
 
@@ -278,12 +285,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	total_done_count    := 0
 	total_done_count    := 0
 	total_test_count    := len(internal_tests)
 	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 {
 	if total_test_count == 0 {
 		// Exit early.
 		// 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)
 	fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
 	defer destroy_report(&report)
 	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: []Task_Data = ---
 	task_data_slots, alloc_error = make([]Task_Data, thread_count)
 	task_data_slots, alloc_error = make([]Task_Data, thread_count)
@@ -496,11 +503,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 
 	setup_signal_handler()
 	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)
 		redraw_report(stdout, report)
 		draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
 		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
 			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 {
 		if test_index, reason, ok := should_stop_test(); ok {
 			#no_bounds_check report.all_test_states[test_index] = .Failed
 			#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)
 					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
 					bypass_progress_overwrite = true
 					signals_were_raised = true
 					signals_were_raised = true
 				}
 				}
@@ -781,7 +790,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 
 		// -- Redraw.
 		// -- Redraw.
 
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 				continue main_loop
 				continue main_loop
 			}
 			}
@@ -791,7 +800,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			}
 			}
 		} else {
 		} else {
 			if total_done_count != last_done_count {
 			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
 				last_done_count = total_done_count
 			}
 			}
 
 
@@ -816,7 +827,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		clear(&log_messages)
 		clear(&log_messages)
 		bytes.buffer_reset(&batch_buffer)
 		bytes.buffer_reset(&batch_buffer)
 
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			redraw_report(batch_writer, report)
 			redraw_report(batch_writer, report)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
 			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)
 	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
 		// One line to space out the results, since we don't have the status
 		// bar in plain mode.
 		// bar in plain mode.
 		fmt.wprintln(batch_writer)
 		fmt.wprintln(batch_writer)
@@ -851,24 +862,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	
 	
 	if total_done_count != total_test_count {
 	if total_done_count != total_test_count {
 		not_run_count := total_test_count - total_done_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,
 		fmt.wprintf(batch_writer,
-			" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
+			message,
 			not_run_count,
 			not_run_count,
 			"test was" if not_run_count == 1 else "tests were")
 			"test was" if not_run_count == 1 else "tests were")
 	}
 	}
 
 
 	if total_success_count == total_test_count {
 	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,
 		fmt.wprintfln(batch_writer,
-			" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
+			message,
 			"The test was" if total_test_count == 1 else "All tests were")
 			"The test was" if total_test_count == 1 else "All tests were")
 	} else if total_failure_count > 0 {
 	} else if total_failure_count > 0 {
 		if total_failure_count == total_test_count {
 		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,
 			fmt.wprintfln(batch_writer,
-				" %s " + SGR_FAILED + "failed." + SGR_RESET,
+				message,
 				"The test" if total_test_count == 1 else "All tests")
 				"The test" if total_test_count == 1 else "All tests")
 		} else {
 		} else {
+			message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
 			fmt.wprintfln(batch_writer,
 			fmt.wprintfln(batch_writer,
-				" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
+				message,
 				total_failure_count,
 				total_failure_count,
 				"" if total_failure_count == 1 else "s")
 				"" 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 {
 		if signals_were_raised {
 			fmt.wprintln(batch_writer, `
 			fmt.wprintln(batch_writer, `
 Signals were raised during this test run. Log messages are likely to have collided with each other.
 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
 		// NOTE(Feoramund): Using these write calls in a signal handler is
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// Either way, we may as well try to salvage what we can.
 		// 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.
 		// This is an attempt at being compliant by avoiding printf.
 		sigbuf: [8]byte
 		sigbuf: [8]byte