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