Slice A: coverage fully out of the GUI process; children stop importing the GUI — v0.9.1 - #167
Merged
Merged
Conversation
…the GUI process Aggregation (combine + report) already ran in a spawn child after an access violation in python.dll inside the GUI process, but two in-process coverage paths survived: CoverageTracker's per-test pass constructed Coverage(cov_file).load() on its worker thread for every completed test on every recalculation, and compute_per_test_coverage did the same on the run-prep thread for coverage-efficiency ordering. Same library, same process, same thread situation as the documented crash. CoverageAggregator now takes optional per_test_names and returns a CoverageResult (totals + per-test executed-line counts + the union size). The tracker and the ordering path both derive their fractions from that — per_test_fractions(total_statements) for the Coverage tab, per_test_fractions() (union) for ordering — so the GUI process never constructs a coverage.Coverage object. compute_per_test_coverage remains as a pure in-process helper for the child and tests; calculate_coverage's tuple return is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
Under the forced spawn start method every child (test process, ProcessMonitor, SystemMonitor, CoverageAggregator, GetTests) re-imports pytest_fly.__main__, which imported main.py, which did `from .gui import fly_main` at module level. So a process monitor that only samples psutil first loaded PySide6 and all eight tab widgets: measured 0.58 s and tens of MB of commit per child, two children per test, and pytest-fly's own Qt binding resident in the test process before the program under test's tests ran — sys.modules pollution, and a real crash source for a PUT on a different Qt binding. The PUT's own imports are untouched: if its tests import PySide6/PyQt they still do, exactly as before. main.fly_main is now a thin lazy wrapper that imports the GUI package at call time (the attribute stays for monkeypatching), and pytest_runner/__init__.py resolves PytestRunner / PytestRunState / GetTests via PEP 562 __getattr__ so unpickling a monitor class no longer drags the orchestration layer in. pytest itself is still imported (interfaces.py builds PyTestFlyExitCode from pytest.ExitCode) — that is a deliberate dependency. After: PySide6 and pytest_fly.gui absent from every child; __main__ import 0.58 s → 0.28 s. test_spawn_child_entry_does_not_import_gui pins this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
…der-test stand-in tests/ is the PUT when pytest-fly runs on itself, so test_pyside_app.py gives every run a real Qt program under test: a CounterWindow driven with qtbot (clicks, reset, a custom signal via waitSignal). Its fourth test runs only inside a pytest-fly child (detected by the exported PYTEST_FLY_FAULTHANDLER) and asserts that pytest_fly.gui is not pre-loaded into the PUT's process — verified to fail against master's module-level GUI import when the parent is `python -m pytest_fly`, and to pass on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
…ironment The isolation check keyed on PYTEST_FLY_FAULTHANDLER, but the suite's own enable_faulthandler tests export that variable into the top-level pytest process, so in CI the check ran outside a child (where the GUI tests legitimately import pytest_fly.gui) and failed. PytestProcess names its process after the test module, so current_process().name is the only signal that is true exactly inside the child. Re-verified: skips under top-level pytest with the variable set, passes as a child on this branch, fails as a child against master's module-level GUI import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #167 +/- ##
==========================================
- Coverage 86.14% 86.08% -0.06%
==========================================
Files 74 74
Lines 6150 6168 +18
==========================================
+ Hits 5298 5310 +12
- Misses 852 858 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice A of the crash review: remove the two biggest native-crash surfaces from the GUI process.
coverage.Coverageobject is ever constructed in the GUI process. Aggregation already ran in a spawn child after an access violation inpython.dll, butCoverageTracker's per-test pass (Coverage(cov_file).load()on its worker thread, once per completed test per recalculation) andcompute_per_test_coverageon the run-prep thread still did.CoverageAggregatornow takes optionalper_test_namesand returns aCoverageResult(totals + per-test executed-line counts + union); the tracker and the ordering path derive their fractions from that.pytest_fly.__main__→main.py, which imported.guiat module level — so each test process,ProcessMonitor,SystemMonitor,CoverageAggregatorandGetTestsloaded PySide6 and all eight tabs first (measured 0.58 s and tens of MB commit per child, two children per test), and pytest-fly's own Qt binding sat in the test process before the PUT's tests ran.main.fly_mainis now a lazy wrapper andpytest_runner/__init__.pyresolves its exports via PEP 562. The PUT is unaffected — if its tests import PySide6/PyQt, they still do exactly as before; only pytest-fly's own side-effect import is gone.Measured
PySide6in a child'ssys.modulespytest_fly.gui.*in a childimport pytest_fly.__main__pytestis still imported by children:interfaces.pybuildsPyTestFlyExitCodefrompytest.ExitCodeby design.Tests
test_child_returns_per_test_executed_lines— child per-test counts matchcompute_per_test_coverage, both fraction conventions.test_spawn_child_entry_does_not_import_gui— subprocess probe pins the import footprint.ruffclean;tyunchanged (pre-existing diagnostics only).Behaviour change
Per-test coverage numbers for the Coverage tab now arrive with the rate-limited aggregation pass rather than being computed on the tracker thread — same values, same cadence as the combined number.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd