packages/sql-faker/tests/Unit/Generation/Coverage/LexicalObservationTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Coverage;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Coverage\LexicalObservation;
11use SqlFaker\Generation\Lexeme\Lexeme;
12use SqlFaker\Generation\Lexeme\LexemeSequence;
13use SqlFaker\Generation\Lexeme\OutputPart;
14use SqlFaker\Generation\Lexeme\ResolvedOutput;
15use SqlFaker\Generation\Lexeme\SpacingConstraint;
16use SqlFaker\Generation\Token\TerminalOccurrence;
17use SqlFaker\Generation\Token\TerminalSequence;
18
19#[CoversClass(LexicalObservation::class)]
20#[UsesClass(Lexeme::class)]
21#[UsesClass(SpacingConstraint::class)]
22#[UsesClass(LexemeSequence::class)]
23#[UsesClass(OutputPart::class)]
24#[UsesClass(ResolvedOutput::class)]
25#[UsesClass(TerminalOccurrence::class)]
26#[UsesClass(TerminalSequence::class)]
27final class LexicalObservationTest extends TestCase
28{
29    public function testFeaturesKeepsValueTextOutOfFiniteDefinitionSets(): void
30    {
31        $origin = new TerminalOccurrence('COMPOUND', 0);
32        $first = new Lexeme('random-123', 'identifier', $origin, 'identifier-domain');
33        $second = new Lexeme('random-456', 'identifier', $origin, 'identifier-domain');
34        $candidate = new LexemeSequence([$first, $second], 'random-id', provenance: static fn (): array => ['source-a', 'version-case:release-17']);
35        $output = new ResolvedOutput([new OutputPart($first, ' ', 'random-id', ['phrase']), new OutputPart($second, '', 'random-id')], candidates: [$candidate]);
36        self::assertSame(['lexeme' => ['identifier-domain'], 'compound' => ['identifier-domain + identifier-domain'],
37            'version-case' => ['release-17'], 'spacing' => ['phrase:space', 'default:join']], (new LexicalObservation())->features($output));
38        self::assertSame(['lexeme' => [], 'compound' => [], 'version-case' => [], 'spacing' => []], (new LexicalObservation())->features(new ResolvedOutput()));
39    }
40
41    public function testSourcesRetainsAllCandidateProvenanceInOccurrenceOrder(): void
42    {
43        $output = new ResolvedOutput(candidates: [new LexemeSequence([], 'empty', provenance: static fn (): array => ['first', 'second', 'version-case:chosen'])]);
44        self::assertSame([['first', 'second', 'version-case:chosen']], (new LexicalObservation())->sources($output));
45    }
46
47    public function testConditionsRetainsTheInputMasksAndAllContributingRules(): void
48    {
49        $output = new ResolvedOutput(candidates: [new LexemeSequence([], 'empty', new SpacingConstraint(1, ['left']), [0 => new SpacingConstraint(2, ['internal'])]), new LexemeSequence([], 'unconstrained')]);
50        self::assertSame([['left' => ['allowed' => 1, 'rules' => ['left']], 'boundaries' => [['allowed' => 2, 'rules' => ['internal']]]], ['left' => null, 'boundaries' => []]], (new LexicalObservation())->conditions($output));
51    }
52
53    public function testRewritesRetainsEachIntermediateReplacementAndOriginalIdentity(): void
54    {
55        $original = new TerminalOccurrence('A', 7, [2], ['root']);
56        $sequence = new TerminalSequence([$original], [$original]);
57        $sequence = $sequence->replace(0, 1, [$original->replaced('B', 'first')], 'first');
58        $sequence = $sequence->replace(0, 1, [$sequence->terminals[0]->replaced('C', 'second')], 'second');
59        $operations = (new LexicalObservation())->rewrites($sequence);
60        self::assertSame('A', $operations[0]['removed'][0]['name']);
61        self::assertSame('B', $operations[0]['inserted'][0]['name']);
62        self::assertSame($operations[0]['inserted'], $operations[1]['removed']);
63        self::assertSame('C', $operations[1]['inserted'][0]['name']);
64        self::assertSame(['id' => 7, 'name' => 'C', 'ancestors' => [2], 'rules' => ['root'], 'rewrite' => 'second'], $operations[1]['inserted'][0]);
65    }
66}
67