aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeroen van Rijn <Kelimion@users.noreply.github.com>2024-08-08 21:40:17 +0200
committerGitHub <noreply@github.com>2024-08-08 21:40:17 +0200
commit1e09ff3b2e6cb2610abe6b23bc0e975d66c9ffa5 (patch)
tree4ece3c6df15abffbea01b0a19d4603b14fdad70f
parentf328929939dfb761eaa4b087938c79f86ebe698a (diff)
parent933f9f9bd17a14c587877e2cea5b2ced5869db5a (diff)
Merge pull request #4043 from Kelimion/expect_leak_or_bad_free
Allow testing for intentional leaks in test runner
-rw-r--r--.github/workflows/ci.yml36
-rw-r--r--core/testing/runner.odin38
-rw-r--r--core/testing/testing.odin22
-rw-r--r--tests/core/compress/test_core_compress.odin10
-rw-r--r--tests/core/mem/test_mem_dynamic_pool.odin129
5 files changed, 153 insertions, 82 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0c9266328..a7da255e8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,10 +32,10 @@ jobs:
gmake -C vendor/miniaudio/src
./odin check examples/all -vet -strict-style -disallow-do -target:netbsd_amd64
./odin check examples/all -vet -strict-style -disallow-do -target:netbsd_arm64
- ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false
- ./odin test tests/core/speed.odin -file -all-packages -o:speed -define:ODIN_TEST_FANCY=false
- ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false
- ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false
+ ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/core/speed.odin -file -all-packages -o:speed -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
(cd tests/issues; ./run.sh)
build_freebsd:
name: FreeBSD Build, Check, and Test
@@ -61,10 +61,10 @@ jobs:
gmake -C vendor/cgltf/src
gmake -C vendor/miniaudio/src
./odin check examples/all -vet -strict-style -disallow-do -target:freebsd_amd64
- ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false
- ./odin test tests/core/speed.odin -file -all-packages -o:speed -define:ODIN_TEST_FANCY=false
- ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false
- ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false
+ ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/core/speed.odin -file -all-packages -o:speed -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
+ ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
(cd tests/issues; ./run.sh)
ci:
strategy:
@@ -118,15 +118,15 @@ jobs:
- name: Odin check examples/all
run: ./odin check examples/all -strict-style
- name: Normal Core library tests
- run: ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false
+ run: ./odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Optimized Core library tests
- run: ./odin test tests/core/speed.odin -o:speed -file -all-packages -define:ODIN_TEST_FANCY=false
+ run: ./odin test tests/core/speed.odin -o:speed -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Vendor library tests
- run: ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false
+ run: ./odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Internals tests
- run: ./odin test tests/internal -all-packages -define:ODIN_TEST_FANCY=false
+ run: ./odin test tests/internal -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Core library benchmarks
- run: ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false
+ run: ./odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: GitHub Issue tests
run: |
cd tests/issues
@@ -190,28 +190,28 @@ jobs:
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
- odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false
+ odin test tests/core/normal.odin -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Optimized core library tests
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
- odin test tests/core/speed.odin -o:speed -file -all-packages -define:ODIN_TEST_FANCY=false
+ odin test tests/core/speed.odin -o:speed -file -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Core library benchmarks
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
- odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false
+ odin test tests/benchmark -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Vendor library tests
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
copy vendor\lua\5.4\windows\*.dll .
- odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false
+ odin test tests/vendor -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Odin internals tests
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat
- odin test tests/internal -all-packages -define:ODIN_TEST_FANCY=false
+ odin test tests/internal -all-packages -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_LOG_LEVEL_MEMORY=fatal
- name: Odin documentation tests
shell: cmd
run: |
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..ea779b8f3 100644
--- a/core/testing/testing.odin
+++ b/core/testing/testing.odin
@@ -4,10 +4,13 @@ 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
+_ :: mem // in case TRACKING_MEMORY is not enabled
// IMPORTANT NOTE: Compiler requires this layout
Test_Signature :: proc(^T)
@@ -136,10 +139,27 @@ 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) {
+ when TRACKING_MEMORY {
+ client_test(t)
+ ta := (^mem.Tracking_Allocator)(context.allocator.data)
+
+ 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)
+
+ clear(&ta.bad_free_array)
+ 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
diff --git a/tests/core/compress/test_core_compress.odin b/tests/core/compress/test_core_compress.odin
index 4ab63ae67..1f3481f35 100644
--- a/tests/core/compress/test_core_compress.odin
+++ b/tests/core/compress/test_core_compress.odin
@@ -87,12 +87,10 @@ shoco_test :: proc(t: ^testing.T) {
}
for v in Shoco_Tests {
- when ODIN_OS == .Windows {
- v := v
- // Compressed source files are not encoded with carriage returns but git replaces raw files lf with crlf on commit (on windows only)
- // So replace crlf with lf on windows
- v.raw, _ = bytes.replace_all(v.raw, { 0xD, 0xA }, { 0xA })
- }
+ v := v
+ // Compressed source files are not encoded with carriage returns but git replaces raw files lf with crlf on commit (on windows only)
+ // So replace crlf with lf on windows
+ v.raw, _ = bytes.replace_all(v.raw, { 0xD, 0xA }, { 0xA }, context.temp_allocator)
expected_raw := len(v.raw)
expected_compressed := len(v.compressed)
diff --git a/tests/core/mem/test_mem_dynamic_pool.odin b/tests/core/mem/test_mem_dynamic_pool.odin
index 80c973c68..d1086cfe6 100644
--- a/tests/core/mem/test_mem_dynamic_pool.odin
+++ b/tests/core/mem/test_mem_dynamic_pool.odin
@@ -5,76 +5,97 @@ import "core:mem"
expect_pool_allocation :: proc(t: ^testing.T, expected_used_bytes, num_bytes, alignment: int) {
- pool: mem.Dynamic_Pool
- mem.dynamic_pool_init(pool = &pool, alignment = alignment)
- pool_allocator := mem.dynamic_pool_allocator(&pool)
-
- element, err := mem.alloc(num_bytes, alignment, pool_allocator)
- testing.expect(t, err == .None)
- testing.expect(t, element != nil)
-
- expected_bytes_left := pool.block_size - expected_used_bytes
- testing.expectf(t, pool.bytes_left == expected_bytes_left,
- `
- Allocated data with size %v bytes, expected %v bytes left, got %v bytes left, off by %v bytes.
- Pool:
- block_size = %v
- out_band_size = %v
- alignment = %v
- unused_blocks = %v
- used_blocks = %v
- out_band_allocations = %v
- current_block = %v
- current_pos = %v
- bytes_left = %v
- `,
- num_bytes, expected_bytes_left, pool.bytes_left, expected_bytes_left - pool.bytes_left,
- pool.block_size,
- pool.out_band_size,
- pool.alignment,
- pool.unused_blocks,
- pool.used_blocks,
- pool.out_band_allocations,
- pool.current_block,
- pool.current_pos,
- pool.bytes_left,
- )
-
- mem.dynamic_pool_destroy(&pool)
- testing.expect(t, pool.used_blocks == nil)
+ pool: mem.Dynamic_Pool
+ mem.dynamic_pool_init(pool = &pool, alignment = alignment)
+ pool_allocator := mem.dynamic_pool_allocator(&pool)
+
+ element, err := mem.alloc(num_bytes, alignment, pool_allocator)
+ testing.expect(t, err == .None)
+ testing.expect(t, element != nil)
+
+ expected_bytes_left := pool.block_size - expected_used_bytes
+ testing.expectf(t, pool.bytes_left == expected_bytes_left,
+ `
+ Allocated data with size %v bytes, expected %v bytes left, got %v bytes left, off by %v bytes.
+ Pool:
+ block_size = %v
+ out_band_size = %v
+ alignment = %v
+ unused_blocks = %v
+ used_blocks = %v
+ out_band_allocations = %v
+ current_block = %v
+ current_pos = %v
+ bytes_left = %v
+ `,
+ num_bytes, expected_bytes_left, pool.bytes_left, expected_bytes_left - pool.bytes_left,
+ pool.block_size,
+ pool.out_band_size,
+ pool.alignment,
+ pool.unused_blocks,
+ pool.used_blocks,
+ pool.out_band_allocations,
+ pool.current_block,
+ pool.current_pos,
+ pool.bytes_left,
+ )
+
+ mem.dynamic_pool_destroy(&pool)
+ testing.expect(t, pool.used_blocks == nil)
}
expect_pool_allocation_out_of_band :: proc(t: ^testing.T, num_bytes, out_band_size: int) {
- testing.expect(t, num_bytes >= out_band_size, "Sanity check failed, your test call is flawed! Make sure that num_bytes >= out_band_size!")
+ testing.expect(t, num_bytes >= out_band_size, "Sanity check failed, your test call is flawed! Make sure that num_bytes >= out_band_size!")
- pool: mem.Dynamic_Pool
- mem.dynamic_pool_init(pool = &pool, out_band_size = out_band_size)
- pool_allocator := mem.dynamic_pool_allocator(&pool)
+ pool: mem.Dynamic_Pool
+ mem.dynamic_pool_init(pool = &pool, out_band_size = out_band_size)
+ pool_allocator := mem.dynamic_pool_allocator(&pool)
- element, err := mem.alloc(num_bytes, allocator = pool_allocator)
- testing.expect(t, err == .None)
- testing.expect(t, element != nil)
- testing.expectf(t, pool.out_band_allocations != nil,
- "Allocated data with size %v bytes, which is >= out_of_band_size and it should be in pool.out_band_allocations, but isn't!",
- )
+ element, err := mem.alloc(num_bytes, allocator = pool_allocator)
+ testing.expect(t, err == .None)
+ testing.expect(t, element != nil)
+ testing.expectf(t, pool.out_band_allocations != nil,
+ "Allocated data with size %v bytes, which is >= out_of_band_size and it should be in pool.out_band_allocations, but isn't!",
+ )
- mem.dynamic_pool_destroy(&pool)
- testing.expect(t, pool.out_band_allocations == nil)
+ mem.dynamic_pool_destroy(&pool)
+ testing.expect(t, pool.out_band_allocations == nil)
}
@(test)
test_dynamic_pool_alloc_aligned :: proc(t: ^testing.T) {
- expect_pool_allocation(t, expected_used_bytes = 16, num_bytes = 16, alignment=8)
+ expect_pool_allocation(t, expected_used_bytes = 16, num_bytes = 16, alignment=8)
}
@(test)
test_dynamic_pool_alloc_unaligned :: proc(t: ^testing.T) {
- expect_pool_allocation(t, expected_used_bytes = 8, num_bytes=1, alignment=8)
- expect_pool_allocation(t, expected_used_bytes = 16, num_bytes=9, alignment=8)
+ expect_pool_allocation(t, expected_used_bytes = 8, num_bytes=1, alignment=8)
+ expect_pool_allocation(t, expected_used_bytes = 16, num_bytes=9, alignment=8)
}
@(test)
test_dynamic_pool_alloc_out_of_band :: proc(t: ^testing.T) {
- expect_pool_allocation_out_of_band(t, num_bytes = 128, out_band_size = 128)
- expect_pool_allocation_out_of_band(t, num_bytes = 129, out_band_size = 128)
+ expect_pool_allocation_out_of_band(t, num_bytes = 128, out_band_size = 128)
+ expect_pool_allocation_out_of_band(t, num_bytes = 129, out_band_size = 128)
+}
+
+@(test)
+test_intentional_leaks :: proc(t: ^testing.T) {
+ testing.expect_leaks(t, intentionally_leaky_test, leak_verifier)
+}
+
+// Not tagged with @(test) because it's run through `test_intentional_leaks`
+intentionally_leaky_test :: proc(t: ^testing.T) {
+ a: [dynamic]int
+ // Intentional leak
+ append(&a, 42)
+
+ // Intentional bad free
+ b := uintptr(&a[0]) + 42
+ free(rawptr(b))
+}
+
+leak_verifier :: proc(t: ^testing.T, ta: ^mem.Tracking_Allocator) {
+ testing.expect_value(t, len(ta.allocation_map), 1)
+ testing.expect_value(t, len(ta.bad_free_array), 1)
} \ No newline at end of file