packages/deriver/tests/Integration/MicroEvaluatorTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate;
9use Deriver\Deriver\DeriverConfig;
10use Deriver\Deriver\Evaluation;
11use Deriver\Deriver\EvaluationOperation;
12use Deriver\Deriver\Evaluator\Evidence\EvidenceKind;
13use Deriver\Deriver\Evaluator\Evidence\Frontier;
14use Deriver\Deriver\Evaluator\Scalar\Identity;
15use Deriver\Deriver\Evaluator\Term;
16use Deriver\Deriver\Evaluator\TermKind;
17use Deriver\Deriver\MicroEvaluator;
18use Deriver\Deriver\Sources\Path;
19use Deriver\Deriver\Sources\SourceRef;
20use Deriver\Exception\EvaluationContractException;
21use Deriver\Exception\InvalidInputException;
22use LogicException;
23use org\bovigo\vfs\vfsStream;
24use Override;
25use phpmock\Mock;
26use phpmock\MockBuilder;
27use PHPUnit\Framework\Attributes\CoversNothing;
28use PHPUnit\Framework\Attributes\Medium;
29use PHPUnit\Framework\Attributes\PreserveGlobalState;
30use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
31use PHPUnit\Framework\TestCase;
32use WeakMap;
33
34#[CoversNothing]
35#[Medium]
36#[RunTestsInSeparateProcesses]
37#[PreserveGlobalState(false)]
38final class MicroEvaluatorTest extends TestCase
39{
40 /**
41 * @throws \phpmock\MockEnabledException
42 */
43 public function testEvaluatesOnlyDemandedArguments(): void
44 {
45 $workingDirectory = '/project';
46 $filesystem = vfsStream::setup('project');
47 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
48 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
49 })->build()->enable();
50 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
51 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
52 })->build()->enable();
53 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
54 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
55 })->build()->enable();
56 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
57 return $workingDirectory;
58 })->build()->enable();
59 $sources = $filesystem;
60 $name = 'target.php';
61 $contents = '<?php function identity($value,$unused){return missing();} function target(){return strlen(identity("hello", expensive()));}';
62 $url = $sources->url() . '/' . $name;
63 file_put_contents($url, $contents);
64 $path = '/' . $sources->path() . '/' . $name;
65 $evaluator = new MicroEvaluator('app.identity', '1', EvaluationOperation::Function, '\IDENTITY', static fn (Evaluation $evaluation): Term => $evaluation->input(0));
66 $deriver = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]));
67 $result = $deriver->derive('target#return');
68 self::assertSame(5, $result[0]->result);
69 $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate): array {
70 if ($candidate->result instanceof Term) {
71 $graph = $candidate->result;
72 $snapshot = $candidate->evidence[0]->snapshot;
73 $pending = [$graph];
74 $seen = new WeakMap();
75 $frontiers1 = [];
76 while ($pending !== []) {
77 $node1 = array_pop($pending);
78 if (isset($seen[$node1])) {
79 continue;
80 }
81 $seen[$node1] = true;
82 array_push($pending, ...array_values($node1->operands));
83 $reason = $node1->attributes['reason'] ?? null;
84 if (!is_string($reason)) {
85 continue;
86 }
87 $source = new SourceRef($snapshot->id, (string) ($node1->attributes['source'] ?? ''), (int) ($node1->attributes['start'] ?? 0), (int) ($node1->attributes['end'] ?? $node1->attributes['start'] ?? 0));
88 $identity = (string) ($node1->attributes['identity'] ?? (new Identity())->key($node1));
89 $frontiers1[$identity] = new Frontier($reason, $source, (string) $node1->literal, ['value'], [$identity], $node1, $reason);
90 }
91 array_push($frontiers, ...array_values($frontiers1));
92 }
93 return $frontiers;
94 }, []);
95 self::assertSame([], $frontiers);
96 self::assertSame([], $result->statistics->expandedBodies);
97 $applications = array_values(array_filter($result[0]->evidence[0]->nodes(), static fn ($node): bool => $node->kind === EvidenceKind::EvaluatorApplication && $node->attributes['id'] === 'app.identity'));
98 self::assertCount(1, $applications);
99 self::assertCount(1, $applications[0]->inputs);
100 }
101 /**
102 * @throws \phpmock\MockEnabledException
103 */
104 public function testUsesNamedArgumentsAndLeavesOtherArgumentsUnopened(): void
105 {
106 $workingDirectory = '/project';
107 $filesystem = vfsStream::setup('project');
108 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
109 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
110 })->build()->enable();
111 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
112 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
113 })->build()->enable();
114 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
115 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
116 })->build()->enable();
117 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
118 return $workingDirectory;
119 })->build()->enable();
120 $sources = $filesystem;
121 $name = 'target.php';
122 $contents = '<?php function target(){return select(unused:missing(),value:21);}';
123 $url = $sources->url() . '/' . $name;
124 file_put_contents($url, $contents);
125 $path = '/' . $sources->path() . '/' . $name;
126 $evaluator = new MicroEvaluator('app.select', '1', EvaluationOperation::Function, 'select', static fn (Evaluation $evaluation): Term => $evaluation->input('value'));
127 $result = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]))->derive('target#return');
128 self::assertSame(21, $result[0]->result);
129 $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate): array {
130 if ($candidate->result instanceof Term) {
131 $graph = $candidate->result;
132 $snapshot = $candidate->evidence[0]->snapshot;
133 $pending = [$graph];
134 $seen = new WeakMap();
135 $frontiers1 = [];
136 while ($pending !== []) {
137 $node = array_pop($pending);
138 if (isset($seen[$node])) {
139 continue;
140 }
141 $seen[$node] = true;
142 array_push($pending, ...array_values($node->operands));
143 $reason = $node->attributes['reason'] ?? null;
144 if (!is_string($reason)) {
145 continue;
146 }
147 $source = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
148 $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
149 $frontiers1[$identity] = new Frontier($reason, $source, (string) $node->literal, ['value'], [$identity], $node, $reason);
150 }
151 array_push($frontiers, ...array_values($frontiers1));
152 }
153 return $frontiers;
154 }, []);
155 self::assertSame([], $frontiers);
156 }
157 /**
158 * @throws \phpmock\MockEnabledException
159 */
160 public function testComposesOperandEvaluationWithBuiltInFunctions(): void
161 {
162 $workingDirectory = '/project';
163 $filesystem = vfsStream::setup('project');
164 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
165 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
166 })->build()->enable();
167 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
168 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
169 })->build()->enable();
170 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
171 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
172 })->build()->enable();
173 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
174 return $workingDirectory;
175 })->build()->enable();
176 $sources = $filesystem;
177 $name = 'target.php';
178 $contents = '<?php function target(){return strlen("abc")+strlen("de");}';
179 $url = $sources->url() . '/' . $name;
180 file_put_contents($url, $contents);
181 $path = '/' . $sources->path() . '/' . $name;
182 $evaluator = new MicroEvaluator('app.product', '1', EvaluationOperation::Binary, '+', static function (Evaluation $evaluation): Term {
183 $left = $evaluation->input(0)->native();
184 $right = $evaluation->input(1)->native();
185 self::assertIsInt($left);
186 self::assertIsInt($right);
187 return Term::constant($left * $right);
188 });
189 self::assertSame(6, Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]))->derive('target#return')[0]->result);
190 self::assertSame(5, Deriver::prepare([$path])->derive('target#return')[0]->result);
191 }
192 /**
193 * @throws \phpmock\MockEnabledException
194 */
195 public function testConditionalEvaluationDemandsOnlyTheSelectedBranch(): void
196 {
197 $workingDirectory = '/project';
198 $filesystem = vfsStream::setup('project');
199 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
200 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
201 })->build()->enable();
202 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
203 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
204 })->build()->enable();
205 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
206 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
207 })->build()->enable();
208 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
209 return $workingDirectory;
210 })->build()->enable();
211 $sources = $filesystem;
212 $name = 'target.php';
213 $contents = '<?php function target($flag){return $flag ? missing() : 42;}';
214 $url = $sources->url() . '/' . $name;
215 file_put_contents($url, $contents);
216 $path = '/' . $sources->path() . '/' . $name;
217 $evaluator = new MicroEvaluator('app.false-branch', '1', EvaluationOperation::Conditional, '', static fn (Evaluation $evaluation): Term => $evaluation->input(1));
218 $result = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]))->derive('target#return');
219 self::assertSame(42, $result[0]->result);
220 $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate): array {
221 if ($candidate->result instanceof Term) {
222 $graph = $candidate->result;
223 $snapshot = $candidate->evidence[0]->snapshot;
224 $pending = [$graph];
225 $seen = new WeakMap();
226 $frontiers1 = [];
227 while ($pending !== []) {
228 $node = array_pop($pending);
229 if (isset($seen[$node])) {
230 continue;
231 }
232 $seen[$node] = true;
233 array_push($pending, ...array_values($node->operands));
234 $reason = $node->attributes['reason'] ?? null;
235 if (!is_string($reason)) {
236 continue;
237 }
238 $source = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
239 $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
240 $frontiers1[$identity] = new Frontier($reason, $source, (string) $node->literal, ['value'], [$identity], $node, $reason);
241 }
242 array_push($frontiers, ...array_values($frontiers1));
243 }
244 return $frontiers;
245 }, []);
246 self::assertSame([], $frontiers);
247 }
248 /**
249 * @throws \phpmock\MockEnabledException
250 */
251 public function testDecliningContinuesWithBuiltInAndSourceEvaluation(): void
252 {
253 $workingDirectory = '/project';
254 $filesystem = vfsStream::setup('project');
255 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
256 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
257 })->build()->enable();
258 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
259 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
260 })->build()->enable();
261 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
262 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
263 })->build()->enable();
264 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
265 return $workingDirectory;
266 })->build()->enable();
267 $sources = $filesystem;
268 $name = 'target.php';
269 $contents = '<?php function helper(){return 5;} function target(){return strlen("abc")+helper();}';
270 $url = $sources->url() . '/' . $name;
271 file_put_contents($url, $contents);
272 $path = '/' . $sources->path() . '/' . $name;
273 $evaluators = [new MicroEvaluator('app.strlen', '1', EvaluationOperation::Function, 'strlen', static fn (): ?Term => null), new MicroEvaluator('app.helper', '1', EvaluationOperation::Function, 'helper', static fn (): ?Term => null), new MicroEvaluator('app.plus', '1', EvaluationOperation::Binary, '+', static fn (): ?Term => null)];
274 self::assertSame(8, Deriver::prepare([$path], new DeriverConfig(evaluators: $evaluators))->derive('target#return')[0]->result);
275 }
276 /**
277 * @throws \phpmock\MockEnabledException
278 */
279 public function testPriorityAndEvidenceDoNotDependOnRegistrationOrder(): void
280 {
281 $workingDirectory = '/project';
282 $filesystem = vfsStream::setup('project');
283 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
284 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
285 })->build()->enable();
286 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
287 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
288 })->build()->enable();
289 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
290 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
291 })->build()->enable();
292 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
293 return $workingDirectory;
294 })->build()->enable();
295 $sources = $filesystem;
296 $name = 'target.php';
297 $contents = '<?php function target(){return choose(99,42);}';
298 $url = $sources->url() . '/' . $name;
299 file_put_contents($url, $contents);
300 $path = '/' . $sources->path() . '/' . $name;
301 $declines = 0;
302 $declining = new MicroEvaluator('app.decline', '1', EvaluationOperation::Function, 'choose', static function (Evaluation $evaluation) use (&$declines): ?Term {
303 $declines++;
304 $evaluation->input(0);
305 return null;
306 }, priority: 10);
307 $selected = new MicroEvaluator('app.selected', '1', EvaluationOperation::Function, 'choose', static fn (Evaluation $evaluation): Term => $evaluation->input(1));
308 $first = Deriver::prepare([$path], new DeriverConfig(evaluators: [$declining, $selected]))->derive('target#return');
309 $second = Deriver::prepare([$path], new DeriverConfig(evaluators: [$selected, $declining]))->derive('target#return');
310 self::assertSame(42, $first[0]->result);
311 self::assertSame(42, $second[0]->result);
312 self::assertSame(2, $declines);
313 $applications = array_values(array_filter($first[0]->evidence[0]->nodes(), static fn ($node): bool => $node->kind === EvidenceKind::EvaluatorApplication));
314 self::assertCount(1, $applications);
315 self::assertSame('app.selected', $applications[0]->attributes['id']);
316 self::assertCount(1, $applications[0]->inputs);
317 self::assertSame($first[0]->evidence[0]->snapshot->id, $second[0]->evidence[0]->snapshot->id);
318 }
319 /**
320 * @throws \phpmock\MockEnabledException
321 */
322 public function testCanReturnAPartialExpressionWithItsDemandedInput(): void
323 {
324 $workingDirectory = '/project';
325 $filesystem = vfsStream::setup('project');
326 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
327 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
328 })->build()->enable();
329 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
330 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
331 })->build()->enable();
332 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
333 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
334 })->build()->enable();
335 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
336 return $workingDirectory;
337 })->build()->enable();
338 $sources = $filesystem;
339 $name = 'target.php';
340 $contents = '<?php function target($value){return external($value);}';
341 $url = $sources->url() . '/' . $name;
342 file_put_contents($url, $contents);
343 $path = '/' . $sources->path() . '/' . $name;
344 $evaluator = new MicroEvaluator('app.external', '1', EvaluationOperation::Function, 'external', static fn (Evaluation $evaluation): Term => new Term(TermKind::Operation, 'external-result', [$evaluation->input(0)]));
345 $result = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]))->derive('target#return');
346 self::assertInstanceOf(Term::class, $result[0]->result);
347 self::assertSame('external-result', $result[0]->result->literal);
348 self::assertNotEmpty($result[0]->result->operands);
349 }
350 /**
351 * @throws \phpmock\MockEnabledException
352 */
353 public function testEvaluationStateIsFreshForEachQuery(): void
354 {
355 $workingDirectory = '/project';
356 $filesystem = vfsStream::setup('project');
357 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
358 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
359 })->build()->enable();
360 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
361 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
362 })->build()->enable();
363 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
364 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
365 })->build()->enable();
366 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
367 return $workingDirectory;
368 })->build()->enable();
369 $sources = $filesystem;
370 $name = 'target.php';
371 $contents = '<?php function target(){return first(second());}';
372 $url = $sources->url() . '/' . $name;
373 file_put_contents($url, $contents);
374 $path = '/' . $sources->path() . '/' . $name;
375 $calls = 0;
376 $evaluators = [new MicroEvaluator('app.first', '1', EvaluationOperation::Function, 'first', static function (Evaluation $evaluation): Term {
377 $evaluation->input(0);
378 return $evaluation->input(0);
379 }), new MicroEvaluator('app.second', '1', EvaluationOperation::Function, 'second', static function () use (&$calls): Term {
380 $calls++;
381 return Term::constant(7);
382 })];
383 $deriver = Deriver::prepare([$path], new DeriverConfig(evaluators: $evaluators));
384 self::assertSame(7, $deriver->derive('target#return')[0]->result);
385 self::assertSame(1, $calls);
386 self::assertSame(7, $deriver->derive('target#return')[0]->result);
387 self::assertSame(2, $calls);
388 }
389 /**
390 * @throws \phpmock\MockEnabledException
391 */
392 public function testDisabledStandardEvaluatorsDoNotDisableConfiguredEvaluators(): void
393 {
394 $workingDirectory = '/project';
395 $filesystem = vfsStream::setup('project');
396 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
397 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
398 })->build()->enable();
399 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
400 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
401 })->build()->enable();
402 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
403 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
404 })->build()->enable();
405 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
406 return $workingDirectory;
407 })->build()->enable();
408 $sources = $filesystem;
409 $name = 'target.php';
410 $contents = '<?php function target(){return answer();}';
411 $url = $sources->url() . '/' . $name;
412 file_put_contents($url, $contents);
413 $path = '/' . $sources->path() . '/' . $name;
414 $evaluator = new MicroEvaluator('app.answer', '1', EvaluationOperation::Function, 'answer', static fn (): Term => Term::constant(42));
415 self::assertSame(42, Deriver::prepare([$path], new DeriverConfig(standardEvaluators: false, evaluators: [$evaluator]))->derive('target#return')[0]->result);
416 }
417 /**
418 * @throws \phpmock\MockEnabledException
419 */
420 public function testInvalidInputDemandReportsTheEvaluatorIdentity(): void
421 {
422 $workingDirectory = '/project';
423 $filesystem = vfsStream::setup('project');
424 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
425 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
426 })->build()->enable();
427 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
428 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
429 })->build()->enable();
430 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
431 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
432 })->build()->enable();
433 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
434 return $workingDirectory;
435 })->build()->enable();
436 $sources = $filesystem;
437 $name = 'target.php';
438 $contents = '<?php function target(){return answer();}';
439 $url = $sources->url() . '/' . $name;
440 file_put_contents($url, $contents);
441 $path = '/' . $sources->path() . '/' . $name;
442 $evaluator = new MicroEvaluator('app.invalid', '1', EvaluationOperation::Function, 'answer', static fn (Evaluation $evaluation): Term => $evaluation->input(3));
443 $deriver = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]));
444 $this->expectException(EvaluationContractException::class);
445 $this->expectExceptionMessage('app.invalid: The micro evaluator requested an absent input: 3');
446 $deriver->derive('target#return');
447 }
448 /**
449 * @throws \phpmock\MockEnabledException
450 */
451 public function testCallbackFailureReportsTheEvaluatorIdentity(): void
452 {
453 $workingDirectory = '/project';
454 $filesystem = vfsStream::setup('project');
455 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
456 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
457 })->build()->enable();
458 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
459 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
460 })->build()->enable();
461 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
462 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
463 })->build()->enable();
464 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
465 return $workingDirectory;
466 })->build()->enable();
467 $sources = $filesystem;
468 $name = 'target.php';
469 $contents = '<?php function target(){return answer();}';
470 $url = $sources->url() . '/' . $name;
471 file_put_contents($url, $contents);
472 $path = '/' . $sources->path() . '/' . $name;
473 $cause = new LogicException('broken evaluation');
474 $evaluator = new MicroEvaluator('app.broken', '1', EvaluationOperation::Function, 'answer', static fn (): Term => throw $cause);
475 $deriver = Deriver::prepare([$path], new DeriverConfig(evaluators: [$evaluator]));
476 $this->expectException(EvaluationContractException::class);
477 $this->expectExceptionMessage('app.broken: broken evaluation');
478 $deriver->derive('target#return');
479 }
480 /**
481 * @throws \phpmock\MockEnabledException
482 */
483 public function testRejectsConflictingMicroEvaluatorsDuringPreparation(): void
484 {
485 $workingDirectory = '/project';
486 $filesystem = vfsStream::setup('project');
487 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
488 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
489 })->build()->enable();
490 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
491 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
492 })->build()->enable();
493 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
494 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
495 })->build()->enable();
496 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
497 return $workingDirectory;
498 })->build()->enable();
499 $evaluators = [new MicroEvaluator('app.first', '1', EvaluationOperation::Function, 'answer', static fn (): Term => Term::constant(1)), new MicroEvaluator('app.second', '1', EvaluationOperation::Function, '\ANSWER', static fn (): Term => Term::constant(2))];
500 $this->expectException(InvalidInputException::class);
501 $this->expectExceptionMessage('EVALUATOR_CONFLICT');
502 Deriver::prepare([], new DeriverConfig(evaluators: $evaluators));
503 }
504 #[Override]
505 protected function tearDown(): void
506 {
507 Mock::disableAll();
508 parent::tearDown();
509 }
510}
511