aboutsummaryrefslogtreecommitdiff
path: root/core/testing
diff options
context:
space:
mode:
authorJeroen van Rijn <Kelimion@users.noreply.github.com>2024-08-08 20:15:08 +0200
committerJeroen van Rijn <Kelimion@users.noreply.github.com>2024-08-08 20:41:32 +0200
commit80d1e1ba82cb21edb0a5263ea616d7105038bc0e (patch)
tree34989f4b3660b4b0dc014c7d8732da8d2225d848 /core/testing
parent94c62fb630265f7c08726e7f15b4fb5cac0a3ad9 (diff)
Allow testing for intentional leaks in test runner
Adds `expect_leak_or_bad_free :: proc(t: ^T, client_test: proc(t: ^T), verifier: Memory_Verifier_Proc)`. It sets up its own `Tracking_Allocator`, runs the `client_test`, and then calls the `verifier` procedure. The verifier can then inspect the contents of the tracking allocator and call `testing.expect*` as sensible for the test in question. Any allocations are then cleared so that the test runner doesn't itself complain about leaks. Additionally, `ODIN_TEST_LOG_LEVEL_MEMORY` has been added as a define to set the severity of the test runner's memory tracker. You can use `-define:ODIN_TEST_LOG_LEVEL_MEMORY=error` to make tests fail rather than warn if leaks or bad frees have been found.
Diffstat (limited to 'core/testing')
-rw-r--r--core/testing/runner.odin38
-rw-r--r--core/testing/testing.odin22
2 files changed, 56 insertions, 4 deletions
diff --git a/core/testing/runner.odin b/core/testing/runner.odin
index 16967e3c7..5482d93e3 100644
--- a/core/testing/runner.odin
+++ b/core/testing/runner.odin
@@ -25,6 +25,8 @@ TEST_THREADS : int : #config(ODIN_TEST_THREADS, 0)
TRACKING_MEMORY : bool : #config(ODIN_TEST_TRACK_MEMORY, true)
// Always report how much memory is used, even when there are no leaks or bad frees.
ALWAYS_REPORT_MEMORY : bool : #config(ODIN_TEST_ALWAYS_REPORT_MEMORY, false)
+// Log level for memory leaks and bad frees: debug, info, warning, error, fatal
+LOG_LEVEL_MEMORY : string : #config(ODIN_TEST_LOG_LEVEL_MEMORY, "warning")
// Specify how much memory each thread allocator starts with.
PER_THREAD_MEMORY : int : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_STACK_DEFAULT_BLOCK_SIZE)
// Select a specific set of tests to run by name.
@@ -63,6 +65,21 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level {
}
}
+get_memory_log_level :: #force_inline proc() -> runtime.Logger_Level {
+ when ODIN_DEBUG {
+ // Always use .Debug in `-debug` mode.
+ return .Debug
+ } else {
+ when LOG_LEVEL_MEMORY == "debug" { return .Debug } else
+ when LOG_LEVEL_MEMORY == "info" { return .Info } else
+ when LOG_LEVEL_MEMORY == "warning" { return .Warning } else
+ when LOG_LEVEL_MEMORY == "error" { return .Error } else
+ when LOG_LEVEL_MEMORY == "fatal" { return .Fatal } else {
+ #panic("Unknown `ODIN_TEST_LOG_LEVEL_MEMORY`: \"" + LOG_LEVEL_MEMORY + "\", possible levels are: \"debug\", \"info\", \"warning\", \"error\", or \"fatal\".")
+ }
+ }
+}
+
JSON :: struct {
total: int,
success: int,
@@ -222,6 +239,10 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
total_success_count := 0
total_done_count := 0
total_test_count := len(internal_tests)
+ when TRACKING_MEMORY {
+ memory_leak_count := 0
+ bad_free_count := 0
+ }
when !FANCY_OUTPUT {
// This is strictly for updating the window title when the progress
@@ -498,6 +519,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
memory_is_in_bad_state := len(tracker.allocation_map) + len(tracker.bad_free_array) > 0
+ memory_leak_count += len(tracker.allocation_map)
+ bad_free_count += len(tracker.bad_free_array)
+
when ALWAYS_REPORT_MEMORY {
should_report := true
} else {
@@ -507,7 +531,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
if should_report {
write_memory_report(batch_writer, tracker, data.it.pkg, data.it.name)
- pkg_log.log(.Warning if memory_is_in_bad_state else .Info, bytes.buffer_to_string(&batch_buffer))
+ memory_log_level := get_memory_log_level() if memory_is_in_bad_state else .Info
+
+ pkg_log.log(memory_log_level, bytes.buffer_to_string(&batch_buffer))
bytes.buffer_reset(&batch_buffer)
}
@@ -891,5 +917,11 @@ To partly mitigate this, redirect STDERR to a file or use the -define:ODIN_TEST_
fmt.assertf(err == nil, "Error writing JSON report: %v", err)
}
- return total_success_count == total_test_count
-}
+ fatal_memory_failures := false
+ when TRACKING_MEMORY {
+ if get_memory_log_level() >= .Error {
+ fatal_memory_failures = (memory_leak_count + bad_free_count) > 0
+ }
+ }
+ return total_success_count == total_test_count && !fatal_memory_failures
+} \ No newline at end of file
diff --git a/core/testing/testing.odin b/core/testing/testing.odin
index 29fe853ef..5282f5a20 100644
--- a/core/testing/testing.odin
+++ b/core/testing/testing.odin
@@ -4,8 +4,10 @@ import "base:intrinsics"
import "base:runtime"
import pkg_log "core:log"
import "core:reflect"
+import "core:sync"
import "core:sync/chan"
import "core:time"
+import "core:mem"
_ :: reflect // alias reflect to nothing to force visibility for -vet
@@ -136,10 +138,28 @@ expect_value :: proc(t: ^T, value, expected: $T, loc := #caller_location) -> boo
return ok
}
+Memory_Verifier_Proc :: #type proc(t: ^T, ta: ^mem.Tracking_Allocator)
+
+expect_leaks :: proc(t: ^T, client_test: proc(t: ^T), verifier: Memory_Verifier_Proc) {
+ {
+ ta: mem.Tracking_Allocator
+ mem.tracking_allocator_init(&ta, context.allocator)
+ defer mem.tracking_allocator_destroy(&ta)
+ context.allocator = mem.tracking_allocator(&ta)
+
+ client_test(t)
+ sync.mutex_lock(&ta.mutex)
+ // The verifier can inspect this local tracking allocator.
+ // And then call `testing.expect_*` as makes sense for the client test.
+ verifier(t, &ta)
+ sync.mutex_unlock(&ta.mutex)
+ }
+ free_all(context.allocator)
+}
set_fail_timeout :: proc(t: ^T, duration: time.Duration, loc := #caller_location) {
chan.send(t.channel, Event_Set_Fail_Timeout {
at_time = time.time_add(time.now(), duration),
location = loc,
})
-}
+} \ No newline at end of file