From 01247c86b12f5fad9bdb978ff4d7429b3d822223 Mon Sep 17 00:00:00 2001 From: Nana Adjei Manu Date: Wed, 20 Aug 2025 21:00:04 +0200 Subject: [PATCH 1/3] Add supprt for new c23 deallocators This commit introduces 2 new functions to add support for `free_sized` and `free_aligned_sized` deallocators introduced in c23. We ensure that `free_sized` and `free_aligned_sized` are skipped if unavailable otherwise we get a `FREE` record recorded for them. Signed-off-by: Nana Adjei Manu --- src/memray/_memray/hooks.cpp | 14 +++ src/memray/_memray/hooks.h | 8 ++ .../free_sized_extension/free_sized_test.cpp | 111 ++++++++++++++++++ .../integration/free_sized_extension/setup.py | 12 ++ tests/integration/test_extensions.py | 47 ++++++++ 5 files changed, 192 insertions(+) create mode 100644 tests/integration/free_sized_extension/free_sized_test.cpp create mode 100644 tests/integration/free_sized_extension/setup.py diff --git a/src/memray/_memray/hooks.cpp b/src/memray/_memray/hooks.cpp index 170f029ab6..5df2421612 100644 --- a/src/memray/_memray/hooks.cpp +++ b/src/memray/_memray/hooks.cpp @@ -196,6 +196,20 @@ free(void* ptr) noexcept } } +#if defined(__GLIBC__) +void +free_sized(void* ptr, size_t size) noexcept +{ + memray::intercept::free(ptr); +} + +void +free_aligned_sized(void* ptr, size_t alignment, size_t size) noexcept +{ + memray::intercept::free(ptr); +} +#endif + void* realloc(void* ptr, size_t size) noexcept { diff --git a/src/memray/_memray/hooks.h b/src/memray/_memray/hooks.h index 0b3fef3c2b..b0f95b1631 100644 --- a/src/memray/_memray/hooks.h +++ b/src/memray/_memray/hooks.h @@ -32,6 +32,8 @@ FOR_EACH_HOOKED_FUNCTION(prctl) \ FOR_EACH_HOOKED_FUNCTION(pvalloc) \ FOR_EACH_HOOKED_FUNCTION(mmap64) + FOR_EACH_HOOKED_FUNCTION(free_sized) \ + FOR_EACH_HOOKED_FUNCTION(free_aligned_sized) #else # define MEMRAY_PLATFORM_HOOKED_FUNCTIONS \ FOR_EACH_HOOKED_FUNCTION(memalign) \ @@ -200,6 +202,12 @@ mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset) noexc #if defined(__GLIBC__) void* mmap64(void* addr, size_t length, int prot, int flags, int fd, off64_t offset) noexcept; + +void +free_sized(void* ptr, size_t size) noexcept; + +void +free_aligned_sized(void* ptr, size_t alignment, size_t size) noexcept; #endif int diff --git a/tests/integration/free_sized_extension/free_sized_test.cpp b/tests/integration/free_sized_extension/free_sized_test.cpp new file mode 100644 index 0000000000..96db110d1a --- /dev/null +++ b/tests/integration/free_sized_extension/free_sized_test.cpp @@ -0,0 +1,111 @@ +#define PY_SSIZE_T_CLEAN +#include + +#include +#include + +#ifdef __linux__ +#include +#endif + +namespace { // unnamed + +extern "C" void +test_free_sized() +{ + void* ptr = malloc(1024); + assert(ptr != nullptr); + + #ifdef __GLIBC__ + extern void free_sized(void* ptr, size_t size); + free_sized(ptr, 1024); + #else + free(ptr); + #endif +} + +extern "C" void +test_free_aligned_sized() +{ + void* ptr = aligned_alloc(64, 1024); + assert(ptr != nullptr); + + #ifdef __GLIBC__ + extern void free_aligned_sized(void* ptr, size_t alignment, size_t size); + free_aligned_sized(ptr, 64, 1024); + #else + free(ptr); + #endif +} + +extern "C" void +test_both_free_functions() +{ + void* ptr1 = malloc(512); + assert(ptr1 != nullptr); + #ifdef __GLIBC__ + extern void free_sized(void* ptr, size_t size); + free_sized(ptr1, 512); + #else + free(ptr1); + #endif + + void* ptr2 = aligned_alloc(32, 256); + assert(ptr2 != nullptr); + #ifdef __GLIBC__ + extern void free_aligned_sized(void* ptr, size_t alignment, size_t size); + free_aligned_sized(ptr2, 32, 256); + #else + free(ptr2); + #endif +} + +PyObject* +run_free_sized_test(PyObject*, PyObject*) +{ + test_free_sized(); + Py_RETURN_NONE; +} + +PyObject* +run_free_aligned_sized_test(PyObject*, PyObject*) +{ + test_free_aligned_sized(); + Py_RETURN_NONE; +} + +PyObject* +run_both_tests(PyObject*, PyObject*) +{ + test_both_free_functions(); + Py_RETURN_NONE; +} + +} // unnamed namespace + +static PyMethodDef +free_sized_methods[] = { + {"run_free_sized_test", run_free_sized_test, METH_NOARGS, "Test free_sized function"}, + {"run_free_aligned_sized_test", run_free_aligned_sized_test, METH_NOARGS, "Test free_aligned_sized function"}, + {"run_both_tests", run_both_tests, METH_NOARGS, "Test both free functions"}, + {nullptr, nullptr, 0, nullptr} +}; + +static PyModuleDef +free_sized_module = { + PyModuleDef_HEAD_INIT, + "free_sized_test", + "Test module for free_sized and free_aligned_sized functions", + -1, + free_sized_methods, + nullptr, + nullptr, + nullptr, + nullptr +}; + +PyMODINIT_FUNC +PyInit_free_sized_test() +{ + return PyModule_Create(&free_sized_module); +} diff --git a/tests/integration/free_sized_extension/setup.py b/tests/integration/free_sized_extension/setup.py new file mode 100644 index 0000000000..44ec21c228 --- /dev/null +++ b/tests/integration/free_sized_extension/setup.py @@ -0,0 +1,12 @@ +from setuptools import Extension, setup + +setup( + name="free_sized_extension", + ext_modules=[ + Extension( + "free_sized_test", + sources=["free_sized_test.cpp"], + language="c++", + ) + ], +) diff --git a/tests/integration/test_extensions.py b/tests/integration/test_extensions.py index c8f6e429ed..47e274c8e5 100644 --- a/tests/integration/test_extensions.py +++ b/tests/integration/test_extensions.py @@ -15,6 +15,7 @@ TEST_MULTITHREADED_EXTENSION = HERE / "multithreaded_extension" TEST_MISBEHAVING_EXTENSION = HERE / "misbehaving_extension" TEST_RPATH_EXTENSION = HERE / "rpath_extension" +TEST_FREE_SIZED_EXTENSION = HERE / "free_sized_extension" @pytest.mark.valgrind @@ -386,3 +387,49 @@ def test_dlopen_with_rpath(tmpdir, monkeypatch): # THEN with Tracker(output): hello_world() + +@pytest.mark.skipif( + not hasattr(ctypes.CDLL(None), "free_sized"), + reason="free_sized not available on this system" +) +def test_free_sized_extension(tmpdir, monkeypatch): + """Test tracking allocations in a native extension which uses free_sized and free_aligned_sized.""" + # GIVEN + output = Path(tmpdir) / "test.bin" + extension_name = "free_sized_extension" + extension_path = tmpdir / extension_name + shutil.copytree(TEST_FREE_SIZED_EXTENSION, extension_path) + subprocess.run( + [sys.executable, str(extension_path / "setup.py"), "build_ext", "--inplace"], + check=True, + cwd=extension_path, + capture_output=True, + ) + + # WHEN + with monkeypatch.context() as ctx: + ctx.setattr(sys, "path", [*sys.path, str(extension_path)]) + from free_sized_test import run_both_tests # type: ignore + + with Tracker(output): + run_both_tests() + + # THEN + records = list(FileReader(output).get_allocation_records()) + assert records + + # Check that at least 2 allocations from malloc and aligned_alloc + mallocs = [record for record in records if record.allocator == AllocatorType.MALLOC] + assert len(mallocs) >= 2 + + # Check that corresponding FREE records - this verifies hooks are working! + mallocs_addr = {record.address for record in mallocs} + frees = [ + record + for record in records + if record.address in mallocs_addr and record.allocator == AllocatorType.FREE + ] + assert len(frees) >= 2 + + assert all(len(malloc.stack_trace()) == 0 for malloc in mallocs) + assert all(len(free.stack_trace()) == 0 for free in frees) From 84370ba3d8ffc39b2e11a3540857b7bda3ba305a Mon Sep 17 00:00:00 2001 From: Nana Adjei Manu Date: Thu, 21 Aug 2025 05:19:40 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Remove `glibc` guards for new functions to support all libc implementations. Updates tests to skip if compilation fails on a specific platform. Signed-off-by: Nana Adjei Manu --- src/memray/_memray/hooks.cpp | 2 - src/memray/_memray/hooks.h | 15 +- .../free_sized_extension/free_sized_test.c | 150 ++++++++++++++++++ .../free_sized_extension/free_sized_test.cpp | 111 ------------- .../integration/free_sized_extension/setup.py | 5 +- tests/integration/test_extensions.py | 58 +++++-- 6 files changed, 212 insertions(+), 129 deletions(-) create mode 100644 tests/integration/free_sized_extension/free_sized_test.c delete mode 100644 tests/integration/free_sized_extension/free_sized_test.cpp diff --git a/src/memray/_memray/hooks.cpp b/src/memray/_memray/hooks.cpp index 5df2421612..10115350b7 100644 --- a/src/memray/_memray/hooks.cpp +++ b/src/memray/_memray/hooks.cpp @@ -196,7 +196,6 @@ free(void* ptr) noexcept } } -#if defined(__GLIBC__) void free_sized(void* ptr, size_t size) noexcept { @@ -208,7 +207,6 @@ free_aligned_sized(void* ptr, size_t alignment, size_t size) noexcept { memray::intercept::free(ptr); } -#endif void* realloc(void* ptr, size_t size) noexcept diff --git a/src/memray/_memray/hooks.h b/src/memray/_memray/hooks.h index b0f95b1631..db2c2b468c 100644 --- a/src/memray/_memray/hooks.h +++ b/src/memray/_memray/hooks.h @@ -31,15 +31,21 @@ FOR_EACH_HOOKED_FUNCTION(memalign) \ FOR_EACH_HOOKED_FUNCTION(prctl) \ FOR_EACH_HOOKED_FUNCTION(pvalloc) \ - FOR_EACH_HOOKED_FUNCTION(mmap64) - FOR_EACH_HOOKED_FUNCTION(free_sized) \ - FOR_EACH_HOOKED_FUNCTION(free_aligned_sized) + FOR_EACH_HOOKED_FUNCTION(mmap64) \ #else # define MEMRAY_PLATFORM_HOOKED_FUNCTIONS \ FOR_EACH_HOOKED_FUNCTION(memalign) \ FOR_EACH_HOOKED_FUNCTION(prctl) #endif +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +# define MEMRAY_C23_HOOKED_FUNCTIONS \ + FOR_EACH_HOOKED_FUNCTION(free_sized) \ + FOR_EACH_HOOKED_FUNCTION(free_aligned_sized) +#else +# define MEMRAY_C23_HOOKED_FUNCTIONS +#endif + #define MEMRAY_HOOKED_FUNCTIONS \ FOR_EACH_HOOKED_FUNCTION(malloc) \ FOR_EACH_HOOKED_FUNCTION(free) \ @@ -53,6 +59,7 @@ FOR_EACH_HOOKED_FUNCTION(dlopen) \ FOR_EACH_HOOKED_FUNCTION(dlclose) \ FOR_EACH_HOOKED_FUNCTION(PyGILState_Ensure) \ + MEMRAY_C23_HOOKED_FUNCTIONS \ MEMRAY_PLATFORM_HOOKED_FUNCTIONS namespace memray::hooks { @@ -202,13 +209,13 @@ mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset) noexc #if defined(__GLIBC__) void* mmap64(void* addr, size_t length, int prot, int flags, int fd, off64_t offset) noexcept; +#endif void free_sized(void* ptr, size_t size) noexcept; void free_aligned_sized(void* ptr, size_t alignment, size_t size) noexcept; -#endif int munmap(void* addr, size_t length) noexcept; diff --git a/tests/integration/free_sized_extension/free_sized_test.c b/tests/integration/free_sized_extension/free_sized_test.c new file mode 100644 index 0000000000..5090bf1f3e --- /dev/null +++ b/tests/integration/free_sized_extension/free_sized_test.c @@ -0,0 +1,150 @@ +#define PY_SSIZE_T_CLEAN +#include + +#include +#include +#include + +__attribute__((weak)) void free_sized(void* ptr, size_t size); +__attribute__((weak)) void free_aligned_sized(void* ptr, size_t alignment, size_t size); + +// Check if C23 functions are available +static int functions_available = -1; + +static void check_functions_available(void) { + if (functions_available == -1) { + functions_available = (free_sized != NULL && free_aligned_sized != NULL); + } +} + +// Structure to hold allocation info +void* +test_free_sized(void) +{ + void* address; + + check_functions_available(); + if (!functions_available) { + return address; + } + + void* ptr = malloc(1024); + assert(ptr != NULL); + + address = ptr; + + free_sized(ptr, 1024); + return address; +} + +void* +test_free_aligned_sized(void) +{ + void* address; + + check_functions_available(); + if (!functions_available) { + return address; + } + + void* ptr = aligned_alloc(64, 1024); + assert(ptr != NULL); + + address = ptr; + + free_aligned_sized(ptr, 64, 1024); + return address; +} + +void* +test_both_free_functions(void) +{ + void* address; + + check_functions_available(); + if (!functions_available) { + return NULL; + } + + void* ptr1 = malloc(512); + assert(ptr1 != NULL); + free_sized(ptr1, 512); + + void* ptr2 = aligned_alloc(32, 256); + assert(ptr2 != NULL); + free_aligned_sized(ptr2, 32, 256); + + address = ptr2; + + return address; +} + +PyObject* +run_free_sized_test(PyObject* self, PyObject* args) +{ + check_functions_available(); + if (!functions_available) { + Py_RETURN_NONE; + } + + void* address = test_free_sized(); + + // Return address for verification + PyObject* result = Py_BuildValue("(KII)", address); + return result; +} + +PyObject* +run_free_aligned_sized_test(PyObject* self, PyObject* args) +{ + check_functions_available(); + if (!functions_available) { + Py_RETURN_NONE; + } + + void* address = test_free_aligned_sized(); + + PyObject* result = Py_BuildValue("(KII)", address); + return result; +} + +PyObject* +run_both_tests(PyObject* self, PyObject* args) +{ + check_functions_available(); + if (!functions_available) { + Py_RETURN_NONE; // Skip test if functions not available + } + + void* address = test_both_free_functions(); + + PyObject* result = Py_BuildValue("(KII)", address); + return result; +} + +static PyMethodDef +free_sized_methods[] = { + {"run_free_sized_test", run_free_sized_test, METH_NOARGS, "Test free_sized function"}, + {"run_free_aligned_sized_test", run_free_aligned_sized_test, METH_NOARGS, "Test free_aligned_sized function"}, + {"run_both_tests", run_both_tests, METH_NOARGS, "Test both free functions"}, + {NULL, NULL, 0, NULL} +}; + +static PyModuleDef +free_sized_module = { + PyModuleDef_HEAD_INIT, + "free_sized_test", + "Test module for free_sized and free_aligned_sized functions", + -1, + free_sized_methods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit_free_sized_test(void) +{ + return PyModule_Create(&free_sized_module); +} diff --git a/tests/integration/free_sized_extension/free_sized_test.cpp b/tests/integration/free_sized_extension/free_sized_test.cpp deleted file mode 100644 index 96db110d1a..0000000000 --- a/tests/integration/free_sized_extension/free_sized_test.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#include - -#include -#include - -#ifdef __linux__ -#include -#endif - -namespace { // unnamed - -extern "C" void -test_free_sized() -{ - void* ptr = malloc(1024); - assert(ptr != nullptr); - - #ifdef __GLIBC__ - extern void free_sized(void* ptr, size_t size); - free_sized(ptr, 1024); - #else - free(ptr); - #endif -} - -extern "C" void -test_free_aligned_sized() -{ - void* ptr = aligned_alloc(64, 1024); - assert(ptr != nullptr); - - #ifdef __GLIBC__ - extern void free_aligned_sized(void* ptr, size_t alignment, size_t size); - free_aligned_sized(ptr, 64, 1024); - #else - free(ptr); - #endif -} - -extern "C" void -test_both_free_functions() -{ - void* ptr1 = malloc(512); - assert(ptr1 != nullptr); - #ifdef __GLIBC__ - extern void free_sized(void* ptr, size_t size); - free_sized(ptr1, 512); - #else - free(ptr1); - #endif - - void* ptr2 = aligned_alloc(32, 256); - assert(ptr2 != nullptr); - #ifdef __GLIBC__ - extern void free_aligned_sized(void* ptr, size_t alignment, size_t size); - free_aligned_sized(ptr2, 32, 256); - #else - free(ptr2); - #endif -} - -PyObject* -run_free_sized_test(PyObject*, PyObject*) -{ - test_free_sized(); - Py_RETURN_NONE; -} - -PyObject* -run_free_aligned_sized_test(PyObject*, PyObject*) -{ - test_free_aligned_sized(); - Py_RETURN_NONE; -} - -PyObject* -run_both_tests(PyObject*, PyObject*) -{ - test_both_free_functions(); - Py_RETURN_NONE; -} - -} // unnamed namespace - -static PyMethodDef -free_sized_methods[] = { - {"run_free_sized_test", run_free_sized_test, METH_NOARGS, "Test free_sized function"}, - {"run_free_aligned_sized_test", run_free_aligned_sized_test, METH_NOARGS, "Test free_aligned_sized function"}, - {"run_both_tests", run_both_tests, METH_NOARGS, "Test both free functions"}, - {nullptr, nullptr, 0, nullptr} -}; - -static PyModuleDef -free_sized_module = { - PyModuleDef_HEAD_INIT, - "free_sized_test", - "Test module for free_sized and free_aligned_sized functions", - -1, - free_sized_methods, - nullptr, - nullptr, - nullptr, - nullptr -}; - -PyMODINIT_FUNC -PyInit_free_sized_test() -{ - return PyModule_Create(&free_sized_module); -} diff --git a/tests/integration/free_sized_extension/setup.py b/tests/integration/free_sized_extension/setup.py index 44ec21c228..87dfe0c107 100644 --- a/tests/integration/free_sized_extension/setup.py +++ b/tests/integration/free_sized_extension/setup.py @@ -5,8 +5,9 @@ ext_modules=[ Extension( "free_sized_test", - sources=["free_sized_test.cpp"], - language="c++", + sources=["free_sized_test.c"], + language="c", + extra_compile_args=["-std=c23"] ) ], ) diff --git a/tests/integration/test_extensions.py b/tests/integration/test_extensions.py index 47e274c8e5..c0d540ea47 100644 --- a/tests/integration/test_extensions.py +++ b/tests/integration/test_extensions.py @@ -1,3 +1,4 @@ +import ctypes import shutil import subprocess import sys @@ -399,27 +400,43 @@ def test_free_sized_extension(tmpdir, monkeypatch): extension_name = "free_sized_extension" extension_path = tmpdir / extension_name shutil.copytree(TEST_FREE_SIZED_EXTENSION, extension_path) - subprocess.run( - [sys.executable, str(extension_path / "setup.py"), "build_ext", "--inplace"], - check=True, - cwd=extension_path, - capture_output=True, - ) + + # Try to build the extension, skip if compilation fails + try: + subprocess.run( + [sys.executable, str(extension_path / "setup.py"), "build_ext", "--inplace"], + check=True, + cwd=extension_path, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + pytest.skip(f"Failed to compile C23 extension: {e}") + except Exception as e: + pytest.skip(f"Unexpected error building extension: {e}") # WHEN with monkeypatch.context() as ctx: ctx.setattr(sys, "path", [*sys.path, str(extension_path)]) - from free_sized_test import run_both_tests # type: ignore + + try: + from free_sized_test import run_both_tests # type: ignore + except ImportError as e: + pytest.skip(f"Failed to import compiled extension: {e}") with Tracker(output): - run_both_tests() + # Get allocation info from the extension + result = run_both_tests() + + # Skip test if functions not available (e.g., on macOS) + if result is None: + pytest.skip("C23 functions not available on this system") # THEN records = list(FileReader(output).get_allocation_records()) assert records # Check that at least 2 allocations from malloc and aligned_alloc - mallocs = [record for record in records if record.allocator == AllocatorType.MALLOC] + mallocs = [record for record in records if record.allocator == AllocatorType.ALIGNED_ALLOC] assert len(mallocs) >= 2 # Check that corresponding FREE records - this verifies hooks are working! @@ -429,7 +446,28 @@ def test_free_sized_extension(tmpdir, monkeypatch): for record in records if record.address in mallocs_addr and record.allocator == AllocatorType.FREE ] - assert len(frees) >= 2 + assert len(frees) == len(mallocs) assert all(len(malloc.stack_trace()) == 0 for malloc in mallocs) assert all(len(free.stack_trace()) == 0 for free in frees) + + # Verify that the specific addresses returned by the extension were tracked + if result is not None: + # result should be a tuple of (address, size, alignment) + expected_address = result[0] + expected_size = result[1] + expected_alignment = result[2] + + # Find the allocation record for this address + matching_allocs = [ + record for record in records + if record.address == expected_address and record.allocator == AllocatorType.ALIGNED_ALLOC + ] + assert len(matching_allocs) >= 1, f"Expected allocation at address {expected_address} not found" + + # Find the corresponding free record + matching_frees = [ + record for record in records + if record.address == expected_address and record.allocator == AllocatorType.FREE + ] + assert len(matching_frees) >= 1, f"Expected free at address {expected_address} not found" From 01561079c7c55de8bc1b5c1d555effc13697d730 Mon Sep 17 00:00:00 2001 From: Nana Adjei Manu Date: Fri, 29 Aug 2025 20:04:46 +0200 Subject: [PATCH 3/3] Fix test line number assertions in test_extensions.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update line number assertions in three failing integration tests to match current file structure after recent code additions. The tests were failing due to line number shifts caused by: - Addition of C23 deallocator support tests - Code review improvements Tests affected: - test_misbehaving_extension: 83 → 85 - test_extension_that_uses_pygilstate_ensure: 154 → 156, 155 → 157 - test_native_dlopen: 226 → 228 Signed-off-by: Nana Adjei Manu --- src/memray/_memray/hooks.h | 2 +- .../free_sized_extension/free_sized_test.c | 40 ++++++------ .../integration/free_sized_extension/setup.py | 5 +- tests/integration/test_extensions.py | 61 +++++++++++-------- 4 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/memray/_memray/hooks.h b/src/memray/_memray/hooks.h index db2c2b468c..4a4d5e9418 100644 --- a/src/memray/_memray/hooks.h +++ b/src/memray/_memray/hooks.h @@ -31,7 +31,7 @@ FOR_EACH_HOOKED_FUNCTION(memalign) \ FOR_EACH_HOOKED_FUNCTION(prctl) \ FOR_EACH_HOOKED_FUNCTION(pvalloc) \ - FOR_EACH_HOOKED_FUNCTION(mmap64) \ + FOR_EACH_HOOKED_FUNCTION(mmap64) #else # define MEMRAY_PLATFORM_HOOKED_FUNCTIONS \ FOR_EACH_HOOKED_FUNCTION(memalign) \ diff --git a/tests/integration/free_sized_extension/free_sized_test.c b/tests/integration/free_sized_extension/free_sized_test.c index 5090bf1f3e..447320fba1 100644 --- a/tests/integration/free_sized_extension/free_sized_test.c +++ b/tests/integration/free_sized_extension/free_sized_test.c @@ -22,15 +22,15 @@ void* test_free_sized(void) { void* address; - + check_functions_available(); if (!functions_available) { return address; } - + void* ptr = malloc(1024); assert(ptr != NULL); - + address = ptr; free_sized(ptr, 1024); @@ -41,17 +41,17 @@ void* test_free_aligned_sized(void) { void* address; - + check_functions_available(); if (!functions_available) { return address; } - + void* ptr = aligned_alloc(64, 1024); assert(ptr != NULL); - + address = ptr; - + free_aligned_sized(ptr, 64, 1024); return address; } @@ -60,22 +60,22 @@ void* test_both_free_functions(void) { void* address; - + check_functions_available(); if (!functions_available) { return NULL; } - + void* ptr1 = malloc(512); assert(ptr1 != NULL); free_sized(ptr1, 512); - + void* ptr2 = aligned_alloc(32, 256); assert(ptr2 != NULL); free_aligned_sized(ptr2, 32, 256); - + address = ptr2; - + return address; } @@ -84,11 +84,11 @@ run_free_sized_test(PyObject* self, PyObject* args) { check_functions_available(); if (!functions_available) { - Py_RETURN_NONE; + Py_RETURN_NONE; } - + void* address = test_free_sized(); - + // Return address for verification PyObject* result = Py_BuildValue("(KII)", address); return result; @@ -99,11 +99,11 @@ run_free_aligned_sized_test(PyObject* self, PyObject* args) { check_functions_available(); if (!functions_available) { - Py_RETURN_NONE; + Py_RETURN_NONE; } - + void* address = test_free_aligned_sized(); - + PyObject* result = Py_BuildValue("(KII)", address); return result; } @@ -115,9 +115,9 @@ run_both_tests(PyObject* self, PyObject* args) if (!functions_available) { Py_RETURN_NONE; // Skip test if functions not available } - + void* address = test_both_free_functions(); - + PyObject* result = Py_BuildValue("(KII)", address); return result; } diff --git a/tests/integration/free_sized_extension/setup.py b/tests/integration/free_sized_extension/setup.py index 87dfe0c107..597eb7ee62 100644 --- a/tests/integration/free_sized_extension/setup.py +++ b/tests/integration/free_sized_extension/setup.py @@ -1,4 +1,5 @@ -from setuptools import Extension, setup +from setuptools import Extension +from setuptools import setup setup( name="free_sized_extension", @@ -7,7 +8,7 @@ "free_sized_test", sources=["free_sized_test.c"], language="c", - extra_compile_args=["-std=c23"] + extra_compile_args=["-std=c23"], ) ], ) diff --git a/tests/integration/test_extensions.py b/tests/integration/test_extensions.py index c0d540ea47..27189d965b 100644 --- a/tests/integration/test_extensions.py +++ b/tests/integration/test_extensions.py @@ -110,7 +110,7 @@ def allocating_function(): # pragma: no cover func, filename, line = bottom_frame assert func == "allocating_function" assert filename.endswith(__file__) - assert line == 83 + assert line == 85 frees = [ event @@ -173,7 +173,7 @@ def foo2(): func, filename, line = bottom_frame assert func == "test_extension_that_uses_pygilstate_ensure" assert filename.endswith(__file__) - assert line == 154 + assert line == 156 # We should have 2 frames here: this function calling `allocator.valloc`, # and `allocator.valloc` calling the C `valloc`. @@ -188,7 +188,7 @@ def foo2(): func, filename, line = caller assert func == "test_extension_that_uses_pygilstate_ensure" assert filename.endswith(__file__) - assert line == 155 + assert line == 157 frees = [ event @@ -244,7 +244,7 @@ def allocating_function(): func, filename, line = bottom_frame assert func == "test_native_dlopen" assert filename.endswith(__file__) - assert line == 226 + assert line == 228 frees = [ event @@ -389,22 +389,28 @@ def test_dlopen_with_rpath(tmpdir, monkeypatch): with Tracker(output): hello_world() + @pytest.mark.skipif( not hasattr(ctypes.CDLL(None), "free_sized"), - reason="free_sized not available on this system" + reason="free_sized not available on this system", ) def test_free_sized_extension(tmpdir, monkeypatch): - """Test tracking allocations in a native extension which uses free_sized and free_aligned_sized.""" + """Test allocations in a native extension using free_sized and free_aligned_sized.""" # GIVEN output = Path(tmpdir) / "test.bin" extension_name = "free_sized_extension" extension_path = tmpdir / extension_name shutil.copytree(TEST_FREE_SIZED_EXTENSION, extension_path) - + # Try to build the extension, skip if compilation fails try: subprocess.run( - [sys.executable, str(extension_path / "setup.py"), "build_ext", "--inplace"], + [ + sys.executable, + str(extension_path / "setup.py"), + "build_ext", + "--inplace", + ], check=True, cwd=extension_path, capture_output=True, @@ -417,7 +423,7 @@ def test_free_sized_extension(tmpdir, monkeypatch): # WHEN with monkeypatch.context() as ctx: ctx.setattr(sys, "path", [*sys.path, str(extension_path)]) - + try: from free_sized_test import run_both_tests # type: ignore except ImportError as e: @@ -426,7 +432,7 @@ def test_free_sized_extension(tmpdir, monkeypatch): with Tracker(output): # Get allocation info from the extension result = run_both_tests() - + # Skip test if functions not available (e.g., on macOS) if result is None: pytest.skip("C23 functions not available on this system") @@ -436,7 +442,9 @@ def test_free_sized_extension(tmpdir, monkeypatch): assert records # Check that at least 2 allocations from malloc and aligned_alloc - mallocs = [record for record in records if record.allocator == AllocatorType.ALIGNED_ALLOC] + mallocs = [ + record for record in records if record.allocator == AllocatorType.ALIGNED_ALLOC + ] assert len(mallocs) >= 2 # Check that corresponding FREE records - this verifies hooks are working! @@ -447,27 +455,32 @@ def test_free_sized_extension(tmpdir, monkeypatch): if record.address in mallocs_addr and record.allocator == AllocatorType.FREE ] assert len(frees) == len(mallocs) - + assert all(len(malloc.stack_trace()) == 0 for malloc in mallocs) assert all(len(free.stack_trace()) == 0 for free in frees) - + # Verify that the specific addresses returned by the extension were tracked if result is not None: - # result should be a tuple of (address, size, alignment) expected_address = result[0] - expected_size = result[1] - expected_alignment = result[2] - + # Find the allocation record for this address matching_allocs = [ - record for record in records - if record.address == expected_address and record.allocator == AllocatorType.ALIGNED_ALLOC + record + for record in records + if record.address == expected_address + and record.allocator == AllocatorType.ALIGNED_ALLOC ] - assert len(matching_allocs) >= 1, f"Expected allocation at address {expected_address} not found" - + assert ( + len(matching_allocs) >= 1 + ), f"Expected allocation at address {expected_address} not found" + # Find the corresponding free record matching_frees = [ - record for record in records - if record.address == expected_address and record.allocator == AllocatorType.FREE + record + for record in records + if record.address == expected_address + and record.allocator == AllocatorType.FREE ] - assert len(matching_frees) >= 1, f"Expected free at address {expected_address} not found" + assert ( + len(matching_frees) >= 1 + ), f"Expected free at address {expected_address} not found"