packages/deriver/tests/Unit/Deriver/Identity/QueryEncodingTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Identity;
6
7use Deriver\Deriver\Evaluator\Projection;
8use Deriver\Deriver\Evaluator\Request\Budget;
9use Deriver\Deriver\Evaluator\Request\EntryPoint;
10use Deriver\Deriver\Evaluator\Request\Query;
11use Deriver\Deriver\Evaluator\Request\QueryScope;
12use Deriver\Deriver\Evaluator\Request\ReturnQuery;
13use Deriver\Deriver\Evaluator\Request\StateQuery;
14use Deriver\Deriver\Evaluator\Request\TupleQuery;
15use Deriver\Deriver\Evaluator\Request\ValueQuery;
16use Deriver\Deriver\Evaluator\Term;
17use Deriver\Deriver\Identity\QueryEncoding;
18use Deriver\Deriver\Identity\ValueGraph;
19use Deriver\Deriver\Lowerer\Graph\ExpressionRef;
20use Deriver\Deriver\Lowerer\Graph\PointRef;
21use Deriver\Deriver\Sources\SourceRef;
22use Deriver\Exception\InvalidInputException;
23use PHPUnit\Framework\Attributes\CoversClass;
24use PHPUnit\Framework\Attributes\Small;
25use PHPUnit\Framework\Attributes\UsesClass;
26use PHPUnit\Framework\TestCase;
27
28/**
29 * @covers \Deriver\Deriver\Identity\QueryEncoding
30 */
31#[CoversClass(QueryEncoding::class)]
32#[UsesClass(EntryPoint::class)]
33#[UsesClass(Budget::class)]
34#[UsesClass(QueryScope::class)]
35#[UsesClass(ReturnQuery::class)]
36#[UsesClass(StateQuery::class)]
37#[UsesClass(TupleQuery::class)]
38#[UsesClass(ValueQuery::class)]
39#[UsesClass(ExpressionRef::class)]
40#[UsesClass(PointRef::class)]
41#[UsesClass(SourceRef::class)]
42#[UsesClass(ValueGraph::class)]
43#[UsesClass(\Deriver\Deriver\Evaluator\Scalar\Identity::class)]
44#[UsesClass(Projection::class)]
45#[UsesClass(Term::class)]
46#[Small]
47final class QueryEncodingTest extends TestCase
48{
49    public function testKeyDoesNotDependOnSharedSourceOrExpressionInstances(): void
50    {
51        $source = new SourceRef('snapshot', "source-\xff.php", 4, 9, 2);
52        $point = new PointRef($source, 'target', 'i3', \Deriver\Deriver\Lowerer\Graph\ObservationPhase::After);
53        $expression = new ExpressionRef($source, 'target', 'r2');
54        $shared = new TupleQuery($point, ['first' => $expression, 'second' => $expression]);
55        $separate = new TupleQuery(
56            new PointRef(clone $source, 'target', 'i3', \Deriver\Deriver\Lowerer\Graph\ObservationPhase::After),
57            ['first' => new ExpressionRef(clone $source, 'target', 'r2'), 'second' => new ExpressionRef(clone $source, 'target', 'r2')],
58        );
59        $encoder = new QueryEncoding();
60        self::assertSame($encoder->key($shared), $encoder->key($separate));
61    }
62
63    public function testKeyPreservesArbitraryIdentifierBytes(): void
64    {
65        $encoder = new QueryEncoding();
66        self::assertNotSame($encoder->key(new ReturnQuery("\xff")), $encoder->key(new ReturnQuery("\xfe")));
67        self::assertNotSame($encoder->key(new ReturnQuery("\xff")), $encoder->key(new ReturnQuery('~b64~/w==')));
68    }
69
70    public function testRecordUsesTheSharedValueGraphForEntryArguments(): void
71    {
72        $query = new ReturnQuery('target', QueryScope::fromEntrypoints([new EntryPoint('main', [Term::constant(7)])]));
73        $graph = new ValueGraph();
74        $record = (new QueryEncoding())->record($query, $graph);
75        self::assertSame('entrypoint', $record['scope']['mode']);
76        self::assertArrayHasKey($record['scope']['entries'][0]['arguments'][0]['value'], $graph->records);
77    }
78    public function testKeyIncludesStructuralBudgetsAndEntryValues(): void
79    {
80        $query = new ReturnQuery('target');
81        $limited = new ReturnQuery('target', budget:new Budget(transfers:1));
82        $encoder = new QueryEncoding();
83        self::assertSame($encoder->key($query), $encoder->key($query));
84        self::assertNotSame($encoder->key($query), $encoder->key($limited));
85    }
86    public function testRecordPreservesEveryObservationKindAndItsExactReferences(): void
87    {
88        $at = new SourceRef('snapshot', 'a.php', 4, 9, 2);
89        $expression = new ExpressionRef($at, 'target', 'r2');
90        $point = new PointRef($at, 'target', 'i3', \Deriver\Deriver\Lowerer\Graph\ObservationPhase::After);
91        $projection = Projection::stateSlot('cache', ['entry',2]);
92        $budget = new Budget(13, 3, 4, 5, 6);
93        $encoder = new QueryEncoding();
94        $graph = new ValueGraph();
95        $returned = $encoder->record(new ReturnQuery('target', budget:$budget), $graph);
96        $value = $encoder->record(new ValueQuery($expression, $projection, budget:$budget), $graph);
97        $state = $encoder->record(new StateQuery($point, 'local', $projection, budget:$budget), $graph);
98        $tuple = $encoder->record(new TupleQuery($point, ['chosen' => $expression], budget:$budget), $graph);
99        self::assertSame(['kind' => 'return','symbol' => 'target','scope' => ['mode' => 'symbolic','entries' => []],'budget' => $budget], $returned);
100        self::assertSame(['kind' => 'value','expression' => $expression,'projection' => $projection,'scope' => ['mode' => 'symbolic','entries' => []],'budget' => $budget], $value);
101        self::assertSame(['kind' => 'state','point' => $point,'variable' => 'local','projection' => $projection,'scope' => ['mode' => 'symbolic','entries' => []],'budget' => $budget], $state);
102        self::assertSame('tuple', $tuple['kind']);
103        self::assertArrayHasKey('point', $tuple);
104        self::assertSame($point, $tuple['point'] ?? null);
105        self::assertArrayHasKey('values', $tuple);
106        self::assertEquals((object)['chosen' => $expression], $tuple['values'] ?? null);
107        self::assertSame($budget, $tuple['budget']);
108        self::assertSame(['mode' => 'symbolic','entries' => []], $tuple['scope']);
109        self::assertSame([], $graph->records);
110    }
111
112    public function testRecordKeepsEntryOrderNamedArgumentsAndReceiverInTheSharedGraph(): void
113    {
114        $shared = Term::constant(7);
115        $receiver = new Term(\Deriver\Deriver\Evaluator\TermKind::Object, 'box', attributes:['class' => 'Box']);
116        $scope = QueryScope::fromEntrypoints([
117            new EntryPoint('Box::run', [0 => $shared,'value' => $shared], $receiver),
118            new EntryPoint('main', [Term::constant(null)]),
119        ]);
120        $graph = new ValueGraph();
121        $record = (new QueryEncoding())->record(new ReturnQuery('target', $scope), $graph);
122        $entries = $record['scope']['entries'];
123        self::assertSame('entrypoint', $record['scope']['mode']);
124        self::assertCount(2, $entries);
125        self::assertSame('Box::run', $entries[0]['symbol']);
126        self::assertSame('main', $entries[1]['symbol']);
127        self::assertSame([
128            ['name' => $graph->scalar(0),'value' => $graph->add($shared)],
129            ['name' => $graph->scalar('value'),'value' => $graph->add($shared)],
130        ], $entries[0]['arguments']);
131        self::assertSame($graph->add($receiver), $entries[0]['receiver']);
132        self::assertNull($entries[1]['receiver']);
133        self::assertSame([['name' => $graph->scalar(0),'value' => $graph->add(Term::constant(null))]], $entries[1]['arguments']);
134        self::assertCount(3, $graph->records);
135    }
136
137    public function testRecordKeepsReceiverPropertiesByNameInTheSharedGraph(): void
138    {
139        $order = Term::constant('name');
140        $graph = new ValueGraph();
141        $record = (new QueryEncoding())->record(new ReturnQuery('target', QueryScope::fromEntrypoints([new EntryPoint('Box::run', [$order], properties:['order' => $order,'table' => Term::constant('users')]), new EntryPoint('main')])), $graph);
142        $entries = $record['scope']['entries'];
143        self::assertSame([['name' => 'order','value' => $graph->add($order)],['name' => 'table','value' => $graph->add(Term::constant('users'))]], $entries[0]['properties']);
144        self::assertSame($entries[0]['arguments'][0]['value'], $entries[0]['properties'][0]['value']);
145        self::assertSame([], $entries[1]['properties']);
146    }
147
148    #[\PHPUnit\Framework\Attributes\DataProvider('providerDistinctQueries')]
149    public function testKeyDistinguishesEverySemanticQueryOption(Query $left, Query $right): void
150    {
151        $encoder = new QueryEncoding();
152        self::assertNotSame($encoder->key($left), $encoder->key($right));
153        self::assertSame($encoder->key($left), $encoder->key(clone $left));
154    }
155
156    /**
157     * @return iterable<string,array{Query,Query}>
158     */
159    public static function providerDistinctQueries(): iterable
160    {
161        $at = new SourceRef('snapshot', 'a.php', 4, 9, 2);
162        $expression = new ExpressionRef($at, 'target', 'r2');
163        $point = new PointRef($at, 'target', 'i3', \Deriver\Deriver\Lowerer\Graph\ObservationPhase::After);
164        $base = new ReturnQuery('target');
165        yield 'symbol' => [$base,new ReturnQuery('other')];
166        yield 'kind' => [$base,new ValueQuery($expression)];
167        yield 'transfers' => [$base,new ReturnQuery('target', budget:new Budget(transfers:3))];
168        yield 'partitions' => [$base,new ReturnQuery('target', budget:new Budget(partitions:3))];
169        yield 'iterations' => [$base,new ReturnQuery('target', budget:new Budget(iterations:3))];
170        yield 'recursion' => [$base,new ReturnQuery('target', budget:new Budget(recursion:3))];
171        yield 'nodes' => [$base,new ReturnQuery('target', budget:new Budget(nodes:3))];
172        yield 'scope' => [$base,new ReturnQuery('target', QueryScope::fromEntrypoints([new EntryPoint('target')]))];
173        yield 'expression' => [new ValueQuery($expression),new ValueQuery(new ExpressionRef($at, 'target', 'r3'))];
174        yield 'path' => [new ValueQuery($expression),new ValueQuery($expression, new Projection(['key']))];
175        yield 'slot' => [new ValueQuery($expression),new ValueQuery($expression, Projection::stateSlot('cache'))];
176        yield 'variable' => [new StateQuery($point, 'a'),new StateQuery($point, 'b')];
177        yield 'phase' => [new StateQuery($point, 'a'),new StateQuery(new PointRef($at, 'target', 'i3', \Deriver\Deriver\Lowerer\Graph\ObservationPhase::Before), 'a')];
178        yield 'tuple labels' => [new TupleQuery($point, ['a' => $expression]),new TupleQuery($point, ['b' => $expression])];
179        yield 'tuple membership' => [new TupleQuery($point, []),new TupleQuery($point, ['a' => $expression])];
180        yield 'snapshot' => [new ValueQuery($expression),new ValueQuery(new ExpressionRef(new SourceRef('other', 'a.php', 4, 9, 2), 'target', 'r2'))];
181    }
182
183    #[\PHPUnit\Framework\Attributes\DataProvider('providerEntryIdentity')]
184    public function testKeyDistinguishesEntryContent(EntryPoint $left, EntryPoint $right): void
185    {
186        $encoder = new QueryEncoding();
187        $a = new ReturnQuery('target', QueryScope::fromEntrypoints([$left]));
188        $b = new ReturnQuery('target', QueryScope::fromEntrypoints([$right]));
189        self::assertNotSame($encoder->key($a), $encoder->key($b));
190        self::assertSame($encoder->key($a), $encoder->key($a));
191        self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $encoder->key($a));
192    }
193
194    /**
195     * @return iterable<string,array{EntryPoint,EntryPoint}>
196     */
197    public static function providerEntryIdentity(): iterable
198    {
199        yield 'symbol' => [new EntryPoint('a'),new EntryPoint('b')];
200        yield 'argument value' => [new EntryPoint('a', [Term::constant(1)]),new EntryPoint('a', [Term::constant(2)])];
201        yield 'argument name' => [new EntryPoint('a', ['x' => Term::constant(1)]),new EntryPoint('a', ['y' => Term::constant(1)])];
202        yield 'argument values' => [new EntryPoint('a', [Term::constant('one')]),new EntryPoint('a', [Term::constant('two')])];
203        yield 'receiver' => [new EntryPoint('Box::run', receiver:new Term(\Deriver\Deriver\Evaluator\TermKind::Object, 'a', attributes:['class' => 'Box'])),new EntryPoint('Box::run')];
204        yield 'binary argument name' => [new EntryPoint('a', ["\xff" => Term::constant(1)]),new EntryPoint('a', ["\xfe" => Term::constant(1)])];
205        yield 'property value' => [new EntryPoint('Box::run', properties:['order' => Term::constant('name')]),new EntryPoint('Box::run', properties:['order' => Term::constant('email')])];
206        yield 'property name' => [new EntryPoint('Box::run', properties:['order' => Term::constant('name')]),new EntryPoint('Box::run', properties:['table' => Term::constant('name')])];
207        yield 'property presence' => [new EntryPoint('Box::run', properties:['order' => Term::constant('name')]),new EntryPoint('Box::run')];
208        yield 'property values' => [new EntryPoint('Box::run', properties:['order' => Term::constant('one')]),new EntryPoint('Box::run', properties:['order' => Term::constant('two')])];
209    }
210
211    public function testRecordRejectsUnknownQueryImplementations(): void
212    {
213        $query = self::createStub(Query::class);
214        $this->expectException(InvalidInputException::class);
215        $this->expectExceptionMessage('Unsupported query implementation.');
216        (new QueryEncoding())->record($query, new ValueGraph());
217    }    public function testKeyDistinguishesCallPathsAndTheirObservationAnchors(): void
218    {
219        $path = new \Deriver\Deriver\Evaluator\Request\CallPath(['entry','...','bar']);
220        $direct = new \Deriver\Deriver\Evaluator\Request\CallPath(['entry','bar']);
221        $atCall = new \Deriver\Deriver\Evaluator\Request\CallPath($path->callers, true);
222        $encoder = new QueryEncoding();
223        self::assertNotSame($encoder->key(new ReturnQuery('foo')), $encoder->key(new ReturnQuery('foo', QueryScope::through($path))));
224        self::assertNotSame($encoder->key(new ReturnQuery('foo', QueryScope::through($direct))), $encoder->key(new ReturnQuery('foo', QueryScope::through($path))));
225        self::assertNotSame($encoder->key(new ReturnQuery('foo', QueryScope::through($atCall))), $encoder->key(new ReturnQuery('foo', QueryScope::through($path))));
226    }
227
228
229}
230