From 64eb62a636c0f022ff7760d26b6060a9a79736b4 Mon Sep 17 00:00:00 2001 From: sonali-dudhia Date: Mon, 13 Jul 2026 19:08:10 +0530 Subject: [PATCH 1/3] fix: run `pest()->afterAll()` hooks by flushing statics after `tearDownAfterClass()` Fixes #1694 Testable::tearDown()'s finally block flushed the afterAll static after every test via TestCaseMethodFactory::tearDown(), so by the time PHPUnit called tearDownAfterClass() (which runs after the last test's tearDown()) the hook had already been nulled out. The per-test flush was redundant: TestCaseMethodFactory::setUp() already flushes statics before re-applying proxies for the next test. Move the flush into tearDownAfterClass()'s finally block instead, so it happens after the hook actually runs. Co-Authored-By: Claude Sonnet 5 --- src/Concerns/Testable.php | 9 +++---- src/Factories/TestCaseMethodFactory.php | 5 ---- tests/Hooks/AfterAllTest.php | 31 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 tests/Hooks/AfterAllTest.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 59dd5db48..af642d437 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -161,7 +161,11 @@ public static function tearDownAfterClass(): void $afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll); } - call_user_func(Closure::bind($afterAll, null, self::class)); + try { + call_user_func(Closure::bind($afterAll, null, self::class)); + } finally { + self::flush(); + } parent::tearDownAfterClass(); } @@ -292,9 +296,6 @@ protected function tearDown(...$arguments): void parent::tearDown(); TestSuite::getInstance()->test = null; - - $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $method->tearDown($this); } } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 8808cf36d..f94bd12e0 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -110,11 +110,6 @@ public function setUp(TestCase $concrete): void $this->factoryProxies->proxy($concrete); } - public function tearDown(TestCase $concrete): void - { - $concrete::flush(); // @phpstan-ignore-line - } - public function getClosure(): Closure { $closure = $this->closure; diff --git a/tests/Hooks/AfterAllTest.php b/tests/Hooks/AfterAllTest.php new file mode 100644 index 000000000..1282247dc --- /dev/null +++ b/tests/Hooks/AfterAllTest.php @@ -0,0 +1,31 @@ +calls->afterAllInFile = 0; +$_SERVER['globalHook']->calls->afterAllTestsRan = 0; + +pest()->afterAll(function () { + $_SERVER['globalHook']->calls->afterAllInFile++; +}); + +it('does not get called before all tests 1', function () { + $_SERVER['globalHook']->calls->afterAllTestsRan++; + expect($_SERVER['globalHook']->calls->afterAllInFile)->toBe(0); +})->repeat(2); + +it('does not get called before all tests 2', function () { + $_SERVER['globalHook']->calls->afterAllTestsRan++; + expect($_SERVER['globalHook']->calls->afterAllInFile)->toBe(0); +}); + +register_shutdown_function(function () { + // Under --parallel, Pest also evaluates this file in the parent process to + // build the suite; the tests (and therefore afterAll) only run in the worker. + // Skip the assertion in any process where no test of this file actually ran. + if ($_SERVER['globalHook']->calls->afterAllTestsRan === 0) { + return; + } + + assertSame(1, $_SERVER['globalHook']->calls->afterAllInFile, 'Expected the [pest()->afterAll()] hook to have been called exactly once.'); +}); From f60d5d753387450759cb6a023f96ae1503deb483 Mon Sep 17 00:00:00 2001 From: sonali-dudhia Date: Mon, 12 Oct 2026 10:52:13 +0530 Subject: [PATCH 2/3] test: assert `afterAll` hooks run via isolated fixture suite Runs a fixture suite in a subprocess and asserts both the `pest()->afterAll()` and file-level `afterAll()` hooks ran once, in order. Fails on unpatched 5.x (only `afterAll` is recorded). Co-Authored-By: Claude Opus 5.5 --- .../Fixtures/Suites/AfterAll/AfterAllTest.php | 15 +++++++++ tests/Fixtures/Suites/AfterAll/Pest.php | 7 +++++ tests/Visual/AfterAll.php | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 tests/Fixtures/Suites/AfterAll/AfterAllTest.php create mode 100644 tests/Fixtures/Suites/AfterAll/Pest.php create mode 100644 tests/Visual/AfterAll.php diff --git a/tests/Fixtures/Suites/AfterAll/AfterAllTest.php b/tests/Fixtures/Suites/AfterAll/AfterAllTest.php new file mode 100644 index 000000000..f89496e26 --- /dev/null +++ b/tests/Fixtures/Suites/AfterAll/AfterAllTest.php @@ -0,0 +1,15 @@ +toBeTrue(); +}); + +it('runs the second test', function (): void { + expect(true)->toBeTrue(); +})->repeat(2); diff --git a/tests/Fixtures/Suites/AfterAll/Pest.php b/tests/Fixtures/Suites/AfterAll/Pest.php new file mode 100644 index 000000000..0d771dc16 --- /dev/null +++ b/tests/Fixtures/Suites/AfterAll/Pest.php @@ -0,0 +1,7 @@ +afterAll(function (): void { + file_put_contents((string) getenv('PEST_AFTER_ALL_MARKER'), 'pest()->afterAll'.PHP_EOL, FILE_APPEND); +}); diff --git a/tests/Visual/AfterAll.php b/tests/Visual/AfterAll.php new file mode 100644 index 000000000..9d1a0f648 --- /dev/null +++ b/tests/Visual/AfterAll.php @@ -0,0 +1,31 @@ + 'DefaultPrinter', + 'COLLISION_IGNORE_DURATION' => 'true', + 'PAO_DISABLE' => '1', + 'PEST_AFTER_ALL_MARKER' => $marker, + ]); + + $process->run(); + + $calls = file_get_contents($marker); + + unlink($marker); + + expect($process->getExitCode())->toBe(0) + ->and($calls)->toBe('pest()->afterAll'.PHP_EOL.'afterAll'.PHP_EOL); +})->skipOnWindows(); From 3508f1eafb964fb1b74d19c2298f13fa93c0da62 Mon Sep 17 00:00:00 2001 From: sonali-dudhia Date: Mon, 12 Oct 2026 12:12:00 +0530 Subject: [PATCH 3/3] style: remove comment from afterAll hook test Co-Authored-By: Claude Opus 5.5 --- tests/Hooks/AfterAllTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Hooks/AfterAllTest.php b/tests/Hooks/AfterAllTest.php index 1282247dc..26e19b9e4 100644 --- a/tests/Hooks/AfterAllTest.php +++ b/tests/Hooks/AfterAllTest.php @@ -20,9 +20,6 @@ }); register_shutdown_function(function () { - // Under --parallel, Pest also evaluates this file in the parent process to - // build the suite; the tests (and therefore afterAll) only run in the worker. - // Skip the assertion in any process where no test of this file actually ran. if ($_SERVER['globalHook']->calls->afterAllTestsRan === 0) { return; }