Fix: a scenario's before_scenario hook failure wrongly fails its siblings under --parallel-scheme feature - #249
Open
taltal-beep wants to merge 1 commit into
Conversation
…ings under --parallel-scheme feature Under `--parallel-scheme feature` with more than one worker process, a single scenario's before_scenario (or after_scenario/before_step/etc.) hook failure caused every other scenario in the same feature to be misreported as failed too - even ones that ran and passed cleanly. Root cause: `runner.hook_failures` is a blunt counter behave increments for any hook exception, regardless of hook type. execute_tests() treated execution_code == 2 (hook_failures > 0) as "the run crashed" and discarded the real, correctly-computed per-scenario results in favor of a synthetic skeleton marking every scenario in the feature as failed. Under `--parallel-scheme feature`, one worker runs every scenario of a feature in a single behave invocation, so hook_failures accumulates across all of them - a single scenario's expected/isolated hook failure (which behave itself correctly isolates to just that scenario, continuing normally) was enough to poison the whole feature's reported results. `--parallel-scheme scenario` never showed this because each scenario gets its own isolated runner.run() call, so hook_failures never spans siblings. Fix: - execute_tests() now only falls back to the synthetic skeleton when there really are no results to report (e.g. the runner crashed/aborted before producing any feature data). When behave completed normally and handed back real per-scenario results despite hook_failures > 0, those real results are used - preserving execution_code 2's existing severity signal for hook types that aren't scenario-scoped (before_all, after_all, before_feature, after_feature, tag hooks), which don't otherwise show up in any individual scenario's status. - The scenario-status classification that builds the final totals/exit code didn't recognize behave's own `hook_error`/`cleanup_error` status values, silently bucketing them as "skipped" and never flagging `failing_non_muted_tests`. Fixed so they count as failures, matching behave's own is_hook_error()/is_failure() semantics. Added a regression test (tests/features/partial_hook_failures.feature + partial_hook_failures/) with 3 scenarios in one feature where only the middle one's before_scenario hook fails, run under `--parallel-processes 2 --parallel-scheme feature`, asserting the correct "2 scenarios passed, 1 failed" instead of "0 passed, 3 failed". Verified: minimal standalone repro, the full existing tests/features/crashing_tests.feature @CRASHING_BEHAVE_HOOK outline (all 20 examples, covering every hook type at 1 and 2 parallel processes) still passes unchanged, and a full tests/features/ suite run shows zero scenario-status regressions against baseline (only the new regression test itself flips from incorrectly-erroring to correctly-passing).
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
Under
--parallel-scheme featurewith more than one worker process, a single scenario'sbefore_scenariohook failure causes every other scenario in the same feature to be misreported as failed too — including scenarios that ran and passed cleanly.--parallel-scheme scenariois unaffected.Found while running a suite with a stochastic failure-injection hook (
before_scenariooccasionally raises to simulate flaky setup) — a BehaveX stage consistently reported "0 passed, N failed" for a feature where, per the actual random draws, only a fraction of scenarios should have failed. The exact same hook, same repo, run via native single-processbehave, correctly reported a normal mix of passed/hook_error.Root cause
runner.hook_failuresis a blunt counter behave'sModelRunner.run_hook()increments for any hook exception, regardless of hook type (before_all,before_scenario,before_step, tag hooks, ...) — see behave'srunner.py._calculate_execution_code_from_runner()treatshook_failures > 0asexecution_code = 2("crashed").execute_tests()then discards the real, already-correctly-computed per-scenario JSON and replaces it with a synthetic skeleton marking every scenario in that feature as'failed'.Under
--parallel-scheme feature, one worker runs every scenario of a feature in a singlebehaveinvocation, sohook_failuresaccumulates across all of them. A single scenario'sbefore_scenariofailure — which behave itself already isolates correctly to just that one scenario (hook_failedon the scenario, execution continues normally) — is enough to poison the whole feature's reported results.--parallel-scheme scenarionever hit this because each scenario gets its own isolatedrunner.run()call — no siblings to poison.Minimal repro
Expected (and what
--parallel-scheme scenario/ nativebehavecorrectly produce): 2 passed, 1 failed.Fix
Two changes, both needed together (the second was masked by the first and only surfaced once real per-scenario results were allowed through):
execute_tests(): only fall back to the synthetic "everything failed" skeleton when there really are no real results to report (e.g. the runner crashed/aborted before producing any feature data). When behave completed normally and handed back real per-scenario results despitehook_failures > 0, those real results are used instead.execution_code == 2's existing severity signal is preserved for hook types that aren't scenario-scoped (before_all,after_all,before_feature,after_feature, tag hooks) — those don't show up in any individual scenario's status, so they still need this signal to correctly drive the final exit code. (Confirmed via the existing@CRASHING_BEHAVE_HOOKoutline intests/features/crashing_tests.feature, which covers exactly this — see Verification.)Scenario-status classification (used to build the final totals and
failing_non_muted_tests): didn't recognize behave's ownhook_error/cleanup_errorstatus values (see behave'smodel_type.pyStatusenum andis_hook_error()), silently bucketing them as "skipped" and never flagging the run as failed. Fixed to count them as failures, matching behave's own semantics.Test
Added
tests/features/partial_hook_failures.feature+tests/features/partial_hook_failures/(feature,environment.py, steps) — 3 scenarios in one feature where only the middle one'sbefore_scenariohook fails, run with--parallel-processes 2 --parallel-scheme feature, asserting2 scenarios passed, 1 failed, 0 skippedand exit code 1 (fails onmaster, passes with this fix).Verification
--parallel-scheme featureand--parallel-scheme scenario, now produce identical, correct2 passed, 1 failed/ exit code 1.tests/features/crashing_tests.feature@CRASHING_BEHAVE_HOOKscenario outline (20 examples: every hook type × 1/2 parallel processes) still passes unchanged — confirms genuinely fatal hook failures (before_all,after_all, etc.) still correctly report exit code 1.tests/features/suite (334 scenarios) diffed scenario-by-scenario againstmaster: zero regressions — the only status change anywhere in the suite is the new regression test itself flipping from incorrectly-erroring to correctly-passing.