Skip to content

Commit 845c1a6

Browse files
authored
TestDox: add --testdox-text and --testdox-html support (#1028)
1 parent 65deae3 commit 845c1a6

14 files changed

+342
-55
lines changed

bin/phpunit-wrapper.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
'result-cache-file:',
1515
'teamcity-file:',
1616
'testdox-file:',
17-
'testdox-color',
18-
'testdox-columns:',
19-
'testdox-summary',
2017
'phpunit-argv:',
2118
]);
2219

@@ -45,7 +42,6 @@
4542
assert(!isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file']));
4643
assert(!isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
4744
assert(!isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
48-
assert(!isset($getopt['testdox-columns']) || $getopt['testdox-columns'] === (string) (int) $getopt['testdox-columns']);
4945

5046
assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv']));
5147
$phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]);
@@ -59,9 +55,6 @@
5955
$getopt['result-cache-file'] ?? null,
6056
$getopt['teamcity-file'] ?? null,
6157
$getopt['testdox-file'] ?? null,
62-
isset($getopt['testdox-color']),
63-
isset($getopt['testdox-columns']) ? (int) $getopt['testdox-columns'] : null,
64-
isset($getopt['testdox-summary']),
6558
);
6659

6760
while (true) {

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
"phpunit/php-timer": "^8",
4646
"phpunit/phpunit": "^12.3.15",
4747
"sebastian/environment": "^8.0.3",
48-
"symfony/console": "^6.4.20 || ^7.3.2",
49-
"symfony/process": "^6.4.20 || ^7.3.0"
48+
"symfony/console": "^6.4.20 || ^7.3.4",
49+
"symfony/process": "^6.4.20 || ^7.3.4"
5050
},
5151
"require-dev": {
5252
"ext-pcntl": "*",

src/Options.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
];
8484

8585
public bool $needsTeamcity;
86+
public bool $needsTestdox;
8687

