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