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