packages/sql-faker/tests/Unit/Generation/Coverage/GrammarCoverageInventoryTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Coverage;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Choice\ByteChoices;
11use SqlFaker\Generation\Coverage\CoverageException;
12use SqlFaker\Generation\Coverage\CoverageSets;
13use SqlFaker\Generation\Coverage\CoverageSnapshotStore;
14use SqlFaker\Generation\Coverage\GenerationTrace;
15use SqlFaker\Generation\Coverage\GeneratorRevision;
16use SqlFaker\Generation\Coverage\GrammarCoverage;
17use SqlFaker\Generation\Coverage\GrammarCoverageInventory;
18use SqlFaker\Generation\Coverage\LexicalObservation;
19use SqlFaker\Generation\Coverage\SnapshotValidation;
20use SqlFaker\Generation\Derivation\CompletionCosts;
21use SqlFaker\Generation\Derivation\DerivationNode;
22use SqlFaker\Grammar\Model\Grammar;
23use SqlFaker\Grammar\Model\NonTerminal;
24use SqlFaker\Grammar\Model\Production;
25use SqlFaker\Grammar\Model\ProductionRule;
26use SqlFaker\Grammar\Model\Terminal;
27
28#[CoversClass(GrammarCoverageInventory::class)]
29#[UsesClass(Grammar::class)]
30#[UsesClass(Production::class)]
31#[UsesClass(ProductionRule::class)]
32#[UsesClass(Terminal::class)]
33#[UsesClass(NonTerminal::class)]
34#[UsesClass(CoverageException::class)]
35#[UsesClass(GrammarCoverage::class)]
36#[UsesClass(GeneratorRevision::class)]
37#[UsesClass(CoverageSnapshotStore::class)]
38#[UsesClass(SnapshotValidation::class)]
39#[UsesClass(GenerationTrace::class)]
40#[UsesClass(CoverageSets::class)]
41#[UsesClass(ByteChoices::class)]
42#[UsesClass(CompletionCosts::class)]
43#[UsesClass(DerivationNode::class)]
44#[UsesClass(LexicalObservation::class)]
45final class GrammarCoverageInventoryTest extends TestCase
46{
47 public function testReachableRulesIncludesRecursionAndEmptyAlternativesButSeparatesUnrelatedRules(): void
48 {
49 $inventory = new GrammarCoverageInventory(
50 (new Grammar(
51 'stmt',
52 [
53 'stmt' => new ProductionRule(
54 'stmt',
55 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
56 ),
57 'expr' => new ProductionRule(
58 'expr',
59 [
60 new Production([new Terminal('1')]),
61 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
62 new Production([]),
63 ],
64 ),
65 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
66 ],
67 ))->identified(),
68 'stmt',
69 'test-v1',
70 );
71 self::assertSame(['stmt' => true, 'expr' => true], $inventory->reachableRules());
72 self::assertCount(5, $inventory->denominator);
73 self::assertCount(6, $inventory->entries);
74 }
75
76 public function testIdKeepsOriginalOrdinalAfterFilteringAndChangesForRewrittenRhs(): void
77 {
78 $inventory = new GrammarCoverageInventory(
79 (new Grammar(
80 'stmt',
81 [
82 'stmt' => new ProductionRule(
83 'stmt',
84 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
85 ),
86 'expr' => new ProductionRule(
87 'expr',
88 [
89 new Production([new Terminal('1')]),
90 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
91 new Production([]),
92 ],
93 ),
94 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
95 ],
96 ))->identified(),
97 'stmt',
98 'test-v1',
99 );
100 $p = new Production([new Terminal('A')], 7, 'original#7');
101 self::assertSame($inventory->id('stmt', $p, 1), $inventory->id('stmt', $p, 3));
102 self::assertNotSame($inventory->id('stmt', $p, 1), $inventory->id('stmt', new Production([new Terminal('B')], 7, 'original#7'), 1));
103 }
104
105 public function testRhsDistinguishesTheSymbolKindAndIncludesEmptyProduction(): void
106 {
107 self::assertSame([], GrammarCoverageInventory::rhs(new Production([])));
108 self::assertSame(
109 ['T:SELECT', 'N:expr'],
110 GrammarCoverageInventory::rhs(
111 (new Grammar(
112 'stmt',
113 [
114 'stmt' => new ProductionRule(
115 'stmt',
116 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
117 ),
118 'expr' => new ProductionRule(
119 'expr',
120 [
121 new Production([new Terminal('1')]),
122 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
123 new Production([]),
124 ],
125 ),
126 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
127 ],
128 ))->identified()->ruleMap['stmt']->alternatives[0],
129 ),
130 );
131 }
132
133 public function testDifferencesRecordsBothRemovedAndTransformedOriginalProductions(): void
134 {
135 $original = (new Grammar(
136 'stmt',
137 ['stmt' => new ProductionRule('stmt', [new Production([new Terminal('A')]), new Production([new Terminal('B')])])],
138 ))->identified();
139 $effective = new Grammar('stmt', ['stmt' => new ProductionRule('stmt', [new Production([new Terminal('C')], 1, 'stmt#1')])]);
140 $inventory = new GrammarCoverageInventory($effective, 'stmt', 'test-v1', $original);
141 self::assertSame(
142 [['origin' => 'stmt#0', 'status' => 'excluded'], ['origin' => 'stmt#1', 'status' => 'transformed']],
143 $inventory->adaptations,
144 );
145 }
146
147 public function testInventoryEntriesRetainOriginalIdentityAndExcludeUnreachableRulesFromTheDenominator(): void
148 {
149 $grammar = new Grammar(
150 'stmt',
151 [
152 'stmt' => new ProductionRule('stmt', [new Production([new Terminal('T')], 7, 'original#7'), new Production([])]),
153 'outside' => new ProductionRule('outside', [new Production([new Terminal('T')])]),
154 ],
155 );
156 $inventory = new GrammarCoverageInventory($grammar, 'stmt', 'profile-a');
157 self::assertSame(
158 [
159 ['rule' => 'stmt', 'ordinal' => 7, 'origin' => 'original#7', 'rhs' => ['T:T'], 'rootReachable' => true],
160 ['rule' => 'stmt', 'ordinal' => 1, 'origin' => null, 'rhs' => [], 'rootReachable' => true],
161 ['rule' => 'outside', 'ordinal' => 0, 'origin' => null, 'rhs' => ['T:T'], 'rootReachable' => false],
162 ],
163 array_values($inventory->entries),
164 );
165 self::assertSame(array_slice(array_keys($inventory->entries), 0, 2), $inventory->denominator);
166 self::assertSame([], $inventory->differences($grammar));
167 self::assertNotSame($inventory->fingerprint, (new GrammarCoverageInventory($grammar, 'stmt', 'profile-b'))->fingerprint);
168 self::assertNotSame($inventory->fingerprint, (new GrammarCoverageInventory($grammar, 'outside', 'profile-a'))->fingerprint);
169 self::assertSame($inventory->fingerprint, (new GrammarCoverageInventory($grammar, 'stmt', 'profile-a'))->fingerprint);
170 }
171
172 public function testProductionIdsDistinguishRulesOrdinalsAndGrammarProfiles(): void
173 {
174 $inventory = new GrammarCoverageInventory(
175 (new Grammar(
176 'stmt',
177 [
178 'stmt' => new ProductionRule(
179 'stmt',
180 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
181 ),
182 'expr' => new ProductionRule(
183 'expr',
184 [
185 new Production([new Terminal('1')]),
186 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
187 new Production([]),
188 ],
189 ),
190 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
191 ],
192 ))->identified(),
193 'stmt',
194 'test-v1',
195 );
196 $production = new Production([]);
197 $first = $inventory->id('stmt', $production, 0);
198 self::assertNotSame($first, $inventory->id('expr', $production, 0));
199 self::assertNotSame($first, $inventory->id('stmt', $production, 1));
200 self::assertNotSame(
201 $first,
202 (new GrammarCoverageInventory(
203 (new Grammar(
204 'stmt',
205 [
206 'stmt' => new ProductionRule(
207 'stmt',
208 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
209 ),
210 'expr' => new ProductionRule(
211 'expr',
212 [
213 new Production([new Terminal('1')]),
214 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
215 new Production([]),
216 ],
217 ),
218 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
219 ],
220 ))->identified(),
221 'stmt',
222 'another',
223 ))->id('stmt', $production, 0),
224 );
225 self::assertStringStartsWith($inventory->fingerprint . ':stmt#0:', $first);
226 }
227
228 public function testReachableRulesCanInspectAPlanRootWithoutChangingTheInventory(): void
229 {
230 $inventory = new GrammarCoverageInventory(
231 (new Grammar(
232 'stmt',
233 [
234 'stmt' => new ProductionRule(
235 'stmt',
236 [new Production([new Terminal('SELECT'), new NonTerminal('expr')]), new Production([new Terminal('DELETE')])],
237 ),
238 'expr' => new ProductionRule(
239 'expr',
240 [
241 new Production([new Terminal('1')]),
242 new Production([new NonTerminal('expr'), new Terminal('+'), new NonTerminal('expr')]),
243 new Production([]),
244 ],
245 ),
246 'outside' => new ProductionRule('outside', [new Production([new Terminal('OUTSIDE')])]),
247 ],
248 ))->identified(),
249 'stmt',
250 'test-v1',
251 );
252 self::assertSame(['expr' => true], $inventory->reachableRules('expr'));
253 self::assertSame('stmt', $inventory->root);
254 self::assertArrayHasKey('stmt', $inventory->reachableRules());
255 }
256}
257