Skip to content

Commit

Permalink
Implement peek command (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaogaotiantian authored Apr 29, 2024
1 parent 869a0de commit eea1294
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 9 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ coredumpy.dump(path='coredumpy.dump')
coredumpy.dump(path=lambda: f"coredumpy_{time.time()}.dump")
# Specify a directory to keep the dump
coredumpy.dump(directory='./dumps')
# Specify the description of the dump for peek
coredumpy.dump(description="a random dump")
```

### load
Expand All @@ -66,6 +68,16 @@ open an unknown dump (not recommended though). You will be in an "observer"
mode where you can access certain types of value of the variables and attributes,
but none of the user-created objects will have the actual functionality.

### peek

If you only need some very basic information of the dump (to figure out which dump
you actually need), you can use `peek` command.

```
coredumpy peek <your_dump_directory>
coredumpy peek <your_dump_file1> <your_dump_file2>
```

## Disclaimer

This library is still in development phase and is not recommended for production use.
Expand Down
22 changes: 22 additions & 0 deletions src/coredumpy/coredumpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pdb
import platform
import tokenize
import textwrap
import types
import warnings
from typing import Callable, Optional, Union
Expand All @@ -25,6 +26,7 @@ class Coredumpy:
def dump(cls,
frame: Optional[types.FrameType] = None,
*,
description: Optional[str] = None,
path: Optional[Union[str, Callable[[], str]]] = None,
directory: Optional[str] = None):
"""
Expand Down Expand Up @@ -73,6 +75,7 @@ def dump(cls,
"objects": PyObjectProxy._objects,
"frame": str(id(curr_frame)),
"files": file_lines,
"description": description,
"metadata": cls.get_metadata()
}, f)

Expand Down Expand Up @@ -100,6 +103,24 @@ def load(cls, path):
pdb_instance.interaction(frame, None)
PyObjectProxy.clear() # pragma: no cover

@classmethod
def peek(cls, path):
with gzip.open(path, "rt") as f:
data = json.load(f)

from coredumpy import __version__
if data["metadata"]["version"] != __version__: # pragma: no cover
print(f"Warning! the dump file is created by {data['metadata']['version']}\n"
f"but the current coredumpy version is {__version__}")
patch_all()
metadata = data["metadata"]
system = metadata["system"]
print(f"{os.path.abspath(path)}")
print(f" Python v{metadata['python_version']} on {system['system']} {system['node']} {system['release']}")
print(f" {metadata['dump_time']}")
if data["description"]:
print(textwrap.indent(data["description"], " "))

@classmethod
def get_metadata(cls):
from coredumpy import __version__
Expand All @@ -118,3 +139,4 @@ def get_metadata(cls):

dump = Coredumpy.dump
load = Coredumpy.load
peek = Coredumpy.peek
18 changes: 13 additions & 5 deletions src/coredumpy/except_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


import sys
import traceback
from typing import Callable, Optional, Union

from .coredumpy import dump
Expand All @@ -22,12 +23,19 @@ def patch_except(path: Optional[Union[str, Callable[[], str]]] = None,
The directory to save the dump file, only works when path is not specified.
"""

def _excepthook(type, value, traceback):
while traceback.tb_next:
traceback = traceback.tb_next
def _get_description(type, value, tb):
side_count = (70 - len(type.__qualname__) - 2) // 2
headline = f"{'=' * side_count} {type.__qualname__} {'=' * side_count}"
return '\n'.join([headline,
''.join(traceback.format_exception(type, value, tb)).strip()])

filename = dump(traceback.tb_frame, path=path, directory=directory)
_original_excepthook(type, value, traceback)
def _excepthook(type, value, tb):
while tb.tb_next:
tb = tb.tb_next

filename = dump(tb.tb_frame, description=_get_description(type, value, tb),
path=path, directory=directory)
_original_excepthook(type, value, tb)
print(f'Your frame stack is dumped, open it with\n'
f'coredumpy load {filename}')

Expand Down
22 changes: 21 additions & 1 deletion src/coredumpy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import argparse
import os

from .coredumpy import load
from .coredumpy import load, peek


def main():
Expand All @@ -15,10 +15,30 @@ def main():
subparsers_load = subparsers.add_parser("load", help="Load a dump file.")
subparsers_load.add_argument("file", type=str, help="The dump file to load.")

subparsers_load = subparsers.add_parser("peek", help="Peek a dump file.")
subparsers_load.add_argument("files", help="The dump file to load.", nargs="+")

args = parser.parse_args()

if args.command == "load":
if os.path.exists(args.file):
load(args.file)
else:
print(f"File {args.file} not found.")
elif args.command == "peek":
for file in args.files:
if os.path.exists(file):
if os.path.isdir(file):
for f in os.listdir(file):
try:
path = os.path.join(file, f)
peek(path)
except Exception:
pass
else:
try:
peek(file)
except Exception:
pass
else:
print(f"File {file} not found.")
10 changes: 9 additions & 1 deletion src/coredumpy/pytest_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ def pytest_addoption(parser):
)


def _get_description(report):
underscore_count = (70 - len(report.head_line) - 2) // 2
headline = f"{'_' * underscore_count} {report.head_line} {'_' * underscore_count}"
return '\n'.join([headline, report.longreprtext])


def pytest_exception_interact(node, call, report):
if not node.config.getoption("--enable-coredumpy"):
return
Expand All @@ -26,7 +32,9 @@ def pytest_exception_interact(node, call, report):
tb = call.excinfo.tb
while tb.tb_next:
tb = tb.tb_next
filename = coredumpy.dump(tb.tb_frame, directory=node.config.getoption("--coredumpy-dir"))
filename = coredumpy.dump(tb.tb_frame,
description=_get_description(report),
directory=node.config.getoption("--coredumpy-dir"))
print(f'Your frame stack is dumped, open it with\n'
f'coredumpy load {filename}')
except Exception: # pragma: no cover
Expand Down
13 changes: 11 additions & 2 deletions src/coredumpy/unittest_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ def patch_unittest(path: Optional[Union[str, Callable[[], str]]] = None,
The directory to save the dump file, only works when path is not specified.
"""

def _get_description(self, test, err):
class_name = f"{test.__class__.__module__}.{test.__class__.__qualname__}"
return '\n'.join(['=' * 70,
f"FAIL: {test._testMethodName} ({class_name})",
'-' * 70,
self._exc_info_to_string(err, test).strip()])

_original_addError = unittest.TestResult.addError
_original_addFailure = unittest.TestResult.addFailure

Expand All @@ -27,7 +34,8 @@ def addError(self, test, err):
while tb.tb_next:
tb = tb.tb_next
try:
filename = dump(tb.tb_frame, path=path, directory=directory)
filename = dump(tb.tb_frame, description=_get_description(self, test, err),
path=path, directory=directory)
print(f'Your frame stack is dumped, open it with\n'
f'coredumpy load {filename}')
except Exception: # pragma: no cover
Expand All @@ -39,7 +47,8 @@ def addFailure(self, test, err):
while tb.tb_next:
tb = tb.tb_next
try:
filename = dump(tb.tb_frame, path=path, directory=directory)
filename = dump(tb.tb_frame, description=_get_description(self, test, err),
path=path, directory=directory)
print(f'Your frame stack is dumped, open it with\n'
f'coredumpy load {filename}')
except Exception: # pragma: no cover
Expand Down
8 changes: 8 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ def run_script(self, script, expected_returncode=0):
self.assertEqual(process.returncode, expected_returncode,
f"script failed with return code {process.returncode}\n{stderr}")
return stdout, stderr

def run_peek(self, paths):
process = subprocess.Popen(normalize_commands(["coredumpy", "peek"] + paths),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout = stdout.decode(errors='backslashreplace')
stderr = stderr.decode(errors='backslashreplace')
return stdout, stderr
20 changes: 20 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ def g(arg):
self.assertIn("return 1 / arg", stdout)
self.assertIn("0", stdout)

def test_peek(self):
with tempfile.TemporaryDirectory() as tmpdir:
script = f"""
import coredumpy
coredumpy.dump(description="test", directory={repr(tmpdir)})
"""
self.run_script(script)
self.run_script(script)
with open(os.path.join(tmpdir, "invalid"), "w") as f:
f.write("{invalid}")

self.assertEqual(len(os.listdir(tmpdir)), 3)
stdout, _ = self.run_peek([tmpdir])
stdout2, _ = self.run_peek([os.path.join(tmpdir, file) for file in os.listdir(tmpdir)])

self.assertEqual(stdout, stdout2)

stdout, _ = self.run_peek([os.path.join(tmpdir, "nosuchfile")])
self.assertIn("not found", stdout)

def test_nonexist_file(self):
stdout, stderr = self.run_test("", "nonexist_dump", [])
self.assertIn("File nonexist_dump not found", stdout)

0 comments on commit eea1294

Please sign in to comment.