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