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