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