packages/sql-faker/tests/Unit/Grammar/Model/GrammarTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Grammar\Model;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use RuntimeException;
11use SqlFaker\Grammar\Model\Grammar;
12use SqlFaker\Grammar\Model\Production;
13use SqlFaker\Grammar\Model\ProductionRule;
14use SqlFaker\Grammar\Model\Terminal;
15use stdClass;
16
17#[CoversClass(Grammar::class)]
18#[CoversClass(ProductionRule::class)]
19#[CoversClass(Production::class)]
20#[CoversClass(Terminal::class)]
21#[UsesClass(\SqlFaker\Grammar\Model\NonTerminal::class)]
22#[UsesClass(\SqlFaker\Generation\Derivation\CompletionCosts::class)]
23#[UsesClass(\SqlFaker\Generation\Derivation\DerivationNode::class)]
24final class GrammarTest extends TestCase
25{
26 public function testExposesTheStartSymbolAndRuleMap(): void
27 {
28 $rule = new ProductionRule('start', [new Production([new Terminal('A')])]);
29 $grammar = new Grammar('start', ['start' => $rule]);
30
31 self::assertSame('start', $grammar->startSymbol);
32 self::assertSame(['start' => $rule], $grammar->ruleMap);
33 }
34
35
36 public function testLoadFromFile(): void
37 {
38 $grammar = Grammar::loadFromFile(__DIR__ . '/../../../../resources/ast/pg-17.2.php');
39
40 self::assertNotEmpty($grammar->ruleMap);
41 }
42
43 public function testLoadFromFileNotFound(): void
44 {
45 $this->expectException(RuntimeException::class);
46 $this->expectExceptionMessage('Grammar file not found');
47
48 Grammar::loadFromFile('/nonexistent/path/grammar.php');
49 }
50
51 public function testLoadFromFileInvalidData(): void
52 {
53 $tmpFile = tempnam(sys_get_temp_dir(), 'grammar_test_');
54 self::assertNotFalse($tmpFile);
55 file_put_contents($tmpFile, '<?php return [];');
56
57 $this->expectException(RuntimeException::class);
58 $this->expectExceptionMessage('Invalid grammar file');
59
60 try {
61 Grammar::loadFromFile($tmpFile);
62 } finally {
63 unlink($tmpFile);
64 }
65 }
66
67 public function testLoadFromFileFailedUnserialize(): void
68 {
69 $tmpFile = tempnam(sys_get_temp_dir(), 'grammar_test_');
70 self::assertNotFalse($tmpFile);
71 $serialized = serialize(new stdClass());
72 file_put_contents($tmpFile, "<?php return ['key' => '" . $serialized . "'];");
73
74 $this->expectException(RuntimeException::class);
75 $this->expectExceptionMessage('Failed to load grammar from');
76
77 try {
78 Grammar::loadFromFile($tmpFile);
79 } finally {
80 unlink($tmpFile);
81 }
82 }
83
84 public function testIdentifiedAssignsOriginalOrdinalsBeforeFiltering(): void
85 {
86 $grammar = new Grammar('stmt', ['stmt' => new ProductionRule('stmt', [new Production([]), new Production([new Terminal('SELECT')])])]);
87 $identified = $grammar->identified();
88 self::assertSame(0, $identified->ruleMap['stmt']->alternatives[0]->ordinal);
89 self::assertSame('stmt#1', $identified->ruleMap['stmt']->alternatives[1]->origin);
90 self::assertEquals($identified, $identified->identified());
91 }
92 public function testIdentifiedPreservesTheSourceAndEveryExistingProductionIdentity(): void
93 {
94 $grammar = new Grammar('stmt', ['stmt' => new ProductionRule('stmt', [
95 new Production([new Terminal('A')]), new Production([new Terminal('B')], 7, 'upstream#7'), new Production([]),
96 ])]);
97 $identified = $grammar->identified();
98 self::assertSame('stmt', $identified->startSymbol);
99 self::assertNull($grammar->ruleMap['stmt']->alternatives[0]->ordinal);
100 self::assertSame([0, 7, 2], array_column($identified->ruleMap['stmt']->alternatives, 'ordinal'));
101 self::assertSame(['stmt#0', 'upstream#7', 'stmt#2'], array_column($identified->ruleMap['stmt']->alternatives, 'origin'));
102 self::assertSame($grammar->ruleMap['stmt']->alternatives[0]->symbols, $identified->ruleMap['stmt']->alternatives[0]->symbols);
103 self::assertSame([], $identified->ruleMap['stmt']->alternatives[2]->symbols);
104 self::assertEquals($identified, $identified->identified());
105 }
106
107}
108