fix(lifecycle): locate function-case hooks from the case file, not surviving tests - #315
Conversation
…rviving tests Hook discovery for a function-based case took the source path from the first surviving test function. Any outer case interceptor may prune tests from the case before LifecycleInterceptor runs, and a fully pruned case then silently lost its #[BeforeClass]/#[AfterClass] hooks. Read the path from CaseDefinition::$file instead: the property postdates the scan and carries the same path whenever a test survives, so behavior only changes for the fully pruned case. A definition without a real source file yields no hooks. Covered by a unit regression test driving runTestCase() over a function-based case with an emptied test set: all four hook groups are discovered from the case file and published to the inner pipeline, the class-level hooks fire exactly once around it, and a synthetic definition without a real file passes through. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, aligns with the stated lifecycle semantics for pruned cases, and is covered by a focused regression test plus a missing-file safety test.
Pull request overview
This PR fixes lifecycle hook discovery for function-based test cases when an outer case interceptor prunes all tests before LifecycleInterceptor runs. Instead of inferring the source file from surviving test functions (which fails when none survive), it now uses CaseDefinition::$file, and it safely yields “no hooks” when the definition has no real source file.
Changes:
- Update
LifecycleInterceptorto discover function-case hooks fromCaseDefinition::$file(and short-circuit to no hooks when the file is not present on disk). - Add a regression test covering the “fully pruned function-case still runs BeforeClass/AfterClass” scenario.
- Add a fixture defining lifecycle-annotated free functions with counters to validate hook execution.
File summaries
| File | Description |
|---|---|
| plugin/lifecycle/src/Internal/LifecycleInterceptor.php | Switch function-case hook discovery to use CaseDefinition::$file and return no hooks when the file doesn’t exist. |
| plugin/lifecycle/tests/Unit/Internal/LifecycleInterceptorTest.php | Add regression coverage for fully pruned function-based cases and for synthetic missing-source definitions. |
| plugin/lifecycle/tests/Unit/Fixture/PrunedFunctionsWithLifecycle.php | Add fixture with lifecycle free functions and counter state to assert hook execution order/count. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
What was changed
Hook discovery for a function-based case now reads the source path from CaseDefinition::$file instead of scanning the surviving test functions for it. A definition without a real source file yields no hooks. Covered by a unit regression test that fails on the previous code.
Why?
Any outer case interceptor may prune tests from a case before LifecycleInterceptor collects its hooks. When every test of a function-based case was pruned, the scan found no path and the case silently lost its #[BeforeClass]/#[AfterClass] hooks. The scan simply predates CaseDefinition::$file and was never migrated: whenever a test survives, both sources give the same path, so behavior only changes for the fully pruned case. The first in-tree interceptor that prunes at case level is #[Skip] (#314), which depends on this fix, so it lands separately with a Skip-independent test.
Checklist