Skip to content

Commit 5b74f62

Browse files
staabmsebastianbergmann
authored andcommitted
Drop DOM dependency in XML Report
to speedup xml report generation
1 parent bca180c commit 5b74f62

File tree

16 files changed

+237
-384
lines changed

16 files changed

+237
-384
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"sebastian/environment": "^8.0.3",
4141
"sebastian/lines-of-code": "^4.0",
4242
"sebastian/version": "^6.0",
43-
"theseer/tokenizer": "^1.3.1"
43+
"theseer/tokenizer": "^2.0"
4444
},
4545
"require-dev": {
4646
"phpunit/phpunit": "^12.4.4"

src/Report/Xml/BuildInformation.php

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,63 +9,47 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Report\Xml;
1111

12-
use function assert;
1312
use function phpversion;
1413
use DateTimeImmutable;
15-
use DOMElement;
1614
use SebastianBergmann\Environment\Runtime;
15+
use XMLWriter;
1716

1817
/**
1918
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
2019
*/
2120
final readonly class BuildInformation
2221
{
23-
private DOMElement $contextNode;
24-
2522
public function __construct(
26-
DOMElement $contextNode,
23+
XMLWriter $xmlWriter,
2724
Runtime $runtime,
2825
DateTimeImmutable $buildDate,
2926
string $phpUnitVersion,
3027
string $coverageVersion
3128
) {
32-
$this->contextNode = $contextNode;
33-
34-
$runtimeNode = $this->nodeByName('runtime');
29+
$xmlWriter->startElement('build');
30+
$xmlWriter->writeAttribute('time', $buildDate->format('D M j G:i:s T Y'));
31+
$xmlWriter->writeAttribute('phpunit', $phpUnitVersion);
32+
$xmlWriter->writeAttribute('coverage', $coverageVersion);
3533

36-
$runtimeNode->setAttribute('name', $runtime->getName());
37-
$runtimeNode->setAttribute('version', $runtime->getVersion());
38-
$runtimeNode->setAttribute('url', $runtime->getVendorUrl());
34+
$xmlWriter->startElement('runtime');
35+
$xmlWriter->writeAttribute('name', $runtime->getName());
36+
$xmlWriter->writeAttribute('version', $runtime->getVersion());
37+
$xmlWriter->writeAttribute('url', $runtime->getVendorUrl());
38+
$xmlWriter->endElement();
3939

40-
$driverNode = $this->nodeByName('driver');
40+
$xmlWriter->startElement('driver');
4141

4242
if ($runtime->hasXdebug()) {
43-
$driverNode->setAttribute('name', 'xdebug');
44-
$driverNode->setAttribute('version', phpversion('xdebug'));
43+
$xmlWriter->writeAttribute('name', 'xdebug');
44+
$xmlWriter->writeAttribute('version', phpversion('xdebug'));
4545
}
4646

4747
if ($runtime->hasPCOV()) {
48-
$driverNode->setAttribute('name', 'pcov');
49-
$driverNode->setAttribute('version', phpversion('pcov'));
48+
$xmlWriter->writeAttribute('name', 'pcov');
49+
$xmlWriter->writeAttribute('version', phpversion('pcov'));
5050
}
51+
$xmlWriter->endElement();
5152

52-
$this->contextNode->setAttribute('time', $buildDate->format('D M j G:i:s T Y'));
53-
54-
$this->contextNode->setAttribute('phpunit', $phpUnitVersion);
55-
$this->contextNode->setAttribute('coverage', $coverageVersion);
56-
}
57-
58-
private function nodeByName(string $name): DOMElement
59-
{
60-
$node = $this->contextNode->appendChild(
61-
$this->contextNode->ownerDocument->createElementNS(
62-
Facade::XML_NAMESPACE,
63-
$name,
64-
),
65-
);
66-
67-
assert($node instanceof DOMElement);
68-
69-
return $node;
53+
$xmlWriter->endElement();
7054
}
7155
}

src/Report/Xml/Coverage.php

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Report\Xml;
1111

12-
use DOMElement;
1312
use XMLWriter;
1413

1514
/**
1615
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
1716
*/
1817
final class Coverage
1918
{
20-
private readonly DOMElement $contextNode;
19+
private readonly XMLWriter $xmlWriter;
2120
private readonly string $line;
2221

23-
public function __construct(DOMElement $context, string $line)
24-
{
25-
$this->contextNode = $context;
26-
$this->line = $line;
22+
public function __construct(
23+
XMLWriter $xmlWriter,
24+
string $line
25+
) {
26+
$this->xmlWriter = $xmlWriter;
27+
$this->line = $line;
2728
}
2829

2930
public function finalize(array $tests): void
3031
{
31-
$writer = new XMLWriter;
32-
$writer->openMemory();
33-
$writer->startElementNs(null, $this->contextNode->nodeName, Facade::XML_NAMESPACE);
32+
$writer = $this->xmlWriter;
33+
$writer->startElement('line');
3434
$writer->writeAttribute('nr', $this->line);
3535

3636
foreach ($tests as $test) {
@@ -39,13 +39,5 @@ public function finalize(array $tests): void
3939
$writer->endElement();
4040
}
4141
$writer->endElement();
42-
43-
$fragment = $this->contextNode->ownerDocument->createDocumentFragment();
44-
$fragment->appendXML($writer->outputMemory());
45-
46-
$this->contextNode->parentNode->replaceChild(
47-
$fragment,
48-
$this->contextNode,
49-
);
5042
}
5143
}

src/Report/Xml/Facade.php

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use function strlen;
2222
use function substr;
2323
use DateTimeImmutable;
24-
use DOMDocument;
2524
use SebastianBergmann\CodeCoverage\CodeCoverage;
2625
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
2726
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
@@ -31,11 +30,11 @@
3130
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
3231
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
3332
use SebastianBergmann\CodeCoverage\Util\Filesystem;
34-
use SebastianBergmann\CodeCoverage\Util\Xml;
3533
use SebastianBergmann\CodeCoverage\Version;
3634
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
3735
use SebastianBergmann\CodeCoverage\XmlException;
3836
use SebastianBergmann\Environment\Runtime;
37+
use XMLWriter;
3938

4039
/**
4140
* @phpstan-import-type TestType from CodeCoverage
@@ -68,15 +67,21 @@ public function process(CodeCoverage $coverage, string $target): void
6867

6968
$report = $coverage->getReport();
7069

70+
$writer = new XMLWriter;
71+
$writer->openUri($this->targetFilePath('index'));
72+
$writer->setIndent(true);
73+
$writer->setIndentString(' ');
7174
$this->project = new Project(
75+
$writer,
7276
$coverage->getReport()->name(),
7377
);
7478

7579
$this->setBuildInformation();
80+
81+
$this->project->startProject();
7682
$this->processTests($coverage->getTests());
7783
$this->processDirectory($report, $this->project);
78-
79-
$this->saveDocument($this->project->asDom(), 'index');
84+
$this->project->finalize();
8085
}
8186

8287
private function setBuildInformation(): void
@@ -121,7 +126,10 @@ private function processDirectory(DirectoryNode $directory, Node $context): void
121126
$directoryName = '/';
122127
}
123128

124-
$directoryObject = $context->addDirectory($directoryName);
129+
$writer = $this->project->getWriter();
130+
$writer->startElement('directory');
131+
$writer->writeAttribute('name', $directoryName);
132+
$directoryObject = $context->addDirectory();
125133

126134
$this->setTotals($directory, $directoryObject->totals());
127135

@@ -132,27 +140,35 @@ private function processDirectory(DirectoryNode $directory, Node $context): void
132140
foreach ($directory->files() as $node) {
133141
$this->processFile($node, $directoryObject);
134142
}
143+
$writer->endElement();
135144
}
136145

137146
/**
138147
* @throws XmlException
139148
*/
140149
private function processFile(FileNode $file, Directory $context): void
141150
{
142-
$fileObject = $context->addFile(
143-
$file->name(),
144-
$file->id() . '.xml',
145-
$file->sha1(),
146-
);
151+
$context->getWriter()->startElement('file');
152+
$context->getWriter()->writeAttribute('name', $file->name());
153+
$context->getWriter()->writeAttribute('href', $file->id() . '.xml');
154+
$context->getWriter()->writeAttribute('hash', $file->sha1());
155+
156+
$fileObject = $context->addFile();
147157

148158
$this->setTotals($file, $fileObject->totals());
149159

160+
$context->getWriter()->endElement();
161+
150162
$path = substr(
151163
$file->pathAsString(),
152164
strlen($this->project->projectSourceDirectory()),
153165
);
154166

155-
$fileReport = new Report($path, $file->sha1());
167+
$writer = new XMLWriter;
168+
$writer->openUri($this->targetFilePath($file->id()));
169+
$writer->setIndent(true);
170+
$writer->setIndentString(' ');
171+
$fileReport = new Report($writer, $path, $file->sha1());
156172

157173
$this->setTotals($file, $fileReport->totals());
158174

@@ -164,6 +180,8 @@ private function processFile(FileNode $file, Directory $context): void
164180
$this->processFunction($function, $fileReport);
165181
}
166182

183+
$fileReport->getWriter()->startElement('coverage');
184+
167185
foreach ($file->lineCoverageData() as $line => $tests) {
168186
if (!is_array($tests) || count($tests) === 0) {
169187
continue;
@@ -172,19 +190,22 @@ private function processFile(FileNode $file, Directory $context): void
172190
$coverage = $fileReport->lineCoverage((string) $line);
173191
$coverage->finalize($tests);
174192
}
193+
$fileReport->getWriter()->endElement();
175194

176195
if ($this->includeSource) {
177196
$fileReport->source()->setSourceCode(
178197
file_get_contents($file->pathAsString()),
179198
);
180199
}
181200

182-
$this->saveDocument($fileReport->asDom(), $file->id());
201+
$fileReport->finalize();
183202
}
184203

185204
private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report $report): void
186205
{
187206
if ($unit instanceof ProcessedClassType) {
207+
$report->getWriter()->startElement('class');
208+
188209
$unitObject = $report->classObject(
189210
$unit->className,
190211
$unit->namespace,
@@ -194,6 +215,8 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report
194215
(float) $unit->crap,
195216
);
196217
} else {
218+
$report->getWriter()->startElement('trait');
219+
197220
$unitObject = $report->traitObject(
198221
$unit->traitName,
199222
$unit->namespace,
@@ -205,6 +228,8 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report
205228
}
206229

207230
foreach ($unit->methods as $method) {
231+
$report->getWriter()->startElement('method');
232+
208233
$unitObject->addMethod(
209234
$method->methodName,
210235
$method->signature,
@@ -215,11 +240,17 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report
215240
(string) $method->coverage,
216241
$method->crap,
217242
);
243+
244+
$report->getWriter()->endElement();
218245
}
246+
247+
$report->getWriter()->endElement();
219248
}
220249

221250
private function processFunction(ProcessedFunctionType $function, Report $report): void
222251
{
252+
$report->getWriter()->startElement('function');
253+
223254
$report->functionObject(
224255
$function->functionName,
225256
$function->signature,
@@ -230,22 +261,30 @@ private function processFunction(ProcessedFunctionType $function, Report $report
230261
(string) $function->coverage,
231262
$function->crap,
232263
);
264+
265+
$report->getWriter()->endElement();
233266
}
234267

235268
/**
236269
* @param array<string, TestType> $tests
237270
*/
238271
private function processTests(array $tests): void
239272
{
273+
$this->project->getWriter()->startElement('tests');
274+
240275
$testsObject = $this->project->tests();
241276

242277
foreach ($tests as $test => $result) {
243278
$testsObject->addTest($test, $result);
244279
}
280+
281+
$this->project->getWriter()->endElement();
245282
}
246283

247284
private function setTotals(AbstractNode $node, Totals $totals): void
248285
{
286+
$totals->getWriter()->startElement('totals');
287+
249288
$loc = $node->linesOfCode();
250289

251290
$totals->setNumLines(
@@ -256,6 +295,16 @@ private function setTotals(AbstractNode $node, Totals $totals): void
256295
$node->numberOfExecutedLines(),
257296
);
258297

298+
$totals->setNumMethods(
299+
$node->numberOfMethods(),
300+
$node->numberOfTestedMethods(),
301+
);
302+
303+
$totals->setNumFunctions(
304+
$node->numberOfFunctions(),
305+
$node->numberOfTestedFunctions(),
306+
);
307+
259308
$totals->setNumClasses(
260309
$node->numberOfClasses(),
261310
$node->numberOfTestedClasses(),
@@ -266,15 +315,7 @@ private function setTotals(AbstractNode $node, Totals $totals): void
266315
$node->numberOfTestedTraits(),
267316
);
268317

269-
$totals->setNumMethods(
270-
$node->numberOfMethods(),
271-
$node->numberOfTestedMethods(),
272-
);
273-
274-
$totals->setNumFunctions(
275-
$node->numberOfFunctions(),
276-
$node->numberOfTestedFunctions(),
277-
);
318+
$totals->getWriter()->endElement();
278319
}
279320

280321
private function targetDirectory(): string
@@ -290,12 +331,4 @@ private function targetFilePath(string $name): string
290331

291332
return $filename;
292333
}
293-
294-
/**
295-
* @throws XmlException
296-
*/
297-
private function saveDocument(DOMDocument $document, string $name): void
298-
{
299-
Filesystem::write($this->targetFilePath($name), Xml::asString($document));
300-
}
301334
}

0 commit comments

Comments
 (0)