packages/deriver/tests/Unit/Deriver/Sources/SourcesTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Sources;
6
7use Deriver\Deriver\Sources\Path;
8use Deriver\Deriver\Sources\Source;
9use Deriver\Deriver\Sources\Sources;
10use Deriver\Exception\InvalidInputException;
11use OutOfBoundsException;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\DataProvider;
14use PHPUnit\Framework\Attributes\Small;
15use PHPUnit\Framework\Attributes\UsesClass;
16use PHPUnit\Framework\TestCase;
17use RuntimeException;
18
19#[CoversClass(Sources::class)]
20#[UsesClass(Path::class)]
21#[UsesClass(Source::class)]
22#[Small]
23final class SourcesTest extends TestCase
24{
25 public function testOffsetGetRetainsSourceIdentityAndCallerOrderWithoutReadingFiles(): void
26 {
27 $first = new Source('z.php', '<?php throw new RuntimeException("must not execute");');
28 $second = new Source('a.php', '<?php', true);
29 $sources = new Sources([$first, $second]);
30 self::assertCount(2, $sources);
31 self::assertSame([$first, $second], iterator_to_array($sources));
32 self::assertTrue(isset($sources[0]));
33 self::assertSame($first, $sources[0]);
34 self::assertSame($second, $sources[1]);
35 self::assertArrayNotHasKey('sources', get_object_vars($sources));
36 }
37
38 public function testCountSupportsAnEmptyCollection(): void
39 {
40 $sources = new Sources([]);
41 self::assertCount(0, $sources);
42 self::assertSame([], iterator_to_array($sources));
43 self::assertFalse(isset($sources[0]));
44 self::assertNull($sources[0] ?? null);
45 $this->expectException(OutOfBoundsException::class);
46 $sources[0];
47 }
48
49 /**
50 * @param list<Source> $sources Captured source descriptions
51 */
52 #[DataProvider('providerInvalidSourcePaths')]
53 public function testRejectsEmptyAndRepeatedNormalizedSourceIdentities(array $sources): void
54 {
55 $this->expectException(InvalidInputException::class);
56 $this->expectExceptionMessage('Source paths must be nonempty and unique:');
57 new Sources($sources);
58 }
59
60 /**
61 * @return iterable<string, array{list<Source>}>
62 */
63 public static function providerInvalidSourcePaths(): iterable
64 {
65 yield 'empty' => [[new Source('', '')]];
66 yield 'normalized empty' => [[new Source('./', '')]];
67 yield 'repeated' => [[new Source('src/a.php', 'first'), new Source('src/a.php', 'second')]];
68 yield 'separator alias' => [[new Source('src/a.php', 'first'), new Source('src\\a.php', 'second')]];
69 yield 'dot alias' => [[new Source('src/a.php', 'first'), new Source('src/old/../a.php', 'second')]];
70 }
71
72 #[DataProvider('providerMissingOffsets')]
73 public function testOffsetExistsRejectsMissingAndNonIntegerOffsets(int|string|null $offset): void
74 {
75 $sources = new Sources([new Source('a.php', '')]);
76 self::assertFalse($sources->offsetExists($offset));
77 $this->expectException(OutOfBoundsException::class);
78 $sources->offsetGet($offset);
79 }
80
81 /**
82 * @return iterable<string, array{int|string|null}>
83 */
84 public static function providerMissingOffsets(): iterable
85 {
86 yield 'negative' => [-1];
87 yield 'past the end' => [1];
88 yield 'numeric string' => ['0'];
89 yield 'string' => ['missing'];
90 yield 'null' => [null];
91 }
92
93 public function testOffsetSetRejectsReplacement(): void
94 {
95 $source = new Source('a.php', '');
96 $sources = new Sources([$source]);
97 $this->expectException(RuntimeException::class);
98 try {
99 $sources[0] = new Source('b.php', '');
100 } finally {
101 self::assertCount(1, $sources);
102 self::assertSame($source, $sources[0]);
103 }
104 }
105
106 public function testOffsetSetRejectsAppending(): void
107 {
108 $sources = new Sources([]);
109 $this->expectException(RuntimeException::class);
110 try {
111 $sources[] = new Source('a.php', '');
112 } finally {
113 self::assertCount(0, $sources);
114 }
115 }
116
117 public function testOffsetUnsetRejectsRemoval(): void
118 {
119 $source = new Source('a.php', '');
120 $sources = new Sources([$source]);
121 $this->expectException(RuntimeException::class);
122 try {
123 unset($sources[0]);
124 } finally {
125 self::assertCount(1, $sources);
126 self::assertSame($source, $sources[0]);
127 }
128 }
129
130 public function testGetIteratorCannotChangeTheCollection(): void
131 {
132 $source = new Source('a.php', '');
133 $sources = new Sources([$source]);
134 $iterator = $sources->getIterator();
135 unset($iterator[0]);
136 self::assertSame($source, $sources[0]);
137 self::assertCount(1, $sources);
138 }
139}
140