packages/deriver/tests/Unit/DeriverTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate;
9use Deriver\Deriver\Candidate\ResultRef;
10use Deriver\Deriver\Candidate\Snapshot;
11use Deriver\Deriver\Candidate\Statistics;
12use Deriver\Deriver\DeriverConfig;
13use Deriver\Deriver\EvaluationOperation;
14use Deriver\Deriver\Evaluator\Builtin\Library;
15use Deriver\Deriver\Evaluator\Call\Evaluators;
16use Deriver\Deriver\Evaluator\Call\Extensions;
17use Deriver\Deriver\Evaluator\Call\StateRegistry;
18use Deriver\Deriver\Evaluator\Evidence\Frontier;
19use Deriver\Deriver\Evaluator\Execution\Call\ArgumentBinding;
20use Deriver\Deriver\Evaluator\Execution\Call\ArgumentOrder;
21use Deriver\Deriver\Evaluator\Execution\Call\CallableCheck;
22use Deriver\Deriver\Evaluator\Execution\Call\CallExecutor;
23use Deriver\Deriver\Evaluator\Execution\Call\CallResolution;
24use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Resolution;
25use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Target;
26use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Transfer;
27use Deriver\Deriver\Evaluator\Execution\Call\TypeBinding;
28use Deriver\Deriver\Evaluator\Execution\Call\TypeCheck;
29use Deriver\Deriver\Evaluator\Execution\Call\UnknownCall;
30use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
31use Deriver\Deriver\Evaluator\Execution\Completion\Assessment;
32use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
33use Deriver\Deriver\Evaluator\Execution\Completion\Exceptional;
34use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
35use Deriver\Deriver\Evaluator\Execution\Context;
36use Deriver\Deriver\Evaluator\Execution\Control\ExceptionMatch;
37use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
38use Deriver\Deriver\Evaluator\Execution\Control\StateJoin;
39use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
40use Deriver\Deriver\Evaluator\Execution\Demand\Cell;
41use Deriver\Deriver\Evaluator\Execution\Demand\Components;
42use Deriver\Deriver\Evaluator\Execution\Demand\Discovery;
43use Deriver\Deriver\Evaluator\Execution\Demand\Key;
44use Deriver\Deriver\Evaluator\Execution\Demand\Table;
45use Deriver\Deriver\Evaluator\Execution\Dependencies;
46use Deriver\Deriver\Evaluator\Execution\Evaluator;
47use Deriver\Deriver\Evaluator\Execution\Havoc;
48use Deriver\Deriver\Evaluator\Execution\InstructionTransfer;
49use Deriver\Deriver\Evaluator\Execution\Memory\Location;
50use Deriver\Deriver\Evaluator\Execution\Memory\Materialization;
51use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
52use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
53use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
54use Deriver\Deriver\Evaluator\Execution\Memory\StorageCapture;
55use Deriver\Deriver\Evaluator\Execution\ObservationCollector;
56use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
57use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
58use Deriver\Deriver\Evaluator\Execution\State;
59use Deriver\Deriver\Evaluator\Execution\Summary\CompletionRecord;
60use Deriver\Deriver\Evaluator\Execution\Summary\Evaluation;
61use Deriver\Deriver\Evaluator\Execution\Summary\Invocation;
62use Deriver\Deriver\Evaluator\Execution\Summary\Isolation;
63use Deriver\Deriver\Evaluator\Execution\Trace\Derivation;
64use Deriver\Deriver\Evaluator\Execution\Transfer\CallableTransfer;
65use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
66use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
67use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
68use Deriver\Deriver\Evaluator\Request\Budget;
69use Deriver\Deriver\Evaluator\Request\EntryPoint;
70use Deriver\Deriver\Evaluator\Request\QueryScope;
71use Deriver\Deriver\Evaluator\Request\ReturnQuery;
72use Deriver\Deriver\Evaluator\Scalar\Identity;
73use Deriver\Deriver\Evaluator\Scalar\Operations;
74use Deriver\Deriver\Evaluator\TargetProfile;
75use Deriver\Deriver\Evaluator\Term;
76use Deriver\Deriver\Execution\DerivationResult;
77use Deriver\Deriver\Execution\QueryExecution;
78use Deriver\Deriver\Execution\ResultAssessment;
79use Deriver\Deriver\Identity\QueryEncoding;
80use Deriver\Deriver\Identity\ValueGraph;
81use Deriver\Deriver\Lowerer\AssignmentLowering;
82use Deriver\Deriver\Lowerer\CallLowering;
83use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
84use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
85use Deriver\Deriver\Lowerer\Declaration\CallableSource;
86use Deriver\Deriver\Lowerer\Declaration\Index;
87use Deriver\Deriver\Lowerer\Declaration\Indexer;
88use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
89use Deriver\Deriver\Lowerer\EffectInspection;
90use Deriver\Deriver\Lowerer\ExpressionLowering;
91use Deriver\Deriver\Lowerer\Graph\BasicBlock;
92use Deriver\Deriver\Lowerer\Graph\CallableGraph;
93use Deriver\Deriver\Lowerer\Graph\Instruction;
94use Deriver\Deriver\Lowerer\Graph\Terminator;
95use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
96use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
97use Deriver\Deriver\Lowerer\InstructionEmitter;
98use Deriver\Deriver\Lowerer\Lowerer;
99use Deriver\Deriver\Lowerer\LoweringScope;
100use Deriver\Deriver\Lowerer\StatementLowering;
101use Deriver\Deriver\MicroEvaluator;
102use Deriver\Deriver\Parser\Node\File;
103use Deriver\Deriver\Parser\Parser;
104use Deriver\Deriver\Parser\ParserConfig;
105use Deriver\Deriver\Parser\TargetSyntax;
106use Deriver\Deriver\Providers\ProviderExpansion;
107use Deriver\Deriver\Query;
108use Deriver\Deriver\Resolver\AssignmentPatterns;
109use Deriver\Deriver\Resolver\ClassScope;
110use Deriver\Deriver\Resolver\ExpressionValidation;
111use Deriver\Deriver\Resolver\MagicContext;
112use Deriver\Deriver\Resolver\Resolver;
113use Deriver\Deriver\Selection\QueryValidation;
114use Deriver\Deriver\Selection\SelectorResolution;
115use Deriver\Deriver\Sources\Collector;
116use Deriver\Deriver\Sources\LineMap;
117use Deriver\Deriver\Sources\Path;
118use Deriver\Deriver\Sources\Source;
119use Deriver\Deriver\Sources\SourceRef;
120use Deriver\Deriver\Sources\Sources;
121use Deriver\Exception\CollectorException;
122use Deriver\Exception\InvalidInputException;
123use Deriver\Exception\ParserException;
124use Deriver\Exception\ResolutionException;
125use org\bovigo\vfs\vfsStream;
126use Override;
127use phpmock\Mock;
128use phpmock\MockBuilder;
129use PHPUnit\Framework\Attributes\CoversClass;
130use PHPUnit\Framework\Attributes\PreserveGlobalState;
131use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
132use PHPUnit\Framework\Attributes\Small;
133use PHPUnit\Framework\Attributes\UsesClass;
134use PHPUnit\Framework\TestCase;
135use WeakMap;
136use WeakReference;
137
138#[UsesClass(Resolver::class)]
139#[UsesClass(ExpressionValidation::class)]
140#[UsesClass(ResolutionException::class)]
141#[UsesClass(ParserConfig::class)]
142#[UsesClass(ParserException::class)]
143#[CoversClass(Deriver::class)]
144#[UsesClass(Query::class)]
145#[UsesClass(SelectorResolution::class)]
146#[UsesClass(QueryExecution::class)]
147#[UsesClass(QueryValidation::class)]
148#[UsesClass(ResultAssessment::class)]
149#[UsesClass(BasicBlock::class)]
150#[UsesClass(CallableGraph::class)]
151#[UsesClass(CallableIdentity::class)]
152#[UsesClass(Instruction::class)]
153#[UsesClass(Terminator::class)]
154#[UsesClass(ArgumentBinding::class)]
155#[UsesClass(ArgumentOrder::class)]
156#[UsesClass(CallExecutor::class)]
157#[UsesClass(CallResolution::class)]
158#[UsesClass(CallableCheck::class)]
159#[UsesClass(Builtins::class)]
160#[UsesClass(Dispatch::class)]
161#[UsesClass(Resolution::class)]
162#[UsesClass(Target::class)]
163#[UsesClass(Transfer::class)]
164#[UsesClass(TypeBinding::class)]
165#[UsesClass(TypeCheck::class)]
166#[UsesClass(UnknownCall::class)]
167#[UsesClass(Completion::class)]
168#[UsesClass(Context::class)]
169#[UsesClass(ExceptionMatch::class)]
170#[UsesClass(ObservationLimit::class)]
171#[UsesClass(StateJoin::class)]
172#[UsesClass(Unwinding::class)]
173#[UsesClass(Cell::class)]
174#[UsesClass(Components::class)]
175#[UsesClass(Discovery::class)]
176#[UsesClass(Key::class)]
177#[UsesClass(Table::class)]
178#[UsesClass(Dependencies::class)]
179#[UsesClass(Havoc::class)]
180#[UsesClass(InstructionTransfer::class)]
181#[UsesClass(Evaluator::class)]
182#[UsesClass(SlotReference::class)]
183#[UsesClass(ObservationCollector::class)]
184#[UsesClass(Conversions::class)]
185#[UsesClass(ScalarErrors::class)]
186#[UsesClass(State::class)]
187#[UsesClass(CompletionRecord::class)]
188#[UsesClass(Evaluation::class)]
189#[UsesClass(Invocation::class)]
190#[UsesClass(Isolation::class)]
191#[UsesClass(CallableTransfer::class)]
192#[UsesClass(MemoryStep::class)]
193#[UsesClass(PureStep::class)]
194#[UsesClass(ReferenceAssignment::class)]
195#[UsesClass(Location::class)]
196#[UsesClass(Materialization::class)]
197#[UsesClass(Memory::class)]
198#[UsesClass(ReferenceConstraint::class)]
199#[UsesClass(StorageCapture::class)]
200#[UsesClass(Library::class)]
201#[UsesClass(Extensions::class)]
202#[UsesClass(ProviderExpansion::class)]
203#[UsesClass(Evaluators::class)]
204#[UsesClass(StateRegistry::class)]
205#[UsesClass(DeriverConfig::class)]
206#[UsesClass(EntryPoint::class)]
207#[UsesClass(Sources::class)]
208#[UsesClass(Collector::class)]
209#[UsesClass(Path::class)]
210#[UsesClass(Snapshot::class)]
211#[UsesClass(Source::class)]
212#[UsesClass(TargetProfile::class)]
213#[UsesClass(Budget::class)]
214#[UsesClass(QueryScope::class)]
215#[UsesClass(ReturnQuery::class)]
216#[UsesClass(ResultRef::class)]
217#[UsesClass(SourceRef::class)]
218#[UsesClass(Alternative::class)]
219#[UsesClass(Assessment::class)]
220#[UsesClass(Derivation::class)]
221#[UsesClass(DerivationResult::class)]
222#[UsesClass(Exceptional::class)]
223#[UsesClass(Frontier::class)]
224#[UsesClass(QueryEncoding::class)]
225#[UsesClass(ValueGraph::class)]
226#[UsesClass(Statistics::class)]
227#[UsesClass(StorageSnapshot::class)]
228#[UsesClass(Parser::class)]
229#[UsesClass(File::class)]
230#[UsesClass(AssignmentLowering::class)]
231#[UsesClass(CallLowering::class)]
232#[UsesClass(Lowerer::class)]
233#[UsesClass(DestructuringLowering::class)]
234#[UsesClass(EffectInspection::class)]
235#[UsesClass(ExpressionLowering::class)]
236#[UsesClass(InstructionEmitter::class)]
237#[UsesClass(LoweringScope::class)]
238#[UsesClass(StatementLowering::class)]
239#[UsesClass(CallableSource::class)]
240#[UsesClass(Indexer::class)]
241#[UsesClass(Index::class)]
242#[UsesClass(Composition::class)]
243#[UsesClass(LineMap::class)]
244#[UsesClass(MagicContext::class)]
245#[UsesClass(AssignmentPatterns::class)]
246#[UsesClass(ClassScope::class)]
247#[UsesClass(TargetSyntax::class)]
248#[UsesClass(Identity::class)]
249#[UsesClass(Operations::class)]
250#[UsesClass(Term::class)]
251#[Small]
252#[RunTestsInSeparateProcesses]
253#[PreserveGlobalState(false)]
254final class DeriverTest extends TestCase
255{
256    /**
257     * @throws \phpmock\MockEnabledException
258     */
259    public function testOnlyPreparationAndDerivationArePublic(): void
260    {
261        $workingDirectory = '/project';
262        $filesystem = vfsStream::setup('project');
263        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
264            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
265        })->build()->enable();
266        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
267            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
268        })->build()->enable();
269        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
270            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
271        })->build()->enable();
272        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
273            return $workingDirectory;
274        })->build()->enable();
275        $deriver = Deriver::prepare([]);
276        $methods = get_class_methods($deriver);
277        sort($methods);
278        self::assertSame(['derive', 'prepare'], $methods);
279        self::assertSame([], get_object_vars($deriver));
280    }
281    /**
282     * @throws \phpmock\MockEnabledException
283     */
284    public function testPrepareUsesDefaultConfiguration(): void
285    {
286        $workingDirectory = '/project';
287        $filesystem = vfsStream::setup('project');
288        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
289            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
290        })->build()->enable();
291        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
292            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
293        })->build()->enable();
294        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
295            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
296        })->build()->enable();
297        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
298            return $workingDirectory;
299        })->build()->enable();
300        $source = $filesystem;
301        $name = 'default.php';
302        $contents = '<?php function target(){return 42;}';
303        $url = $source->url() . '/' . $name;
304        file_put_contents($url, $contents);
305        $path = '/' . $source->path() . '/' . $name;
306        $implicit = Deriver::prepare([$path])->derive('target#return');
307        $explicit = Deriver::prepare([$path], DeriverConfig::default())->derive('target#return');
308        self::assertEquals(iterator_to_array($implicit), iterator_to_array($explicit));
309        self::assertSame('8.3', $implicit[0]->evidence[0]->snapshot->target->version);
310    }
311    /**
312     * @throws \phpmock\MockEnabledException
313     */
314    public function testPrepareTranslatesThePhpVersionIntoParserConfiguration(): void
315    {
316        $workingDirectory = '/project';
317        $filesystem = vfsStream::setup('project');
318        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
319            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
320        })->build()->enable();
321        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
322            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
323        })->build()->enable();
324        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
325            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
326        })->build()->enable();
327        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
328            return $workingDirectory;
329        })->build()->enable();
330        $source = $filesystem;
331        $name = 'version.php';
332        $contents = '<?php class Box{function value(){return 42;}} function target(){return new Box()->value();}';
333        $url = $source->url() . '/' . $name;
334        file_put_contents($url, $contents);
335        $path = '/' . $source->path() . '/' . $name;
336        $result = Deriver::prepare([$path], new DeriverConfig(phpVersion: '8.4'))->derive('target#return');
337        self::assertSame(42, $result[0]->result);
338        self::assertSame('8.4', $result[0]->evidence[0]->snapshot->target->version);
339        $this->expectException(ParserException::class);
340        Deriver::prepare([$path]);
341    }
342    /**
343     * @throws \phpmock\MockEnabledException
344     */
345    public function testPrepareAndDeriveDoNotExecuteApplicationCode(): void
346    {
347        $workingDirectory = '/project';
348        $filesystem = vfsStream::setup('project');
349        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
350            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
351        })->build()->enable();
352        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
353            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
354        })->build()->enable();
355        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
356            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
357        })->build()->enable();
358        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
359            return $workingDirectory;
360        })->build()->enable();
361        $source = $filesystem;
362        $name = 'target.php';
363        $contents = '<?php throw new RuntimeException("never execute"); function target() { $x="before"; $x="after"; return $x . ":done"; }';
364        $url = $source->url() . '/' . $name;
365        file_put_contents($url, $contents);
366        $path = '/' . $source->path() . '/' . $name;
367        $deriver = Deriver::prepare([$path]);
368        $result = $deriver->derive('target#return');
369        self::assertSame('after:done', $result[0]->result);
370        $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate): array {
371            if ($candidate->result instanceof Term) {
372                $graph = $candidate->result;
373                $snapshot = $candidate->evidence[0]->snapshot;
374                $pending = [$graph];
375                $seen = new WeakMap();
376                $frontiers1 = [];
377                while ($pending !== []) {
378                    $node = array_pop($pending);
379                    if (isset($seen[$node])) {
380                        continue;
381                    }
382                    $seen[$node] = true;
383                    array_push($pending, ...array_values($node->operands));
384                    $reason = $node->attributes['reason'] ?? null;
385                    if (!is_string($reason)) {
386                        continue;
387                    }
388                    $source1 = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
389                    $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
390                    $frontiers1[$identity] = new Frontier($reason, $source1, (string) $node->literal, ['value'], [$identity], $node, $reason);
391                }
392                array_push($frontiers, ...array_values($frontiers1));
393            }
394            return $frontiers;
395        }, []);
396        self::assertSame([], $frontiers);
397    }
398    /**
399     * @throws \phpmock\MockEnabledException
400     */
401    public function testPrepareReadsOnlyTheSelectedFilesAfterDiscovery(): void
402    {
403        $workingDirectory = '/project';
404        $filesystem = vfsStream::setup('project', null, ['excluded' => []]);
405        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
406            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
407        })->build()->enable();
408        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
409            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
410        })->build()->enable();
411        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
412            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
413        })->build()->enable();
414        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
415            return $workingDirectory;
416        })->build()->enable();
417        $source = $filesystem;
418        $name = 'target.php';
419        $contents = '<?php function target(){return helper();}';
420        $url = $source->url() . '/' . $name;
421        file_put_contents($url, $contents);
422        $target = '/' . $source->path() . '/' . $name;
423        $name1 = 'helper.php';
424        $contents1 = '<?php function helper(){return 1;}';
425        $url1 = $source->url() . '/' . $name1;
426        file_put_contents($url1, $contents1);
427        $helper = '/' . $source->path() . '/' . $name1;
428        $name2 = 'excluded/broken.php';
429        $contents2 = '<?php function broken(';
430        $url2 = $source->url() . '/' . $name2;
431        file_put_contents($url2, $contents2);
432        $files = [$target, $helper];
433        $name3 = 'helper.php';
434        $contents3 = '<?php function helper(){return 42;}';
435        $url3 = $source->url() . '/' . $name3;
436        file_put_contents($url3, $contents3);
437        $result = Deriver::prepare($files)->derive('target#return');
438        self::assertSame(42, $result[0]->result);
439        self::assertCount(2, $result[0]->evidence[0]->snapshot->sources);
440    }
441    /**
442     * @throws \phpmock\MockEnabledException
443     */
444    public function testPreparedSourcesSurviveChangesAndRemovalBeforeTheFirstQuery(): void
445    {
446        $workingDirectory = '/project';
447        $filesystem = vfsStream::setup('project');
448        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
449            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
450        })->build()->enable();
451        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
452            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
453        })->build()->enable();
454        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
455            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
456        })->build()->enable();
457        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
458            return $workingDirectory;
459        })->build()->enable();
460        $source = $filesystem;
461        $name = 'first.php';
462        $contents = '<?php function first(){return 1;}';
463        $url = $source->url() . '/' . $name;
464        file_put_contents($url, $contents);
465        $firstPath = '/' . $source->path() . '/' . $name;
466        $name1 = 'second.php';
467        $contents1 = '<?php function second(){return 2;}';
468        $url1 = $source->url() . '/' . $name1;
469        file_put_contents($url1, $contents1);
470        $secondPath = '/' . $source->path() . '/' . $name1;
471        $deriver = Deriver::prepare([$firstPath, $secondPath]);
472        $name2 = 'first.php';
473        $contents2 = '<?php function first(){return 99;}';
474        $url2 = $source->url() . '/' . $name2;
475        file_put_contents($url2, $contents2);
476        $path = $secondPath;
477        $path = str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path;
478        self::assertTrue(unlink('vfs:/' . Path::normalize($path)));
479        self::assertSame(1, $deriver->derive('first#return')[0]->result);
480        self::assertSame(2, $deriver->derive('second#return')[0]->result);
481        self::assertSame(1, $deriver->derive('first#return')[0]->result);
482        self::assertSame(99, Deriver::prepare([$firstPath])->derive('first#return')[0]->result);
483    }
484    /**
485     * @throws \phpmock\MockEnabledException
486     */
487    public function testRepeatedQueriesHaveIndependentEvaluationState(): void
488    {
489        $workingDirectory = '/project';
490        $filesystem = vfsStream::setup('project');
491        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
492            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
493        })->build()->enable();
494        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
495            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
496        })->build()->enable();
497        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
498            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
499        })->build()->enable();
500        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
501            return $workingDirectory;
502        })->build()->enable();
503        $source = $filesystem;
504        $name = 'target.php';
505        $contents = '<?php function increment(){static $n=0;return ++$n;} function first(){return increment();} function second(){return increment()+10;}';
506        $url = $source->url() . '/' . $name;
507        file_put_contents($url, $contents);
508        $path = '/' . $source->path() . '/' . $name;
509        $deriver = Deriver::prepare([$path]);
510        $first = $deriver->derive('first#return');
511        self::assertSame(1, $first[0]->result);
512        self::assertSame(11, $deriver->derive('second#return')[0]->result);
513        $again = $deriver->derive('first#return');
514        self::assertSame(1, $again[0]->result);
515        self::assertNotSame($first, $again);
516        self::assertNotSame($first[0], $again[0]);
517        self::assertEquals(iterator_to_array($first), iterator_to_array($again));
518    }
519    /**
520     * @throws \phpmock\MockEnabledException
521     */
522    public function testDeriverDoesNotRetainResults(): void
523    {
524        $workingDirectory = '/project';
525        $filesystem = vfsStream::setup('project');
526        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
527            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
528        })->build()->enable();
529        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
530            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
531        })->build()->enable();
532        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
533            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
534        })->build()->enable();
535        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
536            return $workingDirectory;
537        })->build()->enable();
538        $source = $filesystem;
539        $name = 'target.php';
540        $contents = '<?php function target(){return 42;}';
541        $url = $source->url() . '/' . $name;
542        file_put_contents($url, $contents);
543        $path = '/' . $source->path() . '/' . $name;
544        $deriver = Deriver::prepare([$path]);
545        $result = $deriver->derive('target#return');
546        $reference = WeakReference::create($result);
547        $candidate = WeakReference::create($result[0]);
548        unset($result);
549        self::assertNull($reference->get());
550        self::assertNull($candidate->get());
551        self::assertSame(42, $deriver->derive('target#return')[0]->result);
552    }
553    /**
554     * @throws \phpmock\MockEnabledException
555     */
556    public function testRejectedQueryDoesNotInvalidatePreparedSources(): void
557    {
558        $workingDirectory = '/project';
559        $filesystem = vfsStream::setup('project');
560        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
561            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
562        })->build()->enable();
563        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
564            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
565        })->build()->enable();
566        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
567            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
568        })->build()->enable();
569        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
570            return $workingDirectory;
571        })->build()->enable();
572        $source = $filesystem;
573        $name = 'target.php';
574        $contents = '<?php function target(){return 42;}';
575        $url = $source->url() . '/' . $name;
576        file_put_contents($url, $contents);
577        $path = '/' . $source->path() . '/' . $name;
578        $deriver = Deriver::prepare([$path]);
579        try {
580            $deriver->derive('missing#return');
581            self::fail('An absent target must be rejected.');
582        } catch (InvalidInputException $error) {
583            self::assertStringContainsString('missing', $error->getMessage());
584        }
585        self::assertSame(42, $deriver->derive('target#return')[0]->result);
586    }
587    /**
588     * @throws \phpmock\MockEnabledException
589     */
590    public function testNewPreparationIncludesAddedDeclarations(): void
591    {
592        $workingDirectory = '/project';
593        $filesystem = vfsStream::setup('project');
594        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
595            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
596        })->build()->enable();
597        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
598            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
599        })->build()->enable();
600        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
601            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
602        })->build()->enable();
603        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
604            return $workingDirectory;
605        })->build()->enable();
606        $source = $filesystem;
607        $name = 'target.php';
608        $contents = '<?php function target(){return missing();}';
609        $url = $source->url() . '/' . $name;
610        file_put_contents($url, $contents);
611        $path = '/' . $source->path() . '/' . $name;
612        $deriver = Deriver::prepare([$path]);
613        $before = $deriver->derive('target#return');
614        $name1 = 'added.php';
615        $contents1 = '<?php function missing(){return 7;}';
616        $url1 = $source->url() . '/' . $name1;
617        file_put_contents($url1, $contents1);
618        $added = '/' . $source->path() . '/' . $name1;
619        $after = Deriver::prepare([$path, $added])->derive('target#return');
620        $frontiers = array_reduce(iterator_to_array($before, false), static function (array $frontiers, Candidate $candidate): array {
621            if ($candidate->result instanceof Term) {
622                $graph = $candidate->result;
623                $snapshot = $candidate->evidence[0]->snapshot;
624                $pending = [$graph];
625                $seen = new WeakMap();
626                $frontiers1 = [];
627                while ($pending !== []) {
628                    $node = array_pop($pending);
629                    if (isset($seen[$node])) {
630                        continue;
631                    }
632                    $seen[$node] = true;
633                    array_push($pending, ...array_values($node->operands));
634                    $reason = $node->attributes['reason'] ?? null;
635                    if (!is_string($reason)) {
636                        continue;
637                    }
638                    $source1 = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
639                    $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
640                    $frontiers1[$identity] = new Frontier($reason, $source1, (string) $node->literal, ['value'], [$identity], $node, $reason);
641                }
642                array_push($frontiers, ...array_values($frontiers1));
643            }
644            return $frontiers;
645        }, []);
646        self::assertNotEmpty($frontiers);
647        self::assertSame(7, $after[0]->result);
648        $frontiers2 = array_reduce(iterator_to_array($after, false), static function (array $frontiers2, Candidate $candidate1): array {
649            if ($candidate1->result instanceof Term) {
650                $graph1 = $candidate1->result;
651                $snapshot1 = $candidate1->evidence[0]->snapshot;
652                $pending1 = [$graph1];
653                $seen1 = new WeakMap();
654                $frontiers3 = [];
655                while ($pending1 !== []) {
656                    $node1 = array_pop($pending1);
657                    if (isset($seen1[$node1])) {
658                        continue;
659                    }
660                    $seen1[$node1] = true;
661                    array_push($pending1, ...array_values($node1->operands));
662                    $reason1 = $node1->attributes['reason'] ?? null;
663                    if (!is_string($reason1)) {
664                        continue;
665                    }
666                    $source2 = new SourceRef($snapshot1->id, (string) ($node1->attributes['source'] ?? ''), (int) ($node1->attributes['start'] ?? 0), (int) ($node1->attributes['end'] ?? $node1->attributes['start'] ?? 0));
667                    $identity1 = (string) ($node1->attributes['identity'] ?? (new Identity())->key($node1));
668                    $frontiers3[$identity1] = new Frontier($reason1, $source2, (string) $node1->literal, ['value'], [$identity1], $node1, $reason1);
669                }
670                array_push($frontiers2, ...array_values($frontiers3));
671            }
672            return $frontiers2;
673        }, []);
674        self::assertSame([], $frontiers2);
675        self::assertNotSame($before[0]->evidence[0]->snapshot->id, $after[0]->evidence[0]->snapshot->id);
676        self::assertEquals(iterator_to_array($before), iterator_to_array($deriver->derive('target#return')));
677    }
678    /**
679     * @throws \phpmock\MockEnabledException
680     */
681    public function testNewPreparationKeepsClosureReferencesWithinItsSnapshot(): void
682    {
683        $workingDirectory = '/project';
684        $filesystem = vfsStream::setup('project');
685        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
686            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
687        })->build()->enable();
688        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
689            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
690        })->build()->enable();
691        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
692            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
693        })->build()->enable();
694        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
695            return $workingDirectory;
696        })->build()->enable();
697        $source = $filesystem;
698        $name = 'target.php';
699        $contents = '<?php function target(){$f=fn()=>7;return $f();}';
700        $url = $source->url() . '/' . $name;
701        file_put_contents($url, $contents);
702        $path = '/' . $source->path() . '/' . $name;
703        $first = Deriver::prepare([$path])->derive('target#return');
704        $name1 = 'added.php';
705        $contents1 = '<?php function unrelated(){}';
706        $url1 = $source->url() . '/' . $name1;
707        file_put_contents($url1, $contents1);
708        $added = '/' . $source->path() . '/' . $name1;
709        $result = Deriver::prepare([$path, $added])->derive('target#return');
710        self::assertSame(7, $result[0]->result);
711        $manifest = $result[0]->evidence[0]->snapshot;
712        self::assertNotSame($first[0]->evidence[0]->snapshot->id, $manifest->id);
713        self::assertSame([$manifest->id], array_values(array_unique(array_map(static fn ($node): ?string => $node->source?->snapshotId, array_filter($result[0]->evidence[0]->nodes(), static fn ($node): bool => $node->source !== null)))));
714    }
715    /**
716     * @throws \phpmock\MockEnabledException
717     */
718    public function testPrepareConvertsConfiguredMicroEvaluators(): void
719    {
720        $workingDirectory = '/project';
721        $filesystem = vfsStream::setup('project');
722        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
723            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
724        })->build()->enable();
725        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
726            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
727        })->build()->enable();
728        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
729            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
730        })->build()->enable();
731        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
732            return $workingDirectory;
733        })->build()->enable();
734        $source = $filesystem;
735        $name = 'target.php';
736        $contents = '<?php function target(){return count(unknown());}';
737        $url = $source->url() . '/' . $name;
738        file_put_contents($url, $contents);
739        $path = '/' . $source->path() . '/' . $name;
740        $deriver = Deriver::prepare([$path], new DeriverConfig(evaluators: [new MicroEvaluator('count-one', '1', EvaluationOperation::Function, 'count', static fn (): Term => Term::constant(1))]));
741        $result = $deriver->derive('target#return');
742        self::assertSame(1, $result[0]->result);
743        $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate): array {
744            if ($candidate->result instanceof Term) {
745                $graph = $candidate->result;
746                $snapshot = $candidate->evidence[0]->snapshot;
747                $pending = [$graph];
748                $seen = new WeakMap();
749                $frontiers1 = [];
750                while ($pending !== []) {
751                    $node = array_pop($pending);
752                    if (isset($seen[$node])) {
753                        continue;
754                    }
755                    $seen[$node] = true;
756                    array_push($pending, ...array_values($node->operands));
757                    $reason = $node->attributes['reason'] ?? null;
758                    if (!is_string($reason)) {
759                        continue;
760                    }
761                    $source1 = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
762                    $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
763                    $frontiers1[$identity] = new Frontier($reason, $source1, (string) $node->literal, ['value'], [$identity], $node, $reason);
764                }
765                array_push($frontiers, ...array_values($frontiers1));
766            }
767            return $frontiers;
768        }, []);
769        self::assertSame([], $frontiers);
770    }
771    /**
772     * @throws \phpmock\MockEnabledException
773     */
774    public function testPrepareRejectsMalformedSourceWithoutAQuery(): void
775    {
776        $workingDirectory = '/project';
777        $filesystem = vfsStream::setup('project');
778        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
779            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
780        })->build()->enable();
781        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
782            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
783        })->build()->enable();
784        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
785            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
786        })->build()->enable();
787        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
788            return $workingDirectory;
789        })->build()->enable();
790        $source = $filesystem;
791        $name = 'broken.php';
792        $contents = '<?php function broken(';
793        $url = $source->url() . '/' . $name;
794        file_put_contents($url, $contents);
795        $path = '/' . $source->path() . '/' . $name;
796        $this->expectException(ParserException::class);
797        Deriver::prepare([$path]);
798    }
799    /**
800     * @throws \phpmock\MockEnabledException
801     */
802    public function testPrepareReportsResolutionFailuresSeparatelyFromSyntaxErrors(): void
803    {
804        $workingDirectory = '/project';
805        $filesystem = vfsStream::setup('project');
806        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
807            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
808        })->build()->enable();
809        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
810            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
811        })->build()->enable();
812        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
813            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
814        })->build()->enable();
815        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
816            return $workingDirectory;
817        })->build()->enable();
818        $source = $filesystem;
819        $name = 'scope.php';
820        $contents = '<?php function target() { return self::class; }';
821        $url = $source->url() . '/' . $name;
822        file_put_contents($url, $contents);
823        $path = '/' . $source->path() . '/' . $name;
824        $this->expectException(ResolutionException::class);
825        $this->expectExceptionMessage('scope.php:1:');
826        Deriver::prepare([$path]);
827    }
828    /**
829     * @throws \phpmock\MockEnabledException
830     */
831    public function testPrepareRejectsFilesRemovedAfterDiscovery(): void
832    {
833        $workingDirectory = '/project';
834        $filesystem = vfsStream::setup('project');
835        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
836            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
837        })->build()->enable();
838        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
839            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
840        })->build()->enable();
841        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
842            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
843        })->build()->enable();
844        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
845            return $workingDirectory;
846        })->build()->enable();
847        $source = $filesystem;
848        $name = 'target.php';
849        $contents = '<?php function target(){return 1;}';
850        $url = $source->url() . '/' . $name;
851        file_put_contents($url, $contents);
852        $path = '/' . $source->path() . '/' . $name;
853        $files = [$path];
854        $path1 = $path;
855        $path1 = str_starts_with($path1, '/') ? $path1 : $workingDirectory . '/' . $path1;
856        self::assertTrue(unlink('vfs:/' . Path::normalize($path1)));
857        $this->expectException(CollectorException::class);
858        Deriver::prepare($files);
859    }
860    /**
861     * @throws \phpmock\MockEnabledException
862     */
863    public function testPrepareRejectsDirectoriesInsteadOfExpandingThem(): void
864    {
865        $workingDirectory = '/project';
866        $filesystem = vfsStream::setup('project');
867        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
868            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
869        })->build()->enable();
870        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
871            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
872        })->build()->enable();
873        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
874            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
875        })->build()->enable();
876        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
877            return $workingDirectory;
878        })->build()->enable();
879        $this->expectException(CollectorException::class);
880        Deriver::prepare([__DIR__]);
881    }
882    /**
883     * @throws \phpmock\MockEnabledException
884     */
885    public function testPrepareRejectsRepeatedPaths(): void
886    {
887        $workingDirectory = '/project';
888        $filesystem = vfsStream::setup('project');
889        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
890            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
891        })->build()->enable();
892        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
893            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
894        })->build()->enable();
895        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
896            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
897        })->build()->enable();
898        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
899            return $workingDirectory;
900        })->build()->enable();
901        $source = $filesystem;
902        $name = 'target.php';
903        $contents = '<?php function target(){return 1;}';
904        $url = $source->url() . '/' . $name;
905        file_put_contents($url, $contents);
906        $path = '/' . $source->path() . '/' . $name;
907        $this->expectException(CollectorException::class);
908        Deriver::prepare([$path, $path]);
909    }
910    /**
911     * @throws \phpmock\MockEnabledException
912     */
913    public function testCandidateEvidenceSurvivesTheDeriver(): void
914    {
915        $workingDirectory = '/project';
916        $filesystem = vfsStream::setup('project');
917        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
918            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
919        })->build()->enable();
920        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
921            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
922        })->build()->enable();
923        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
924            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
925        })->build()->enable();
926        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
927            return $workingDirectory;
928        })->build()->enable();
929        $source = $filesystem;
930        $name = 'target.php';
931        $contents = '<?php function target(){return 42;}';
932        $url = $source->url() . '/' . $name;
933        file_put_contents($url, $contents);
934        $path = '/' . $source->path() . '/' . $name;
935        $result = Deriver::prepare([$path])->derive('target#return');
936        $proof = $result[0]->evidence[0];
937        $path1 = $path;
938        $path1 = str_starts_with($path1, '/') ? $path1 : $workingDirectory . '/' . $path1;
939        self::assertSame(hash_file('sha256', 'vfs:/' . Path::normalize($path1)), $proof->snapshot->sources[$path]);
940        self::assertSame($proof->root, $proof->nodes()[0]);
941        self::assertSame(42, $result[0]->result);
942    }
943    /**
944     * @throws \phpmock\MockEnabledException
945     */
946    public function testPrepareRejectsIncompleteDeclarations(): void
947    {
948        $workingDirectory = '/project';
949        $filesystem = vfsStream::setup('project');
950        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
951            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
952        })->build()->enable();
953        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
954            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
955        })->build()->enable();
956        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
957            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
958        })->build()->enable();
959        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
960            return $workingDirectory;
961        })->build()->enable();
962        $source = $filesystem;
963        $name = 'target.php';
964        $contents = '<?php class C { use MissingTrait; }';
965        $url = $source->url() . '/' . $name;
966        file_put_contents($url, $contents);
967        $path = '/' . $source->path() . '/' . $name;
968        $this->expectException(InvalidInputException::class);
969        Deriver::prepare([$path]);
970    }
971    #[Override]
972    protected function tearDown(): void
973    {
974        Mock::disableAll();
975        parent::tearDown();
976    }
977}
978