Skip to content

Commit aa69cc0

Browse files
committed
#[RequiresPhpunitExtension] attribute
1 parent 29be78d commit aa69cc0

File tree

12 files changed

+431
-0
lines changed

12 files changed

+431
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Framework\Attributes;
11+
12+
use Attribute;
13+
use PHPUnit\Runner\Extension\Extension;
14+
15+
/**
16+
* @immutable
17+
*
18+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
19+
*/
20+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
21+
final readonly class RequiresPhpunitExtension
22+
{
23+
/**
24+
* @var list<class-string<Extension>>
25+
*/
26+
private array $extensionClasses;
27+
28+
/**
29+
* @param class-string<Extension> $extensionClasses
30+
*/
31+
public function __construct(string ...$extensionClasses)
32+
{
33+
$this->extensionClasses = $extensionClasses;
34+
}
35+
36+
/**
37+
* @return list<class-string<Extension>>
38+
*/
39+
public function extensionClasses(): array
40+
{
41+
return $this->extensionClasses;
42+
}
43+
}

src/Metadata/Api/Requirements.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
use const PHP_OS_FAMILY;
1414
use const PHP_VERSION;
1515
use function addcslashes;
16+
use function array_column;
17+
use function array_diff;
1618
use function assert;
1719
use function extension_loaded;
1820
use function function_exists;
21+
use function implode;
1922
use function ini_get;
2023
use function method_exists;
2124
use function phpversion;
@@ -29,8 +32,10 @@
2932
use PHPUnit\Metadata\RequiresPhp;
3033
use PHPUnit\Metadata\RequiresPhpExtension;
3134
use PHPUnit\Metadata\RequiresPhpunit;
35+
use PHPUnit\Metadata\RequiresPhpunitExtension;
3236
use PHPUnit\Metadata\RequiresSetting;
3337
use PHPUnit\Runner\Version;
38+
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
3439

3540
/**
3641
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -86,6 +91,21 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam
8691
}
8792
}
8893

94+
if ($metadata->isRequiresPhpunitExtension()) {
95+
assert($metadata instanceof RequiresPhpunitExtension);
96+
97+
$configuration = ConfigurationRegistry::get();
98+
99+
$extensionBootstrappers = array_column($configuration->extensionBootstrappers(), 'className');
100+
101+
if ($configuration->noExtensions() || $missingExtensions = array_diff($metadata->extensionClasses(), $extensionBootstrappers)) {
102+
$notSatisfied[] = sprintf(
103+
'PHPUnit extension(s) "%s" required.',
104+
implode('", "', $missingExtensions ?? $metadata->extensionClasses()),
105+
);
106+
}
107+
}
108+
89109
if ($metadata->isRequiresOperatingSystemFamily()) {
90110
assert($metadata instanceof RequiresOperatingSystemFamily);
91111

src/Metadata/Metadata.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace PHPUnit\Metadata;
1111

1212
use PHPUnit\Metadata\Version\Requirement;
13+
use PHPUnit\Runner\Extension\Extension;
1314

1415
/**
1516
* @immutable
@@ -390,6 +391,22 @@ public static function requiresPhpunitOnMethod(Requirement $versionRequirement):
390391
return new RequiresPhpunit(self::METHOD_LEVEL, $versionRequirement);
391392
}
392393

394+
/**
395+
* @param class-string<Extension> $extensionClasses
396+
*/
397+
public static function requiresPhpunitExtensionOnClass(string ...$extensionClasses): RequiresPhpunitExtension
398+
{
399+
return new RequiresPhpunitExtension(self::CLASS_LEVEL, ...$extensionClasses);
400+
}
401+
402+
/**
403+
* @param class-string<Extension> $extensionClasses
404+
*/
405+
public static function requiresPhpunitExtensionOnMethod(string ...$extensionClasses): RequiresPhpunitExtension
406+
{
407+
return new RequiresPhpunitExtension(self::METHOD_LEVEL, ...$extensionClasses);
408+
}
409+
393410
/**
394411
* @param non-empty-string $setting
395412
* @param non-empty-string $value
@@ -831,6 +848,14 @@ public function isRequiresPhpunit(): bool
831848
return false;
832849
}
833850

851+
/**
852+
* @phpstan-assert-if-true RequiresPhpunitExtension $this
853+
*/
854+
public function isRequiresPhpunitExtension(): bool
855+
{
856+
return false;
857+
}
858+
834859
/**
835860
* @phpstan-assert-if-true RequiresSetting $this
836861
*/

src/Metadata/MetadataCollection.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,16 @@ public function isRequiresPhpunit(): self
483483
);
484484
}
485485

