Skip to content

Commit a20da3e

Browse files
Add solution from Nimut#23
1 parent 87d4f86 commit a20da3e

File tree

10 files changed

+192
-99
lines changed

10 files changed

+192
-99
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/.idea
33
/.Log
44
composer.lock
5+
.phpunit.result.cache

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727
],
2828
"require": {
29-
"php": "^8.0",
29+
"php": "^8.1",
3030
"ext-dom": "*",
3131
"ext-json": "*",
3232
"ext-simplexml": "*",
@@ -37,7 +37,8 @@
3737
"require-dev": {
3838
"phpunit/phpunit": "^9.3 || ^10.0",
3939
"symfony/filesystem": ">=2.7 <7.0",
40-
"phpspec/prophecy": "^1.0"
40+
"phpspec/prophecy": "^1.0",
41+
"phpspec/prophecy-phpunit": "^2.0"
4142
},
4243
"suggest": {
4344
"friendsofphp/php-cs-fixer": "Tool to automatically fix PHP coding standards issues"

src/PhpunitMerger/Command/CoverageCommand.php

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
namespace Nimut\PhpunitMerger\Command;
66

77
use SebastianBergmann\CodeCoverage\CodeCoverage;
8+
use SebastianBergmann\CodeCoverage\Driver\Driver;
89
use SebastianBergmann\CodeCoverage\Driver\Selector;
910
use SebastianBergmann\CodeCoverage\Filter;
11+
use SebastianBergmann\CodeCoverage\Filter as CodeCoverageFilter;
1012
use SebastianBergmann\CodeCoverage\Report\Clover;
1113
use SebastianBergmann\CodeCoverage\Report\Html\Facade;
12-
use SebastianBergmann\CodeCoverage\Report\Thresholds;
1314
use Symfony\Component\Console\Command\Command;
1415
use Symfony\Component\Console\Input\InputArgument;
1516
use Symfony\Component\Console\Input\InputInterface;
@@ -22,42 +23,42 @@ class CoverageCommand extends Command
2223
protected function configure()
2324
{
2425
$this->setName('coverage')
25-
->setDescription('Merges multiple PHPUnit coverage php files into one')
26-
->addArgument(
27-
'directory',
28-
InputArgument::REQUIRED,
29-
'The directory containing PHPUnit coverage php files'
30-
)
31-
->addArgument(
32-
'file',
33-
InputArgument::OPTIONAL,
34-
'The file where to write the merged result. Default: Standard output'
35-
)
36-
->addOption(
37-
'html',
38-
null,
39-
InputOption::VALUE_REQUIRED,
40-
'The directory where to write the code coverage report in HTML format'
41-
)
42-
->addOption(
43-
'lowUpperBound',
44-
null,
45-
InputOption::VALUE_REQUIRED,
46-
'The lowUpperBound value to be used for HTML format'
47-
)
48-
->addOption(
49-
'highLowerBound',
50-
null,
51-
InputOption::VALUE_REQUIRED,
52-
'The highLowerBound value to be used for HTML format'
53-
);
26+
->setDescription('Merges multiple PHPUnit coverage php files into one')
27+
->addArgument(
28+
'directory',
29+
InputArgument::REQUIRED,
30+
'The directory containing PHPUnit coverage php files'
31+
)
32+
->addArgument(
33+
'file',
34+
InputArgument::OPTIONAL,
35+
'The file where to write the merged result. Default: Standard output'
36+
)
37+
->addOption(
38+
'html',
39+
null,
40+
InputOption::VALUE_REQUIRED,
41+
'The directory where to write the code coverage report in HTML format'
42+
)
43+
->addOption(
44+
'lowUpperBound',
45+
null,
46+
InputOption::VALUE_REQUIRED,
47+
'The lowUpperBound value to be used for HTML format'
48+
)
49+
->addOption(
50+
'highLowerBound',
51+
null,
52+
InputOption::VALUE_REQUIRED,
53+
'The highLowerBound value to be used for HTML format'
54+
);
5455
}
5556

5657
protected function execute(InputInterface $input, OutputInterface $output)
5758
{
5859
$finder = new Finder();
5960
$finder->files()
60-
->in(realpath($input->getArgument('directory')));
61+
->in(realpath($input->getArgument('directory')));
6162

6263
$codeCoverage = $this->getCodeCoverage();
6364

@@ -108,12 +109,7 @@ private function writeCodeCoverage(CodeCoverage $codeCoverage, OutputInterface $
108109

109110
private function writeHtmlReport(CodeCoverage $codeCoverage, string $destination, int $lowUpperBound, int $highLowerBound)
110111
{
111-
if (class_exists('SebastianBergmann\\CodeCoverage\\Report\\Thresholds')) {
112-
$writer = new Facade('', null, Thresholds::from($lowUpperBound, $highLowerBound));
113-
} else {
114-
$writer = new Facade($lowUpperBound, $highLowerBound);
115-
}
116-
112+
$writer = new Facade($lowUpperBound, $highLowerBound);
117113
$writer->process($codeCoverage, $destination);
118114
}
119-
}
115+
}

src/PhpunitMerger/Command/LogCommand.php

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,57 @@ class LogCommand extends Command
2222
*/
2323
private $domElements = [];
2424

25+
private $keysToCalculate = ['assertions', 'time', 'tests', 'errors', 'failures', 'skipped'];
26+
2527
protected function configure()
2628
{
2729
$this->setName('log')
28-
->setDescription('Merges multiple PHPUnit JUnit xml files into one')
29-
->addArgument(
30-
'directory',
31-
InputArgument::REQUIRED,
32-
'The directory containing PHPUnit JUnit xml files'
33-
)
34-
->addArgument(
35-
'file',
36-
InputArgument::REQUIRED,
37-
'The file where to write the merged result'
38-
);
30+
->setDescription('Merges multiple PHPUnit JUnit xml files into one')
31+
->addArgument(
32+
'directory',
33+
InputArgument::REQUIRED,
34+
'The directory containing PHPUnit JUnit xml files'
35+
)
36+
->addArgument(
37+
'file',
38+
InputArgument::REQUIRED,
39+
'The file where to write the merged result'
40+
);
3941
}
4042

4143
protected function execute(InputInterface $input, OutputInterface $output)
4244
{
4345
$finder = new Finder();
4446
$finder->files()
45-
->in(realpath($input->getArgument('directory')));
47+
->in(realpath($input->getArgument('directory')))->sortByName(true);
4648

4749
$this->document = new \DOMDocument('1.0', 'UTF-8');
4850
$this->document->formatOutput = true;
4951

5052
$root = $this->document->createElement('testsuites');
53+
$baseSuite = $this->document->createElement('testsuite');
54+
$baseSuite->setAttribute('name', 'All Suites');
55+
$baseSuite->setAttribute('tests', '0');
56+
$baseSuite->setAttribute('assertions', '0');
57+
$baseSuite->setAttribute('errors', '0');
58+
$baseSuite->setAttribute('failures', '0');
59+
$baseSuite->setAttribute('skipped', '0');
60+
$baseSuite->setAttribute('time', '0');
61+
62+
$this->domElements['All Suites'] = $baseSuite;
63+
64+
$root->appendChild($baseSuite);
5165
$this->document->appendChild($root);
5266

5367
foreach ($finder as $file) {
5468
try {
5569
$xml = new \SimpleXMLElement(file_get_contents($file->getRealPath()));
5670
$xmlArray = json_decode(json_encode($xml), true);
5771
if (!empty($xmlArray)) {
58-
$this->addTestSuites($root, $xmlArray);
72+
$this->addTestSuites($baseSuite, $xmlArray);
5973
}
6074
} catch (\Exception $exception) {
61-
// Initial fallthrough
75+
$output->writeln(sprintf('<error>Error in file %s: %s</error>', $file->getRealPath(), $exception->getMessage()));
6276
}
6377
}
6478

@@ -67,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
6781
$domElement->removeAttribute('parent');
6882
}
6983
}
70-
84+
$this->calculateTopLevelStats();
7185
$file = $input->getArgument('file');
7286
if (!is_dir(dirname($file))) {
7387
@mkdir(dirname($file), 0777, true);
@@ -82,7 +96,7 @@ private function addTestSuites(\DOMElement $parent, array $testSuites)
8296
foreach ($testSuites as $testSuite) {
8397
if (empty($testSuite['@attributes']['name'])) {
8498
if (!empty($testSuite['testsuite'])) {
85-
$this->addTestSuites($parent, $testSuite['testsuite']);
99+
$this->addTestSuites($parent, $testSuite);
86100
}
87101
continue;
88102
}
@@ -95,7 +109,6 @@ private function addTestSuites(\DOMElement $parent, array $testSuites)
95109
$element->setAttribute('parent', $parent->getAttribute('name'));
96110
$attributes = $testSuite['@attributes'] ?? [];
97111
foreach ($attributes as $key => $value) {
98-
$value = $key === 'name' ? $value : 0;
99112
$element->setAttribute($key, (string)$value);
100113
}
101114
$parent->appendChild($element);
@@ -126,30 +139,51 @@ private function addTestCases(\DOMElement $parent, array $testCases)
126139
if (isset($this->domElements[$name])) {
127140
continue;
128141
}
129-
130142
$element = $this->document->createElement('testcase');
131143
foreach ($attributes as $key => $value) {
132144
$element->setAttribute($key, (string)$value);
133-
if (!is_numeric($value)) {
134-
continue;
135-
}
136-
$this->addAttributeValueToTestSuite($parent, $key, $value);
145+
}
146+
if (isset($testCase['failure']) || isset($testCase['warning']) || isset($testCase['error'])) {
147+
$this->addChildElements($testCase, $element);
137148
}
138149
$parent->appendChild($element);
139150
$this->domElements[$name] = $element;
140151
}
141152
}
142153

143-
private function addAttributeValueToTestSuite(\DOMElement $element, $key, $value)
154+
private function addChildElements(array $tree, \DOMElement $element)
144155
{
145-
$currentValue = $element->hasAttribute($key) ? $element->getAttribute($key) : 0;
146-
$element->setAttribute($key, (string)($currentValue + $value));
156+
foreach ($tree as $key => $value) {
157+
if ($key == '@attributes') {
158+
continue;
159+
}
160+
$child = $this->document->createElement($key);
161+
$child->nodeValue = $value;
162+
$element->appendChild($child);
163+
}
164+
}
147165

148-
if ($element->hasAttribute('parent')) {
149-
$parent = $element->getAttribute('parent');
150-
if (isset($this->domElements[$parent])) {
151-
$this->addAttributeValueToTestSuite($this->domElements[$parent], $key, $value);
166+
private function calculateTopLevelStats()
167+
{
168+
/** @var \DOMElement $topNode */
169+
$suites = $this->document->getElementsByTagName('testsuites')->item(0);
170+
$topNode = $suites->firstChild;
171+
if ($topNode->hasChildNodes()) {
172+
$stats = array_flip($this->keysToCalculate);
173+
$stats = array_map(function ($_value) {
174+
return 0;
175+
}, $stats);
176+
foreach ($topNode->childNodes as $child) {
177+
$attributes = $child->attributes;
178+
foreach ($attributes as $key => $value) {
179+
if (in_array($key, $this->keysToCalculate)) {
180+
$stats[$key] += $value->nodeValue;
181+
}
182+
}
183+
}
184+
foreach ($stats as $key => $value) {
185+
$topNode->setAttribute($key, (string)$value);
152186
}
153187
}
154188
}
155-
}
189+
}

tests/PhpunitMerger/Command/AbstractCommandTestCase.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,21 @@
99

1010
abstract class AbstractCommandTestCase extends TestCase
1111
{
12-
/**
13-
* @var string
14-
*/
15-
protected $logDirectory = __DIR__ . '/../../../.Log/';
16-
17-
/**
18-
* @var string
19-
*/
20-
protected $outputFile = 'foo.bar';
12+
protected string $logDirectory = __DIR__ . '/../../../.Log/';
13+
protected string $outputFile = 'foo.bar';
2114

2215
public function assertOutputFileNotExists()
2316
{
2417
$filesystem = new Filesystem();
18+
self::assertDirectoryExists($this->logDirectory, $this->logDirectory . ' does not exists');
2519
$filesystem->remove($this->logDirectory . $this->outputFile);
2620

2721
$this->assertFileDoesNotExist($this->logDirectory . $this->outputFile);
2822
}
2923

3024
public function assertOutputDirectoryNotExists()
3125
{
26+
self::assertDirectoryExists($this->logDirectory, $this->logDirectory . ' does not exists');
3227
$filesystem = new Filesystem();
3328
$filesystem->remove($this->logDirectory . dirname($this->outputFile));
3429

tests/PhpunitMerger/Command/Coverage/CoverageCommandTest.php

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111

1212
class CoverageCommandTest extends AbstractCommandTestCase
1313
{
14-
/**
15-
* @var string
16-
*/
17-
protected $outputFile = 'coverage.xml';
14+
protected string $outputFile = 'coverage.xml';
1815

1916
public function testCoverageWritesOutputFile()
2017
{
@@ -27,8 +24,7 @@ public function testCoverageWritesOutputFile()
2724
$this->logDirectory . $this->outputFile,
2825
]
2926
);
30-
$output = $this->getMockBuilder(OutputInterface::class)
31-
->getMock();
27+
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
3228
$output->method('write')->willThrowException(new \Exception());
3329

3430
$command = new CoverageCommand();
@@ -47,8 +43,7 @@ public function testCoverageWritesStandardOutput()
4743
$this->logDirectory . 'coverage/',
4844
]
4945
);
50-
$output = $this->getMockBuilder(OutputInterface::class)
51-
->getMock();
46+
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
5247

5348
$command = new CoverageCommand();
5449
$command->run($input, $output);
@@ -66,8 +61,7 @@ public function testCoverageWritesHtmlReport()
6661
'--html=' . $this->logDirectory . dirname($this->outputFile),
6762
]
6863
);
69-
$output = $this->getMockBuilder(OutputInterface::class)
70-
->getMock();
64+
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
7165

7266
$command = new CoverageCommand();
7367
$command->run($input, $output);
@@ -89,8 +83,7 @@ public function testCoverageWritesHtmlReportWithCustomBounds()
8983
'--highLowerBound=70',
9084
]
9185
);
92-
$output = $this->getMockBuilder(OutputInterface::class)
93-
->getMock();
86+
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
9487

9588
$command = new CoverageCommand();
9689
$command->run($input, $output);
@@ -122,8 +115,7 @@ public function testCoverageWritesOutputFileAndHtmlReport()
122115
$this->logDirectory . $this->outputFile,
123116
]
124117
);
125-
$output = $this->getMockBuilder(OutputInterface::class)
126-
->getMock();
118+
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
127119
$output->method('write')->willThrowException(new \Exception());
128120

129121
$command = new CoverageCommand();

0 commit comments

Comments
 (0)