8788
/**
8889
* @param non-empty-string $phpunit
@@ -109,6 +110,7 @@ public function __construct(
109110
public int $totalShards,
110111
) {
111112
$this->needsTeamcity = $configuration->outputIsTeamCity() || $configuration->hasLogfileTeamcity();
113+
$this->needsTestdox = $configuration->outputIsTestDox() || $configuration->hasLogfileTestdoxText() || $configuration->hasLogfileTestdoxHtml();
112114
}
113115

114116
/** @param non-empty-string $cwd */
@@ -568,6 +570,18 @@ public static function setInputDefinition(InputDefinition $inputDefinition): voi
568570
InputOption::VALUE_REQUIRED,
569571
'@see PHPUnit guide, chapter: ' . $chapter,
570572
),
573+
new InputOption(
574+
'testdox-text',
575+
null,
576+
InputOption::VALUE_REQUIRED,
577+
'@see PHPUnit guide, chapter: ' . $chapter,
578+
),
579+
new InputOption(
580+
'testdox-html',
581+
null,
582+
InputOption::VALUE_REQUIRED,
583+
'@see PHPUnit guide, chapter: ' . $chapter,
584+
),
571585
new InputOption(
572586
'coverage-clover',
573587
null,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ParaTest\TestDox;
6+
7+
use PHPUnit\Logging\TestDox\TestResult as TestDoxTestMethod;
8+
use PHPUnit\Logging\TestDox\TestResultCollection;
9+
use ReflectionException;
10+
use ReflectionMethod;
11+
use SplFileInfo;
12+
13+
use function array_merge;
14+
use function assert;
15+
use function file_get_contents;
16+
use function is_subclass_of;
17+
use function ksort;
18+
use function uksort;
19+
use function unserialize;
20+
use function usort;
21+
22+
/** @internal */
23+
final readonly class TestDoxResultsMerger
24+
{
25+
/**
26+
* @param list<SplFileInfo> $testdoxFiles
27+
*
28+
* @return array<string, TestResultCollection>
29+
*/
30+
public function getResultsFromTestdoxFiles(array $testdoxFiles): array
31+
{
32+
/** @var array<string, TestResultCollection> $testMethodsGroupedByClass */
33+
$testMethodsGroupedByClass = [];
34+
foreach ($testdoxFiles as $testdoxFile) {
35+
if (! $testdoxFile->isFile()) {
36+
continue;
37+
}
38+
39+
$testdoxFileContents = file_get_contents($testdoxFile->getPathname());
40+
assert($testdoxFileContents !== false);
41+
42+
/** @var array<string, TestResultCollection> $testMethodsGroupedByClassInTestdoxFile */
43+
$testMethodsGroupedByClassInTestdoxFile = unserialize($testdoxFileContents);
44+
foreach ($testMethodsGroupedByClassInTestdoxFile as $className => $testResultCollection) {
45+
if (! isset($testMethodsGroupedByClass[$className])) {
46+
$testMethodsGroupedByClass[$className] = $testResultCollection;
47+
} else {
48+
$combinedTestResultCollection = TestResultCollection::fromArray([
49+
...$testMethodsGroupedByClass[$className]->asArray(),
50+
...$testResultCollection->asArray(),
51+
]);
52+
$testMethodsGroupedByClass[$className] = $combinedTestResultCollection;
53+
}
54+
}
55+
}
56+
57+
return $this->orderTestdoxResults($testMethodsGroupedByClass);
58+
}
59+
60+
/**
61+
* @param array<string, TestResultCollection> $testdoxResults
62+
*
63+
* @return array<string, TestResultCollection>
64+
*/
65+
private function orderTestdoxResults(array $testdoxResults): array
66+
{
67+
// @see \PHPUnit\Logging\TestDox\TestResultCollector::testMethodsGroupedByClass
68+
$orderedTestdoxResults = [];
69+
70+
foreach ($testdoxResults as $prettifiedClassName => $tests) {
71+
$testsByDeclaringClass = [];
72+
73+
foreach ($tests as $test) {
74+
try {
75+
$declaringClassName = (new ReflectionMethod($test->test()->className(), $test->test()->methodName()))->getDeclaringClass()->getName();
76+
} catch (ReflectionException) {
77+
$declaringClassName = $test->test()->className();
78+
}
79+
80+
if (! isset($testsByDeclaringClass[$declaringClassName])) {
81+
$testsByDeclaringClass[$declaringClassName] = [];
82+
}
83+
84+
$testsByDeclaringClass[$declaringClassName][] = $test;
85+
}
86+
87+
foreach ($testsByDeclaringClass as $declaringClassName) {
88+
usort(
89+
$declaringClassName,
90+
static function (TestDoxTestMethod $a, TestDoxTestMethod $b): int {
91+
return $a->test()->line() <=> $b->test()->line();
92+
},
93+
);
94+
}
95+
96+
uksort(
97+
$testsByDeclaringClass,
98+
/**
99+
* @param class-string $a
100+
* @param class-string $b
101+
*/
102+
static function (string $a, string $b): int {
103+
if (is_subclass_of($b, $a)) {
104+
return -1;
105+
}
106+
107+
if (is_subclass_of($a, $b)) {
108+
return 1;
109+
}
110+
111+
return 0;
112+
},
113+
);
114+
115+
$tests = [];
116+
117+
foreach ($testsByDeclaringClass as $_tests) {
118+
$tests = array_merge($tests, $_tests);
119+
}
120+
121+
$orderedTestdoxResults[$prettifiedClassName] = TestResultCollection::fromArray($tests);
122+
}
123+
124+
ksort($orderedTestdoxResults);
125+
126+
return $orderedTestdoxResults;
127+
}
128+
}

src/WrapperRunner/ApplicationForWrapperWorker.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
use PHPUnit\TextUI\Output\Default\UnexpectedOutputPrinter;
3535
use PHPUnit\TextUI\Output\DefaultPrinter;
3636
use PHPUnit\TextUI\Output\NullPrinter;
37-
use PHPUnit\TextUI\Output\TestDox\ResultPrinter as TestDoxResultPrinter;
3837
use PHPUnit\TextUI\TestSuiteFilterProcessor;
3938
use PHPUnit\Util\ExcludeList;
4039

@@ -67,8 +66,6 @@ public function __construct(
6766
private readonly ?string $resultCacheFile,
6867
private readonly ?string $teamcityFile,
6968
private readonly ?string $testdoxFile,
70-
private readonly bool $testdoxColor,
71-
private readonly ?int $testdoxColumns,
7269
) {
7370
}
7471

@@ -254,12 +251,8 @@ public function end(): void
254251
$result = TestResultFacade::result();
255252
if (isset($this->testdoxResultCollector)) {
256253
assert(isset($this->testdoxFile));
257-
assert(isset($this->testdoxColumns));
258254

259-
(new TestDoxResultPrinter(DefaultPrinter::from($this->testdoxFile), $this->testdoxColor, $this->testdoxColumns, false))->print(
260-
$result,
261-
$this->testdoxResultCollector->testMethodsGroupedByClass(),
262-
);
255+
file_put_contents($this->testdoxFile, serialize($this->testdoxResultCollector->testMethodsGroupedByClass()));
263256
}
264257

265258
file_put_contents($this->testResultFile, serialize($result));

src/WrapperRunner/ResultPrinter.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
namespace ParaTest\WrapperRunner;
66

77
use ParaTest\Options;
8+
use PHPUnit\Logging\TestDox\TestResultCollection as TestDoxTestResultCollection;
89
use PHPUnit\Runner\TestSuiteSorter;
910
use PHPUnit\TestRunner\TestResult\TestResult;
1011
use PHPUnit\TextUI\Output\Default\ResultPrinter as DefaultResultPrinter;
1112
use PHPUnit\TextUI\Output\Printer;
1213
use PHPUnit\TextUI\Output\SummaryPrinter;
14+
use PHPUnit\TextUI\Output\TestDox\ResultPrinter as TestDoxResultPrinter;
1315
use PHPUnit\Util\Color;
1416
use SebastianBergmann\CodeCoverage\Driver\Selector;
1517
use SebastianBergmann\CodeCoverage\Filter;
@@ -174,10 +176,10 @@ public function printFeedback(
174176
}
175177

176178
/**
177-
* @param list<SplFileInfo> $teamcityFiles
178-
* @param list<SplFileInfo> $testdoxFiles
179+
* @param list<SplFileInfo> $teamcityFiles
180+
* @param array<string,TestDoxTestResultCollection> $testdoxResults
179181
*/
180-
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles): void
182+
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxResults): void
181183
{
182184
if ($this->options->needsTeamcity) {
183185
$teamcityProgress = $this->tailMultiple($teamcityFiles);
@@ -218,7 +220,12 @@ public function printResults(TestResult $testResult, array $teamcityFiles, array
218220
);
219221

220222
if ($this->options->configuration->outputIsTestDox()) {
221-
$this->output->write($this->tailMultiple($testdoxFiles));
223+
(new TestDoxResultPrinter(
224+
$this->printer,
225+
$this->options->configuration->colors(),
226+
$this->options->configuration->columns(),
227+
$this->options->configuration->testDoxOutputWithSummary(),
228+
))->print($testResult, $testdoxResults);
222229

223230
$defaultResultPrinter = new DefaultResultPrinter(
224231
$this->printer,

src/WrapperRunner/WrapperRunner.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
use ParaTest\JUnit\Writer;
1010
use ParaTest\Options;
1111
use ParaTest\RunnerInterface;
12+
use ParaTest\TestDox\TestDoxResultsMerger;
13+
use PHPUnit\Logging\TestDox\HtmlRenderer as TestDoxHtmlRenderer;
14+
use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxPlainTextRenderer;
15+
use PHPUnit\Logging\TestDox\TestResultCollection as TestDoxTestResultCollection;
1216
use PHPUnit\Runner\CodeCoverage;
1317
use PHPUnit\Runner\ResultCache\DefaultResultCache;
1418
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
1519
use PHPUnit\TestRunner\TestResult\TestResult;
1620
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
21+
use PHPUnit\TextUI\Output\DefaultPrinter;
1722
use PHPUnit\TextUI\ShellExitCodeCalculator;
1823
use PHPUnit\Util\ExcludeList;
1924
use SplFileInfo;
@@ -305,13 +310,16 @@ private function complete(TestResult $testResultSum): int
305310
$resultCacheSum->persist();
306311
}
307312

313+
$testdoxResults = (new TestDoxResultsMerger())->getResultsFromTestdoxFiles($this->testdoxFiles);
314+
308315
$this->printer->printResults(
309316
$testResultSum,
310317
$this->teamcityFiles,
311-
$this->testdoxFiles,
318+
$testdoxResults,
312319
);
313320
$this->generateCodeCoverageReports();
314-
$this->generateLogs();
321+
$this->generateJunitLog();
322+
$this->generateTestDoxLogs($testdoxResults);
315323

316324
$exitcode = (new ShellExitCodeCalculator())->calculate(
317325
$this->options->configuration,
@@ -354,7 +362,7 @@ protected function generateCodeCoverageReports(): void
354362
);
355363
}
356364

357-
private function generateLogs(): void
365+
private function generateJunitLog(): void
358366
{
359367
if ($this->junitFiles === []) {
360368
return;
@@ -371,6 +379,22 @@ private function generateLogs(): void
371379
);
372380
}
373381

382+
/** @param array<string,TestDoxTestResultCollection> $testdoxResults */
383+
private function generateTestDoxLogs(array $testdoxResults): void
384+
{
385+
if ($this->options->configuration->hasLogfileTestdoxText()) {
386+
$testdoxTextContent = (new TestDoxPlainTextRenderer())->render($testdoxResults);
387+
DefaultPrinter::from($this->options->configuration->logfileTestdoxText())->print($testdoxTextContent);
388+
}
389+
390+
if (! $this->options->configuration->hasLogfileTestdoxHtml()) {
391+
return;
392+
}
393+
394+
$testdoxHtmlContent = (new TestDoxHtmlRenderer())->render($testdoxResults);
395+
DefaultPrinter::from($this->options->configuration->logfileTestdoxHtml())->print($testdoxHtmlContent);
396+
}
397+
374398
/** @param list<SplFileInfo> $files */
375399
private function clearFiles(array $files): void
376400
{

src/WrapperRunner/WrapperWorker.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ final class WrapperWorker
3939
public readonly SplFileInfo $junitFile;
4040
public readonly SplFileInfo $coverageFile;
4141
public readonly SplFileInfo $teamcityFile;
42-
4342
public readonly SplFileInfo $testdoxFile;
43+
4444
private ?string $currentlyExecuting = null;
4545
private Process $process;
4646
private int $inExecution = 0;
@@ -85,7 +85,7 @@ public function __construct(
8585
$this->teamcityFile = new SplFileInfo($commonTmpFilePath . 'teamcity');
8686
}
8787

88-
if ($options->configuration->outputIsTestDox()) {
88+
if ($options->needsTestdox) {
8989
$this->testdoxFile = new SplFileInfo($commonTmpFilePath . 'testdox');
9090
}
9191

@@ -111,12 +111,6 @@ public function __construct(
111111
if (isset($this->testdoxFile)) {
112112
$parameters[] = '--testdox-file';
113113
$parameters[] = $this->testdoxFile->getPathname();
114-
if ($options->configuration->colors()) {
115-
$parameters[] = '--testdox-color';
116-
}
117-
118-
$parameters[] = '--testdox-columns';
119-
$parameters[] = (string) $options->configuration->columns();
120114
}
121115

122116
$phpunitArguments = [$options->phpunit];

test/Unit/OptionsTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ public function testNeedsTeamcityGetsActivatedBothByLogTeamcityAndTeamcityFlags(
135135
self::assertTrue($options->needsTeamcity);
136136
}
137137

138+
public function testNeedsTestdoxGetsActivatedBothByTestdoxOutputOrATestdoxLogFile(): void
139+
{
140+
$options = $this->createOptionsFromArgv(['--testdox' => true], __DIR__);
141+
142+
self::assertTrue($options->needsTestdox);
143+
144+
$options = $this->createOptionsFromArgv(['--testdox-text' => 'LOG-TESTDOX-TEXT'], __DIR__);
145+
146+
self::assertTrue($options->needsTestdox);
147+
148+
$options = $this->createOptionsFromArgv(['--testdox-html' => 'LOG-TESTDOX-HTML'], __DIR__);
149+
150+
self::assertTrue($options->needsTestdox);
151+
}
152+
138153
public function testShardOptionsDefaultValues(): void
139154
{
140155
$options = $this->createOptionsFromArgv([], __DIR__);

0 commit comments

Comments
 (0)