diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c9fbdc..a131e6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7, 3.8, 3.9, 3.11] # List of Python versions to test against + python-version: [3.7, 3.8, 3.9, 3.11, 3.12, 3.13] # List of Python versions to test against steps: - name: Checkout code @@ -46,7 +46,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.11 + python-version: 3.13 update-environment: false - name: Install dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e692a8..f826279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Version 0.4.3 + +- Fixed invalid source code behavior when the source file doesn't exist or couldn't be read (#28) + # Version 0.4.2 - Fixed a bug that can mark 2+ threads as a faulting thread (#26), diff --git a/backtracepython/source_code_handler.py b/backtracepython/source_code_handler.py index e88d36e..53af0e5 100644 --- a/backtracepython/source_code_handler.py +++ b/backtracepython/source_code_handler.py @@ -42,7 +42,7 @@ def collect(self, report): if new_max_line > source["maxLine"]: source["maxLine"] = new_max_line - for source_code_path in source_code: + for source_code_path in list(source_code): source = source_code[source_code_path] source_code_content = self.read_source( source_code_path, source["startLine"] - 1, source["maxLine"] diff --git a/setup.py b/setup.py index c77e1be..a7bcab8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="backtracepython", - version="0.4.2", + version="0.4.3", description="Backtrace.io error reporting tool for Python", author="Backtrace.io", author_email="team@backtrace.io", diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py new file mode 100644 index 0000000..562998e --- /dev/null +++ b/tests/test_source_code_handler.py @@ -0,0 +1,77 @@ +import os + +from backtracepython.source_code_handler import SourceCodeHandler + + +def write_file(path, content): + with open(str(path), "w") as f: + f.write(content) + + +def make_report(source_paths): + """Build a minimal report whose main thread stack references the given file paths.""" + stack = [ + {"sourceCode": path, "line": 10, "funcName": "test"} for path in source_paths + ] + return { + "mainThread": "main", + "threads": { + "main": {"stack": stack}, + }, + } + + +def test_collect_removes_unreadable_sources_without_runtime_error(): + """Reproduces RuntimeError: dictionary changed size during iteration. + + When every source file in the stack is unreadable, collect() used to pop + entries from the source_code dict while iterating over it. + """ + handler = SourceCodeHandler(tab_width=4, context_line_count=3) + report = make_report( + [ + "/nonexistent/path/a.py", + "/nonexistent/path/b.py", + ] + ) + + # Before the fix this raised: + # RuntimeError: dictionary changed size during iteration + result = handler.collect(report) + + assert result["sourceCode"] == {} + + +def test_collect_keeps_readable_sources(tmp_path): + """Verify that readable source files are collected normally.""" + source_file = tmp_path / "real.py" + write_file(source_file, "foobarbaz") + + handler = SourceCodeHandler(tab_width=4, context_line_count=1) + report = make_report([str(source_file)]) + + result = handler.collect(report) + + assert str(source_file) in result["sourceCode"] + assert "text" in result["sourceCode"][str(source_file)] + + +def test_collect_mixed_readable_and_unreadable(tmp_path): + """Mix of existing and missing files""" + source_file = tmp_path / "exists.py" + write_file(source_file, "foobarbaz") + + handler = SourceCodeHandler(tab_width=4, context_line_count=3) + report = make_report( + [ + "/nonexistent/path/missing.py", + str(source_file), + "/another/missing/file.py", + ] + ) + + result = handler.collect(report) + + assert str(source_file) in result["sourceCode"] + assert "/nonexistent/path/missing.py" not in result["sourceCode"] + assert "/another/missing/file.py" not in result["sourceCode"] diff --git a/tox.ini b/tox.ini index 4dd7c9f..cb945c5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py37, py38, py39, py310 +envlist = py27, py37, py38, py39, py310, py312, py313 skipsdist = True [testenv]