packages/sql-faker/tests/Unit/Generation/Exception/LexicalExceptionTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Exception;
6
7use Faker\Factory;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Exception\LexicalException;
12use SqlFaker\Generation\Value\RandomCharacters;
13use SqlFaker\Grammar\Resource\SqlVersion;
14use SqlFaker\Grammar\Resource\SqlVersionRegistry;
15use SqlFaker\MySql\Generation\LexicalGrammar;
16use SqlFaker\MySql\Generation\Tokenization\KeywordIndex;
17use SqlFaker\MySql\Generation\Tokenization\MySqlTokenizer;
18use SqlFaker\MySql\Generation\Value\LiteralGenerator;
19
20#[CoversClass(LexicalException::class)]
21#[UsesClass(LexicalGrammar::class)]
22#[UsesClass(LiteralGenerator::class)]
23#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\LiteralGenerator::class)]
24#[UsesClass(\SqlFaker\Sqlite\Generation\Value\LiteralGenerator::class)]
25#[UsesClass(SqlVersion::class)]
26#[UsesClass(KeywordIndex::class)]
27#[UsesClass(\SqlFaker\PostgreSql\Generation\Tokenization\KeywordIndex::class)]
28#[UsesClass(\SqlFaker\Sqlite\Generation\Tokenization\KeywordIndex::class)]
29#[UsesClass(RandomCharacters::class)]
30#[UsesClass(SqlVersionRegistry::class)]
31#[UsesClass(MySqlTokenizer::class)]
32#[UsesClass(\SqlFaker\MySql\Generation\Tokenization\MySqlQuoting::class)]
33#[UsesClass(\SqlFaker\Generation\Candidate\ChoiceLexemeGenerator::class)]
34#[UsesClass(\SqlFaker\Generation\Candidate\FixedLexemeGenerator::class)]
35#[UsesClass(\SqlFaker\Generation\Candidate\IntegerLexemeGenerator::class)]
36#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeInput::class)]
37#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
38#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
39#[UsesClass(\SqlFaker\Generation\Candidate\SequenceLexemeGenerator::class)]
40#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
41#[UsesClass(\SqlFaker\Generation\Lexeme\ResolvedOutput::class)]
42#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
43#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
44#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
45#[UsesClass(\SqlFaker\Generation\Token\TerminalSequence::class)]
46#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
47#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
48#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\DefinitionFactory::class)]
49#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\KeywordLexemeGenerator::class)]
50#[UsesClass(\SqlFaker\Generation\Plan\GenerationPlan::class)]
51#[UsesClass(\SqlFaker\Generation\Plan\ProductionPattern::class)]
52#[UsesClass(\SqlFaker\Generation\Lexeme\Lexeme::class)]
53#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
54#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
55#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
56#[UsesClass(\SqlFaker\Generation\Output\SqlSerializer::class)]
57#[UsesClass(\SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule::class)]
58#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
59#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
60#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
61#[UsesClass(\SqlFaker\MySql\Generation\Spacing\CloneAddressSpacingRule::class)]
62#[UsesClass(\SqlFaker\MySql\Generation\Spacing\FunctionSpacingRule::class)]
63#[UsesClass(\SqlFaker\MySql\Generation\Spacing\QualifiedNameSpacingRule::class)]
64#[UsesClass(\SqlFaker\MySql\Generation\Spacing\VariableSpacingRule::class)]
65#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\BoundedIntegerLexemeGenerator::class)]
66#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\ReplicationTablePatternLexemeGenerator::class)]
67#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\SizeNumberLexemeGenerator::class)]
68#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\FactorLexemeGenerator::class)]
69#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\PrecisionLexemeGenerator::class)]
70#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
71#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
72#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
73#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
74#[UsesClass(\SqlFaker\MySql\Generation\Value\DollarQuotedDomain::class)]
75#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\DollarQuotedDomain::class)]
76#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
77#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
78#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
79#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetLexemeGenerator::class)]
80#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetValueLexemeGenerator::class)]
81#[UsesClass(\SqlFaker\MySql\Generation\Value\IdentifierDomain::class)]
82#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\IdentifierDomain::class)]
83#[UsesClass(\SqlFaker\Sqlite\Generation\Value\IdentifierDomain::class)]
84#[UsesClass(\SqlFaker\MySql\Generation\Value\QuotedDomain::class)]
85#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\QuotedDomain::class)]
86#[UsesClass(\SqlFaker\Sqlite\Generation\Value\QuotedDomain::class)]
87#[UsesClass(\SqlFaker\MySql\Generation\Value\RadixDomain::class)]
88#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
89#[UsesClass(\SqlFaker\Generation\Value\Utf8::class)]
90#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\LexicalDefinition::class)]
91#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\LexicalDefinition::class)]
92#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\LexicalDefinition::class)]
93final class LexicalExceptionTest extends TestCase
94{
95    public function testTokenizingUnsupportedInputReportsTheOffsetAndTheInput(): void
96    {
97        $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
98
99        $this->expectException(LexicalException::class);
100        $this->expectExceptionMessage('Unsupported MySQL lexical input at offset 0:');
101
102        $lexical->tokenize("\x00");
103    }
104
105    public function testRealizingAnUnknownTerminalReportsTheTerminal(): void
106    {
107        $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
108
109        $this->expectException(LexicalException::class);
110        $this->expectExceptionMessage('Unsupported MySQL terminal for mysql-8.4.7: NOT_A_TERMINAL');
111
112        $lexical->realize(['NOT_A_TERMINAL']);
113    }
114
115    public function testUnsupportedTerminalNamesTheTerminalAndTheProfile(): void
116    {
117        self::assertSame(
118            'Unsupported MySQL terminal for mysql-8.4.7: NOT_A_TERMINAL',
119            LexicalException::unsupportedTerminal('MySQL', 'mysql-8.4.7', 'NOT_A_TERMINAL')->getMessage(),
120        );
121    }
122
123    public function testUnsupportedInputNamesTheOffsetAndTheText(): void
124    {
125        self::assertSame(
126            'Unsupported SQLite lexical input at offset 3: abc',
127            LexicalException::unsupportedInput('SQLite', 3, 'abc')->getMessage(),
128        );
129    }
130
131    public function testNoProgressNamesTheOffsetTheReadGotStuckAt(): void
132    {
133        self::assertSame(
134            'SQLite lexer made no progress at offset 3: abc',
135            LexicalException::noProgress('SQLite', 3, 'abc')->getMessage(),
136        );
137    }
138
139    public function testUnterminatedQuotedTokenNamesTheText(): void
140    {
141        self::assertSame(
142            "Unterminated MySQL quoted token: 'abc",
143            LexicalException::unterminatedQuotedToken('MySQL', "'abc")->getMessage(),
144        );
145    }
146
147    public function testUnterminatedBracketIdentifierNamesTheDialect(): void
148    {
149        self::assertSame(
150            'Unterminated SQLite bracket identifier.',
151            LexicalException::unterminatedBracketIdentifier('SQLite')->getMessage(),
152        );
153    }
154
155    public function testUnterminatedBlockCommentNamesTheDialect(): void
156    {
157        self::assertSame(
158            'Unterminated PostgreSQL block comment.',
159            LexicalException::unterminatedBlockComment('PostgreSQL')->getMessage(),
160        );
161    }
162
163    public function testUnterminatedDollarQuotedStringNamesTheDialect(): void
164    {
165        self::assertSame(
166            'Unterminated MySQL dollar-quoted string.',
167            LexicalException::unterminatedDollarQuotedString('MySQL')->getMessage(),
168        );
169    }
170
171    public function testLexemeDoesNotRealizeTerminalNamesBoth(): void
172    {
173        self::assertSame(
174            'Requested MySQL lexeme does not realize IDENT: 42',
175            LexicalException::lexemeDoesNotRealizeTerminal('MySQL', 'IDENT', '42')->getMessage(),
176        );
177    }
178
179    public function testNoWitnessForLexemeNamesBoth(): void
180    {
181        self::assertSame(
182            'MySQL lexical catalog has no IDENT witness for: 42',
183            LexicalException::noWitnessForLexeme('MySQL', 'IDENT', '42')->getMessage(),
184        );
185    }
186
187    public function testRenderedSubstitutesBytesJsonHasNoEncodingFor(): void
188    {
189        self::assertSame('["\\ufffd"]', LexicalException::rendered(["\xB1"]));
190    }
191
192    public function testRenderedWritesAnOrdinarySequenceAsJson(): void
193    {
194        self::assertSame('["IDENT"]', LexicalException::rendered(['IDENT']));
195    }
196
197    public function testRoundTripMismatchCarriesBothSequencesAndTheText(): void
198    {
199        $message = LexicalException::roundTripMismatch(
200            'MySQL',
201            'mysql-8.4.7',
202            ['SELECT_SYM'],
203            ['IDENT'],
204            'select',
205        )->getMessage();
206
207        self::assertSame(
208            "MySQL lexical round-trip failed for mysql-8.4.7.\n"
209            . "Expected: [\"SELECT_SYM\"]\n"
210            . "Actual: [\"IDENT\"]\n"
211            . 'SQL: select',
212            $message,
213        );
214    }
215}
216