Skip to content

Commit 36ca583

Browse files
Implement #[TestDoxFormatter] and #[TestDoxFormatterExternal] attributes for configuring a custom TestDox formatter for tests that use data from data providers
1 parent 016505e commit 36ca583

24 files changed

+859
-0
lines changed

ChangeLog-12.3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes of the PHPUnit 12.3 release series are documented in this fi
88

99
* [#3795](https://github.com/sebastianbergmann/phpunit/issues/3795): Bootstrap scripts specific to test suites
1010
* [#6268](https://github.com/sebastianbergmann/phpunit/pull/6268): `#[IgnorePHPUnitWarnings]` attribute for ignoring PHPUnit warnings
11+
* `#[TestDoxFormatter]` and `#[TestDoxFormatterExternal]` attributes for configuring a custom TestDox formatter for tests that use data from data providers
1112
* `TestRunner\ChildProcessErrored` event
1213
* `Configuration::includeTestSuites()` and `Configuration::excludeTestSuites()`
1314

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
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+
14+
/**
15+
* @immutable
16+
*
17+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
#[Attribute(Attribute::TARGET_METHOD)]
20+
final readonly class TestDoxFormatter
21+
{
22+
/**
23+
* @var non-empty-string
24+
*/
25+
private string $methodName;
26+
27+
/**
28+
* @param non-empty-string $methodName
29+
*/
30+
public function __construct(string $methodName)
31+
{
32+
$this->methodName = $methodName;
33+
}
34+
35+
/**
36+
* @return non-empty-string
37+
*/
38+
public function methodName(): string
39+
{
40+
return $this->methodName;
41+
}
42+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
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+
14+
/**
15+
* @immutable
16+
*
17+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
#[Attribute(Attribute::TARGET_METHOD)]
20+
final readonly class TestDoxFormatterExternal
21+
{
22+
/**
23+
* @var class-string
24+
*/
25+
private string $className;
26+
27+
/**
28+
* @var non-empty-string
29+
*/
30+
private string $methodName;
31+
32+
/**
33+
* @param class-string $className
34+
* @param non-empty-string $methodName
35+
*/
36+
public function __construct(string $className, string $methodName)
37+
{
38+
$this->className = $className;
39+
$this->methodName = $methodName;
40+
}
41+
42+
/**
43+
* @return class-string
44+
*/
45+
public function className(): string
46+
{
47+
return $this->className;
48+
}
49+
50+
/**
51+
* @return non-empty-string
52+
*/
53+
public function methodName(): string
54+
{
55+
return $this->methodName;
56+
}
57+
}

src/Logging/TestDox/NamePrettifier.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
namespace PHPUnit\Logging\TestDox;
1111

12+
use const PHP_EOL;
1213
use function array_key_exists;
1314
use function array_keys;
1415
use function array_map;
@@ -38,14 +39,19 @@
3839
use function strtoupper;
3940
use function substr;
4041
use function trim;
42+
use PHPUnit\Event\Code\TestMethodBuilder;
43+
use PHPUnit\Event\Facade as EventFacade;
4144
use PHPUnit\Framework\TestCase;
4245
use PHPUnit\Metadata\Parser\Registry as MetadataRegistry;
4346
use PHPUnit\Metadata\TestDox;
47+
use PHPUnit\Metadata\TestDoxFormatter;
4448
use PHPUnit\Util\Color;
4549
use PHPUnit\Util\Exporter;
50+
use PHPUnit\Util\Filter;
4651
use ReflectionEnum;
4752
use ReflectionMethod;
4853
use ReflectionObject;
54+
use Throwable;
4955

5056
/**
5157
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -64,6 +70,11 @@ final class NamePrettifier
6470
*/
6571
private array $prettifiedTestCases = [];
6672

73+
/**
74+
* @var array<non-empty-string, true>
75+
*/
76+
private array $erroredFormatters = [];
77+
6778
/**
6879
* @param class-string $className
6980
*/
@@ -192,6 +203,7 @@ public function prettifyTestCase(TestCase $test, bool $colorize): string
192203
}
193204

