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