From 80d1e1ba82cb21edb0a5263ea616d7105038bc0e Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Thu, 8 Aug 2024 20:15:08 +0200 Subject: [PATCH 1/5] 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. --- core/testing/runner.odin | 38 ++++++- core/testing/testing.odin | 22 +++- tests/core/mem/test_mem_dynamic_pool.odin | 117 ++++++++++++---------- 3 files changed, 122 insertions(+), 55 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 diff --git a/tests/core/mem/test_mem_dynamic_pool.odin b/tests/core/mem/test_mem_dynamic_pool.odin index 80c973c68..b6a54f09a 100644 --- a/tests/core/mem/test_mem_dynamic_pool.odin +++ b/tests/core/mem/test_mem_dynamic_pool.odin @@ -5,76 +5,91 @@ 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) + 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) + 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, - ) + 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) + 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 + append(&a, 42) +} + +leak_verifier :: proc(t: ^testing.T, ta: ^mem.Tracking_Allocator) { + testing.expect_value(t, len(ta.allocation_map), 1) } \ No newline at end of file From 4d27898418e3bdf8d6de5baccc135e2146085349 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Thu, 8 Aug 2024 20:58:25 +0200 Subject: [PATCH 2/5] Use test runner's own tracking allocator. --- core/testing/testing.odin | 15 +++++++-------- tests/core/mem/test_mem_dynamic_pool.odin | 6 ++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/testing/testing.odin b/core/testing/testing.odin index 5282f5a20..b7cd23adf 100644 --- a/core/testing/testing.odin +++ b/core/testing/testing.odin @@ -141,20 +141,19 @@ expect_value :: proc(t: ^T, value, expected: $T, loc := #caller_location) -> boo 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) - + 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) + verifier(t, ta) sync.mutex_unlock(&ta.mutex) + + clear(&ta.bad_free_array) + free_all(context.allocator) } - free_all(context.allocator) } set_fail_timeout :: proc(t: ^T, duration: time.Duration, loc := #caller_location) { diff --git a/tests/core/mem/test_mem_dynamic_pool.odin b/tests/core/mem/test_mem_dynamic_pool.odin index b6a54f09a..d1086cfe6 100644 --- a/tests/core/mem/test_mem_dynamic_pool.odin +++ b/tests/core/mem/test_mem_dynamic_pool.odin @@ -87,9 +87,15 @@ test_intentional_leaks :: proc(t: ^testing.T) { // 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 From a05b73c632b271a8137ed742e7eb9e58c91bee74 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Thu, 8 Aug 2024 21:02:35 +0200 Subject: [PATCH 3/5] Keep -vet happy when mem tracking is disabled. --- core/testing/testing.odin | 1 + 1 file changed, 1 insertion(+) diff --git a/core/testing/testing.odin b/core/testing/testing.odin index b7cd23adf..ea779b8f3 100644 --- a/core/testing/testing.odin +++ b/core/testing/testing.odin @@ -10,6 +10,7 @@ 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) From b82cfc5f15366da531d7ec9548a9892571e7aff9 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Thu, 8 Aug 2024 21:15:59 +0200 Subject: [PATCH 4/5] Fix shoco heisenleak --- tests/core/compress/test_core_compress.odin | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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) From 933f9f9bd17a14c587877e2cea5b2ced5869db5a Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Thu, 8 Aug 2024 21:31:30 +0200 Subject: [PATCH 5/5] Enable test leak = fatal on CI. --- .github/workflows/ci.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 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: |