486+
public function isRequiresPhpunitExtension(): self
487+
{
488+
return new self(
489+
...array_filter(
490+
$this->metadata,
491+
static fn (Metadata $metadata): bool => $metadata->isRequiresPhpunitExtension(),
492+
),
493+
);
494+
}
495+
486496
public function isRequiresSetting(): self
487497
{
488498
return new self(

src/Metadata/Parser/AttributeParser.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use PHPUnit\Framework\Attributes\RequiresPhp;
5959
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
6060
use PHPUnit\Framework\Attributes\RequiresPhpunit;
61+
use PHPUnit\Framework\Attributes\RequiresPhpunitExtension;
6162
use PHPUnit\Framework\Attributes\RequiresSetting;
6263
use PHPUnit\Framework\Attributes\RunClassInSeparateProcess;
6364
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
@@ -288,6 +289,15 @@ public function forClass(string $className): MetadataCollection
288289

289290
break;
290291

292+
case RequiresPhpunitExtension::class:
293+
assert($attributeInstance instanceof RequiresPhpunitExtension);
294+
295+
$result[] = Metadata::requiresPhpunitExtensionOnClass(
296+
...$attributeInstance->extensionClasses(),
297+
);
298+
299+
break;
300+
291301
case RequiresSetting::class:
292302
assert($attributeInstance instanceof RequiresSetting);
293303

@@ -641,6 +651,15 @@ public function forMethod(string $className, string $methodName): MetadataCollec
641651

642652
break;
643653

654+
case RequiresPhpunitExtension::class:
655+
assert($attributeInstance instanceof RequiresPhpunitExtension);
656+
657+
$result[] = Metadata::requiresPhpunitExtensionOnMethod(
658+
...$attributeInstance->extensionClasses(),
659+
);
660+
661+
break;
662+
644663
case RequiresSetting::class:
645664
assert($attributeInstance instanceof RequiresSetting);
646665

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Metadata;
11+
12+
use PHPUnit\Runner\Extension\Extension;
13+
14+
/**
15+
* @immutable
16+
*
17+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
final readonly class RequiresPhpunitExtension extends Metadata
20+
{
21+
/**
22+
* @var list<class-string<Extension>>
23+
*/
24+
private array $extensionClasses;
25+
26+
/**
27+
* @param class-string<Extension> $extensionClasses
28+
*/
29+
public function __construct(int $level, string ...$extensionClasses)
30+
{
31+
parent::__construct($level);
32+
33+
$this->extensionClasses = $extensionClasses;
34+
}
35+
36+
public function isRequiresPhpunitExtension(): true
37+
{
38+
return true;
39+
}
40+
41+
/**
42+
* @return list<class-string<Extension>>
43+
*/
44+
public function extensionClasses(): array
45+
{
46+
return $this->extensionClasses;
47+
}
48+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\TestFixture\Metadata\Attribute;
11+
12+
use PHPUnit\Framework\Attributes\RequiresPhpunitExtension;
13+
use PHPUnit\Framework\TestCase;
14+
15+
#[RequiresPhpunitExtension(SomeClass::class)]
16+
final class RequiresPhpunitExtensionTest extends TestCase
17+
{
18+
#[RequiresPhpunitExtension(SomeClass::class, SomeOtherClass::class)]
19+
public function testOne(): void
20+
{
21+
}
22+
}

tests/_files/RequirementsTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPUnit\Framework\Attributes\RequiresPhp;
1717
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
1818
use PHPUnit\Framework\Attributes\RequiresPhpunit;
19+
use PHPUnit\Framework\Attributes\RequiresPhpunitExtension;
1920
use PHPUnit\Framework\Attributes\RequiresSetting;
2021
use PHPUnit\Framework\Attributes\TestDox;
2122
use PHPUnit\Framework\Attributes\Ticket;
@@ -363,4 +364,9 @@ public function testVersionConstraintRegexpIgnoresWhitespace(): void
363364
public function testSettingDisplayErrorsOn(): void
364365
{
365366
}
367+
368+
#[RequiresPhpunitExtension(SomeClass::class, SomeOtherClass::class)]
369+
public function testPHPUnitExtensionRequired(): void
370+
{
371+
}
366372
}

tests/unit/Metadata/Api/RequirementsTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ public static function missingRequirementsProvider(): array
123123
'PHP ^1.0 is required.',
124124
'PHPUnit ^2.0 is required.',
125125
]],
126+
['testPHPUnitExtensionRequired', [
127+
'PHPUnit extension(s) "PHPUnit\TestFixture\SomeClass", "PHPUnit\TestFixture\SomeOtherClass" required.',
128+
]],
126129
];
127130
}
128131

tests/unit/Metadata/MetadataCollectionTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPUnit\Framework\TestCase;
1717
use PHPUnit\Metadata\Version\ComparisonRequirement;
1818
use PHPUnit\Util\VersionComparisonOperator;
19+
use stdClass;
1920

2021
#[CoversClass(MetadataCollection::class)]
2122
#[CoversClass(MetadataCollectionIterator::class)]
@@ -48,6 +49,7 @@
4849
#[UsesClass(RequiresPhp::class)]
4950
#[UsesClass(RequiresPhpExtension::class)]
5051
#[UsesClass(RequiresPhpunit::class)]
52+
#[UsesClass(RequiresPhpunitExtension::class)]
5153
#[UsesClass(RequiresSetting::class)]
5254
#[UsesClass(RunClassInSeparateProcess::class)]
5355
#[UsesClass(RunInSeparateProcess::class)]
@@ -404,6 +406,14 @@ public function test_Can_be_filtered_for_RequiresPhpunit(): void
404406
$this->assertTrue($collection->asArray()[0]->isRequiresPhpunit());
405407
}
406408

409+
public function test_Can_be_filtered_for_RequiresPhpunitExtension(): void
410+
{
411+
$collection = $this->collectionWithOneOfEach()->isRequiresPhpunitExtension();
412+
413+
$this->assertCount(1, $collection);
414+
$this->assertTrue($collection->asArray()[0]->isRequiresPhpunitExtension());
415+
}
416+
407417
public function test_Can_be_filtered_for_RequiresSetting(): void
408418
{
409419
$collection = $this->collectionWithOneOfEach()->isRequiresSetting();
@@ -563,6 +573,7 @@ private function collectionWithOneOfEach(): MetadataCollection
563573
new VersionComparisonOperator('>='),
564574
),
565575
),
576+
Metadata::requiresPhpunitExtensionOnClass(stdClass::class),
566577
Metadata::requiresSettingOnClass('foo', 'bar'),
567578
Metadata::runClassInSeparateProcess(),
568579
Metadata::runInSeparateProcess(),

0 commit comments

Comments
 (0)