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