diff --git a/src/Framework/Attributes/After.php b/src/Framework/Attributes/After.php index bdf6eb9909d..6d36d29138f 100644 --- a/src/Framework/Attributes/After.php +++ b/src/Framework/Attributes/After.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class After { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/Attributes/AfterClass.php b/src/Framework/Attributes/AfterClass.php index 14b854934b8..d4a9d6f4c5e 100644 --- a/src/Framework/Attributes/AfterClass.php +++ b/src/Framework/Attributes/AfterClass.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class AfterClass { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/Attributes/Before.php b/src/Framework/Attributes/Before.php index b30af6deef2..c2af00b7fa2 100644 --- a/src/Framework/Attributes/Before.php +++ b/src/Framework/Attributes/Before.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class Before { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/Attributes/BeforeClass.php b/src/Framework/Attributes/BeforeClass.php index 1159fb93126..2bb5a07b5f3 100644 --- a/src/Framework/Attributes/BeforeClass.php +++ b/src/Framework/Attributes/BeforeClass.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class BeforeClass { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/Attributes/PostCondition.php b/src/Framework/Attributes/PostCondition.php index af6312dde41..8eb40fe03d6 100644 --- a/src/Framework/Attributes/PostCondition.php +++ b/src/Framework/Attributes/PostCondition.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class PostCondition { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/Attributes/PreCondition.php b/src/Framework/Attributes/PreCondition.php index 583c2b2a687..5f47fc599a0 100644 --- a/src/Framework/Attributes/PreCondition.php +++ b/src/Framework/Attributes/PreCondition.php @@ -19,4 +19,15 @@ #[Attribute(Attribute::TARGET_METHOD)] final readonly class PreCondition { + private int $priority; + + public function __construct(int $priority = 0) + { + $this->priority = $priority; + } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index d671832d0f2..1ad10c99526 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -89,6 +89,7 @@ use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Metadata\Api\Groups; use PHPUnit\Metadata\Api\HookMethods; +use PHPUnit\Metadata\Api\HookMethodsCollection; use PHPUnit\Metadata\Api\Requirements; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Runner\DeprecationCollector\Facade as DeprecationCollector; @@ -2294,7 +2295,7 @@ private function performAssertionsOnOutput(): void } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable * @@ -2311,7 +2312,7 @@ private function invokeBeforeClassHookMethods(array $hookMethods, Event\Emitter } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable */ @@ -2326,7 +2327,7 @@ private function invokeBeforeTestHookMethods(array $hookMethods, Event\Emitter $ } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable */ @@ -2341,7 +2342,7 @@ private function invokePreConditionHookMethods(array $hookMethods, Event\Emitter } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable */ @@ -2356,7 +2357,7 @@ private function invokePostConditionHookMethods(array $hookMethods, Event\Emitte } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable */ @@ -2371,7 +2372,7 @@ private function invokeAfterTestHookMethods(array $hookMethods, Event\Emitter $e } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods + * @param array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} $hookMethods * * @throws Throwable * @@ -2388,18 +2389,16 @@ private function invokeAfterClassHookMethods(array $hookMethods, Event\Emitter $ } /** - * @param array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} $hookMethods - * @param 'testAfterLastTestMethodCalled'|'testAfterTestMethodCalled'|'testBeforeFirstTestMethodCalled'|'testBeforeTestMethodCalled'|'testPostConditionCalled'|'testPreConditionCalled' $calledMethod - * @param 'testAfterLastTestMethodFinished'|'testAfterTestMethodFinished'|'testBeforeFirstTestMethodFinished'|'testBeforeTestMethodFinished'|'testPostConditionFinished'|'testPreConditionFinished' $finishedMethod - * @param list $hookMethods + * @param 'testAfterLastTestMethodCalled'|'testAfterTestMethodCalled'|'testBeforeFirstTestMethodCalled'|'testBeforeTestMethodCalled'|'testPostConditionCalled'|'testPreConditionCalled' $calledMethod + * @param 'testAfterLastTestMethodFinished'|'testAfterTestMethodFinished'|'testBeforeFirstTestMethodFinished'|'testBeforeTestMethodFinished'|'testPostConditionFinished'|'testPreConditionFinished' $finishedMethod * * @throws Throwable */ - private function invokeHookMethods(array $hookMethods, Event\Emitter $emitter, string $calledMethod, string $finishedMethod): void + private function invokeHookMethods(HookMethodsCollection $hookMethodsCollection, Event\Emitter $emitter, string $calledMethod, string $finishedMethod): void { $methodsInvoked = []; - foreach ($hookMethods as $methodName) { + foreach ($hookMethodsCollection as $methodName) { if ($this->methodDoesNotExistOrIsDeclaredInTestCase($methodName)) { continue; } diff --git a/src/Metadata/After.php b/src/Metadata/After.php index 57696cff4cf..ab77eeca390 100644 --- a/src/Metadata/After.php +++ b/src/Metadata/After.php @@ -16,8 +16,25 @@ */ final readonly class After extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isAfter(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Metadata/AfterClass.php b/src/Metadata/AfterClass.php index 35a3ec20b89..3f28ff8e71c 100644 --- a/src/Metadata/AfterClass.php +++ b/src/Metadata/AfterClass.php @@ -16,8 +16,25 @@ */ final readonly class AfterClass extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isAfterClass(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Metadata/Api/HookMethod.php b/src/Metadata/Api/HookMethod.php new file mode 100644 index 00000000000..cda0073179f --- /dev/null +++ b/src/Metadata/Api/HookMethod.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Metadata\Api; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +final readonly class HookMethod +{ + public function __construct( + public string $methodName, + public int $priority = 0, + ) { + } +} diff --git a/src/Metadata/Api/HookMethods.php b/src/Metadata/Api/HookMethods.php index a529f4ab779..7be1ca4e7a2 100644 --- a/src/Metadata/Api/HookMethods.php +++ b/src/Metadata/Api/HookMethods.php @@ -9,11 +9,16 @@ */ namespace PHPUnit\Metadata\Api; -use function array_unshift; use function assert; use function class_exists; use PHPUnit\Framework\TestCase; +use PHPUnit\Metadata\After; +use PHPUnit\Metadata\AfterClass; +use PHPUnit\Metadata\Before; +use PHPUnit\Metadata\BeforeClass; use PHPUnit\Metadata\Parser\Registry; +use PHPUnit\Metadata\PostCondition; +use PHPUnit\Metadata\PreCondition; use PHPUnit\Util\Reflection; use ReflectionClass; @@ -23,14 +28,14 @@ final class HookMethods { /** - * @var array, before: list, preCondition: list, postCondition: list, after: list, afterClass: list}> + * @var array */ private static array $hookMethods = []; /** * @param class-string $className * - * @return array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} + * @return array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} */ public function hookMethods(string $className): array { @@ -53,37 +58,58 @@ public function hookMethods(string $className): array if ($method->isStatic()) { if ($metadata->isBeforeClass()->isNotEmpty()) { - array_unshift( - self::$hookMethods[$className]['beforeClass'], - $methodName, + $beforeClass = $metadata->isBeforeClass()->asArray()[0]; + assert($beforeClass instanceof BeforeClass); + + self::$hookMethods[$className]['beforeClass']->add( + new HookMethod($methodName, $beforeClass->priority()), ); } if ($metadata->isAfterClass()->isNotEmpty()) { - self::$hookMethods[$className]['afterClass'][] = $methodName; + $afterClass = $metadata->isAfterClass()->asArray()[0]; + assert($afterClass instanceof AfterClass); + + self::$hookMethods[$className]['afterClass']->add( + new HookMethod($methodName, $afterClass->priority()), + ); } } if ($metadata->isBefore()->isNotEmpty()) { - array_unshift( - self::$hookMethods[$className]['before'], - $methodName, + $before = $metadata->isBefore()->asArray()[0]; + assert($before instanceof Before); + + self::$hookMethods[$className]['before']->add( + new HookMethod($methodName, $before->priority()), ); } if ($metadata->isPreCondition()->isNotEmpty()) { - array_unshift( - self::$hookMethods[$className]['preCondition'], - $methodName, + $preCondition = $metadata->isPreCondition()->asArray()[0]; + assert($preCondition instanceof PreCondition); + + self::$hookMethods[$className]['preCondition']->add( + new HookMethod($methodName, $preCondition->priority()), ); } if ($metadata->isPostCondition()->isNotEmpty()) { - self::$hookMethods[$className]['postCondition'][] = $methodName; + $postCondition = $metadata->isPostCondition()->asArray()[0]; + assert($postCondition instanceof PostCondition); + + self::$hookMethods[$className]['postCondition']->add( + new HookMethod($methodName, $postCondition->priority()), + ); } if ($metadata->isAfter()->isNotEmpty()) { - self::$hookMethods[$className]['after'][] = $methodName; + $after = $metadata->isAfter()->asArray()[0]; + assert($after instanceof After); + + self::$hookMethods[$className]['after']->add( + new HookMethod($methodName, $after->priority()), + ); } } @@ -91,17 +117,17 @@ public function hookMethods(string $className): array } /** - * @return array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} + * @return array{beforeClass: HookMethodsCollection, before: HookMethodsCollection, preCondition: HookMethodsCollection, postCondition: HookMethodsCollection, after: HookMethodsCollection, afterClass: HookMethodsCollection} */ private function emptyHookMethodsArray(): array { return [ - 'beforeClass' => ['setUpBeforeClass'], - 'before' => ['setUp'], - 'preCondition' => ['assertPreConditions'], - 'postCondition' => ['assertPostConditions'], - 'after' => ['tearDown'], - 'afterClass' => ['tearDownAfterClass'], + 'beforeClass' => HookMethodsCollection::defaultBeforeClass(), + 'before' => HookMethodsCollection::defaultBefore(), + 'preCondition' => HookMethodsCollection::defaultPreCondition(), + 'postCondition' => HookMethodsCollection::defaultPostCondition(), + 'after' => HookMethodsCollection::defaultAfter(), + 'afterClass' => HookMethodsCollection::defaultAfterClass(), ]; } } diff --git a/src/Metadata/Api/HookMethodsCollection.php b/src/Metadata/Api/HookMethodsCollection.php new file mode 100644 index 00000000000..6bb949b0755 --- /dev/null +++ b/src/Metadata/Api/HookMethodsCollection.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Metadata\Api; + +use function array_map; +use function count; +use function usort; +use ArrayObject; +use Countable; +use IteratorAggregate; +use Traversable; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + * + * @implements IteratorAggregate + */ +final class HookMethodsCollection implements Countable, IteratorAggregate +{ + /** + * @var non-empty-list + */ + private array $hookMethods; + + public static function defaultBeforeClass(): static + { + return new self(new HookMethod('setUpBeforeClass', priority: 0), shouldPrepend: true); + } + + public static function defaultBefore(): static + { + return new self(new HookMethod('setUp', priority: 0), shouldPrepend: true); + } + + public static function defaultPreCondition(): static + { + return new self(new HookMethod('assertPreConditions', priority: 0), shouldPrepend: true); + } + + public static function defaultPostCondition(): static + { + return new self(new HookMethod('assertPostConditions', priority: 0)); + } + + public static function defaultAfter(): static + { + return new self(new HookMethod('tearDown', priority: 0)); + } + + public static function defaultAfterClass(): static + { + return new self(new HookMethod('tearDownAfterClass', priority: 0)); + } + + private function __construct(HookMethod $default, private bool $shouldPrepend = false) + { + $this->hookMethods = [$default]; + } + + public function add(HookMethod $hookMethod): static + { + if ($this->shouldPrepend) { + $this->hookMethods = [$hookMethod, ...$this->hookMethods]; + } else { + $this->hookMethods[] = $hookMethod; + } + + return $this; + } + + public function getIterator(): Traversable + { + $hookMethods = $this->hookMethods; + + usort($hookMethods, static fn (HookMethod $hookMethod1, HookMethod $hookMethod2) => $hookMethod2->priority <=> $hookMethod1->priority); + + return new ArrayObject( + array_map(static fn (HookMethod $hookMethod) => $hookMethod->methodName, $hookMethods), + ); + } + + public function count(): int + { + return count($this->hookMethods); + } +} diff --git a/src/Metadata/Before.php b/src/Metadata/Before.php index 95ec085b7bd..6a531c960af 100644 --- a/src/Metadata/Before.php +++ b/src/Metadata/Before.php @@ -16,8 +16,25 @@ */ final readonly class Before extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isBefore(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Metadata/BeforeClass.php b/src/Metadata/BeforeClass.php index 17fecb394f4..e640a772fd9 100644 --- a/src/Metadata/BeforeClass.php +++ b/src/Metadata/BeforeClass.php @@ -16,8 +16,25 @@ */ final readonly class BeforeClass extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isBeforeClass(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index e55e858d8fa..f97bfa94923 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -26,14 +26,14 @@ */ private int $level; - public static function after(): After + public static function after(int $priority = 0): After { - return new After(self::METHOD_LEVEL); + return new After(self::METHOD_LEVEL, $priority); } - public static function afterClass(): AfterClass + public static function afterClass(int $priority = 0): AfterClass { - return new AfterClass(self::METHOD_LEVEL); + return new AfterClass(self::METHOD_LEVEL, $priority); } public static function backupGlobalsOnClass(bool $enabled): BackupGlobals @@ -56,14 +56,14 @@ public static function backupStaticPropertiesOnMethod(bool $enabled): BackupStat return new BackupStaticProperties(self::METHOD_LEVEL, $enabled); } - public static function before(): Before + public static function before(int $priority = 0): Before { - return new Before(self::METHOD_LEVEL); + return new Before(self::METHOD_LEVEL, $priority); } - public static function beforeClass(): BeforeClass + public static function beforeClass(int $priority = 0): BeforeClass { - return new BeforeClass(self::METHOD_LEVEL); + return new BeforeClass(self::METHOD_LEVEL, $priority); } /** @@ -250,14 +250,14 @@ public static function ignorePhpunitDeprecationsOnMethod(): IgnorePhpunitDepreca return new IgnorePhpunitDeprecations(self::METHOD_LEVEL); } - public static function postCondition(): PostCondition + public static function postCondition(int $priority = 0): PostCondition { - return new PostCondition(self::METHOD_LEVEL); + return new PostCondition(self::METHOD_LEVEL, $priority); } - public static function preCondition(): PreCondition + public static function preCondition(int $priority = 0): PreCondition { - return new PreCondition(self::METHOD_LEVEL); + return new PreCondition(self::METHOD_LEVEL, $priority); } public static function preserveGlobalStateOnClass(bool $enabled): PreserveGlobalState diff --git a/src/Metadata/Parser/AttributeParser.php b/src/Metadata/Parser/AttributeParser.php index 254aea2b9a6..4b044d12851 100644 --- a/src/Metadata/Parser/AttributeParser.php +++ b/src/Metadata/Parser/AttributeParser.php @@ -378,12 +378,16 @@ public function forMethod(string $className, string $methodName): MetadataCollec switch ($attribute->getName()) { case After::class: - $result[] = Metadata::after(); + assert($attributeInstance instanceof After); + + $result[] = Metadata::after($attributeInstance->priority()); break; case AfterClass::class: - $result[] = Metadata::afterClass(); + assert($attributeInstance instanceof AfterClass); + + $result[] = Metadata::afterClass($attributeInstance->priority()); break; @@ -402,12 +406,16 @@ public function forMethod(string $className, string $methodName): MetadataCollec break; case Before::class: - $result[] = Metadata::before(); + assert($attributeInstance instanceof Before); + + $result[] = Metadata::before($attributeInstance->priority()); break; case BeforeClass::class: - $result[] = Metadata::beforeClass(); + assert($attributeInstance instanceof BeforeClass); + + $result[] = Metadata::beforeClass($attributeInstance->priority()); break; @@ -541,12 +549,16 @@ public function forMethod(string $className, string $methodName): MetadataCollec break; case PostCondition::class: - $result[] = Metadata::postCondition(); + assert($attributeInstance instanceof PostCondition); + + $result[] = Metadata::postCondition($attributeInstance->priority()); break; case PreCondition::class: - $result[] = Metadata::preCondition(); + assert($attributeInstance instanceof PreCondition); + + $result[] = Metadata::preCondition($attributeInstance->priority()); break; diff --git a/src/Metadata/PostCondition.php b/src/Metadata/PostCondition.php index 0cfdee3dd1b..53ddeecde1a 100644 --- a/src/Metadata/PostCondition.php +++ b/src/Metadata/PostCondition.php @@ -16,8 +16,25 @@ */ final readonly class PostCondition extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isPostCondition(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/src/Metadata/PreCondition.php b/src/Metadata/PreCondition.php index 862c79f97df..420993a1a74 100644 --- a/src/Metadata/PreCondition.php +++ b/src/Metadata/PreCondition.php @@ -16,8 +16,25 @@ */ final readonly class PreCondition extends Metadata { + private int $priority; + + /** + * @param 0|1 $level + */ + protected function __construct(int $level, int $priority) + { + parent::__construct($level); + + $this->priority = $priority; + } + public function isPreCondition(): bool { return true; } + + public function priority(): int + { + return $this->priority; + } } diff --git a/tests/_files/TestWithHookMethodsPrioritizedTest.php b/tests/_files/TestWithHookMethodsPrioritizedTest.php new file mode 100644 index 00000000000..96b0afe9c24 --- /dev/null +++ b/tests/_files/TestWithHookMethodsPrioritizedTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture; + +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\AfterClass; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\PostCondition; +use PHPUnit\Framework\Attributes\PreCondition; +use PHPUnit\Framework\TestCase; + +final class TestWithHookMethodsPrioritizedTest extends TestCase +{ + #[BeforeClass(priority: 1)] + public static function beforeFirstTest(): void + { + } + + #[AfterClass(priority: 6)] + public static function afterLastTest(): void + { + } + + #[Before(priority: 2)] + protected function beforeEachTest(): void + { + } + + #[PreCondition(priority: 3)] + protected function preConditions(): void + { + } + + #[PostCondition(priority: 4)] + protected function postConditions(): void + { + } + + #[After(priority: 5)] + protected function afterEachTest(): void + { + } +} diff --git a/tests/end-to-end/_files/BeforeTestMethodWithPrioritizedAttributeTest.php b/tests/end-to-end/_files/BeforeTestMethodWithPrioritizedAttributeTest.php new file mode 100644 index 00000000000..053ee663bcf --- /dev/null +++ b/tests/end-to-end/_files/BeforeTestMethodWithPrioritizedAttributeTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\DeprecatedAnnotationsTestFixture; + +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\TestCase; + +final class BeforeTestMethodWithPrioritizedAttributeTest extends TestCase +{ + protected function setUp(): void + { + } + + public function testOne(): void + { + $this->assertTrue(true); + } + + #[Before(priority: 1)] + protected function beforeMethodWithHighPriority(): void + { + } + + #[Before(priority: -1)] + protected function beforeMethodWithLowPriority(): void + { + } +} diff --git a/tests/end-to-end/metadata/before-test-method-configured-with-prioritized-attribute.phpt b/tests/end-to-end/metadata/before-test-method-configured-with-prioritized-attribute.phpt new file mode 100644 index 00000000000..27689e2ac65 --- /dev/null +++ b/tests/end-to-end/metadata/before-test-method-configured-with-prioritized-attribute.phpt @@ -0,0 +1,44 @@ +--TEST-- +The right events are emitted in the right order for a successful test that has a before-test method that is configured with attribute +--FILE-- +run($_SERVER['argv']); + +print file_get_contents($traceFile); + +unlink($traceFile); +--EXPECTF-- +PHPUnit Started (PHPUnit %s using %s) +Test Runner Configured +Test Suite Loaded (1 test) +Event Facade Sealed +Test Runner Started +Test Suite Sorted +Test Runner Execution Started (1 test) +Test Suite Started (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest, 1 test) +Test Preparation Started (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::testOne) +Before Test Method Called (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::beforeMethodWithHighPriority) +Before Test Method Called (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::setUp) +Before Test Method Called (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::beforeMethodWithLowPriority) +Before Test Method Finished: +- PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::beforeMethodWithHighPriority +- PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::setUp +- PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::beforeMethodWithLowPriority +Test Prepared (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::testOne) +Test Passed (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::testOne) +Test Finished (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest::testOne) +Test Suite Finished (PHPUnit\DeprecatedAnnotationsTestFixture\BeforeTestMethodWithPrioritizedAttributeTest, 1 test) +Test Runner Execution Finished +Test Runner Finished +PHPUnit Finished (Shell Exit Code: 0) diff --git a/tests/unit/Metadata/Api/HookMethodsCollectionTest.php b/tests/unit/Metadata/Api/HookMethodsCollectionTest.php new file mode 100644 index 00000000000..dd9f9e92793 --- /dev/null +++ b/tests/unit/Metadata/Api/HookMethodsCollectionTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace unit\Metadata\Api; + +use function iterator_to_array; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Small; +use PHPUnit\Framework\TestCase; +use PHPUnit\Metadata\Api\HookMethod; +use PHPUnit\Metadata\Api\HookMethodsCollection; + +#[CoversClass(HookMethodsCollection::class)] +#[Small] +#[Group('metadata')] +final class HookMethodsCollectionTest extends TestCase +{ + public static function provider(): iterable + { + return [ + [ + HookMethodsCollection::defaultBeforeClass()->add(new HookMethod('someMethod')), + ['someMethod', 'setUpBeforeClass'], + ], + [ + HookMethodsCollection::defaultBefore()->add(new HookMethod('someMethod')), + ['someMethod', 'setUp'], + ], + [ + HookMethodsCollection::defaultPreCondition()->add(new HookMethod('someMethod')), + ['someMethod', 'assertPreConditions'], + ], + [ + HookMethodsCollection::defaultPostCondition()->add(new HookMethod('someMethod')), + ['assertPostConditions', 'someMethod'], + ], + [ + HookMethodsCollection::defaultAfter()->add(new HookMethod('someMethod')), + ['tearDown', 'someMethod'], + ], + [ + HookMethodsCollection::defaultAfterClass()->add(new HookMethod('someMethod')), + ['tearDownAfterClass', 'someMethod'], + ], + [ + HookMethodsCollection::defaultBeforeClass() + ->add(new HookMethod('methodWithHighPriority', priority: 1)) + ->add(new HookMethod('methodWithVeryLowPriority', priority: -10)) + ->add(new HookMethod('methodWithLowPriority', priority: -1)) + ->add(new HookMethod('methodWithVeryHighPriority', priority: 10)) + ->add(new HookMethod('methodWithoutPriority')), + [ + 'methodWithVeryHighPriority', + 'methodWithHighPriority', + 'methodWithoutPriority', + 'setUpBeforeClass', + 'methodWithLowPriority', + 'methodWithVeryLowPriority', + ], + ], + ]; + } + + #[DataProvider('provider')] + public function testIterator(HookMethodsCollection $hookMethodsCollection, array $expected): void + { + $this->assertSame($expected, iterator_to_array($hookMethodsCollection)); + } +} diff --git a/tests/unit/Metadata/Api/HookMethodsTest.php b/tests/unit/Metadata/Api/HookMethodsTest.php index 24561868889..0580989708a 100644 --- a/tests/unit/Metadata/Api/HookMethodsTest.php +++ b/tests/unit/Metadata/Api/HookMethodsTest.php @@ -9,10 +9,12 @@ */ namespace PHPUnit\Metadata\Api; +use function array_keys; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; +use PHPUnit\TestFixture\TestWithHookMethodsPrioritizedTest; use PHPUnit\TestFixture\TestWithHookMethodsTest; use PHPUnit\TestFixture\TestWithoutHookMethodsTest; @@ -23,26 +25,14 @@ final class HookMethodsTest extends TestCase { public function testReturnsDefaultHookMethodsForClassThatDoesNotExist(): void { - $this->assertSame( + $this->assertEquals( [ - 'beforeClass' => [ - 'setUpBeforeClass', - ], - 'before' => [ - 'setUp', - ], - 'preCondition' => [ - 'assertPreConditions', - ], - 'postCondition' => [ - 'assertPostConditions', - ], - 'after' => [ - 'tearDown', - ], - 'afterClass' => [ - 'tearDownAfterClass', - ], + 'beforeClass' => HookMethodsCollection::defaultBeforeClass(), + 'before' => HookMethodsCollection::defaultBefore(), + 'preCondition' => HookMethodsCollection::defaultPreCondition(), + 'postCondition' => HookMethodsCollection::defaultPostCondition(), + 'after' => HookMethodsCollection::defaultAfter(), + 'afterClass' => HookMethodsCollection::defaultAfterClass(), ], (new HookMethods)->hookMethods('does not exist'), ); @@ -50,26 +40,14 @@ public function testReturnsDefaultHookMethodsForClassThatDoesNotExist(): void public function testReturnsDefaultHookMethodsInTestClassWithoutHookMethods(): void { - $this->assertSame( + $this->assertEquals( [ - 'beforeClass' => [ - 'setUpBeforeClass', - ], - 'before' => [ - 'setUp', - ], - 'preCondition' => [ - 'assertPreConditions', - ], - 'postCondition' => [ - 'assertPostConditions', - ], - 'after' => [ - 'tearDown', - ], - 'afterClass' => [ - 'tearDownAfterClass', - ], + 'beforeClass' => HookMethodsCollection::defaultBeforeClass(), + 'before' => HookMethodsCollection::defaultBefore(), + 'preCondition' => HookMethodsCollection::defaultPreCondition(), + 'postCondition' => HookMethodsCollection::defaultPostCondition(), + 'after' => HookMethodsCollection::defaultAfter(), + 'afterClass' => HookMethodsCollection::defaultAfterClass(), ], (new HookMethods)->hookMethods(TestWithoutHookMethodsTest::class), ); @@ -77,40 +55,67 @@ public function testReturnsDefaultHookMethodsInTestClassWithoutHookMethods(): vo public function testFindsHookMethodsInTestClassWithHookMethods(): void { - $this->assertSame( - [ - 'beforeClass' => [ - 'beforeFirstTestWithAnnotation', - 'beforeFirstTestWithAttribute', - 'setUpBeforeClass', - ], - 'before' => [ - 'beforeEachTestWithAnnotation', - 'beforeEachTestWithAttribute', - 'setUp', - ], - 'preCondition' => [ - 'preConditionsWithAnnotation', - 'preConditionsWithAttribute', - 'assertPreConditions', - ], - 'postCondition' => [ - 'assertPostConditions', - 'postConditionsWithAttribute', - 'postConditionsWithAnnotation', - ], - 'after' => [ - 'tearDown', - 'afterEachTestWithAttribute', - 'afterEachTestWithAnnotation', - ], - 'afterClass' => [ - 'tearDownAfterClass', - 'afterLastTestWithAttribute', - 'afterLastTestWithAnnotation', - ], - ], - (new HookMethods)->hookMethods(TestWithHookMethodsTest::class), - ); + $hookMethods = (new HookMethods)->hookMethods(TestWithHookMethodsTest::class); + $this->assertSame(['beforeClass', 'before', 'preCondition', 'postCondition', 'after', 'afterClass'], array_keys($hookMethods)); + + $beforeClassHooks = HookMethodsCollection::defaultBeforeClass(); + $beforeClassHooks->add(new HookMethod('beforeFirstTestWithAttribute')); + $beforeClassHooks->add(new HookMethod('beforeFirstTestWithAnnotation')); + $this->assertEquals($beforeClassHooks, $hookMethods['beforeClass']); + + $beforeHooks = HookMethodsCollection::defaultBefore(); + $beforeHooks->add(new HookMethod('beforeEachTestWithAttribute')); + $beforeHooks->add(new HookMethod('beforeEachTestWithAnnotation')); + $this->assertEquals($beforeHooks, $hookMethods['before']); + + $preConditionHooks = HookMethodsCollection::defaultPreCondition(); + $preConditionHooks->add(new HookMethod('preConditionsWithAttribute')); + $preConditionHooks->add(new HookMethod('preConditionsWithAnnotation')); + $this->assertEquals($preConditionHooks, $hookMethods['preCondition']); + + $postConditionHooks = HookMethodsCollection::defaultPostCondition(); + $postConditionHooks->add(new HookMethod('postConditionsWithAttribute')); + $postConditionHooks->add(new HookMethod('postConditionsWithAnnotation')); + $this->assertEquals($postConditionHooks, $hookMethods['postCondition']); + + $afterHooks = HookMethodsCollection::defaultAfter(); + $afterHooks->add(new HookMethod('afterEachTestWithAttribute')); + $afterHooks->add(new HookMethod('afterEachTestWithAnnotation')); + $this->assertEquals($afterHooks, $hookMethods['after']); + + $afterClassHooks = HookMethodsCollection::defaultAfterClass(); + $afterClassHooks->add(new HookMethod('afterLastTestWithAttribute')); + $afterClassHooks->add(new HookMethod('afterLastTestWithAnnotation')); + $this->assertEquals($afterClassHooks, $hookMethods['afterClass']); + } + + public function testFindsHookMethodsInTestClassWithHookMethodsPrioritized(): void + { + $hookMethods = (new HookMethods)->hookMethods(TestWithHookMethodsPrioritizedTest::class); + $this->assertSame(['beforeClass', 'before', 'preCondition', 'postCondition', 'after', 'afterClass'], array_keys($hookMethods)); + + $beforeClassHooks = HookMethodsCollection::defaultBeforeClass(); + $beforeClassHooks->add(new HookMethod('beforeFirstTest', priority: 1)); + $this->assertEquals($beforeClassHooks, $hookMethods['beforeClass']); + + $beforeHooks = HookMethodsCollection::defaultBefore(); + $beforeHooks->add(new HookMethod('beforeEachTest', priority: 2)); + $this->assertEquals($beforeHooks, $hookMethods['before']); + + $preConditionHooks = HookMethodsCollection::defaultPreCondition(); + $preConditionHooks->add(new HookMethod('preConditions', priority: 3)); + $this->assertEquals($preConditionHooks, $hookMethods['preCondition']); + + $postConditionHooks = HookMethodsCollection::defaultPostCondition(); + $postConditionHooks->add(new HookMethod('postConditions', priority: 4)); + $this->assertEquals($postConditionHooks, $hookMethods['postCondition']); + + $afterHooks = HookMethodsCollection::defaultAfter(); + $afterHooks->add(new HookMethod('afterEachTest', priority: 5)); + $this->assertEquals($afterHooks, $hookMethods['after']); + + $afterClassHooks = HookMethodsCollection::defaultAfterClass(); + $afterClassHooks->add(new HookMethod('afterLastTest', priority: 6)); + $this->assertEquals($afterClassHooks, $hookMethods['afterClass']); } }