packages/deriver/tests/Unit/Deriver/Sources/CollectorTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Sources;
6
7use Deriver\Deriver\Sources\Collector;
8use Deriver\Deriver\Sources\Path;
9use Deriver\Deriver\Sources\Source;
10use Deriver\Deriver\Sources\Sources;
11use Deriver\Exception\CollectorException;
12use org\bovigo\vfs\vfsStream;
13use Override;
14use phpmock\Mock;
15use phpmock\MockBuilder;
16use PHPUnit\Framework\Attributes\CoversClass;
17use PHPUnit\Framework\Attributes\DataProvider;
18use PHPUnit\Framework\Attributes\PreserveGlobalState;
19use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
20use PHPUnit\Framework\Attributes\Small;
21use PHPUnit\Framework\Attributes\UsesClass;
22use PHPUnit\Framework\TestCase;
23
24#[CoversClass(Collector::class)]
25#[UsesClass(Path::class)]
26#[UsesClass(Source::class)]
27#[UsesClass(Sources::class)]
28#[Small]
29#[RunTestsInSeparateProcesses]
30#[PreserveGlobalState(false)]
31final class CollectorTest extends TestCase
32{
33 /**
34 * @throws \phpmock\MockEnabledException
35 */
36 public function testCollectReadsOnlySelectedFilesInCallerOrderWithoutExecutingOrParsing(): void
37 {
38 $workingDirectory = '/project';
39 $filesystem = vfsStream::setup('project');
40 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
41 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
42 })->build()->enable();
43 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
44 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
45 })->build()->enable();
46 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
47 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
48 })->build()->enable();
49 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
50 return $workingDirectory;
51 })->build()->enable();
52 $directory = $filesystem;
53 $throwing = '<?php throw new RuntimeException("must not execute");';
54 $malformed = "<?php function invalid( \xff";
55 $name = 'z.php';
56 $url = $directory->url() . '/' . $name;
57 file_put_contents($url, $throwing);
58 $last = '/' . $directory->path() . '/' . $name;
59 $name1 = 'a.php';
60 $url1 = $directory->url() . '/' . $name1;
61 file_put_contents($url1, $malformed);
62 $first = '/' . $directory->path() . '/' . $name1;
63 $name2 = 'unselected.php';
64 $contents = '<?php exit(1);';
65 $url2 = $directory->url() . '/' . $name2;
66 file_put_contents($url2, $contents);
67 $sources = (new Collector())->collect([$last, $first]);
68 self::assertCount(2, $sources);
69 self::assertSame($last, $sources[0]->path);
70 self::assertSame($throwing, $sources[0]->contents);
71 self::assertFalse($sources[0]->declarationsOnly);
72 self::assertSame($first, $sources[1]->path);
73 self::assertSame($malformed, $sources[1]->contents);
74 }
75 /**
76 * @throws \phpmock\MockEnabledException
77 */
78 public function testCollectAcceptsAnEmptySelection(): void
79 {
80 $workingDirectory = '/project';
81 $filesystem = vfsStream::setup('project');
82 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
83 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
84 })->build()->enable();
85 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
86 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
87 })->build()->enable();
88 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
89 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
90 })->build()->enable();
91 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
92 return $workingDirectory;
93 })->build()->enable();
94 self::assertCount(0, (new Collector())->collect([]));
95 }
96 /**
97 * @throws \phpmock\MockEnabledException
98 */
99 public function testCollectCapturesFreshContentsAndPreservesEarlierSourcesAfterDeletion(): void
100 {
101 $workingDirectory = '/project';
102 $filesystem = vfsStream::setup('project');
103 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
104 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
105 })->build()->enable();
106 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
107 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
108 })->build()->enable();
109 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
110 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
111 })->build()->enable();
112 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
113 return $workingDirectory;
114 })->build()->enable();
115 $directory = $filesystem;
116 $name = 'source.php';
117 $contents = 'first';
118 $url = $directory->url() . '/' . $name;
119 file_put_contents($url, $contents);
120 $path = '/' . $directory->path() . '/' . $name;
121 $collector = new Collector();
122 $first = $collector->collect([$path]);
123 $name1 = 'source.php';
124 $contents1 = 'second';
125 $url1 = $directory->url() . '/' . $name1;
126 file_put_contents($url1, $contents1);
127 $second = $collector->collect([$path]);
128 $path1 = $path;
129 $path1 = str_starts_with($path1, '/') ? $path1 : $workingDirectory . '/' . $path1;
130 self::assertTrue(unlink('vfs:/' . Path::normalize($path1)));
131 self::assertSame('first', $first[0]->contents);
132 self::assertSame('second', $second[0]->contents);
133 }
134 /**
135 * @throws \phpmock\MockEnabledException
136 */
137 public function testCollectNormalizesSourcePaths(): void
138 {
139 $workingDirectory = '/project';
140 $filesystem = vfsStream::setup('project', null, ['nested' => []]);
141 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
142 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
143 })->build()->enable();
144 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
145 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
146 })->build()->enable();
147 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
148 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
149 })->build()->enable();
150 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
151 return $workingDirectory;
152 })->build()->enable();
153 $directory = $filesystem;
154 $name = 'nested/source.php';
155 $contents = '';
156 $url = $directory->url() . '/' . $name;
157 file_put_contents($url, $contents);
158 $path = '/' . $directory->path() . '/' . $name;
159 $sources = (new Collector())->collect(['/' . $directory->path() . '/nested/../nested/./source.php']);
160 self::assertSame($path, $sources[0]->path);
161 self::assertSame('', $sources[0]->contents);
162 }
163 /**
164 * @throws \phpmock\MockEnabledException
165 */
166 public function testCollectRejectsRepeatedPathsAfterNormalization(): void
167 {
168 $workingDirectory = '/project';
169 $filesystem = vfsStream::setup('project');
170 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
171 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
172 })->build()->enable();
173 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
174 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
175 })->build()->enable();
176 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
177 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
178 })->build()->enable();
179 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
180 return $workingDirectory;
181 })->build()->enable();
182 $directory = $filesystem;
183 $name = 'source.php';
184 $contents = '<?php';
185 $url = $directory->url() . '/' . $name;
186 file_put_contents($url, $contents);
187 $path = '/' . $directory->path() . '/' . $name;
188 $this->expectException(CollectorException::class);
189 $this->expectExceptionMessage('Source paths must be nonempty and unique:');
190 (new Collector())->collect([$path, '/' . $directory->path() . '/./source.php']);
191 }
192 /**
193 * @throws \phpmock\MockEnabledException
194 */
195 #[DataProvider('providerUnreadableFiles')]
196 public function testCollectRejectsWrappersDirectoriesAndMissingFiles(string $path): void
197 {
198 $workingDirectory = '/project';
199 $filesystem = vfsStream::setup('project');
200 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
201 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
202 })->build()->enable();
203 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
204 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
205 })->build()->enable();
206 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
207 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
208 })->build()->enable();
209 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
210 return $workingDirectory;
211 })->build()->enable();
212 $this->expectException(CollectorException::class);
213 $this->expectExceptionMessage('Source must be a readable local file:');
214 (new Collector())->collect([$path]);
215 }
216 /**
217 * @return iterable<string, array{string}>
218 */
219 public static function providerUnreadableFiles(): iterable
220 {
221 yield 'wrapper' => ['php://memory'];
222 yield 'readable local wrapper' => ['file://' . __FILE__];
223 yield 'directory' => [__DIR__];
224 yield 'missing' => [__DIR__ . '/does-not-exist.php'];
225 }
226 #[Override]
227 protected function tearDown(): void
228 {
229 Mock::disableAll();
230 parent::tearDown();
231 }
232}
233