packages/sql-faker/tests/Unit/Generation/Choice/BytePlanEncoderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Generation\Choice;
6
7use Closure;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Candidate\ChoiceLexemeGenerator;
12use SqlFaker\Generation\Candidate\ValueLexemeGenerator;
13use SqlFaker\Generation\Choice\ByteChoices;
14use SqlFaker\Generation\Choice\BytePlanCompiler;
15use SqlFaker\Generation\Choice\BytePlanEncoder;
16use SqlFaker\Generation\Choice\PlanBuilder;
17use SqlFaker\Generation\Derivation\Completion\CompletionWitness;
18use SqlFaker\Generation\Derivation\Completion\PatternProductions;
19use SqlFaker\Generation\Derivation\CompletionCosts;
20use SqlFaker\Generation\Derivation\CompletionFrontier;
21use SqlFaker\Generation\Derivation\CompletionMemo;
22use SqlFaker\Generation\Derivation\CompletionReduction;
23use SqlFaker\Generation\Derivation\CompletionState;
24use SqlFaker\Generation\Derivation\ConstrainedCompletion;
25use SqlFaker\Generation\Derivation\ConstraintDependencies;
26use SqlFaker\Generation\Derivation\Derivation;
27use SqlFaker\Generation\Derivation\DerivationTrace;
28use SqlFaker\Generation\Derivation\TerminationAnalyzer;
29use SqlFaker\Generation\Derivation\TerminationCost;
30use SqlFaker\Generation\Derivation\TokenGenerator;
31use SqlFaker\Generation\Lexeme\Lexeme;
32use SqlFaker\Generation\Lexeme\LexemeBoundary;
33use SqlFaker\Generation\Lexeme\LexemeCandidates;
34use SqlFaker\Generation\Lexeme\LexemeInput;
35use SqlFaker\Generation\Lexeme\LexemeSequence;
36use SqlFaker\Generation\Lexeme\LexicalGrammar;
37use SqlFaker\Generation\Lexeme\OutputPart;
38use SqlFaker\Generation\Lexeme\ResolvedOutput;
39use SqlFaker\Generation\Lexeme\SpacingConstraint;
40use SqlFaker\Generation\Output\BoundaryCompletion;
41use SqlFaker\Generation\Output\CandidateResolver;
42use SqlFaker\Generation\Output\CombinedSpacingRule;
43use SqlFaker\Generation\Output\ReverseLexemeGenerator;
44use SqlFaker\Generation\Plan\GenerationPlan;
45use SqlFaker\Generation\Plan\ProductionPattern;
46use SqlFaker\Generation\Token\ProductionOccurrence;
47use SqlFaker\Generation\Token\TerminalOccurrence;
48use SqlFaker\Generation\Token\TerminalSequence;
49use SqlFaker\Generation\Value\CharacterDomain;
50use SqlFaker\Generation\Value\ValueChoices;
51use SqlFaker\Grammar\Model\Grammar;
52use SqlFaker\Grammar\Model\NonTerminal;
53use SqlFaker\Grammar\Model\Production;
54use SqlFaker\Grammar\Model\ProductionRule;
55use SqlFaker\Grammar\Model\Terminal;
56
57#[CoversClass(BytePlanEncoder::class)]
58#[UsesClass(GenerationPlan::class)]
59#[UsesClass(ProductionPattern::class)]
60#[UsesClass(PlanBuilder::class)]
61#[UsesClass(ByteChoices::class)]
62#[UsesClass(BytePlanCompiler::class)]
63#[UsesClass(CompletionCosts::class)]
64#[UsesClass(Grammar::class)]
65#[UsesClass(NonTerminal::class)]
66#[UsesClass(Production::class)]
67#[UsesClass(ProductionRule::class)]
68#[UsesClass(Terminal::class)]
69#[UsesClass(Derivation::class)]
70#[UsesClass(DerivationTrace::class)]
71#[UsesClass(TerminationAnalyzer::class)]
72#[UsesClass(TerminationCost::class)]
73#[UsesClass(ChoiceLexemeGenerator::class)]
74#[UsesClass(Lexeme::class)]
75#[UsesClass(LexemeCandidates::class)]
76#[UsesClass(LexemeInput::class)]
77#[UsesClass(LexemeSequence::class)]
78#[UsesClass(ValueLexemeGenerator::class)]
79#[UsesClass(CandidateResolver::class)]
80#[UsesClass(OutputPart::class)]
81#[UsesClass(ResolvedOutput::class)]
82#[UsesClass(ReverseLexemeGenerator::class)]
83#[UsesClass(CombinedSpacingRule::class)]
84#[UsesClass(LexemeBoundary::class)]
85#[UsesClass(SpacingConstraint::class)]
86#[UsesClass(ProductionOccurrence::class)]
87#[UsesClass(TerminalOccurrence::class)]
88#[UsesClass(TerminalSequence::class)]
89#[UsesClass(TokenGenerator::class)]
90#[UsesClass(CompletionState::class)]
91#[UsesClass(CompletionFrontier::class)]
92#[UsesClass(ConstrainedCompletion::class)]
93#[UsesClass(ValueChoices::class)]
94#[UsesClass(BoundaryCompletion::class)]
95#[UsesClass(CompletionMemo::class)]
96#[UsesClass(CompletionReduction::class)]
97#[UsesClass(ConstraintDependencies::class)]
98#[UsesClass(PatternProductions::class)]
99#[UsesClass(CompletionWitness::class)]
100#[UsesClass(CharacterDomain::class)]
101final class BytePlanEncoderTest extends TestCase
102{
103    public function testEncodeWritesBytesTheCompilerDecodesToTheRecordedChoices(): void
104    {
105        $grammar = new Grammar(
106            'root',
107            [
108                'root' => new ProductionRule('root', [new Production([new NonTerminal('choice'), new NonTerminal('choice')])]),
109                'choice' => new ProductionRule('choice', [new Production([new Terminal('T')]), new Production([new Terminal('U')])]),
110            ],
111        );
112        $lexical = $this->createMock(LexicalGrammar::class);
113        $lexical->method('isNonOutput')->willReturn(false);
114        $literalDomain = new CharacterDomain(array_map(chr(...), range(0, 255)), 0, 255);
115        $lexemePipeline = new ReverseLexemeGenerator(
116            new ChoiceLexemeGenerator(
117                new ValueLexemeGenerator('T', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
118                new ValueLexemeGenerator('U', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
119            ),
120            new CandidateResolver(new CombinedSpacingRule()),
121            'fixture',
122        );
123        $lexical->method('resolveSequence')->willReturnCallback(
124            static fn (TerminalSequence $sequence, ?GenerationPlan $plan, Closure $choose) => $lexemePipeline->generate($sequence, $plan, $choose),
125        );
126        $builder = new PlanBuilder($grammar, $lexical);
127        $input = (new BytePlanEncoder())->encode(
128            $builder,
129            null,
130            3,
131            static fn (int $count, array $candidates): int => $count - 1,
132            static fn (int $count): int => 1,
133        );
134        $plan = (new BytePlanCompiler())->compile($input, $builder);
135        self::assertSame(pack('V', 0), substr($input, 0, 4));
136        self::assertSame(3, $plan->expansionBudget());
137        self::assertEquals(ProductionPattern::at(1), $plan->patternAt('choice', 0));
138        self::assertEquals(ProductionPattern::at(1), $plan->patternAt('choice', 1));
139        self::assertSame('second', $plan->lexemeAt('U', 0));
140        self::assertSame('second', $plan->lexemeAt('U', 1));
141    }
142
143    public function testEncodeMakesOpenLexicalChoicesTheWayTheCompilerReadsThePadding(): void
144    {
145        $grammar = new Grammar(
146            'root',
147            [
148                'root' => new ProductionRule('root', [new Production([new NonTerminal('choice'), new NonTerminal('choice')])]),
149                'choice' => new ProductionRule('choice', [new Production([new Terminal('T')]), new Production([new Terminal('U')])]),
150            ],
151        );
152        $lexical = $this->createMock(LexicalGrammar::class);
153        $lexical->method('isNonOutput')->willReturn(false);
154        $literalDomain = new CharacterDomain(array_map(chr(...), range(0, 255)), 0, 255);
155        $lexemePipeline = new ReverseLexemeGenerator(
156            new ChoiceLexemeGenerator(
157                new ValueLexemeGenerator('T', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
158                new ValueLexemeGenerator('U', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
159            ),
160            new CandidateResolver(new CombinedSpacingRule()),
161            'fixture',
162        );
163        $lexical->method('resolveSequence')->willReturnCallback(
164            static fn (TerminalSequence $sequence, ?GenerationPlan $plan, Closure $choose) => $lexemePipeline->generate($sequence, $plan, $choose),
165        );
166        $builder = new PlanBuilder($grammar, $lexical);
167        $constraints = GenerationPlan::fromRule('root')->requiringNonEmpty();
168        $input = (new BytePlanEncoder())->encode($builder, $constraints, 3, static fn (int $count, array $candidates): int => 0);
169        $plan = (new BytePlanCompiler())->compile($input, $builder, $constraints);
170        self::assertSame("\x00\x00\x00\x00\x00\x00\x00\x00\x00", $input);
171        self::assertEquals(ProductionPattern::at(0), $plan->patternAt('choice', 0));
172        self::assertSame('first', $plan->lexemeAt('T', 0));
173        self::assertSame('first', $plan->lexemeAt('T', 1));
174        self::assertTrue($plan->requiresNonEmpty());
175    }
176
177    public function testEncodeStopsRecordingLexicalChoicesAtTheFirstNullAnswer(): void
178    {
179        $grammar = new Grammar(
180            'root',
181            [
182                'root' => new ProductionRule('root', [new Production([new NonTerminal('choice'), new NonTerminal('choice'), new NonTerminal('choice')])]),
183                'choice' => new ProductionRule('choice', [new Production([new Terminal('T')]), new Production([new Terminal('U')])]),
184            ],
185        );
186        $lexical = $this->createMock(LexicalGrammar::class);
187        $lexical->method('isNonOutput')->willReturn(false);
188        $literalDomain = new CharacterDomain(array_map(chr(...), range(0, 255)), 0, 255);
189        $lexemePipeline = new ReverseLexemeGenerator(
190            new ChoiceLexemeGenerator(
191                new ValueLexemeGenerator('T', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
192                new ValueLexemeGenerator('U', $literalDomain, ['first', 'second'], 'fixture', 'fixture-literal'),
193            ),
194            new CandidateResolver(new CombinedSpacingRule()),
195            'fixture',
196        );
197        $lexical->method('resolveSequence')->willReturnCallback(
198            static fn (TerminalSequence $sequence, ?GenerationPlan $plan, Closure $choose) => $lexemePipeline->generate($sequence, $plan, $choose),
199        );
200        $builder = new PlanBuilder($grammar, $lexical);
201        $answers = [1, null, 1];
202        $input = (new BytePlanEncoder())->encode(
203            $builder,
204            null,
205            4,
206            static fn (int $count, array $candidates): int => 0,
207            static function (int $count) use (&$answers): ?int {
208                return array_shift($answers);
209            },
210        );
211        $plan = (new BytePlanCompiler())->compile($input, $builder);
212        self::assertSame("\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00", $input);
213        self::assertSame(['first', 'first', 'second'], [$plan->lexemeAt('T', 0), $plan->lexemeAt('T', 1), $plan->lexemeAt('T', 2)]);
214    }
215
216    public function testEncodeWritesTheHeaderRelativeToTheConstraintsMinimumAndDefaultMaximum(): void
217    {
218        $grammar = new Grammar(
219            'root',
220            [
221                'root' => new ProductionRule('root', [new Production([new NonTerminal('choice'), new NonTerminal('choice')])]),
222                'choice' => new ProductionRule('choice', [new Production([new Terminal('T')]), new Production([new Terminal('U')])]),
223            ],
224        );
225        $lexical = $this->createMock(LexicalGrammar::class);
226        $lexical->method('isNonOutput')->willReturn(false);
227        $literalDomain = new CharacterDomain(array_map(chr(...), range(0, 255)), 0, 255);
228        $lexemePipeline = new ReverseLexemeGenerator(
229            new ChoiceLexemeGenerator(
230                new ValueLexemeGenerator('T', $literalDomain, ['first'], 'fixture', 'fixture-literal'),
231                new ValueLexemeGenerator('U', $literalDomain, ['first'], 'fixture', 'fixture-literal'),
232            ),
233            new CandidateResolver(new CombinedSpacingRule()),
234            'fixture',
235        );
236        $lexical->method('resolveSequence')->willReturnCallback(
237            static fn (TerminalSequence $sequence, ?GenerationPlan $plan, Closure $choose) => $lexemePipeline->generate($sequence, $plan, $choose),
238        );
239        $builder = new PlanBuilder($grammar, $lexical);
240        $choice = GenerationPlan::fromRule('choice');
241        $largest = (new BytePlanEncoder())->encode($builder, null, BytePlanCompiler::MAXIMUM_BUDGET, static fn (int $count, array $candidates): int => 0);
242        $single = (new BytePlanEncoder())->encode($builder, $choice, 1, static fn (int $count, array $candidates): int => 1);
243        self::assertSame(pack('V', BytePlanCompiler::MAXIMUM_BUDGET - 3), substr($largest, 0, 4));
244        self::assertSame(BytePlanCompiler::MAXIMUM_BUDGET, (new BytePlanCompiler())->compile($largest, $builder)->expansionBudget());
245        self::assertSame("\x00\x00\x00\x00\x01", $single);
246        self::assertEquals(ProductionPattern::at(1), (new BytePlanCompiler())->compile($single, $builder, $choice)->patternAt('choice', 0));
247    }
248
249    public function testInterleavePairsProductionBytesWithLexicalBytesAndPadsTheShorterStream(): void
250    {
251        self::assertSame('', BytePlanEncoder::interleave('', ''));
252        self::assertSame("\x01\x00\x02\x00\x03", BytePlanEncoder::interleave("\x01\x02\x03", ''));
253        self::assertSame("\x01\x05\x02\x06", BytePlanEncoder::interleave("\x01\x02", "\x05\x06"));
254        self::assertSame("\x01\x05\x00\x06", BytePlanEncoder::interleave("\x01", "\x05\x06"));
255    }
256}
257