Skip to content

Commit 4c071c2

Browse files
feature symfony#59384 [PhpUnitBridge] Enable configuring mock namespaces with attributes (HypeMC)
This PR was merged into the 7.3 branch. Discussion ---------- [PhpUnitBridge] Enable configuring mock namespaces with attributes | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This PR adds the ability to configure clock and DNS mock namespaces through attributes, removing the need to add them to `phpunit.xml`: ```php #[DnsSensitive(Foo::class)] class FooTest extends KernelTestCase { #[TimeSensitive(Bar::class)] public function testFoo() { // ... } } ``` Commits ------- 130cc26 [PhpUnitBridge] Enable configuring mock ns with attributes
2 parents 6298bfa + 130cc26 commit 4c071c2

17 files changed

+537
-11
lines changed

.github/workflows/phpunit-bridge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535
php-version: "7.2"
3636

3737
- name: Lint
38-
run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {}
38+
run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e /Attribute/ -e /Extension/ -e /Metadata/ -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\PhpUnit\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
15+
final class DnsSensitive
16+
{
17+
public function __construct(
18+
public readonly ?string $class = null,
19+
) {
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\PhpUnit\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
15+
final class TimeSensitive
16+
{
17+
public function __construct(
18+
public readonly ?string $class = null,
19+
) {
20+
}
21+
}

src/Symfony/Bridge/PhpUnit/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Enable configuring clock and DNS mock namespaces with attributes
8+
49
7.2
510
---
611

src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
use PHPUnit\Event\Test\Finished;
1616
use PHPUnit\Event\Test\FinishedSubscriber;
1717
use PHPUnit\Metadata\Group;
18+
use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive;
1819
use Symfony\Bridge\PhpUnit\ClockMock;
20+
use Symfony\Bridge\PhpUnit\Metadata\AttributeReader;
1921

2022
/**
2123
* @internal
2224
*/
2325
class DisableClockMockSubscriber implements FinishedSubscriber
2426
{
27+
public function __construct(
28+
private AttributeReader $reader,
29+
) {
30+
}
31+
2532
public function notify(Finished $event): void
2633
{
2734
$test = $event->test();
@@ -33,7 +40,12 @@ public function notify(Finished $event): void
3340
foreach ($test->metadata() as $metadata) {
3441
if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
3542
ClockMock::withClockMock(false);
43+
break;
3644
}
3745
}
46+
47+
if ($this->reader->forClassAndMethod($test->className(), $test->methodName(), TimeSensitive::class)) {
48+
ClockMock::withClockMock(false);
49+
}
3850
}
3951
}

src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
use PHPUnit\Event\Test\Finished;
1616
use PHPUnit\Event\Test\FinishedSubscriber;
1717
use PHPUnit\Metadata\Group;
18+
use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive;
1819
use Symfony\Bridge\PhpUnit\DnsMock;
20+
use Symfony\Bridge\PhpUnit\Metadata\AttributeReader;
1921

2022
/**
2123
* @internal
2224
*/
2325
class DisableDnsMockSubscriber implements FinishedSubscriber
2426
{
27+
public function __construct(
28+
private AttributeReader $reader,
29+
) {
30+
}
31+
2532
public function notify(Finished $event): void
2633
{
2734
$test = $event->test();
@@ -33,7 +40,12 @@ public function notify(Finished $event): void
3340
foreach ($test->metadata() as $metadata) {
3441
if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) {
3542
DnsMock::withMockedHosts([]);
43+
break;
3644
}
3745
}
46+
47+
if ($this->reader->forClassAndMethod($test->className(), $test->methodName(), DnsSensitive::class)) {
48+
DnsMock::withMockedHosts([]);
49+
}
3850
}
3951
}

src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
use PHPUnit\Event\Test\PreparationStarted;
1616
use PHPUnit\Event\Test\PreparationStartedSubscriber;
1717
use PHPUnit\Metadata\Group;
18+
use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive;
1819
use Symfony\Bridge\PhpUnit\ClockMock;
20+
use Symfony\Bridge\PhpUnit\Metadata\AttributeReader;
1921

2022
/**
2123
* @internal
2224
*/
2325
class EnableClockMockSubscriber implements PreparationStartedSubscriber
2426
{
27+
public function __construct(
28+
private AttributeReader $reader,
29+
) {
30+
}
31+
2532
public function notify(PreparationStarted $event): void
2633
{
2734
$test = $event->test();
@@ -33,7 +40,12 @@ public function notify(PreparationStarted $event): void
3340
foreach ($test->metadata() as $metadata) {
3441
if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
3542
ClockMock::withClockMock(true);
43+
break;
3644
}
3745
}
46+
47+
if ($this->reader->forClassAndMethod($test->className(), $test->methodName(), TimeSensitive::class)) {
48+
ClockMock::withClockMock(true);
49+
}
3850
}
3951
}

