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