packages/sql-faker/tests/Unit/Compiler/Bison/Lexer/SourceCursorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Compiler\Bison\Lexer;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\TestCase;
9use SqlFaker\Compiler\Bison\Lexer\SourceCursor;
10
11#[CoversClass(SourceCursor::class)]
12final class SourceCursorTest extends TestCase
13{
14    public function testAtEndIsTrueForAnEmptySource(): void
15    {
16        self::assertTrue((new SourceCursor(''))->atEnd());
17    }
18
19    public function testAtEndIsFalseWhileACharacterRemains(): void
20    {
21        self::assertFalse((new SourceCursor('a'))->atEnd());
22    }
23
24    public function testOffsetStartsAtTheBeginningOfTheSource(): void
25    {
26        self::assertSame(0, (new SourceCursor('abc'))->offset());
27    }
28
29    public function testOffsetCountsWhatHasBeenConsumed(): void
30    {
31        $cursor = new SourceCursor('abc');
32
33        $cursor->advance(2);
34
35        self::assertSame(2, $cursor->offset());
36    }
37
38    public function testCurrentDoesNotConsumeTheCharacterItReports(): void
39    {
40        $cursor = new SourceCursor('ab');
41
42        self::assertSame('a', $cursor->current());
43        self::assertSame(0, $cursor->offset());
44    }
45
46    public function testCurrentIsEmptyAtTheEndOfTheSource(): void
47    {
48        self::assertSame('', (new SourceCursor(''))->current());
49    }
50
51    public function testPeekReportsTheCharacterAfterTheCursor(): void
52    {
53        self::assertSame('b', (new SourceCursor('ab'))->peek());
54    }
55
56    public function testPeekReportsNothingPastTheLastCharacter(): void
57    {
58        self::assertNull((new SourceCursor('a'))->peek());
59    }
60
61    public function testStartsWithComparesFromTheCursorOnwards(): void
62    {
63        $cursor = new SourceCursor('%{ code %}');
64
65        $cursor->advance();
66
67        self::assertTrue($cursor->startsWith('{ code'));
68    }
69
70    public function testStartsWithIgnoresWhatTheCursorHasPassed(): void
71    {
72        $cursor = new SourceCursor('%{ code %}');
73
74        $cursor->advance();
75
76        self::assertFalse($cursor->startsWith('%{'));
77    }
78
79    public function testAdvanceMovesTheCursorForward(): void
80    {
81        $cursor = new SourceCursor('abc');
82
83        $cursor->advance();
84
85        self::assertSame('b', $cursor->current());
86    }
87
88    public function testAdvanceStopsAtTheEndOfTheSource(): void
89    {
90        $cursor = new SourceCursor('ab');
91
92        $cursor->advance(99);
93
94        self::assertSame(2, $cursor->offset());
95    }
96
97    public function testSkipWhitespaceStopsAtTheFirstOtherCharacter(): void
98    {
99        $cursor = new SourceCursor("  \n\t x");
100
101        $cursor->skipWhitespace();
102
103        self::assertSame('x', $cursor->current());
104    }
105
106    public function testSkipWhitespaceLeavesANonBlankCursorAlone(): void
107    {
108        $cursor = new SourceCursor('x  ');
109
110        $cursor->skipWhitespace();
111
112        self::assertSame(0, $cursor->offset());
113    }
114
115    public function testTakeWhileReturnsTheAcceptedRun(): void
116    {
117        $cursor = new SourceCursor('abc123');
118
119        self::assertSame('abc', $cursor->takeWhile(static fn (string $c): bool => ctype_alpha($c)));
120        self::assertSame('123', $cursor->takeRest());
121    }
122
123    public function testTakeWhileConsumesNothingWhenTheFirstCharacterIsRejected(): void
124    {
125        $cursor = new SourceCursor('abc');
126
127        self::assertSame('', $cursor->takeWhile(static fn (string $c): bool => $c === 'z'));
128        self::assertSame(0, $cursor->offset());
129    }
130
131    public function testTakeUntilConsumesTheTerminatorWithoutReturningIt(): void
132    {
133        $cursor = new SourceCursor('body %} rest');
134
135        self::assertSame('body ', $cursor->takeUntil('%}'));
136        self::assertSame(' rest', $cursor->takeRest());
137    }
138
139    public function testTakeUntilAnAbsentTerminatorConsumesTheRest(): void
140    {
141        $cursor = new SourceCursor('body without a close');
142
143        self::assertSame('body without a close', $cursor->takeUntil('%}'));
144        self::assertTrue($cursor->atEnd());
145    }
146
147    public function testTakeQuotedDropsTheQuotesAndUnescapes(): void
148    {
149        $cursor = new SourceCursor('"a\\"b" tail');
150
151        self::assertSame('a"b', $cursor->takeQuoted('"'));
152        self::assertSame(' tail', $cursor->takeRest());
153    }
154
155    public function testTakeQuotedConsumesTheRestWhenTheRunIsNeverClosed(): void
156    {
157        $cursor = new SourceCursor("'abc");
158
159        self::assertSame('abc', $cursor->takeQuoted("'"));
160        self::assertTrue($cursor->atEnd());
161    }
162
163    public function testTakeQuotedEndsOnATrailingBackslash(): void
164    {
165        $cursor = new SourceCursor("'ab\\");
166
167        self::assertSame('ab', $cursor->takeQuoted("'"));
168        self::assertTrue($cursor->atEnd());
169    }
170
171    public function testTextBetweenReadsBackOverConsumedInput(): void
172    {
173        $cursor = new SourceCursor('abcdef');
174
175        $cursor->advance(4);
176
177        self::assertSame('bcd', $cursor->textBetween(1, 4));
178    }
179
180    public function testTextBetweenIsEmptyWhenTheOffsetsMeetOrCross(): void
181    {
182        $cursor = new SourceCursor('abcdef');
183
184        self::assertSame('', $cursor->textBetween(4, 4));
185        self::assertSame('', $cursor->textBetween(4, 1));
186    }
187
188    public function testTakeRestReturnsEverythingLeft(): void
189    {
190        $cursor = new SourceCursor('abc');
191
192        $cursor->advance();
193
194        self::assertSame('bc', $cursor->takeRest());
195    }
196
197    public function testTakeRestIsEmptyOnceTheSourceIsConsumed(): void
198    {
199        $cursor = new SourceCursor('abc');
200
201        $cursor->takeRest();
202
203        self::assertSame('', $cursor->takeRest());
204    }
205}
206