194205
$testDox = MetadataRegistry::parser()->forMethod($test::class, $test->name())->isTestDox()->isMethodLevel();
206+
$callback = MetadataRegistry::parser()->forMethod($test::class, $test->name())->isTestDoxFormatter();
195207
$isCustomized = false;
196208

197209
if ($testDox->isNotEmpty()) {
@@ -200,6 +212,12 @@ public function prettifyTestCase(TestCase $test, bool $colorize): string
200212
assert($testDox instanceof TestDox);
201213

202214
[$result, $isCustomized] = $this->processTestDox($test, $testDox, $colorize);
215+
} elseif ($callback->isNotEmpty()) {
216+
$callback = $callback->asArray()[0];
217+
218+
assert($callback instanceof TestDoxFormatter);
219+
220+
[$result, $isCustomized] = $this->processTestDoxFormatter($test, $callback);
203221
} else {
204222
$result = $this->prettifyTestMethodName($test->name());
205223
}
@@ -338,4 +356,85 @@ private function processTestDox(TestCase $test, TestDox $testDox, bool $colorize
338356

339357
return [$result, $placeholdersUsed];
340358
}
359+
360+
/**
361+
* @return array{0: string, 1: bool}
362+
*/
363+
private function processTestDoxFormatter(TestCase $test, TestDoxFormatter $formatter): array
364+
{
365+
$className = $formatter->className();
366+
$methodName = $formatter->methodName();
367+
$formatterIdentifier = $className . '::' . $methodName;
368+
369+
if (isset($this->erroredFormatters[$formatterIdentifier])) {
370+
return [$this->prettifyTestMethodName($test->name()), false];
371+
}
372+
373+
if (!method_exists($className, $methodName)) {
374+
EventFacade::emitter()->testTriggeredPhpunitError(
375+
TestMethodBuilder::fromTestCase($test, false),
376+
sprintf(
377+
'Method %s::%s() cannot be used as a TestDox formatter because it does not exist',
378+
$className,
379+
$methodName,
380+
),
381+
);
382+
383+
$this->erroredFormatters[$formatterIdentifier] = true;
384+
385+
return [$this->prettifyTestMethodName($test->name()), false];
386+
}
387+
388+
$reflector = new ReflectionMethod($className, $methodName);
389+
390+
if (!$reflector->isPublic()) {
391+
EventFacade::emitter()->testTriggeredPhpunitError(
392+
TestMethodBuilder::fromTestCase($test, false),
393+
sprintf(
394+
'Method %s::%s() cannot be used as a TestDox formatter because it is not public',
395+
$className,
396+
$methodName,
397+
),
398+
);
399+
400+
$this->erroredFormatters[$formatterIdentifier] = true;
401+
402+
return [$this->prettifyTestMethodName($test->name()), false];
403+
}
404+
405+
if (!$reflector->isStatic()) {
406+
EventFacade::emitter()->testTriggeredPhpunitError(
407+
TestMethodBuilder::fromTestCase($test, false),
408+
sprintf(
409+
'Method %s::%s() cannot be used as a TestDox formatter because it is not static',
410+
$className,
411+
$methodName,
412+
),
413+
);
414+
415+
$this->erroredFormatters[$formatterIdentifier] = true;
416+
417+
return [$this->prettifyTestMethodName($test->name()), false];
418+
}
419+
420+
try {
421+
return [$reflector->invokeArgs(null, array_values($test->providedData())), true];
422+
} catch (Throwable $t) {
423+
EventFacade::emitter()->testTriggeredPhpunitError(
424+
TestMethodBuilder::fromTestCase($test, false),
425+
sprintf(
426+
'TestDox formatter %s::%s() triggered an error: %s%s%s',
427+
$className,
428+
$methodName,
429+
$t->getMessage(),
430+
PHP_EOL,
431+
Filter::stackTraceFromThrowableAsString($t),
432+
),
433+
);
434+
435+
$this->erroredFormatters[$formatterIdentifier] = true;
436+
437+
return [$this->prettifyTestMethodName($test->name()), false];
438+
}
439+
}
341440
}

