Skip to content

Commit

Permalink
Add test and coverage (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaogaotiantian authored Apr 22, 2024
1 parent dd8424e commit 6033323
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]
cover_pylib = True
source = coredumpy
52 changes: 52 additions & 0 deletions .github/workflows/build_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: build and test

on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
build_test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest, macos-14]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
- python-version: '3.9'
os: macos-14
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Build
run: python -m build
- name: Install on Unix
if: matrix.os != 'windows-latest'
run: pip install dist/*.whl
- name: Install on Windows
if: matrix.os == 'windows-latest'
run: pip install (Get-ChildItem dist/*.whl)
- name: Test
run: python -m unittest
- name: Generate Coverage Report
run: |
coverage run --source coredumpy --parallel-mode -m unittest
coverage combine
coverage xml -i
env:
COVERAGE_RUN: True
- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: gaogaotiantian/coredumpy
file: ./coverage.xml
26 changes: 26 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: lint

on:
push:
branches:
- master
pull_request:

jobs:
lint:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependency
run: pip install flake8 mypy
- name: Run flake8
run: flake8 src/ tests/ --count --ignore=W503 --max-line-length=127 --statistics
- name: Run mypy
run: mypy src/
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ name = "coredumpy"
authors = [{name = "Tian Gao", email = "[email protected]"}]
description = "A utility tool to dump python stacks"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
license = {file = "LICENSE"}
dynamic = ["version"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down
8 changes: 7 additions & 1 deletion src/coredumpy/coredumpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
import json
import linecache
import os
import tokenize
import pdb

Expand Down Expand Up @@ -32,9 +33,13 @@ def dump(cls, path, frame=None):
json.dump({
"objects": PyObjectProxy._objects,
"frame": str(id(curr_frame)),
"files": {filename: tokenize.open(filename).readlines() for filename in files}
"files": {filename: tokenize.open(filename).readlines()
for filename in files
if os.path.exists(filename)}
}, f)

PyObjectProxy.clear()

@classmethod
def load(cls, path):
with open(path, "r") as f:
Expand All @@ -48,6 +53,7 @@ def load(cls, path):
pdb_instance = pdb.Pdb()
pdb_instance.reset()
pdb_instance.interaction(frame, None)
PyObjectProxy.clear()


dump = Coredumpy.dump
Expand Down
12 changes: 10 additions & 2 deletions src/coredumpy/py_object_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ def clear(cls):

@classmethod
def add_object(cls, obj):
if id(obj) not in cls._objects:
if str(id(obj)) not in cls._objects:
# label the id
cls._objects[str(id(obj))] = {}
cls._objects[str(id(obj))] = cls.dump_object(obj)
return cls._objects[str(id(obj))]

@classmethod
def dump_object(cls, obj):
Expand Down Expand Up @@ -77,7 +80,12 @@ def load_objects(cls, data):
@classmethod
def default_encode(cls, obj):
data = {"type": type(obj).__name__, "attrs": {}}
if isinstance(obj, (types.ModuleType, types.FunctionType, types.BuiltinFunctionType)):
if isinstance(obj, (types.ModuleType,
types.FunctionType,
types.BuiltinFunctionType,
types.LambdaType,
types.MethodType,
)):
return data
for attr, value in inspect.getmembers(obj):
if not attr.startswith("__") and not callable(value):
Expand Down
Empty file added tests/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


import os
import subprocess
import tempfile
import textwrap
import unittest

from .util import normalize_commands


class TestBase(unittest.TestCase):
def run_test(self, script, dumppath, commands):
script = textwrap.dedent(script)
with tempfile.TemporaryDirectory() as tmpdir:
with open(f"{tmpdir}/script.py", "w") as f:
f.write(script)
subprocess.run(normalize_commands(["python", f"{tmpdir}/script.py"]),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

process = subprocess.Popen(normalize_commands(["coredumpy", "load", dumppath]),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

stdout, stderr = process.communicate("\n".join(commands).encode())
stdout = stdout.decode()
stderr = stderr.decode()
try:
os.remove(dumppath)
except FileNotFoundError:
pass
return stdout, stderr

def run_script(self, script):
script = textwrap.dedent(script)
with tempfile.TemporaryDirectory() as tmpdir:
with open(f"{tmpdir}/script.py", "w") as f:
f.write(script)
process = subprocess.Popen(normalize_commands(["python", f"{tmpdir}/script.py"]),
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout = stdout.decode()
stderr = stderr.decode()
return stdout, stderr
56 changes: 56 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


from .base import TestBase


class TestBasic(TestBase):
def test_simple(self):
script = """
import coredumpy
def g(arg):
coredumpy.dump("coredumpy_dump")
return arg
def f():
x = 142857
y = [3, {'a': (4, None)}]
g(y)
f()
"""
stdout, _ = self.run_test(script, "coredumpy_dump", [
"w",
"p arg",
"u",
"p x",
"q"
])

self.assertIn("-> f()", stdout)
self.assertIn("script.py(10)<module>", stdout)
self.assertIn("-> g(y)", stdout)
self.assertIn("script.py(4)g()", stdout)
self.assertIn("[3, {'a': [4, None]}]", stdout)
self.assertIn("142857", stdout)

def test_except(self):
script = """
import coredumpy
coredumpy.patch_excepthook()
def g(arg):
return 1 / arg
g(0)
"""
stdout, _ = self.run_test(script, "coredumpy_dump", [
"w",
"p arg",
"u",
"p x",
"q"
])
self.assertIn("return 1 / arg", stdout)
self.assertIn("0", stdout)

def test_nonexist_file(self):
stdout, stderr = self.run_test("", "nonexist_dump", [])
self.assertIn("File nonexist_dump not found", stdout)
26 changes: 26 additions & 0 deletions tests/test_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


from .base import TestBase


class TestPatch(TestBase):
def test_inspect(self):
script = """
import inspect
from coredumpy.patch import patch_all
patch_all()
class FakeFrame:
def __init__(self):
self._coredumpy_type = "frame"
class FakeCode:
def __init__(self):
self._coredumpy_type = "code"
assert inspect.isframe(FakeFrame()), "isframe not patched"
assert inspect.iscode(FakeCode()), "iscode not patched"
print("patch inspect success")
"""

stdout, stderr = self.run_script(script)
self.assertIn("patch inspect success", stdout, stderr)
40 changes: 40 additions & 0 deletions tests/test_py_object_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


from coredumpy.py_object_proxy import PyObjectProxy

from .base import TestBase


class TestPyObjectProxy(TestBase):
def tearDown(self):
PyObjectProxy.clear()
return super().tearDown()

def test_basic(self):
class A:
def __init__(self, x):
self.x = x
obj = A(142857)
data = PyObjectProxy.add_object(obj)
for i, o in PyObjectProxy._objects.items():
PyObjectProxy.load_object(i, o)
proxy = PyObjectProxy.load_object(str(id(obj)), data)
self.assertEqual(proxy.x, 142857)
self.assertEqual(dir(proxy), ['x'])
self.assertIn('<A object at 0x', repr(proxy))

def test_nonexist_attr(self):
class A:
def __init__(self, x):
self.x = x
o = A(142857)
obj = PyObjectProxy.add_object(o)
proxy = PyObjectProxy.load_object(str(id(o)), obj)
with self.assertRaises(AttributeError):
proxy.y

def test_invalid(self):
with self.assertRaises(ValueError):
PyObjectProxy.load_object("1", None)
14 changes: 14 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/gaogaotiantian/coredumpy/blob/master/NOTICE.txt


import os


def normalize_commands(commands):
if os.getenv("COVERAGE_RUN"):
if commands[0] == "python":
commands = ["coverage", "run", "--parallel-mode"] + commands[1:]
elif commands[0] == "coredumpy":
commands = ["coverage", "run", "--parallel-mode", "-m", "coredumpy"] + commands[1:]
return commands

0 comments on commit 6033323

Please sign in to comment.