packages/sql-faker/tests/Unit/Generation/Output/BoundaryCompletionTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Output;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Lexeme\Lexeme;
12use SqlFaker\Generation\Lexeme\LexemeBoundary;
13use SqlFaker\Generation\Lexeme\LexemeCandidates;
14use SqlFaker\Generation\Lexeme\LexemeGenerator;
15use SqlFaker\Generation\Lexeme\LexemeInput;
16use SqlFaker\Generation\Lexeme\LexemeSequence;
17use SqlFaker\Generation\Lexeme\OutputPart;
18use SqlFaker\Generation\Lexeme\ResolvedOutput;
19use SqlFaker\Generation\Lexeme\SpacingConstraint;
20use SqlFaker\Generation\Lexeme\SpacingRule;
21use SqlFaker\Generation\Output\BoundaryCompletion;
22use SqlFaker\Generation\Output\CandidateResolver;
23use SqlFaker\Generation\Output\ReverseLexemeGenerator;
24use SqlFaker\Generation\Token\TerminalOccurrence;
25use SqlFaker\Generation\Token\TerminalSequence;
26
27#[CoversClass(BoundaryCompletion::class)]
28#[UsesClass(CandidateResolver::class)]
29#[UsesClass(OutputPart::class)]
30#[UsesClass(ResolvedOutput::class)]
31#[UsesClass(ReverseLexemeGenerator::class)]
32#[UsesClass(Lexeme::class)]
33#[UsesClass(LexemeCandidates::class)]
34#[UsesClass(LexemeInput::class)]
35#[UsesClass(LexemeSequence::class)]
36#[UsesClass(SpacingConstraint::class)]
37#[UsesClass(LexemeBoundary::class)]
38#[UsesClass(TerminalOccurrence::class)]
39#[UsesClass(TerminalSequence::class)]
40#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
41#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
42final class BoundaryCompletionTest extends TestCase
43{
44 #[DataProvider('providerMarkers')]
45 public function testAcceptsOnlyCandidatesWhosePendingLeftBoundaryHasACompletion(bool $marker): void
46 {
47 $lexemes = self::createStub(LexemeGenerator::class);
48 $lexemes->method('generate')->willReturnCallback(static fn (LexemeInput $input): LexemeCandidates => match ($input->terminal()->name) {
49 'L' => LexemeCandidates::of(new LexemeSequence([new Lexeme('l', 'identifier', $input->terminal(), 'left')], 'left')),
50 'E' => LexemeCandidates::of(new LexemeSequence([], 'marker')),
51 default => LexemeCandidates::of(
52 new LexemeSequence([new Lexeme('r', 'identifier', $input->terminal(), 'right')], 'join', new SpacingConstraint(SpacingConstraint::JOIN, ['join-required'])),
53 new LexemeSequence([new Lexeme('r', 'identifier', $input->terminal(), 'right')], 'space', new SpacingConstraint(SpacingConstraint::SPACE, ['space-required'])),
54 ),
55 });
56 $spacing = self::createStub(SpacingRule::class);
57 $spacing->method('apply')->willReturn(new SpacingConstraint(SpacingConstraint::SPACE, ['left-space']));
58 $generator = new ReverseLexemeGenerator($lexemes, new CandidateResolver($spacing), 'test');
59 $result = $generator->generate(TerminalSequence::fromNames($marker ? ['L', 'E', 'R'] : ['L', 'R']), null, static fn (int $count): int => 0);
60 self::assertSame('l r', implode('', $result->pieces()));
61 self::assertSame('join', $result->rejections[0]['candidate']);
62 self::assertSame(['uncompletable-left-boundary'], $result->rejections[0]['rules']);
63 }
64
65 public function testAcceptsUsesTheSampledLeftValueInsteadOfAnUnrelatedDefaultWitness(): void
66 {
67 $lexemes = self::createStub(LexemeGenerator::class);
68 $lexemes->method('generate')->willReturnCallback(static fn (LexemeInput $input): LexemeCandidates => $input->terminal()->name === 'L'
69 ? LexemeCandidates::of(new LexemeSequence([new Lexeme($input->values?->value(0, 'sample', new \SqlFaker\Generation\Value\CharacterDomain(['a', 'b'], 1, 1)) ?? 'a', 'identifier', $input->terminal(), 'sample')], 'left'))
70 : LexemeCandidates::of(new LexemeSequence([new Lexeme('r', 'identifier', $input->terminal(), 'right')], 'join', new SpacingConstraint(SpacingConstraint::JOIN)), new LexemeSequence([new Lexeme('r', 'identifier', $input->terminal(), 'right')], 'space', new SpacingConstraint(SpacingConstraint::SPACE))));
71 $spacing = self::createStub(SpacingRule::class);
72 $spacing->method('apply')->willReturnCallback(static fn (LexemeBoundary $boundary): SpacingConstraint => new SpacingConstraint($boundary->left->text === 'a' ? SpacingConstraint::JOIN : SpacingConstraint::SPACE));
73 $generator = new ReverseLexemeGenerator($lexemes, new CandidateResolver($spacing), 'test');
74 $result = $generator->generate(TerminalSequence::fromNames(['L', 'R']), null, static fn (int $count): int => 0, static fn (int $count): int => $count - 1);
75 self::assertSame('b r', implode('', $result->pieces()));
76 self::assertSame('join', $result->rejections[0]['candidate']);
77 self::assertSame(SpacingConstraint::SPACE, $result->parts[0]->allowed);
78 }
79
80 public function testAcceptsRejectsMissingOrPlannedAwayWitnesses(): void
81 {
82 $lexemes = self::createStub(LexemeGenerator::class);
83 $lexemes->method('generate')->willReturn(null);
84 $spacing = self::createStub(SpacingRule::class);
85 $completion = new BoundaryCompletion($lexemes, new CandidateResolver($spacing));
86 $sequence = TerminalSequence::fromNames(['L']);
87 $pending = new ResolvedOutput([], new SpacingConstraint(SpacingConstraint::SPACE));
88 self::assertFalse($completion->accepts($sequence, 0, $pending));
89 self::assertFalse($completion->accepts($sequence, -1, $pending));
90 self::assertTrue($completion->accepts($sequence, -1, new ResolvedOutput()));
91 }
92
93 /**
94 * @return iterable<array{bool}>
95 */
96 public static function providerMarkers(): iterable
97 {
98 yield [false];
99 yield [true];
100 }
101}
102