Skip to content

fix: skip user hooks when replaying cached skipped, incomplete, and failed tests - #1806

Open
oddvalue wants to merge 1 commit into
pestphp:5.xfrom
oddvalue:fix/tia-replay-skipped-teardown
Open

fix: skip user hooks when replaying cached skipped, incomplete, and failed tests#1806
oddvalue wants to merge 1 commit into
pestphp:5.xfrom
oddvalue:fix/tia-replay-skipped-teardown

Conversation

@oddvalue

Copy link
Copy Markdown
Contributor

What:

  • Bug Fix
  • New Feature

Description:

When TIA replays a cached Pass/Risky result, Testable::setUp() sets $__replay and returns early, and tearDown() checks $__replay and short-circuits too — neither parent::setUp() (which, in Laravel suites, boots the application) nor the user's beforeEach/afterEach chains run. That symmetry is the established semantic of replay: a replayed test executes no user hooks.

The cached Skipped, Incomplete, and Failure branches broke that symmetry: they throw (markTestSkipped() / markTestIncomplete() / AssertionFailedError) from inside setUp() before parent::setUp() has run, while leaving $__replay at ReplayType::None. PHPUnit's runBare() still invokes the after-test hooks when setUp() throws, and because $__replay was None, Pest's tearDown() did not short-circuit — the full afterEach chain and parent::tearDown() ran against a test case that was never set up (no booted app, no Facade::$app, empty container).

In a Laravel suite, any afterEach that touches the framework (e.g. DB::flushQueryLog()) then throws A facade root has not been set, and PHPUnit converts the cached skip into an error (TestCase::runBare() explicitly promotes a SkippedWithMessageException to an error when tearDown throws). This also made TIA results oscillate between runs: the errored result evicted the cached skip, the next run executed the test green and re-cached the skip, and the run after that errored again.

The fix marks the replay state before the throwing branches dispatch, so tearDown() short-circuits for every replayed result exactly as it does on the Pass/Risky path. __runTest() is unreachable after a setUp() throw, so the hoisted __beginReplay() has no effect on the Pass/Risky behaviour, and the replayed skip/incomplete statuses are re-recorded unchanged, keeping the cache stable.

The regression test seeds a TIA graph with cached pass/skipped/incomplete results for a fixture whose afterEach throws (and logs to a spy file), then replays it in a subprocess: all three results replay with their cached statuses, exit code 0, and no hook executes. Without the fix, the cached skip becomes a RuntimeException error from the afterEach hook.

Related:

…ailed tests

When Tia replays a cached skipped, incomplete, or failed result, setUp()
throws before parent::setUp() has booted anything, but $__replay was left
at ReplayType::None. PHPUnit still invokes the after-test hooks after a
setUp() throw, so tearDown() ran the full afterEach chain (and
parent::tearDown()) against a test case that was never set up. In Laravel
suites, any afterEach touching the framework then failed with "A facade
root has not been set", turning cached skips into errors and making
replay results oscillate between runs.

Mark the replay state before throwing, so tearDown() short-circuits for
every replayed result exactly as it does for the Pass/Risky replay path.

Fixes pestphp#1785
@einar-hansen

Copy link
Copy Markdown

Just want to +1 this — we ran into the exact same thing yesterday and spent half a day chasing it before finding this PR. 😅

Our setup: ~11k test Laravel suite where tearDown() resolves tenancy from the container, and 14 tests that skip at runtime when ffmpeg isn't installed. Replays kept flip-flopping — run it once, 14 errors ("A facade root has not been set"), run it again, all green, run it again, 14 errors. No code changes in between. Drove us a bit crazy until we instrumented the graph and saw the cached skips getting overwritten by the teardown errors, exactly like this PR describes.

So: confirmed independently, fix works great on a big real-world suite. Would love to see this merged 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TIA cached-replay run fires before/afterEach hooks for skipped tests outside the Laravel test bootstrap → "A facade root has not been set."

2 participants