diff --git a/Makefile b/Makefile index 66f151c9c..f4af84e7e 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ dist-info: python3 setup.py dist_info debug: - HPY_DEBUG=1 make all + HPY_DEBUG_BUILD=1 make all autogen: python3 -m hpy.tools.autogen . @@ -53,7 +53,7 @@ valgrind: PYTHONMALLOC=malloc valgrind --suppressions=hpy/tools/valgrind/python.supp --suppressions=hpy/tools/valgrind/hpy.supp --leak-check=full --show-leak-kinds=definite,indirect --log-file=/tmp/valgrind-output python -m pytest --valgrind --valgrind-log=/tmp/valgrind-output test/ docs-examples-tests: - python docs/examples/simple-example/setup.py install + python docs/examples/simple-example/setup.py --hpy-abi=universal install python docs/examples/mixed-example/setup.py install - python docs/examples/snippets/setup.py install + python docs/examples/snippets/setup.py --hpy-abi=universal install pytest docs/examples/tests.py diff --git a/c_test/Makefile b/c_test/Makefile index e49a5cbf1..8af6908e5 100644 --- a/c_test/Makefile +++ b/c_test/Makefile @@ -2,12 +2,16 @@ CC ?= gcc INCLUDE=-I.. -I../hpy/devel/include -I../hpy/debug/src/include CFLAGS = -O0 -UNDEBUG -g -Wall -Werror -Wfatal-errors $(INCLUDE) -DHPY_UNIVERSAL_ABI -test: test_debug_handles +test: test_debug_handles test_stacktrace ./test_debug_handles + ./test_stacktrace test_debug_handles: test_debug_handles.o ../hpy/debug/src/dhqueue.o $(CC) -o $@ $^ +test_stacktrace: test_stacktrace.o ../hpy/debug/src/stacktrace.o + $(CC) -o $@ $^ + %.o : %.c $(CC) -c $(CFLAGS) $< -o $@ diff --git a/c_test/test_stacktrace.c b/c_test/test_stacktrace.c new file mode 100644 index 000000000..5d02eced8 --- /dev/null +++ b/c_test/test_stacktrace.c @@ -0,0 +1,22 @@ +// Smoke test for the create_stacktrace function + +#include +#include "acutest.h" // https://github.com/mity/acutest +#include "hpy/debug/src/debug_internal.h" + +void test_create_stacktrace(void) +{ + char *trace; + create_stacktrace(&trace, 16); + if (trace != NULL) { + printf("\n\nTRACE:\n%s\n\n", trace); + free(trace); + } +} + +#define MYTEST(X) { #X, X } + +TEST_LIST = { + MYTEST(test_create_stacktrace), + { NULL, NULL } +}; \ No newline at end of file diff --git a/docs/debug-mode.rst b/docs/debug-mode.rst index cc41b8cc0..fc918b990 100644 --- a/docs/debug-mode.rst +++ b/docs/debug-mode.rst @@ -28,6 +28,129 @@ The debugging context can already check for: * Writing to a memory which should be read-only, for example, the buffer returned by ``HPyUnicode_AsUTF8AndSize``. -An HPy module may be explicitly loaded in debug mode using:: + +Activating Debug Mode +--------------------- + +The debug mode works only for extensions built with HPy universal ABI. + +To enable the debug mode, use environment variable ``HPY_DEBUG``. +If ``HPY_DEBUG=1``, then all HPy modules are loaded with debug context. +Alternatively ``HPY_DEBUG`` can be set to a comma separated list of names +of the modules that should be loaded in the debug mode. + +In order to verify that your extension is being loaded in the HPy debug mode, +use environment variable ``HPY_LOG``. If this variable is set, then all HPy +extensions built in universal ABI mode print a message, such as: + +.. code-block:: console + + > import snippets + Loading 'snippets' in HPy universal mode with a debug context + +.. Note: the output above is tested in test_leak_detector_with_traces_output + +If the extension is built in CPython ABI mode, then the ``HPY_LOG`` +environment variable has no effect. + +An HPy extension module may be also explicitly loaded in debug mode using:: mod = hpy.universal.load(module_name, so_filename, debug=True) + +When loading HPy extensions explicitly, environment variables ``HPY_LOG`` +and ``HPY_DEBUG`` have no effect for that extension. + + +Using Debug Mode +---------------- + +The HPy debug module exposes ``LeakDetector`` class for detection of +leaked handles. ``LeakDetector`` can be used to check that some code does +not leave behind unclosed ``HPy`` handles. For example: + +.. literalinclude:: examples/tests.py + :language: python + :start-at: def test_leak_detector + :end-before: # END: test_leak_detector + +Additionally, the debug module also exposes pytest fixture ``hpy_debug`` that +, for the time being, enables the ``LeakDetector``, but may also enable other +useful debugging facilities. + +.. literalinclude:: examples/tests.py + :language: python + :start-at: from hpy.debug.pytest import hpy_debug + :end-at: # Run some HPy extension code + +**ATTENTION**: the usage of ``LeakDetector`` or ``hpy_debug`` by itself does not +enable the HPy debug mode! If the debug mode is not enabled for any extension, +then those features do nothing useful (but also nothing harmful). + +When dealing with handle leaks, it is useful to get a stack trace of the +allocation of the leaked handle. This feature has large memory requirements +and is therefore opt-in. It can be activated by: + +.. literalinclude:: examples/tests.py + :language: python + :start-at: hpy.debug.set_handle_stack_trace_limit + :end-at: hpy.debug.set_handle_stack_trace_limit + +and disabled by: + +.. literalinclude:: examples/tests.py + :language: python + :start-at: hpy.debug.disable_handle_stack_traces + :end-at: hpy.debug.disable_handle_stack_traces + +.. Note: the following output is tested in test_leak_detector_with_traces_output + +Example +------- + +Following HPy function leaks a handle: + +.. literalinclude:: examples/snippets/snippets.c + :start-after: // BEGIN: test_leak_stacktrace + :end-before: // END: test_leak_stacktrace + +When this script is executed in debug mode: + +.. literalinclude:: examples/debug-example.py + :language: python + :end-before: print("SUCCESS") + +The output is:: + + Traceback (most recent call last): + File "/path/to/hpy/docs/examples/debug-example.py", line 7, in + snippets.test_leak_stacktrace() + File "/path/to/hpy/debug/leakdetector.py", line 43, in __exit__ + self.stop() + File "/path/to/hpy/debug/leakdetector.py", line 36, in stop + raise HPyLeakError(leaks) + hpy.debug.leakdetector.HPyLeakError: 1 unclosed handle: + + Allocation stacktrace: + /path/to/site-packages/hpy-0.0.4.dev227+gd7eeec6.d20220510-py3.8-linux-x86_64.egg/hpy/universal.cpython-38d-x86_64-linux-gnu.so(debug_ctx_Long_FromLong+0x45) [0x7f1d928c48c4] + /path/to/site-packages/hpy_snippets-0.0.0-py3.8-linux-x86_64.egg/snippets.hpy.so(+0x122c) [0x7f1d921a622c] + /path/to/site-packages/hpy_snippets-0.0.0-py3.8-linux-x86_64.egg/snippets.hpy.so(+0x14b1) [0x7f1d921a64b1] + /path/to/site-packages/hpy-0.0.4.dev227+gd7eeec6.d20220510-py3.8-linux-x86_64.egg/hpy/universal.cpython-38d-x86_64-linux-gnu.so(debug_ctx_CallRealFunctionFromTrampoline+0xca) [0x7f1d928bde1e] + /path/to/site-packages/hpy_snippets-0.0.0-py3.8-linux-x86_64.egg/snippets.hpy.so(+0x129b) [0x7f1d921a629b] + /path/to/site-packages/hpy_snippets-0.0.0-py3.8-linux-x86_64.egg/snippets.hpy.so(+0x1472) [0x7f1d921a6472] + /path/to/libpython3.8d.so.1.0(+0x10a022) [0x7f1d93807022] + /path/to/libpython3.8d.so.1.0(+0x1e986b) [0x7f1d938e686b] + /path/to/libpython3.8d.so.1.0(+0x2015e9) [0x7f1d938fe5e9] + /path/to/libpython3.8d.so.1.0(_PyEval_EvalFrameDefault+0x1008c) [0x7f1d938f875a] + /path/to/libpython3.8d.so.1.0(PyEval_EvalFrameEx+0x64) [0x7f1d938e86b8] + /path/to/libpython3.8d.so.1.0(_PyEval_EvalCodeWithName+0xfaa) [0x7f1d938fc8af] + /path/to/libpython3.8d.so.1.0(PyEval_EvalCodeEx+0x86) [0x7f1d938fca25] + /path/to/libpython3.8d.so.1.0(PyEval_EvalCode+0x4b) [0x7f1d938e862b] + +For the time being, HPy uses the glibc ``backtrace`` and ``backtrace_symbols`` +`functions `_. +Therefore all their caveats and limitations apply. Usual recommendations to get +more symbols in the traces and not only addresses, such as ``snippets.hpy.so(+0x122c)``, are: + +* link your native code with ``-rdynamic`` flag (``LDFLAGS="-rdynamic"``) +* build your code without optimizations and with debug symbols (``CFLAGS="-O0 -g"``) +* use ``addr2line`` command line utility, e.g.: ``addr2line -e /path/to/snippets.hpy.so -C -f +0x122c`` diff --git a/docs/examples/debug-example.py b/docs/examples/debug-example.py new file mode 100644 index 000000000..b61ab34cb --- /dev/null +++ b/docs/examples/debug-example.py @@ -0,0 +1,10 @@ +# Run with HPY_DEBUG=1 +import hpy.debug +import snippets + +hpy.debug.set_handle_stack_trace_limit(16) +from hpy.debug.pytest import LeakDetector +with LeakDetector() as ld: + snippets.test_leak_stacktrace() + +print("SUCCESS") # Should not be actually printed diff --git a/docs/examples/snippets/snippets.c b/docs/examples/snippets/snippets.c index b22b78217..e8d84e09a 100644 --- a/docs/examples/snippets/snippets.c +++ b/docs/examples/snippets/snippets.c @@ -38,11 +38,26 @@ static HPy test_foo_and_is_same_object_impl(HPyContext *ctx, HPy self, return HPyLong_FromLong(ctx, is_same_object(ctx, args[0], args[1])); } +// BEGIN: test_leak_stacktrace +HPyDef_METH(test_leak_stacktrace, "test_leak_stacktrace", + test_leak_stacktrace_impl, HPyFunc_NOARGS) +static HPy test_leak_stacktrace_impl(HPyContext *ctx, HPy self) +{ + HPy num = HPyLong_FromLong(ctx, 42); + if (HPy_IsNull(num)) { + return HPy_NULL; + } + // No HPy_Close(ctx, num); + return HPy_Dup(ctx, ctx->h_None); +} +// END: test_leak_stacktrace + // ------------------------------------ // Dummy module definition, so that we can test the snippets static HPyDef *Methods[] = { &test_foo_and_is_same_object, + &test_leak_stacktrace, NULL, }; diff --git a/docs/examples/tests.py b/docs/examples/tests.py index 04baa2718..15979f6b3 100644 --- a/docs/examples/tests.py +++ b/docs/examples/tests.py @@ -1,3 +1,9 @@ +import os +import os.path +import re +import subprocess +import sys + import simple import mixed import hpyvarargs @@ -22,3 +28,45 @@ def test_snippets(): x = 2 assert snippets.test_foo_and_is_same_object(x, x) == 1 assert snippets.test_foo_and_is_same_object(x, 42) == 0 + + +def test_leak_detector(): + from hpy.debug.pytest import LeakDetector + with LeakDetector() as ld: + # add_ints is an HPy C function. If it forgets to close a handle, + # LeakDetector will complain + assert mixed.add_ints(40, 2) == 42 +# END: test_leak_detector + +from hpy.debug.pytest import hpy_debug +def test_that_uses_leak_detector_fixture(hpy_debug): + # Run some HPy extension code + assert mixed.add_ints(40, 2) == 42 + + +def test_leak_detector_with_traces(): + import hpy.debug + hpy.debug.set_handle_stack_trace_limit(16) + assert mixed.add_ints(40, 2) == 42 + hpy.debug.disable_handle_stack_traces() + + +def test_leak_detector_with_traces_output(): + # Update the debug documentation if anything here changes! + env = os.environ.copy() + env['HPY_DEBUG'] = '1' + env['HPY_LOG'] = '1' + script = os.path.join(os.path.dirname(__file__), 'debug-example.py') + result = subprocess.run([sys.executable, script], env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Rudimentary check that the output contains what we have in the documentation + out = result.stdout.decode('latin-1') + assert out == "Loading 'snippets' in HPy universal mode with a debug context" + os.linesep + err = result.stderr.decode('latin-1') + assert 'hpy.debug.leakdetector.HPyLeakError: 1 unclosed handle:' in err + assert re.search('', err) + assert 'Allocation stacktrace:' in err + if sys.platform.startswith("linux"): + assert 'snippets.hpy.so' in err # Should be somewhere in the stack trace + else: + assert 'At the moment this is only supported on Linux with glibc' in err diff --git a/hpy/debug/__init__.py b/hpy/debug/__init__.py index ad73250f2..71ece5392 100644 --- a/hpy/debug/__init__.py +++ b/hpy/debug/__init__.py @@ -1 +1,11 @@ from .leakdetector import HPyDebugError, HPyLeakError, LeakDetector + + +def set_handle_stack_trace_limit(limit): + from hpy.universal import _debug + _debug.set_handle_stack_trace_limit(limit) + + +def disable_handle_stack_traces(): + from hpy.universal import _debug + _debug.set_handle_stack_trace_limit(None) diff --git a/hpy/debug/src/_debugmod.c b/hpy/debug/src/_debugmod.c index 0b9b0a4db..9d0573f36 100644 --- a/hpy/debug/src/_debugmod.c +++ b/hpy/debug/src/_debugmod.c @@ -161,6 +161,27 @@ static UHPy set_on_invalid_handle_impl(HPyContext *uctx, UHPy u_self, UHPy u_arg return HPy_Dup(uctx, uctx->h_None); } +HPyDef_METH(set_handle_stack_trace_limit, "set_handle_stack_trace_limit", + set_handle_stack_trace_limit_impl, HPyFunc_O, .doc= + "Set the limit to captured HPy handles allocations stack traces. " + "None means do not capture the stack traces.") +static UHPy set_handle_stack_trace_limit_impl(HPyContext *uctx, UHPy u_self, UHPy u_arg) +{ + HPyContext *dctx = hpy_debug_get_ctx(uctx); + HPyDebugInfo *info = get_info(dctx); + if (HPy_Is(uctx, u_arg, uctx->h_None)) { + info->handle_alloc_stacktrace_limit = 0; + } else { + assert(!HPyErr_Occurred(uctx)); + HPy_ssize_t newlimit = HPyLong_AsSsize_t(uctx, u_arg); + if (newlimit == -1 && HPyErr_Occurred(uctx)) { + return HPy_NULL; + } + info->handle_alloc_stacktrace_limit = newlimit; + } + return HPy_Dup(uctx, uctx->h_None); +} + /* ~~~~~~ DebugHandleType and DebugHandleObject ~~~~~~~~ @@ -253,12 +274,14 @@ static UHPy DebugHandle_repr_impl(HPyContext *uctx, UHPy self) UHPy uh_id = HPy_NULL; UHPy uh_args = HPy_NULL; UHPy uh_result = HPy_NULL; + UHPy h_trace_header = HPy_NULL; + UHPy h_trace = HPy_NULL; const char *fmt = NULL; if (dh->handle->is_closed) - fmt = ""; + fmt = "\n%s%s"; else - fmt = ""; + fmt = "\n%s%s"; // XXX: switch to HPyUnicode_FromFormat when we have it uh_fmt = HPyUnicode_FromString(uctx, fmt); @@ -269,10 +292,24 @@ static UHPy DebugHandle_repr_impl(HPyContext *uctx, UHPy self) if (HPy_IsNull(uh_id)) goto exit; + const char *trace_header; + const char *trace; + if (dh->handle->allocation_stacktrace) { + trace_header = "Allocation stacktrace:\n"; + trace = dh->handle->allocation_stacktrace; + } else { + trace_header = "To get the stack trace of where it was allocated use:\nhpy.debug."; + trace = set_handle_stack_trace_limit.meth.name; + } + h_trace_header = HPyUnicode_FromString(uctx, trace_header); + h_trace = HPyUnicode_FromString(uctx, trace); + if (dh->handle->is_closed) - uh_args = HPyTuple_FromArray(uctx, (UHPy[]){uh_id}, 1); + uh_args = HPyTuple_FromArray(uctx, (UHPy[]){uh_id, + h_trace_header, h_trace}, 3); else - uh_args = HPyTuple_FromArray(uctx, (UHPy[]){uh_id, dh->handle->uh}, 2); + uh_args = HPyTuple_FromArray(uctx, (UHPy[]){uh_id, dh->handle->uh, + h_trace_header, h_trace}, 4); if (HPy_IsNull(uh_args)) goto exit; @@ -282,6 +319,8 @@ static UHPy DebugHandle_repr_impl(HPyContext *uctx, UHPy self) HPy_Close(uctx, uh_fmt); HPy_Close(uctx, uh_id); HPy_Close(uctx, uh_args); + HPy_Close(uctx, h_trace); + HPy_Close(uctx, h_trace_header); return uh_result; } @@ -336,6 +375,7 @@ static HPyDef *module_defines[] = { &get_protected_raw_data_max_size, &set_protected_raw_data_max_size, &set_on_invalid_handle, + &set_handle_stack_trace_limit, NULL }; diff --git a/hpy/debug/src/debug_ctx.c b/hpy/debug/src/debug_ctx.c index eec2a1c92..b497d0a4a 100644 --- a/hpy/debug/src/debug_ctx.c +++ b/hpy/debug/src/debug_ctx.c @@ -37,6 +37,7 @@ int hpy_debug_ctx_init(HPyContext *dctx, HPyContext *uctx) info->uh_on_invalid_handle = HPy_NULL; info->closed_handles_queue_max_size = DEFAULT_CLOSED_HANDLES_QUEUE_MAX_SIZE; info->protected_raw_data_max_size = DEFAULT_PROTECTED_RAW_DATA_MAX_SIZE; + info->handle_alloc_stacktrace_limit = 0; info->protected_raw_data_size = 0; DHQueue_init(&info->open_handles); DHQueue_init(&info->closed_handles); diff --git a/hpy/debug/src/debug_handles.c b/hpy/debug/src/debug_handles.c index 0b5b92107..c0e67a53a 100644 --- a/hpy/debug/src/debug_handles.c +++ b/hpy/debug/src/debug_handles.c @@ -44,6 +44,8 @@ DHPy DHPy_open(HPyContext *dctx, UHPy uh) if (info->closed_handles.size >= info->closed_handles_queue_max_size) { handle = DHQueue_popfront(&info->closed_handles); DebugHandle_free_raw_data(info, handle, true); + if (handle->allocation_stacktrace) + free(handle->allocation_stacktrace); } else { handle = malloc(sizeof(DebugHandle)); @@ -51,6 +53,12 @@ DHPy DHPy_open(HPyContext *dctx, UHPy uh) return HPyErr_NoMemory(info->uctx); } } + if (info->handle_alloc_stacktrace_limit > 0) { + create_stacktrace(&handle->allocation_stacktrace, + info->handle_alloc_stacktrace_limit); + } else { + handle->allocation_stacktrace = NULL; + } handle->uh = uh; handle->generation = info->current_generation; handle->is_closed = 0; @@ -167,6 +175,8 @@ void DHPy_free(HPyContext *dctx, DHPy dh) DebugHandle *handle = as_DebugHandle(dh); HPyDebugInfo *info = get_info(dctx); DebugHandle_free_raw_data(info, handle, true); + if (handle->allocation_stacktrace) + free(handle->allocation_stacktrace); // this is not strictly necessary, but it increases the chances that you // get a clear segfault if you use a freed handle handle->uh = HPy_NULL; diff --git a/hpy/debug/src/debug_internal.h b/hpy/debug/src/debug_internal.h index 55e245b61..e5bda5199 100644 --- a/hpy/debug/src/debug_internal.h +++ b/hpy/debug/src/debug_internal.h @@ -141,6 +141,8 @@ typedef struct DebugHandle { // pointer to and size of any raw data associated with // the lifetime of the handle: void *associated_data; + // allocation_stacktrace information if available + char *allocation_stacktrace; HPy_ssize_t associated_data_size; struct DebugHandle *prev; struct DebugHandle *next; @@ -206,6 +208,9 @@ typedef struct { HPy_ssize_t closed_handles_queue_max_size; // configurable by the user HPy_ssize_t protected_raw_data_max_size; HPy_ssize_t protected_raw_data_size; + // Limit for the stack traces captured for allocated handles + // Value 0 implies that stack traces should not be captured + HPy_ssize_t handle_alloc_stacktrace_limit; DHQueue open_handles; DHQueue closed_handles; } HPyDebugInfo; @@ -223,4 +228,6 @@ void raw_data_protect(void* data, HPy_ssize_t size); /* Return value: 0 indicates success, any different value indicates an error */ int raw_data_free(void *data, HPy_ssize_t size); +void create_stacktrace(char **target, HPy_ssize_t max_frames_count); + #endif /* HPY_DEBUG_INTERNAL_H */ diff --git a/hpy/debug/src/stacktrace.c b/hpy/debug/src/stacktrace.c new file mode 100644 index 000000000..d991430f0 --- /dev/null +++ b/hpy/debug/src/stacktrace.c @@ -0,0 +1,93 @@ +#include "debug_internal.h" + +#if (__linux__ && __GNU_LIBRARY__) + +// Basic implementation that uses backtrace from glibc + +#include +#include +#include + +static inline int max_s(size_t a, size_t b) { + return a > b ? a : b; +} + +void create_stacktrace(char **target, HPy_ssize_t max_frames_count) { + const size_t skip_frames = 2; + size_t max_stack_size = (size_t) max_frames_count; + void* stack = calloc(sizeof(void*), max_stack_size); + if (stack == NULL) { + *target = NULL; + return; + } + + size_t stack_size = backtrace(stack, max_stack_size); + if (stack_size <= skip_frames) { + *target = NULL; + free(stack); + return; + } + + char** symbols = backtrace_symbols(stack, stack_size); + if (symbols == NULL) { + *target = NULL; + free(stack); + return; + } + + size_t buffer_size = 1024; + size_t buffer_index = 0; + char *buffer = malloc(buffer_size); + if (buffer == NULL) { + *target = NULL; + free(symbols); + free(stack); + return; + } + + size_t i; + for (i = skip_frames; i < stack_size; ++i) { + size_t current_len = strlen(symbols[i]); + size_t required_buffer_size = buffer_index + current_len + 1; + if (required_buffer_size > buffer_size) { + buffer_size = max_s(buffer_size * 2, required_buffer_size); + char *new_buffer = realloc(buffer, buffer_size); + if (new_buffer == NULL) { + // allocation failed, we can still provide at least part of + // the stack trace that is currently in the buffer + break; + } + buffer = new_buffer; + } + memcpy(buffer + buffer_index, symbols[i], current_len); + buffer[buffer_index + current_len] = '\n'; + buffer_index = required_buffer_size; + } + + // override the last '\n' to '\0' + assert(stack_size - skip_frames > 0); + assert(buffer[buffer_index - 1] == '\n'); + buffer[buffer_index - 1] = '\0'; + char *shorter_buffer = realloc(buffer, buffer_index); + if (shorter_buffer != NULL) { + buffer = shorter_buffer; + } + *target = buffer; + + free(symbols); + free(stack); +} + +#else + +#include + +void create_stacktrace(char **target, HPy_ssize_t max_frames_count) { + const char msg[] = + "Current HPy build does not support getting C stack traces.\n" + "At the moment this is only supported on Linux with glibc."; + *target = malloc(sizeof(msg)); + memcpy(*target, msg, sizeof(msg)); +} + +#endif \ No newline at end of file diff --git a/hpy/devel/__init__.py b/hpy/devel/__init__.py index 18838f3b7..b6a527050 100644 --- a/hpy/devel/__init__.py +++ b/hpy/devel/__init__.py @@ -123,17 +123,26 @@ def handle_hpy_ext_modules(dist, attr, hpy_ext_modules): def __bootstrap__(): - import sys, pkg_resources + from sys import modules + from os import environ + from pkg_resources import resource_filename from hpy.universal import load - ext_filepath = pkg_resources.resource_filename(__name__, {ext_file!r}) - m = load({module_name!r}, ext_filepath) + env_debug = environ.get('HPY_DEBUG') + is_debug = env_debug is not None and (env_debug == "1" or __name__ in env_debug.split(",")) + ext_filepath = resource_filename(__name__, {ext_file!r}) + if 'HPY_LOG' in environ: + if is_debug: + print("Loading {module_name!r} in HPy universal mode with a debug context") + else: + print("Loading {module_name!r} in HPy universal mode") + m = load({module_name!r}, ext_filepath, debug=is_debug) m.__file__ = ext_filepath m.__loader__ = __loader__ m.__name__ = __name__ m.__package__ = __package__ m.__spec__ = __spec__ m.__spec__.origin = ext_filepath - sys.modules[__name__] = m + modules[__name__] = m __bootstrap__() """ diff --git a/setup.py b/setup.py index e360c4854..5b48b5e2a 100644 --- a/setup.py +++ b/setup.py @@ -37,8 +37,7 @@ with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: LONG_DESCRIPTION = f.read() - -if 'HPY_DEBUG' in os.environ: +if 'HPY_DEBUG_BUILD' in os.environ: # -fkeep-inline-functions is needed to make sure that the stubs for HPy_* # functions are available to call inside GDB EXTRA_COMPILE_ARGS = [ @@ -118,6 +117,7 @@ def get_scm_config(): 'hpy/debug/src/debug_handles.c', 'hpy/debug/src/dhqueue.c', 'hpy/debug/src/memprotect.c', + 'hpy/debug/src/stacktrace.c', 'hpy/debug/src/_debugmod.c', 'hpy/debug/src/autogen_debug_wrappers.c', ], diff --git a/test/debug/test_handles_leak.py b/test/debug/test_handles_leak.py index 59698db96..ac493612e 100644 --- a/test/debug/test_handles_leak.py +++ b/test/debug/test_handles_leak.py @@ -1,8 +1,28 @@ import pytest +from hpy.debug import set_handle_stack_trace_limit, disable_handle_stack_traces + @pytest.fixture def hpy_abi(): return "debug" + +class AllocationTraceEnabler: + def __enter__(self): + set_handle_stack_trace_limit(32) + + def __exit__(self, exc_type, exc_val, exc_tb): + disable_handle_stack_traces() + + +@pytest.fixture(params=["with stacktrace", "no stacktrace"]) +def with_alloc_trace(request): + if request.param == "with stacktrace": + with AllocationTraceEnabler(): + yield + else: + yield + + def make_leak_module(compiler): # for convenience return compiler.make_module(""" @@ -76,7 +96,7 @@ def test_leak_from_method(compiler): leaks = [dh.obj for dh in _debug.get_open_handles(gen)] assert leaks == ["a"] -def test_DebugHandle_id(compiler): +def test_DebugHandle_id(compiler, with_alloc_trace): from hpy.universal import _debug mod = make_leak_module(compiler) gen = _debug.new_generation() @@ -124,13 +144,13 @@ def test_DebugHandle_compare(compiler): with pytest.raises(TypeError): a1 < 'hello' -def test_DebugHandle_repr(compiler): +def test_DebugHandle_repr(compiler, with_alloc_trace): from hpy.universal import _debug mod = make_leak_module(compiler) gen = _debug.new_generation() mod.leak('hello') h_hello, = _debug.get_open_handles(gen) - assert repr(h_hello) == "" % h_hello.id + assert repr(h_hello).startswith("" % h_hello.id) def test_LeakDetector(compiler): import pytest @@ -156,7 +176,7 @@ def test_LeakDetector(compiler): assert 'hello' not in msg assert 'world' not in msg -def test_closed_handles(compiler): +def test_closed_handles(compiler, with_alloc_trace): from hpy.universal import _debug mod = make_leak_module(compiler) gen = _debug.new_generation() @@ -167,7 +187,7 @@ def test_closed_handles(compiler): assert h_hello.is_closed assert _debug.get_open_handles(gen) == [] assert h_hello in _debug.get_closed_handles() - assert repr(h_hello) == "" % h_hello.id + assert repr(h_hello).startswith("" % h_hello.id) def test_closed_handles_queue_max_size(compiler): from hpy.universal import _debug