fix: prevent SubtestReport from overwriting parent test result#94
Open
jasonwbarnett wants to merge 1 commit intobuildkite:mainfrom
Open
fix: prevent SubtestReport from overwriting parent test result#94jasonwbarnett wants to merge 1 commit intobuildkite:mainfrom
jasonwbarnett wants to merge 1 commit intobuildkite:mainfrom
Conversation
SubtestReport objects (from pytest>=9.0 built-in subtests or the pytest-subtests plugin) share the same nodeid as the parent test. The collector's pytest_runtest_logreport hook processes these reports via update_test_result(), which stores results in self.in_flight[nodeid]. Each subtest report overwrites the previous one (last-write-wins), and the parent test's own call-phase report arrives with outcome="passed" (because the subtest context manager swallows exceptions), overwriting any subtest failures — producing silent false positives. Strategy: - Detect SubtestReport via duck-typing (hasattr(report.context, 'msg')) - Propagate subtest failures to the parent's in-flight entry - Ignore passing/skipped subtests (must not overwrite a failure) - Guard against the parent's "passed" call report overwriting a subtest-induced failure - Clean up tracking state in finalize_test Produces one JSON entry per parent test with the correct result, preserving valid pytest nodeids for bktec retries and muting. Fixes buildkite#93
Contributor
Author
|
I forgot to mention we are running this fix in production already. |
|
Hey @jasonwbarnett thanks for this! I am not a python expert but I had a look and the changes look sound (they only add new behaviour related to subtests) and there are tests and you've verified that it works on production. I'll just get another review from the team as a sanity check due to my aforementioned lack of python knowledge 😅 |
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.
Problem
When using pytest subtests (pytest>=9.0 built-in or the
pytest-subtestsplugin),SubtestReportobjects share the samenodeidas the parent test. The collector'spytest_runtest_logreporthook processes these reports viaupdate_test_result(), which stores results inself.in_flight[nodeid]. Each subtest report overwrites the previous one (last-write-wins), and the parent test's own call-phase report arrives withoutcome="passed"(because the subtest context manager swallows exceptions), overwriting any subtest failures.Result: Tests with failing subtests are reported as
"passed"in the JSON output and Buildkite Test Engine — a silent false positive.Reproduction
https://github.com/altana-ai/bktec-subtest-repro — 14 test permutations, reproducible in 3 commands:
10 out of 10 tests with subtest failures are silently reported as "passed".
Why "ignore subtest reports" (option 1 from #93) doesn't work
In pytest 9.0+, the parent test's own
call-phaseTestReportarrives withoutcome="passed"even when subtests fail. The__exit__method on the subtest context manager returnsTrue, swallowing exceptions. While pytest 9 does mutate the outcome to"failed"viapytest_report_teststatus, that mutation happens inside the terminal reporter'spytest_runtest_logreport— the collector's hook runs first and sees"passed".If you just ignore
SubtestReportobjects, the collector only sees the parent's"passed"report → same broken result.Solution
Three targeted changes to
pytest_runtest_logreport(~30 lines of new logic):Detect
SubtestReportobjects via duck-typing (hasattr(report.context, 'msg')) — works for both pytest>=9.0 built-in and thepytest-subtestsplugin without importing either.Propagate subtest failures to the parent's in-flight entry. Ignore passing/skipped subtests so they don't overwrite a previous failure.
Guard against the parent test's
"passed"call-phase report overwriting a subtest-induced failure.The fix produces one JSON entry per parent test with the correct
"result": "failed"status and the subtest's failure reason. Thescopeandnamefields contain the real pytest nodeid, sobktecretries, muting, and test splitting continue to work without any changes.Compatibility
subtestsfixturepytest-subtestsplugin_is_subtest_report()returnsFalseimmediatelyValidated: 10/10 broken tests fixed, 0 regressions
After applying the fix and re-running the same 14 permutations from the reproduction repo:
test_all_subtests_passtest_first_fails_rest_passpassedfirst subtest failstest_last_fails_rest_passpassedlast subtest failstest_middle_failspassedmiddle subtest failstest_all_subtests_failpassedC failstest_single_subtest_failspassedsingle subtest failstest_code_outside_subtests_passespassedB failstest_code_outside_subtests_failstest_no_subtests_passes/failstest_subtests_with_kwargspassedassert 0 > 0test_subtests_msg_and_kwargspassedassert -1 > 0test_many_subtests_one_failurepassediteration 7 failstest_subtest_no_msgpassedunnamed subtest failsTest plan
uv run pytest— all 85 existing tests pass, 5 integration tests skipped (need pytest>=9.0)uv run pylint src/— 10.00/10pytest-subtestsplugin'sSubTestReport(capital T) and pytest 9.0+'s built-inSubtestReportFixes #93