Skip to content

Commit 2df114f

Browse files
committed
[TASK] Unit-test XmlFileLoader
guides-cli's unit suite covers Command and Logger only, so the config loader is reachable exclusively through fixtures that render a whole site. Those prove what a reader sees; they cannot cheaply reach the shapes an author is unlikely to write while the loader handles them explicitly. XmlFileLoaderTest pins eight: a version read as written, a trailing zero kept in release as well, the quotes stripped from version and release, the same quotes kept on every other attribute, an attribute-less <project>, a file whose only child is <project>, a <project> nested in an <extension> not being taken for the project, and a file without a <project> not gaining an empty one. The third and fourth are the pair worth stating: the stripping is backward compatibility for two attributes, not a general unquoting rule, so a title that really is quoted keeps its quotes. Nothing covered that. Each case was seen failing on a defect built for it - DOM reading removed, trim widened to every attribute, trim dropped, the direct-child search degraded to a subtree search, the project key withheld when empty, and the project key set unconditionally. Every case appears in at least one failure list. Two appeared in none until the assertion was split: `?? []` reported a missing project config and an empty one as the same state. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_014H1xwaAmrQRWUA3vx8bJcD Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
1 parent dd7033c commit 2df114f

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Cli\Config;
15+
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\Config\FileLocator;
19+
20+
use function count;
21+
use function file_put_contents;
22+
use function sys_get_temp_dir;
23+
use function tempnam;
24+
use function unlink;
25+
26+
/**
27+
* The integration fixtures render a whole site and prove what a reader sees. This covers the shapes
28+
* an author is unlikely to write but the loader handles explicitly, and pins which attributes the
29+
* backward-compatible quote stripping is allowed to touch.
30+
*/
31+
final class XmlFileLoaderTest extends TestCase
32+
{
33+
private string|null $file = null;
34+
35+
protected function tearDown(): void
36+
{
37+
if ($this->file === null) {
38+
return;
39+
}
40+
41+
unlink($this->file);
42+
$this->file = null;
43+
}
44+
45+
/** @param array<string, string> $expected */
46+
#[DataProvider('provideProjectElements')]
47+
public function testReadsProjectAttributes(string $projectElement, array $expected): void
48+
{
49+
$root = $this->loadRoot($projectElement);
50+
51+
// Asserted separately from the contents: an empty project config and a missing one are two
52+
// different states, and `?? []` would report them as the same.
53+
self::assertArrayHasKey('project', $root);
54+
self::assertSame($expected, $root['project']);
55+
}
56+
57+
/** @return iterable<string, array{string, array<string, string>}> */
58+
public static function provideProjectElements(): iterable
59+
{
60+
// phpize() would turn this into the float 0.1. The DOM keeps the digit.
61+
yield 'a version is read as written' => [
62+
'<project title="T" version="0.10" release="0.10.0"/>',
63+
['title' => 'T', 'version' => '0.10', 'release' => '0.10.0'],
64+
];
65+
66+
// release gets no special treatment beyond version, and a release is not always longer than
67+
// the version it belongs to: `1.0` is the shape phpize turns into the int 1.
68+
yield 'a trailing zero survives in release as well' => [
69+
'<project version="1.0" release="1.0"/>',
70+
['version' => '1.0', 'release' => '1.0'],
71+
];
72+
73+
// The workaround for the coercion this branch removes; still honoured for files that adopted it.
74+
yield 'single quotes are stripped from version and release' => [
75+
'<project version="\'3.0\'" release="\'3.0.0\'"/>',
76+
['version' => '3.0', 'release' => '3.0.0'],
77+
];
78+
79+
// The stripping is backward compatibility for two attributes, not a general unquoting rule:
80+
// a title that really is quoted keeps its quotes.
81+
yield 'single quotes are kept everywhere else' => [
82+
'<project title="\'T\'" copyright="\'2026\'"/>',
83+
['title' => "'T'", 'copyright' => "'2026'"],
84+
];
85+
86+
yield 'an attribute-less project yields an empty project config' => [
87+
'<project/>',
88+
[],
89+
];
90+
}
91+
92+
/**
93+
* Detaching <project> from a file that has no other child leaves convertDomElementToArray() with an
94+
* empty element, for which it returns null rather than an array.
95+
*/
96+
public function testAProjectOnlyFileStillYieldsItsProject(): void
97+
{
98+
self::assertSame(
99+
['version' => '0.10'],
100+
$this->loadRoot('<project version="0.10"/>')['project'] ?? [],
101+
);
102+
}
103+
104+
/** The schema lets an extension carry arbitrary children; a <project> among them is not the project. */
105+
public function testAProjectInsideAnExtensionIsNotRead(): void
106+
{
107+
$root = $this->loadRoot(
108+
'<extension class="Some\Extension"><project version="9.9"/></extension>'
109+
. '<project version="0.10"/>',
110+
);
111+
112+
self::assertSame(['version' => '0.10'], $root['project'] ?? []);
113+
}
114+
115+
public function testAFileWithoutAProjectHasNoProjectKey(): void
116+
{
117+
self::assertArrayNotHasKey('project', $this->loadRoot(''));
118+
}
119+
120+
/**
121+
* Loads a guides.xml built around $body and returns the config of the file itself, which the loader
122+
* appends last.
123+
*
124+
* @return array<string, mixed>
125+
*/
126+
private function loadRoot(string $body): array
127+
{
128+
$file = tempnam(sys_get_temp_dir(), 'guides-xml-');
129+
self::assertIsString($file);
130+
$this->file = $file;
131+
132+
file_put_contents(
133+
$file,
134+
'<?xml version="1.0" encoding="UTF-8" ?>'
135+
. '<guides xmlns="https://www.phpdoc.org/guides">' . $body . '</guides>',
136+
);
137+
138+
$configs = (new XmlFileLoader(new FileLocator()))->load($file, 'xml');
139+
140+
return $configs[count($configs) - 1];
141+
}
142+
}

0 commit comments

Comments
 (0)