From 2eb8a7e8638a63b36ddc6e706f50cbb312fbc615 Mon Sep 17 00:00:00 2001 From: Ivan Yurchenko Date: Sun, 6 Nov 2022 08:44:21 +0200 Subject: [PATCH] dumper: survive errors on __dir__, __sizeof__ Closes #126 --- integration_tests/resources/mock_inferior.py | 32 +++++++++++++++++++- integration_tests/test_dumper.py | 27 ++++++++++++----- pyheap/src/dumper_inferior.py | 27 ++++++++++++----- test_inferiors/inferior-simple.py | 31 +++++++++++++++++++ 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/integration_tests/resources/mock_inferior.py b/integration_tests/resources/mock_inferior.py index 35467a6..152e465 100644 --- a/integration_tests/resources/mock_inferior.py +++ b/integration_tests/resources/mock_inferior.py @@ -19,12 +19,42 @@ import time from pathlib import Path from threading import Thread, Event -from typing import Any +from typing import Any, NoReturn heap_file = sys.argv[1] dump_str_repr = sys.argv[2].lower() == "true" +class DisabledOperations: + def __new__(cls) -> "DisabledOperations": + prohibited = { + "__dir__", + "__str__", + "__repr__", + "__doc__", + "__eq__", + "__ge__", + "__gt__", + "__getattribute__", + "__hash__", + "__le__", + "__lt__", + "__ne__", + "__setattr__", + "__sizeof__", + } + for attr in prohibited: + + def error(*args: Any, **kwargs: Any) -> NoReturn: + raise ValueError(f"prohibited to call {attr}") + + setattr(cls, attr, error) + return super().__new__(cls) + + +disabled_operations = DisabledOperations() + + class MyThread(Thread): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) diff --git a/integration_tests/test_dumper.py b/integration_tests/test_dumper.py index f372e4d..fcffd2b 100644 --- a/integration_tests/test_dumper.py +++ b/integration_tests/test_dumper.py @@ -43,8 +43,8 @@ def test_dumper(tmp_path: Path, dump_str_repr: bool) -> None: stderr=subprocess.PIPE, ) - print(r.stdout) - print(r.stderr) + print(r.stdout.decode("utf-8")) + print(r.stderr.decode("utf-8")) # Not all errors may be propagated as return code assert r.returncode == 0 @@ -86,7 +86,7 @@ def _check_threads_and_objects( frame = main_thread.stack_trace[0] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 66 + assert frame.lineno == 96 assert frame.co_name == "function3" assert set(frame.locals.keys()) == { "a", @@ -119,7 +119,7 @@ def _check_threads_and_objects( frame = main_thread.stack_trace[1] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 78 + assert frame.lineno == 108 assert frame.co_name == "function2" assert set(frame.locals.keys()) == {"a", "b"} obj = heap.objects[frame.locals["a"]] @@ -138,7 +138,7 @@ def _check_threads_and_objects( frame = main_thread.stack_trace[2] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 82 + assert frame.lineno == 112 assert frame.co_name == "function1" assert set(frame.locals.keys()) == {"a", "b", "c"} @@ -165,7 +165,7 @@ def _check_threads_and_objects( frame = main_thread.stack_trace[3] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 85 + assert frame.lineno == 115 assert frame.co_name == "" expected_locals = { "__name__", @@ -184,11 +184,14 @@ def _check_threads_and_objects( "time", "Path", "Any", + "NoReturn", "Thread", "Event", "heap_file", "MyThread", "my_thread", + "DisabledOperations", + "disabled_operations", "some_string", "some_list", "some_tuple", @@ -207,6 +210,14 @@ def _check_threads_and_objects( assert obj.attributes.keys() == _ATTRS_FOR_STR assert obj.str_repr == ("hello world" if dump_str_repr else None) + obj = heap.objects[frame.locals["disabled_operations"]] + disabled_operations_type = _find_type_by_name(heap, "DisabledOperations") + assert obj == HeapObject( + type=disabled_operations_type, size=0, referents={disabled_operations_type} + ) + assert obj.attributes == {} + assert obj.str_repr == ("" if dump_str_repr else None) + obj = heap.objects[frame.locals["some_list"]] # assert obj.address == frame.locals["some_list"] assert obj.type == _find_type_by_name(heap, "list") @@ -252,7 +263,7 @@ def _check_threads_and_objects( frame = second_thread.stack_trace[0] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 35 + assert frame.lineno == 65 assert frame.co_name == "_thread_inner" assert set(frame.locals.keys()) == { "self", @@ -278,7 +289,7 @@ def _check_threads_and_objects( frame = second_thread.stack_trace[1] assert frame.co_filename == mock_inferior_file - assert frame.lineno == 38 + assert frame.lineno == 68 assert frame.co_name == "run" assert set(frame.locals.keys()) == {"self", "__class__"} diff --git a/pyheap/src/dumper_inferior.py b/pyheap/src/dumper_inferior.py index 689042c..c655525 100644 --- a/pyheap/src/dumper_inferior.py +++ b/pyheap/src/dumper_inferior.py @@ -161,6 +161,7 @@ def _dump_heap() -> str: common_types=common_types, additional_objects_to_visit=objects_to_visit, progress_reporter=progress_reporter, + messages=messages, ) writer.write_unsigned_int(len(types)) @@ -426,6 +427,7 @@ def _write_objects_and_return_types( common_types: Set[Type], additional_objects_to_visit: List[Any], progress_reporter: ProgressReporter, + messages: List[str], ) -> Tuple[Dict[int, str], int]: seen_ids = set() to_visit: List[Any] = [] @@ -468,8 +470,14 @@ def _write_objects_and_return_types( writer.write_unsigned_long(id(obj)) # Type writer.write_unsigned_long(id(type_)) + # Size - writer.write_unsigned_int(sys.getsizeof(obj)) + obj_size = 0 + try: + obj_size = sys.getsizeof(obj) + except Exception as e: + messages.append(f"Error getting size of {type_}: {e}") + writer.write_unsigned_int(obj_size) # Referents writer.write_unsigned_int(len(referents)) @@ -479,13 +487,16 @@ def _write_objects_and_return_types( # Attributes -- write them only for non-"common" types. if type_ not in common_types: attrs: List[Tuple[str, object]] = [] - for attr in dir(obj): - try: - attr_value = inspect.getattr_static(obj, attr) - to_visit.append(attr_value) - attrs.append((attr, attr_value)) - except (AttributeError, ValueError): - pass + try: + for attr in dir(obj): + try: + attr_value = inspect.getattr_static(obj, attr) + to_visit.append(attr_value) + attrs.append((attr, attr_value)) + except (AttributeError, ValueError): + pass + except Exception as e: + messages.append(f"Error collecting attributes of type {type_}: {e}") writer.write_unsigned_int(len(attrs)) for attr, attr_value in attrs: diff --git a/test_inferiors/inferior-simple.py b/test_inferiors/inferior-simple.py index a1b3971..05d372c 100644 --- a/test_inferiors/inferior-simple.py +++ b/test_inferiors/inferior-simple.py @@ -15,6 +15,7 @@ # import time from threading import Thread +from typing import Any, NoReturn # Circular reference a = ["a"] @@ -25,6 +26,36 @@ huge_list = ["x" * 100_000] +class DisabledOperations: + def __new__(cls) -> "DisabledOperations": + prohibited = { + "__dir__", + "__str__", + "__repr__", + "__doc__", + "__eq__", + "__ge__", + "__gt__", + "__getattribute__", + "__hash__", + "__le__", + "__lt__", + "__ne__", + "__setattr__", + "__sizeof__", + } + for attr in prohibited: + + def error(*args: Any, **kwargs: Any) -> NoReturn: + raise ValueError(f"prohibited to call {attr}") + + setattr(cls, attr, error) + return super().__new__(cls) + + +disabled_operations = DisabledOperations() + + # Thread. class MyThread(Thread): def _x(self, bbb, cccc):