packages/sql-faker/tests/Unit/Generation/Plan/RulePlanTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Generation\Plan;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Plan\LexemeConstraint;
11use SqlFaker\Generation\Plan\ProductionPattern;
12use SqlFaker\Generation\Plan\RulePlan;
13
14#[CoversClass(RulePlan::class)]
15#[UsesClass(ProductionPattern::class)]
16#[UsesClass(LexemeConstraint::class)]
17final class RulePlanTest extends TestCase
18{
19    public function testAllowingRefinementsIntersectAndLeaveTheOriginalPlanIntact(): void
20    {
21        $original = RulePlan::any()->allowing(ProductionPattern::containing('ID'));
22        $refined = $original->allowing(ProductionPattern::excluding(ProductionPattern::containing('DOT')))
23            ->withLexeme('ID', LexemeConstraint::oneOf('users', 'orders'))
24            ->withLexeme('ID', LexemeConstraint::oneOf('users'));
25        self::assertTrue($original->pattern?->matches(['ID', 'DOT', 'ID']));
26        self::assertFalse($refined->pattern?->matches(['ID', 'DOT', 'ID']));
27        self::assertTrue($refined->pattern->matches(['ID']));
28        self::assertSame('users', $refined->lexemes['ID']->choose(static fn (int $count): int => 0));
29        self::assertSame([], $original->lexemes);
30    }
31
32    public function testWithRuleMergingPreservesChildAndListRelationships(): void
33    {
34        $left = RulePlan::any()->withChild('expr', 0, RulePlan::any()->allowing(ProductionPattern::exactly('ID')));
35        $right = RulePlan::any()->withChild('expr', 1, RulePlan::any()->allowing(ProductionPattern::exactly('INTEGER')));
36        $plan = RulePlan::any()->withRule('expr', $left)->withRule('expr', $right)
37            ->withItems(RulePlan::any(), RulePlan::any());
38        self::assertCount(2, $plan->rules['expr']->children['expr']);
39        self::assertCount(2, $plan->items ?? []);
40        self::assertSame([], $left->rules);
41    }
42
43
44    public function testAnyLeavesEveryKindOfChoiceUnspecified(): void
45    {
46        $plan = RulePlan::any();
47        self::assertNull($plan->pattern);
48        self::assertNull($plan->items);
49        self::assertSame([], $plan->rules);
50        self::assertSame([], $plan->children);
51        self::assertSame([], $plan->lexemes);
52    }
53
54    public function testWithChildIntersectsOnlyTheSelectedOperand(): void
55    {
56        $plan = RulePlan::any()->withChild('expr', 1, RulePlan::any()->allowing(ProductionPattern::containing('ID')))
57            ->withChild('expr', 1, RulePlan::any()->allowing(ProductionPattern::excluding(ProductionPattern::containing('DOT'))));
58        self::assertArrayNotHasKey(0, $plan->children['expr']);
59        self::assertTrue($plan->children['expr'][1]->pattern?->matches(['ID']));
60        self::assertFalse($plan->children['expr'][1]->pattern->matches(['ID', 'DOT', 'ID']));
61    }
62
63    public function testWithLexemeIntersectsDomainsWithoutChangingOtherTokens(): void
64    {
65        $original = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('users', 'orders'))
66            ->withLexeme('STRING', LexemeConstraint::oneOf("'Alice'"));
67        $refined = $original->withLexeme('ID', LexemeConstraint::oneOf('users'));
68        self::assertFalse($refined->lexemes['ID']->accepts('orders'));
69        self::assertTrue($original->lexemes['ID']->accepts('orders'));
70        self::assertSame($original->lexemes['STRING'], $refined->lexemes['STRING']);
71    }
72
73    public function testWithItemsRetainsHeterogeneousOutputOrder(): void
74    {
75        $name = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('name'));
76        $score = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('score'));
77        self::assertSame([$name, $score], RulePlan::any()->withItems($name, $score)->items);
78    }
79
80    public function testMergeIntersectsCorrespondingListItems(): void
81    {
82        $names = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('name', 'score'));
83        $name = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('name'));
84        $score = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('score'));
85        $merged = RulePlan::any()->withItems($names, $names)->merge(RulePlan::any()->withItems($name, $score));
86        self::assertEquals([$name, $score], $merged->items);
87        self::assertTrue($names->lexemes['ID']->accepts('score'));
88        self::assertTrue($names->lexemes['ID']->accepts('name'));
89    }
90
91
92    public function testWithItemsAcceptsNamedArgumentsAndKeepsEveryItemInOrder(): void
93    {
94        $id = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('id'));
95        $name = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('name'));
96        $score = RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('score'));
97        self::assertSame([$id, $name, $score], RulePlan::any()->withItems($id, name: $name, score: $score)->items);
98    }
99
100    public function testMergeCombinesNestedRulesAndPreservesIndependentBindings(): void
101    {
102        $outer = RulePlan::any()->withRule('name', RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('users', 'orders')));
103        $other = RulePlan::any()->withRule('name', RulePlan::any()->withLexeme('ID', LexemeConstraint::oneOf('users')))
104            ->withRule('value', RulePlan::any()->withLexeme('INTEGER', LexemeConstraint::oneOf('1')));
105        $merged = $outer->merge($other);
106        self::assertTrue($merged->rules['name']->lexemes['ID']->accepts('users'));
107        self::assertFalse($merged->rules['name']->lexemes['ID']->accepts('orders'));
108        self::assertSame($other->rules['value'], $merged->rules['value']);
109        self::assertTrue($outer->rules['name']->lexemes['ID']->accepts('orders'));
110    }
111
112}
113