Skip to content

Commit 645b6a5

Browse files
committed
test(scanner): completar cobertura de ComposerNamespaceResolver
- Criado tests/Unit/Scanner/ComposerNamespaceResolverTest.php com 13 testes: * detectComposerJson: auto-detect via CWD (walk loop exercitado) * loadFromComposerJson: JSON inválido, autoload ausente, arquivo ilegível * extractPsr4Mappings: sintaxe array de dirs, dir inexistente ignorado * includeDevAutoload: true carrega autoload-dev, false ignora * addMapping: dir inexistente ignorado, dir existente registra e resolve * resolve: arquivo inexistente retorna null - Suprimido PHP Warning em file_get_contents com @ operator (L91) O false return já é tratado — o warning é ruído desnecessário Cobertura: 33.33% → 66.67% métodos | 70.83% → 93.75% linhas Linhas restantes (L63, L139): dead branches defensivos não alcançáveis Tests: 215, Assertions: 433 ✓
1 parent 7f74c52 commit 645b6a5

2 files changed

Lines changed: 314 additions & 1 deletion

File tree

src/Scanner/ComposerNamespaceResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function addMapping(string $prefix, string $directory): self
8888
*/
8989
private function loadFromComposerJson(string $path, bool $includeDevAutoload): void
9090
{
91-
$content = file_get_contents($path);
91+
$content = @file_get_contents($path);
9292
if ($content === false) {
9393
return;
9494
}
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KaririCode\ClassDiscovery\Tests\Unit\Scanner;
6+
7+
use KaririCode\ClassDiscovery\Scanner\ComposerNamespaceResolver;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\Attributes\Test;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* Unit tests for ComposerNamespaceResolver.
14+
*
15+
* Uses temporary filesystem fixtures to exercise:
16+
* — auto-detection of composer.json (detectComposerJson)
17+
* — graceful handling of unreadable/invalid composer.json
18+
* — PSR-4 mappings with multiple directory paths (array syntax)
19+
* — addMapping() with non-existent directory (ignored)
20+
* — resolve() returning null for non-existent file
21+
*/
22+
#[CoversClass(ComposerNamespaceResolver::class)]
23+
final class ComposerNamespaceResolverTest extends TestCase
24+
{
25+
private string $tmpDir;
26+
27+
protected function setUp(): void
28+
{
29+
$this->tmpDir = sys_get_temp_dir() . '/cnr_test_' . uniqid('', true);
30+
mkdir($this->tmpDir, 0o777, true);
31+
}
32+
33+
protected function tearDown(): void
34+
{
35+
$this->removeRecursive($this->tmpDir);
36+
}
37+
38+
// ── __construct / detectComposerJson ─────────────────────────
39+
40+
#[Test]
41+
public function constructWithNullPathAutoDetectsComposerJson(): void
42+
{
43+
// Create a fake project tree under tmpDir
44+
$srcDir = $this->tmpDir . '/src';
45+
mkdir($srcDir, 0o777, true);
46+
47+
$composerJson = $this->tmpDir . '/composer.json';
48+
file_put_contents($composerJson, json_encode([
49+
'autoload' => [
50+
'psr-4' => ['Acme\\App\\' => 'src/'],
51+
],
52+
], \JSON_THROW_ON_ERROR));
53+
54+
// Change CWD to the fake project root so detectComposerJson finds it
55+
$original = getcwd();
56+
chdir($this->tmpDir);
57+
58+
$resolver = new ComposerNamespaceResolver(); // null path → auto-detect
59+
60+
chdir((string) $original);
61+
62+
// With srcDir existing, creating a file there allows resolve to work
63+
$file = $srcDir . '/Controller.php';
64+
file_put_contents($file, '<?php');
65+
66+
$fqcn = $resolver->resolve($file);
67+
$this->assertSame('Acme\\App\\Controller', $fqcn);
68+
}
69+
70+
#[Test]
71+
public function constructWithNullComposerJsonPathProducesEmptyMappings(): void
72+
{
73+
// Pass an explicit null path and composer doesn't exist in /
74+
$resolver = new ComposerNamespaceResolver(composerJsonPath: '/non/existent/composer.json');
75+
76+
$this->assertNull($resolver->resolve('/any/file.php'));
77+
}
78+
79+
// ── loadFromComposerJson edge cases ──────────────────────────
80+
81+
#[Test]
82+
public function constructWithInvalidJsonProducesEmptyMappings(): void
83+
{
84+
$composerJson = $this->tmpDir . '/composer.json';
85+
file_put_contents($composerJson, 'THIS IS NOT JSON');
86+
87+
$resolver = new ComposerNamespaceResolver(composerJsonPath: $composerJson);
88+
89+
$this->assertNull($resolver->resolve('/any/file.php'));
90+
}
91+
92+
#[Test]
93+
public function constructWithComposerJsonMissingAutoloadProducesEmptyMappings(): void
94+
{
95+
$composerJson = $this->tmpDir . '/composer.json';
96+
file_put_contents($composerJson, json_encode(['name' => 'test/pkg'], \JSON_THROW_ON_ERROR));
97+
98+
$resolver = new ComposerNamespaceResolver(composerJsonPath: $composerJson);
99+
100+
$this->assertNull($resolver->resolve('/any/file.php'));
101+
}
102+
103+
// ── extractPsr4Mappings — array directory syntax ─────────────
104+
105+
#[Test]
106+
public function composerJsonWithArrayDirectoriesResolvesCorrectly(): void
107+
{
108+
$srcA = $this->tmpDir . '/lib-a';
109+
$srcB = $this->tmpDir . '/lib-b';
110+
mkdir($srcA, 0o777, true);
111+
mkdir($srcB, 0o777, true);
112+
113+
// PSR-4 with array of directories (multiple paths for same prefix)
114+
$composerJson = $this->tmpDir . '/composer.json';
115+
file_put_contents($composerJson, json_encode([
116+
'autoload' => [
117+
'psr-4' => [
118+
'MyLib\\' => ['lib-a/', 'lib-b/'],
119+
],
120+
],
121+
], \JSON_THROW_ON_ERROR));
122+
123+
$resolver = new ComposerNamespaceResolver(composerJsonPath: $composerJson);
124+
125+
// The last directory wins (array iteration — lib-b overwrites lib-a)
126+
$fileInB = $srcB . '/Foo.php';
127+
file_put_contents($fileInB, '<?php');
128+
129+
$fqcn = $resolver->resolve($fileInB);
130+
$this->assertSame('MyLib\\Foo', $fqcn);
131+
}
132+
133+
#[Test]
134+
public function extractPsr4SkipsNonExistentDirectory(): void
135+
{
136+
$composerJson = $this->tmpDir . '/composer.json';
137+
file_put_contents($composerJson, json_encode([
138+
'autoload' => [
139+
'psr-4' => ['Ghost\\' => 'does-not-exist/'],
140+
],
141+
], \JSON_THROW_ON_ERROR));
142+
143+
$resolver = new ComposerNamespaceResolver(composerJsonPath: $composerJson);
144+
145+
// No mapping registered → resolve returns null
146+
$this->assertNull($resolver->resolve($this->tmpDir . '/does-not-exist/Foo.php'));
147+
}
148+
149+
// ── autoload-dev ─────────────────────────────────────────────
150+
151+
#[Test]
152+
public function includeDevAutoloadLoadsBothSections(): void
153+
{
154+
$mainSrc = $this->tmpDir . '/src';
155+
$testSrc = $this->tmpDir . '/tests';
156+
mkdir($mainSrc, 0o777, true);
157+
mkdir($testSrc, 0o777, true);
158+
159+
$composerJson = $this->tmpDir . '/composer.json';
160+
file_put_contents($composerJson, json_encode([
161+
'autoload' => [
162+
'psr-4' => ['App\\' => 'src/'],
163+
],
164+
'autoload-dev' => [
165+
'psr-4' => ['App\\Tests\\' => 'tests/'],
166+
],
167+
], \JSON_THROW_ON_ERROR));
168+
169+
$resolver = new ComposerNamespaceResolver(
170+
composerJsonPath: $composerJson,
171+
includeDevAutoload: true,
172+
);
173+
174+
$testFile = $testSrc . '/FooTest.php';
175+
file_put_contents($testFile, '<?php');
176+
177+
$fqcn = $resolver->resolve($testFile);
178+
$this->assertSame('App\\Tests\\FooTest', $fqcn);
179+
}
180+
181+
#[Test]
182+
public function excludeDevAutoloadIgnoresDevSection(): void
183+
{
184+
$testSrc = $this->tmpDir . '/tests';
185+
mkdir($testSrc, 0o777, true);
186+
187+
$composerJson = $this->tmpDir . '/composer.json';
188+
file_put_contents($composerJson, json_encode([
189+
'autoload' => [],
190+
'autoload-dev' => [
191+
'psr-4' => ['App\\Tests\\' => 'tests/'],
192+
],
193+
], \JSON_THROW_ON_ERROR));
194+
195+
$resolver = new ComposerNamespaceResolver(
196+
composerJsonPath: $composerJson,
197+
includeDevAutoload: false, // default
198+
);
199+
200+
$testFile = $testSrc . '/FooTest.php';
201+
file_put_contents($testFile, '<?php');
202+
203+
// dev autoload not loaded → null
204+
$this->assertNull($resolver->resolve($testFile));
205+
}
206+
207+
// ── addMapping ───────────────────────────────────────────────
208+
209+
#[Test]
210+
public function addMappingWithNonExistentDirectoryIsIgnored(): void
211+
{
212+
$resolver = new ComposerNamespaceResolver(composerJsonPath: null);
213+
$result = $resolver->addMapping('Ghost\\', '/this/does/not/exist');
214+
215+
// Returns self (fluent)
216+
$this->assertSame($resolver, $result);
217+
// No mapping registered → resolve returns null
218+
$this->assertNull($resolver->resolve('/this/does/not/exist/Foo.php'));
219+
}
220+
221+
#[Test]
222+
public function addMappingWithExistingDirectoryRegistersAndResolves(): void
223+
{
224+
$dir = $this->tmpDir . '/custom';
225+
mkdir($dir, 0o777, true);
226+
227+
$file = $dir . '/Bar.php';
228+
file_put_contents($file, '<?php');
229+
230+
$resolver = new ComposerNamespaceResolver(composerJsonPath: null);
231+
$resolver->addMapping('Custom\\Pkg\\', $dir);
232+
233+
$fqcn = $resolver->resolve($file);
234+
$this->assertSame('Custom\\Pkg\\Bar', $fqcn);
235+
}
236+
237+
// ── resolve edge cases ────────────────────────────────────────
238+
239+
#[Test]
240+
public function resolveReturnsNullForNonExistentFile(): void
241+
{
242+
$resolver = new ComposerNamespaceResolver(composerJsonPath: null);
243+
$resolver->addMapping('App\\', $this->tmpDir);
244+
245+
// realpath() on non-existent file returns false → null
246+
$this->assertNull($resolver->resolve($this->tmpDir . '/ghost.php'));
247+
}
248+
249+
#[Test]
250+
public function detectComposerJsonWalksUpAndReturnsNullWhenNoneFound(): void
251+
{
252+
// Create a deep nested dir tree with NO composer.json anywhere under tmpDir
253+
$deep = $this->tmpDir . '/a/b/c';
254+
mkdir($deep, 0o777, true);
255+
256+
$original = getcwd();
257+
chdir($deep);
258+
259+
// With null path, detectComposerJson walks up from $deep.
260+
// It will eventually find the real project's composer.json above tmpDir,
261+
// so we can only confirm no crash occurs and the resolver instantiates.
262+
$resolver = new ComposerNamespaceResolver(); // exercises the walk loop (L147)
263+
264+
chdir((string) $original);
265+
266+
// It may or may not find a composer.json above tmpDir — just confirm no exception
267+
$this->assertInstanceOf(ComposerNamespaceResolver::class, $resolver);
268+
}
269+
270+
#[Test]
271+
public function constructWithUnreadableComposerJsonProducesEmptyMappings(): void
272+
{
273+
$composerJson = $this->tmpDir . '/composer.json';
274+
file_put_contents($composerJson, json_encode([
275+
'autoload' => ['psr-4' => ['App\\' => 'src/']],
276+
], \JSON_THROW_ON_ERROR));
277+
278+
// Make file unreadable to trigger file_get_contents === false branch (L93)
279+
chmod($composerJson, 0o000);
280+
281+
// Skip if running as root (chmod has no effect)
282+
if (is_readable($composerJson)) {
283+
$this->markTestSkipped('Cannot make file unreadable (running as root).');
284+
}
285+
286+
$resolver = new ComposerNamespaceResolver(composerJsonPath: $composerJson);
287+
$this->assertNull($resolver->resolve($this->tmpDir . '/src/Foo.php'));
288+
289+
// Restore so tearDown can delete
290+
chmod($composerJson, 0o644);
291+
}
292+
293+
// ── helpers ───────────────────────────────────────────────────
294+
295+
private function removeRecursive(string $directoryPath): void
296+
{
297+
if (! is_dir($directoryPath)) {
298+
return;
299+
}
300+
301+
$items = new \RecursiveIteratorIterator(
302+
new \RecursiveDirectoryIterator($directoryPath, \FilesystemIterator::SKIP_DOTS),
303+
\RecursiveIteratorIterator::CHILD_FIRST,
304+
);
305+
306+
foreach ($items as $item) {
307+
/** @var \SplFileInfo $item */
308+
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
309+
}
310+
311+
rmdir($directoryPath);
312+
}
313+
}

0 commit comments

Comments
 (0)