123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- //+private
- package testing
- /*
- (c) Copyright 2024 Feoramund <[email protected]>.
- Made available under Odin's BSD-3 license.
- List of contributors:
- Feoramund: Total rewrite.
- */
- import "base:runtime"
- import "core:encoding/ansi"
- import "core:fmt"
- import "core:io"
- import "core:mem"
- import "core:path/filepath"
- import "core:strings"
- // Definitions of colors for use in the test runner.
- SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
- SGR_READY :: ansi.CSI + ansi.FG_BRIGHT_BLACK + ansi.SGR
- SGR_RUNNING :: ansi.CSI + ansi.FG_YELLOW + ansi.SGR
- SGR_SUCCESS :: ansi.CSI + ansi.FG_GREEN + ansi.SGR
- SGR_FAILED :: ansi.CSI + ansi.FG_RED + ansi.SGR
- MAX_PROGRESS_WIDTH :: 100
- // More than enough bytes to cover long package names, long test names, dozens
- // of ANSI codes, et cetera.
- LINE_BUFFER_SIZE :: (MAX_PROGRESS_WIDTH * 8 + 224) * runtime.Byte
- PROGRESS_COLUMN_SPACING :: 2
- Package_Run :: struct {
- name: string,
- header: string,
- frame_ready: bool,
- redraw_buffer: [LINE_BUFFER_SIZE]byte,
- redraw_string: string,
- last_change_state: Test_State,
- last_change_name: string,
- tests: []Internal_Test,
- test_states: []Test_State,
- }
- Report :: struct {
- packages: []Package_Run,
- packages_by_name: map[string]^Package_Run,
- pkg_column_len: int,
- test_column_len: int,
- progress_width: int,
- all_tests: []Internal_Test,
- all_test_states: []Test_State,
- }
- // Organize all tests by package and sort out test state data.
- make_report :: proc(internal_tests: []Internal_Test) -> (report: Report, error: runtime.Allocator_Error) {
- assert(len(internal_tests) > 0, "make_report called with no tests")
- packages: [dynamic]Package_Run
- report.all_tests = internal_tests
- report.all_test_states = make([]Test_State, len(internal_tests)) or_return
- // First, figure out what belongs where.
- #no_bounds_check cur_pkg := internal_tests[0].pkg
- pkg_start: int
- // This loop assumes the tests are sorted by package already.
- for it, index in internal_tests {
- if cur_pkg != it.pkg {
- #no_bounds_check {
- append(&packages, Package_Run {
- name = cur_pkg,
- tests = report.all_tests[pkg_start:index],
- test_states = report.all_test_states[pkg_start:index],
- }) or_return
- }
- when PROGRESS_WIDTH == 0 {
- report.progress_width = max(report.progress_width, index - pkg_start)
- }
- pkg_start = index
- report.pkg_column_len = max(report.pkg_column_len, len(cur_pkg))
- cur_pkg = it.pkg
- }
- report.test_column_len = max(report.test_column_len, len(it.name))
- }
- // Handle the last (or only) package.
- #no_bounds_check {
- append(&packages, Package_Run {
- name = cur_pkg,
- header = cur_pkg,
- tests = report.all_tests[pkg_start:],
- test_states = report.all_test_states[pkg_start:],
- }) or_return
- }
- when PROGRESS_WIDTH == 0 {
- report.progress_width = max(report.progress_width, len(internal_tests) - pkg_start)
- } else {
- report.progress_width = PROGRESS_WIDTH
- }
- report.progress_width = min(report.progress_width, MAX_PROGRESS_WIDTH)
- report.pkg_column_len = PROGRESS_COLUMN_SPACING + max(report.pkg_column_len, len(cur_pkg))
- shrink(&packages) or_return
- for &pkg in packages {
- pkg.header = fmt.aprintf("%- *[1]s[", pkg.name, report.pkg_column_len)
- assert(len(pkg.header) > 0, "Error allocating package header string.")
- // This is safe because the array is done resizing, and it has the same
- // lifetime as the map.
- report.packages_by_name[pkg.name] = &pkg
- }
- // It's okay to discard the dynamic array's allocator information here,
- // because its capacity has been shrunk to its length, it was allocated by
- // the caller's context allocator, and it will be deallocated by the same.
- //
- // `delete_slice` is equivalent to `delete_dynamic_array` in this case.
- report.packages = packages[:]
- return
- }
- destroy_report :: proc(report: ^Report) {
- for pkg in report.packages {
- delete(pkg.header)
- }
- delete(report.packages)
- delete(report.packages_by_name)
- delete(report.all_test_states)
- }
- redraw_package :: proc(w: io.Writer, report: Report, pkg: ^Package_Run) {
- if pkg.frame_ready {
- io.write_string(w, pkg.redraw_string)
- return
- }
- // Write the output line here so we can cache it.
- line_builder := strings.builder_from_bytes(pkg.redraw_buffer[:])
- line_writer := strings.to_writer(&line_builder)
- highest_run_index: int
- failed_count: int
- done_count: int
- #no_bounds_check for i := 0; i < len(pkg.test_states); i += 1 {
- switch pkg.test_states[i] {
- case .Ready:
- continue
- case .Running:
- highest_run_index = max(highest_run_index, i)
- case .Successful:
- done_count += 1
- case .Failed:
- failed_count += 1
- done_count += 1
- }
- }
- start := max(0, highest_run_index - (report.progress_width - 1))
- end := min(start + report.progress_width, len(pkg.test_states))
- // This variable is to keep track of the last ANSI code emitted, in
- // order to avoid repeating the same code over in a sequence.
- //
- // This should help reduce screen flicker.
- last_state := Test_State(-1)
- io.write_string(line_writer, pkg.header)
- #no_bounds_check for state in pkg.test_states[start:end] {
- switch state {
- case .Ready:
- if last_state != state {
- io.write_string(line_writer, SGR_READY)
- last_state = state
- }
- case .Running:
- if last_state != state {
- io.write_string(line_writer, SGR_RUNNING)
- last_state = state
- }
- case .Successful:
- if last_state != state {
- io.write_string(line_writer, SGR_SUCCESS)
- last_state = state
- }
- case .Failed:
- if last_state != state {
- io.write_string(line_writer, SGR_FAILED)
- last_state = state
- }
- }
- io.write_byte(line_writer, '|')
- }
- for _ in 0 ..< report.progress_width - (end - start) {
- io.write_byte(line_writer, ' ')
- }
- io.write_string(line_writer, SGR_RESET + "] ")
- ticker: string
- if done_count == len(pkg.test_states) {
- ticker = "[package done]"
- if failed_count > 0 {
- ticker = fmt.tprintf("%s (" + SGR_FAILED + "%i" + SGR_RESET + " failed)", ticker, failed_count)
- }
- } else {
- if len(pkg.last_change_name) == 0 {
- #no_bounds_check pkg.last_change_name = pkg.tests[0].name
- }
- switch pkg.last_change_state {
- case .Ready:
- ticker = fmt.tprintf(SGR_READY + "%s" + SGR_RESET, pkg.last_change_name)
- case .Running:
- ticker = fmt.tprintf(SGR_RUNNING + "%s" + SGR_RESET, pkg.last_change_name)
- case .Failed:
- ticker = fmt.tprintf(SGR_FAILED + "%s" + SGR_RESET, pkg.last_change_name)
- case .Successful:
- ticker = fmt.tprintf(SGR_SUCCESS + "%s" + SGR_RESET, pkg.last_change_name)
- }
- }
- if done_count == len(pkg.test_states) {
- fmt.wprintfln(line_writer, " % 4i :: %s",
- len(pkg.test_states),
- ticker,
- )
- } else {
- fmt.wprintfln(line_writer, "% 4i/% 4i :: %s",
- done_count,
- len(pkg.test_states),
- ticker,
- )
- }
- pkg.redraw_string = strings.to_string(line_builder)
- pkg.frame_ready = true
- io.write_string(w, pkg.redraw_string)
- }
- redraw_report :: proc(w: io.Writer, report: Report) {
- // If we print a line longer than the user's terminal can handle, it may
- // wrap around, shifting the progress report out of alignment.
- //
- // There are ways to get the current terminal width, and that would be the
- // ideal way to handle this, but it would require system-specific code such
- // as setting STDIN to be non-blocking in order to read the response from
- // the ANSI DSR escape code, or reading environment variables.
- //
- // The DECAWM escape codes control whether or not the terminal will wrap
- // long lines or overwrite the last visible character.
- // This should be fine for now.
- //
- // Note that we only do this for the animated summary; log messages are
- // still perfectly fine to wrap, as they're printed in their own batch,
- // whereas the animation depends on each package being only on one line.
- //
- // Of course, if you resize your terminal while it's printing, things can
- // still break...
- fmt.wprint(w, ansi.CSI + ansi.DECAWM_OFF)
- for &pkg in report.packages {
- redraw_package(w, report, &pkg)
- }
- fmt.wprint(w, ansi.CSI + ansi.DECAWM_ON)
- }
- needs_to_redraw :: proc(report: Report) -> bool {
- for pkg in report.packages {
- if !pkg.frame_ready {
- return true
- }
- }
- return false
- }
- draw_status_bar :: proc(w: io.Writer, threads_string: string, total_done_count, total_test_count: int) {
- if total_done_count == total_test_count {
- // All tests are done; print a blank line to maintain the same height
- // of the progress report.
- fmt.wprintln(w)
- } else {
- fmt.wprintfln(w,
- "%s % 4i/% 4i :: total",
- threads_string,
- total_done_count,
- total_test_count)
- }
- }
- write_memory_report :: proc(w: io.Writer, tracker: ^mem.Tracking_Allocator, pkg, name: string) {
- fmt.wprintf(w,
- "<% 10M/% 10M> <% 10M> (% 5i/% 5i) :: %s.%s",
- tracker.current_memory_allocated,
- tracker.total_memory_allocated,
- tracker.peak_memory_allocated,
- tracker.total_free_count,
- tracker.total_allocation_count,
- pkg,
- name)
- for ptr, entry in tracker.allocation_map {
- fmt.wprintf(w,
- "\n +++ leak % 10M @ %p [%s:%i:%s()]",
- entry.size,
- ptr,
- filepath.base(entry.location.file_path),
- entry.location.line,
- entry.location.procedure)
- }
- for entry in tracker.bad_free_array {
- fmt.wprintf(w,
- "\n +++ bad free @ %p [%s:%i:%s()]",
- entry.memory,
- filepath.base(entry.location.file_path),
- entry.location.line,
- entry.location.procedure)
- }
- }
|