src/Metadata/Metadata.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,15 @@ public static function testDoxOnMethod(string $text): TestDox
463463
return new TestDox(self::METHOD_LEVEL, $text);
464464
}
465465

466+
/**
467+
* @param class-string $className
468+
* @param non-empty-string $methodName
469+
*/
470+
public static function testDoxFormatter(string $className, string $methodName): TestDoxFormatter
471+
{
472+
return new TestDoxFormatter(self::METHOD_LEVEL, $className, $methodName);
473+
}
474+
466475
/**
467476
* @param array<array<mixed>> $data
468477
* @param ?non-empty-string $name
@@ -906,6 +915,14 @@ public function isTestDox(): bool
906915
return false;
907916
}
908917

918+
/**
919+
* @phpstan-assert-if-true TestDoxFormatter $this
920+
*/
921+
public function isTestDoxFormatter(): bool
922+
{
923+
return false;
924+
}
925+
909926
/**
910927
* @phpstan-assert-if-true TestWith $this
911928
*/

src/Metadata/MetadataCollection.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,16 @@ public function isTestDox(): self
551551
);
552552
}
553553

554+
public function isTestDoxFormatter(): self
555+
{
556+
return new self(
557+
...array_filter(
558+
$this->metadata,
559+
static fn (Metadata $metadata): bool => $metadata->isTestDoxFormatter(),
560+
),
561+
);
562+
}
563+
554564
public function isTestWith(): self
555565
{
556566
return new self(

src/Metadata/Parser/AttributeParser.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
use PHPUnit\Framework\Attributes\Small;
7575
use PHPUnit\Framework\Attributes\Test;
7676
use PHPUnit\Framework\Attributes\TestDox;
77+
use PHPUnit\Framework\Attributes\TestDoxFormatter;
78+
use PHPUnit\Framework\Attributes\TestDoxFormatterExternal;
7779
use PHPUnit\Framework\Attributes\TestWith;
7880
use PHPUnit\Framework\Attributes\TestWithJson;
7981
use PHPUnit\Framework\Attributes\Ticket;
@@ -854,6 +856,20 @@ public function forMethod(string $className, string $methodName): MetadataCollec
854856

855857
break;
856858

859+
case TestDoxFormatter::class:
860+
assert($attributeInstance instanceof TestDoxFormatter);
861+
862+
$result[] = Metadata::testDoxFormatter($className, $attributeInstance->methodName());
863+
864+
break;
865+
866+
case TestDoxFormatterExternal::class:
867+
assert($attributeInstance instanceof TestDoxFormatterExternal);
868+
869+
$result[] = Metadata::testDoxFormatter($attributeInstance->className(), $attributeInstance->methodName());
870+
871+
break;
872+
857873
case TestWith::class:
858874
assert($attributeInstance instanceof TestWith);
859875

src/Metadata/TestDoxFormatter.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
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+
/**
13+
* @immutable
14+
*
15+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
16+
*/
17+
final readonly class TestDoxFormatter extends Metadata
18+
{
19+
/**
20+
* @var class-string
21+
*/
22+
private string $className;
23+
24+
/**
25+
* @var non-empty-string
26+
*/
27+
private string $methodName;
28+
29+
/**
30+
* @param int<0, 1> $level
31+
* @param class-string $className
32+
* @param non-empty-string $methodName
33+
*/
34+
protected function __construct(int $level, string $className, string $methodName)
35+
{
36+
parent::__construct($level);
37+
38+
$this->className = $className;
39+
$this->methodName = $methodName;
40+
}
41+
42+
public function isTestDoxFormatter(): true
43+
{
44+
return true;
45+
}
46+
47+
/**
48+
* @return class-string
49+
*/
50+
public function className(): string
51+
{
52+
return $this->className;
53+
}
54+
55+
/**
56+
* @return non-empty-string
57+
*/
58+
public function methodName(): string
59+
{
60+
return $this->methodName;
61+
}
62+
}

0 commit comments

Comments
 (0)