src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
use PHPUnit\Event\TestSuite\Loaded;
1616
use PHPUnit\Event\TestSuite\LoadedSubscriber;
1717
use PHPUnit\Metadata\Group;
18+
use Symfony\Bridge\PhpUnit\Attribute\TimeSensitive;
1819
use Symfony\Bridge\PhpUnit\ClockMock;
20+
use Symfony\Bridge\PhpUnit\Metadata\AttributeReader;
1921

2022
/**
2123
* @internal
2224
*/
2325
class RegisterClockMockSubscriber implements LoadedSubscriber
2426
{
27+
public function __construct(
28+
private AttributeReader $reader,
29+
) {
30+
}
31+
2532
public function notify(Loaded $event): void
2633
{
2734
foreach ($event->testSuite()->tests() as $test) {
@@ -34,6 +41,10 @@ public function notify(Loaded $event): void
3441
ClockMock::register($test->className());
3542
}
3643
}
44+
45+
foreach ($this->reader->forClassAndMethod($test->className(), $test->methodName(), TimeSensitive::class) as $attribute) {
46+
ClockMock::register($attribute->class ?? $test->className());
47+
}
3748
}
3849
}
3950
}

src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
use PHPUnit\Event\TestSuite\Loaded;
1616
use PHPUnit\Event\TestSuite\LoadedSubscriber;
1717
use PHPUnit\Metadata\Group;
18+
use Symfony\Bridge\PhpUnit\Attribute\DnsSensitive;
1819
use Symfony\Bridge\PhpUnit\DnsMock;
20+
use Symfony\Bridge\PhpUnit\Metadata\AttributeReader;
1921

2022
/**
2123
* @internal
2224
*/
2325
class RegisterDnsMockSubscriber implements LoadedSubscriber
2426
{
27+
public function __construct(
28+
private AttributeReader $reader,
29+
) {
30+
}
31+
2532
public function notify(Loaded $event): void
2633
{
2734
foreach ($event->testSuite()->tests() as $test) {
@@ -34,6 +41,10 @@ public function notify(Loaded $event): void
3441
DnsMock::register($test->className());
3542
}
3643
}
44+
45+
foreach ($this->reader->forClassAndMethod($test->className(), $test->methodName(), DnsSensitive::class) as $attribute) {
46+
DnsMock::register($attribute->class ?? $test->className());
47+
}
3748
}
3849
}
3950
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\PhpUnit\Metadata;
13+
14+
/**
15+
* @template T of object
16+
*/
17+
final class AttributeReader
18+
{
19+
/**
20+
* @var array<string, array<class-string<T>, list<T>>>
21+
*/
22+
private array $cache = [];
23+
24+
/**
25+
* @param class-string $className
26+
* @param class-string<T> $name
27+
*
28+
* @return list<T>
29+
*/
30+
public function forClass(string $className, string $name): array
31+
{
32+
$attributes = $this->cache[$className] ??= $this->readAttributes(new \ReflectionClass($className));
33+
34+
return $attributes[$name] ?? [];
35+
}
36+
37+
/**
38+
* @param class-string $className
39+
* @param class-string<T> $name
40+
*
41+
* @return list<T>
42+
*/
43+
public function forMethod(string $className, string $methodName, string $name): array
44+
{
45+
$attributes = $this->cache[$className.'::'.$methodName] ??= $this->readAttributes(new \ReflectionMethod($className, $methodName));
46+
47+
return $attributes[$name] ?? [];
48+
}
49+
50+
/**
51+
* @param class-string $className
52+
* @param class-string<T> $name
53+
*
54+
* @return list<T>
55+
*/
56+
public function forClassAndMethod(string $className, string $methodName, string $name): array
57+
{
58+
return [
59+
...$this->forClass($className, $name),
60+
...$this->forMethod($className, $methodName, $name),
61+
];
62+
}
63+
64+
private function readAttributes(\ReflectionClass|\ReflectionMethod $reflection): array
65+
{
66+
$attributeInstances = [];
67+
68+
foreach ($reflection->getAttributes() as $attribute) {
69+
if (!str_starts_with($name = $attribute->getName(), 'Symfony\\Bridge\\PhpUnit\\Attribute\\')) {
70+
continue;
71+
}
72+
73+
$attributeInstances[$name][] = $attribute->newInstance();
74+
}
75+
76+
return $attributeInstances;
77+
}
78+
}

0 commit comments

Comments
 (0)