Skip to content

Commit 9e4b7a2

Browse files
Merge branch '11.0'
2 parents 073b2ff + fefc2a8 commit 9e4b7a2

File tree

8 files changed

+110
-89
lines changed

8 files changed

+110
-89
lines changed

ChangeLog-12.3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
44

5+
## [12.3.8] - 2025-MM-DD
6+
7+
* [#1092](https://github.com/sebastianbergmann/php-code-coverage/issues/1092): Error in `DOMDocument::saveXML()` is not handled
8+
59
## [12.3.7] - 2025-09-10
610

711
### Changed
@@ -56,6 +60,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
5660

5761
* [#1080](https://github.com/sebastianbergmann/php-code-coverage/pull/1080): Support for reporting code coverage information in OpenClover XML format; unlike the existing Clover XML reporter, which remains unchanged, the XML documents generated by this new reporter validate against the OpenClover project's XML schema definition, with one exception: we do not generate the `<testproject>` element. This feature is experimental and the generated XML might change in order to improve compliance with the OpenClover project's XML schema definition further. Such changes will be made in bugfix and/or minor releases even if they break backward compatibility.
5862

63+
[12.3.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.7...main
5964
[12.3.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.6...12.3.7
6065
[12.3.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.5...12.3.6
6166
[12.3.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.4...12.3.5

src/Report/Clover.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,31 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use function count;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function is_string;
1614
use function ksort;
1715
use function max;
1816
use function range;
19-
use function str_contains;
2017
use function time;
2118
use DOMDocument;
2219
use SebastianBergmann\CodeCoverage\CodeCoverage;
2320
use SebastianBergmann\CodeCoverage\Node\File;
2421
use SebastianBergmann\CodeCoverage\Util\Filesystem;
22+
use SebastianBergmann\CodeCoverage\Util\Xml;
2523
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
2624

2725
final class Clover
2826
{
2927
/**
28+
* @param null|non-empty-string $target
29+
* @param null|non-empty-string $name
30+
*
3031
* @throws WriteOperationFailedException
3132
*/
3233
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
3334
{
3435
$time = (string) time();
3536

36-
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
37-
$xmlDocument->formatOutput = true;
37+
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
3838

3939
$xmlCoverage = $xmlDocument->createElement('coverage');
4040
$xmlCoverage->setAttribute('generated', $time);
@@ -216,16 +216,10 @@ public function process(CodeCoverage $coverage, ?string $target = null, ?string
216216
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
217217
$xmlProject->appendChild($xmlMetrics);
218218

219-
$buffer = $xmlDocument->saveXML();
219+
$buffer = Xml::asString($xmlDocument);
220220

221221
if ($target !== null) {
222-
if (!str_contains($target, '://')) {
223-
Filesystem::createDirectory(dirname($target));
224-
}
225-
226-
if (@file_put_contents($target, $buffer) === false) {
227-
throw new WriteOperationFailedException($target);
228-
}
222+
Filesystem::write($target, $buffer);
229223
}
230224

231225
return $buffer;

src/Report/Cobertura.php

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
use const DIRECTORY_SEPARATOR;
1313
use function basename;
1414
use function count;
15-
use function dirname;
16-
use function file_put_contents;
1715
use function preg_match;
1816
use function range;
19-
use function str_contains;
2017
use function str_replace;
2118
use function time;
2219
use DOMImplementation;
2320
use SebastianBergmann\CodeCoverage\CodeCoverage;
2421
use SebastianBergmann\CodeCoverage\Node\File;
2522
use SebastianBergmann\CodeCoverage\Util\Filesystem;
23+
use SebastianBergmann\CodeCoverage\Util\Xml;
2624
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
2725

2826
final class Cobertura
2927
{
3028
/**
29+
* @param null|non-empty-string $target
30+
*
3131
* @throws WriteOperationFailedException
3232
*/
3333
public function process(CodeCoverage $coverage, ?string $target = null): string
@@ -44,10 +44,9 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
4444
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
4545
);
4646

47-
$document = $implementation->createDocument('', '', $documentType);
48-
$document->xmlVersion = '1.0';
49-
$document->encoding = 'UTF-8';
50-
$document->formatOutput = true;
47+
$document = $implementation->createDocument('', '', $documentType);
48+
$document->xmlVersion = '1.0';
49+
$document->encoding = 'UTF-8';
5150

5251
$coverageElement = $document->createElement('coverage');
5352

@@ -289,16 +288,10 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
289288

290289
$coverageElement->setAttribute('complexity', (string) $complexity);
291290

292-
$buffer = $document->saveXML();
291+
$buffer = Xml::asString($document);
293292

294293
if ($target !== null) {
295-
if (!str_contains($target, '://')) {
296-
Filesystem::createDirectory(dirname($target));
297-
}
298-
299-
if (@file_put_contents($target, $buffer) === false) {
300-
throw new WriteOperationFailedException($target);
301-
}
294+
Filesystem::write($target, $buffer);
302295
}
303296

304297
return $buffer;

src/Report/Crap4j.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use function date;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function htmlspecialchars;
1614
use function is_string;
1715
use function round;
18-
use function str_contains;
1916
use DOMDocument;
2017
use SebastianBergmann\CodeCoverage\CodeCoverage;
2118
use SebastianBergmann\CodeCoverage\Node\File;
2219
use SebastianBergmann\CodeCoverage\Util\Filesystem;
20+
use SebastianBergmann\CodeCoverage\Util\Xml;
2321
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
2422

2523
final readonly class Crap4j
@@ -32,12 +30,14 @@ public function __construct(int $threshold = 30)
3230
}
3331

3432
/**
33+
* @param null|non-empty-string $target
34+
* @param null|non-empty-string $name
35+
*
3536
* @throws WriteOperationFailedException
3637
*/
3738
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
3839
{
39-
$document = new DOMDocument('1.0', 'UTF-8');
40-
$document->formatOutput = true;
40+
$document = new DOMDocument('1.0', 'UTF-8');
4141

4242
$root = $document->createElement('crap_result');
4343
$document->appendChild($root);
@@ -119,16 +119,10 @@ public function process(CodeCoverage $coverage, ?string $target = null, ?string
119119
$root->appendChild($stats);
120120
$root->appendChild($methodsNode);
121121

122-
$buffer = $document->saveXML();
122+
$buffer = Xml::asString($document);
123123

124124
if ($target !== null) {
125-
if (!str_contains($target, '://')) {
126-
Filesystem::createDirectory(dirname($target));
127-
}
128-
129-
if (@file_put_contents($target, $buffer) === false) {
130-
throw new WriteOperationFailedException($target);
131-
}
125+
Filesystem::write($target, $buffer);
132126
}
133127

134128
return $buffer;

src/Report/PHP.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use const PHP_EOL;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function serialize;
16-
use function str_contains;
1714
use SebastianBergmann\CodeCoverage\CodeCoverage;
1815
use SebastianBergmann\CodeCoverage\Util\Filesystem;
1916
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
2017

2118
final class PHP
2219
{
20+
/**
21+
* @param null|non-empty-string $target
22+
*
23+
* @throws WriteOperationFailedException
24+
*/
2325
public function process(CodeCoverage $coverage, ?string $target = null): string
2426
{
2527
$coverage->clearCache();
@@ -28,13 +30,7 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
2830
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
2931

3032
if ($target !== null) {
31-
if (!str_contains($target, '://')) {
32-
Filesystem::createDirectory(dirname($target));
33-
}
34-
35-
if (@file_put_contents($target, $buffer) === false) {
36-
throw new WriteOperationFailedException($target);
37-
}
33+
Filesystem::write($target, $buffer);
3834
}
3935

4036
return $buffer;

src/Report/Xml/Facade.php

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@
1010
namespace SebastianBergmann\CodeCoverage\Report\Xml;
1111

1212
use const DIRECTORY_SEPARATOR;
13-
use const PHP_EOL;
1413
use function count;
1514
use function dirname;
1615
use function file_get_contents;
17-
use function file_put_contents;
1816
use function is_array;
1917
use function is_dir;
2018
use function is_file;
2119
use function is_writable;
22-
use function libxml_clear_errors;
23-
use function libxml_get_errors;
24-
use function libxml_use_internal_errors;
2520
use function sprintf;
2621
use function strlen;
2722
use function substr;
@@ -33,7 +28,8 @@
3328
use SebastianBergmann\CodeCoverage\Node\File;
3429
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
3530
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
36-
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
31+
use SebastianBergmann\CodeCoverage\Util\Filesystem;
32+
use SebastianBergmann\CodeCoverage\Util\Xml;
3733
use SebastianBergmann\CodeCoverage\Version;
3834
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
3935
use SebastianBergmann\CodeCoverage\XmlException;
@@ -107,7 +103,7 @@ private function initTargetDirectory(string $directory): void
107103
// @codeCoverageIgnoreEnd
108104
}
109105

110-
DirectoryUtil::createDirectory($directory);
106+
Filesystem::createDirectory($directory);
111107
}
112108

113109
/**
@@ -287,38 +283,8 @@ private function saveDocument(DOMDocument $document, string $name): void
287283
{
288284
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
289285

290-
$document->formatOutput = true;
291-
$document->preserveWhiteSpace = false;
292286
$this->initTargetDirectory(dirname($filename));
293287

294-
file_put_contents($filename, $this->documentAsString($document));
295-
}
296-
297-
/**
298-
* @throws XmlException
299-
*
300-
* @see https://bugs.php.net/bug.php?id=79191
301-
*/
302-
private function documentAsString(DOMDocument $document): string
303-
{
304-
$xmlErrorHandling = libxml_use_internal_errors(true);
305-
$xml = $document->saveXML();
306-
307-
if ($xml === false) {
308-
// @codeCoverageIgnoreStart
309-
$message = 'Unable to generate the XML';
310-
311-
foreach (libxml_get_errors() as $error) {
312-
$message .= PHP_EOL . $error->message;
313-
}
314-
315-
throw new XmlException($message);
316-
// @codeCoverageIgnoreEnd
317-
}
318-
319-
libxml_clear_errors();
320-
libxml_use_internal_errors($xmlErrorHandling);
321-
322-
return $xml;
288+
Filesystem::write($filename, Xml::asString($document));
323289
}
324290
}

src/Util/Filesystem.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Util;
1111

12+
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
13+
use function dirname;
14+
use function file_put_contents;
1215
use function is_dir;
1316
use function mkdir;
1417
use function sprintf;
18+
use function str_contains;
1519

1620
/**
1721
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -34,4 +38,20 @@ public static function createDirectory(string $directory): void
3438
);
3539
}
3640
}
41+
42+
/**
43+
* @param non-empty-string $target
44+
*
45+
* @throws WriteOperationFailedException
46+
*/
47+
public static function write(string $target, string $buffer): void
48+
{
49+
if (!str_contains($target, '://')) {
50+
self::createDirectory(dirname($target));
51+
}
52+
53+
if (@file_put_contents($target, $buffer) === false) {
54+
throw new WriteOperationFailedException($target);
55+
}
56+
}
3757
}

src/Util/Xml.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
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 SebastianBergmann\CodeCoverage\Util;
11+
12+
use const PHP_EOL;
13+
use function libxml_clear_errors;
14+
use function libxml_get_errors;
15+
use function libxml_use_internal_errors;
16+
use DOMDocument;
17+
use SebastianBergmann\CodeCoverage\XmlException;
18+
19+
/**
20+
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
21+
*/
22+
final readonly class Xml
23+
{
24+
/**
25+
* @throws XmlException
26+
*
27+
* @see https://bugs.php.net/bug.php?id=79191
28+
*/
29+
public static function asString(DOMDocument $document): string
30+
{
31+
$xmlErrorHandling = libxml_use_internal_errors(true);
32+
33+
$document->formatOutput = true;
34+
$document->preserveWhiteSpace = false;
35+
36+
$buffer = $document->saveXML();
37+
38+
if ($buffer === false) {
39+
$message = 'Unable to generate the XML';
40+
41+
foreach (libxml_get_errors() as $error) {
42+
$message .= PHP_EOL . $error->message;
43+
}
44+
45+
throw new XmlException($message);
46+
}
47+
48+
libxml_clear_errors();
49+
libxml_use_internal_errors($xmlErrorHandling);
50+
51+
return $buffer;
52+
}
53+
}

0 commit comments

Comments
 (0)