ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
BrickTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
23use Closure;
30use PHPUnit\Framework\TestCase;
31use PHPUnit\Framework\Attributes\DataProvider;
32
33class BrickTest extends TestCase
34{
35 public function testConstruct(): void
36 {
37 $this->assertInstanceOf(Brick::class, new Brick());
38 }
39
40 public function testApply(): void
41 {
42 $expected = 'aloha';
43 $ok = $this->getMockBuilder(Result::class)->getMock();
44 $ok->method('isOk')->willReturn(true);
45 $ok->method('value')->willReturn($expected);
46 $intermediate = $this->getMockBuilder(Intermediate::class)->disableOriginalConstructor()->getMock();
47 $intermediate->method('done')->willReturn(true);
48 $intermediate->method('transform')->willReturnCallback(fn() => $ok);
49 $brick = new Brick();
50 $result = $brick->apply(static fn(Intermediate $x, Closure $cc): Result => (
51 $cc(new Ok($intermediate))
52 ), 'abcde');
53
54 $this->assertTrue($result->isOk());
55 $this->assertEquals($expected, $result->value());
56 }
57
58 public function testThatAllInputMustBeConsumed(): void
59 {
60 $intermediate = $this->getMockBuilder(Intermediate::class)->disableOriginalConstructor()->getMock();
61 $intermediate->expects(self::once())->method('done')->willReturn(false);
62 $intermediate->expects(self::never())->method('transform');
63
64 $brick = new Brick();
65 $result = $brick->apply(static fn(Intermediate $x, Closure $cc): Result => (
66 $cc(new Ok($intermediate))
67 ), 'abcde');
68
69 $this->assertFalse($result->isOk());
70 }
71
72 public function testAFailingParser(): void
73 {
74 $brick = new Brick();
75 $result = $brick->apply(static fn(Intermediate $x, Closure $cc): Result => (
76 $cc(new Error('something happened'))
77 ), 'abcde');
78
79 $this->assertFalse($result->isOk());
80 }
81
82 public function testToTransformation(): void
83 {
84 $brick = new Brick();
85 $parser = $brick->sequence(['foo']);
86 $transformation = $brick->toTransformation($parser);
87 $this->assertInstanceOf(Transformation::class, $transformation);
88 $result = $transformation->applyTo(new Ok('foo'));
89 $this->assertTrue($result->isOk());
90 $this->assertEquals('foo', $result->value());
91 }
92
93 public function testToTransformationFailed(): void
94 {
95 $brick = new Brick();
96 $parser = $brick->sequence(['foo']);
97 $transformation = $brick->toTransformation($parser);
98 $this->assertInstanceOf(Transformation::class, $transformation);
99 $result = $transformation->applyTo(new Ok('fox'));
100 $this->assertFalse($result->isOk());
101 }
102
103 public function testRange(): void
104 {
105 $brick = new Brick();
106
107 $parse = $brick->range(0, 0x14);
108
109 foreach (array_fill(0, 0x14 + 1, null) as $i => $_) {
110 $result = $brick->apply($parse, chr($i));
111 $this->assertTrue($result->isOk());
112 $this->assertEquals(chr($i), $result->value());
113 }
114 }
115
116 public function testOutOfRange(): void
117 {
118 $brick = new Brick();
119
120 $parse = $brick->range(1, 0x14);
121
122 $this->assertFalse($brick->apply($parse, "\x0")->isOk());
123 $this->assertFalse($brick->apply($parse, "\x15")->isOk());
124 }
125
126 public function testEither(): void
127 {
128 $brick = new Brick();
129
130 $parse = $brick->either([
131 'a' => $brick->range(0x10, 0x20), // False.
132 $brick->range(0x21, 0x30), // False without key.
133 'b' => $brick->range(0x0, 0x5), // True.
134 ]);
135
136 $result = $brick->apply($parse, "\x0");
137
138 $this->assertTrue($result->isOk());
139 $this->assertEquals("\x0", $result->value()['b']);
140 }
141
142 public function testSequence(): void
143 {
144 $brick = new Brick();
145
146 $parse = $brick->sequence([
147 'first' => $brick->range(ord('a'), ord('b')),
148 'second' => $brick->range(ord('c'), ord('d')),
149 ]);
150
151 $result = $brick->apply($parse, 'ad');
152
153 $this->assertTrue($result->isOk());
154 $this->assertEquals('a', $result->value()['first']);
155 $this->assertEquals('d', $result->value()['second']);
156 }
157
158 #[DataProvider('repeatProvider')]
159 public function testRepeat(int $min, ?int $max, array $succeed, array $fail): void
160 {
161 $brick = new Brick();
162 $parse = $brick->repeat($min, $max, $brick->range(ord('a'), ord('z')));
163
164 foreach ($succeed as $input) {
165 $result = $brick->apply($parse, $input);
166
167 $this->assertTrue($result->isOk());
168 $this->assertEquals($input, $result->value());
169 }
170
171 foreach ($fail as $input) {
172 $result = $brick->apply($parse, $input);
173 $this->assertFalse($result->isOk());
174 }
175 }
176
177 public static function repeatProvider(): array
178 {
179 return [
180 'Ranges are inclusive' => [3, 3, ['abc'], ['ab', 'abcd']],
181 'Null is used for infinity' => [0, null, ['', 'abcdefghijklmop'], []],
182 'Minimum is 0' => [-1, 3, ['', 'a', 'ab', 'abc'], ['abcd']],
183 'Minimum of the end range is the start range' => [3, 2, ['abc'], ['ab', 'abcd']],
184 ];
185 }
186
187 #[DataProvider('characterProvider')]
188 public function testCharacters(string $method, string $input, bool $isOk): void
189 {
190 $brick = new Brick();
191
192 $parse = $brick->either(str_split($input)); // No character is allowed to pass.
193 if ($isOk) {
194 $parse = $brick->repeat(0, null, $brick->$method()); // All characters must pass.
195 }
196
197 $result = $brick->apply($parse, $input);
198
199 $this->assertEquals($isOk, $result->isOk());
200 if ($isOk) {
201 $this->assertEquals($input, $result->value());
202 }
203 }
204
205 private static function breakIntoPieces(int $x, string $break_me): array
206 {
207 $len = (int) floor(strlen($break_me) / $x);
208
209 return array_map(
210 fn($i) => substr($break_me, $i * $len, $len),
211 range(0, $x - !(strlen($break_me) % $x))
212 );
213 }
214
215 public static function characterProvider(): array
216 {
217 $alpha = array_fill(ord('a'), ord('z') - ord('a') + 1, '');
218 array_walk($alpha, function (&$value, int $i) {
219 $value = chr($i);
220 });
221 $alpha = implode('', $alpha);
222 $alpha .= strtoupper($alpha);
223
224 // Circumvent error when running this test with Xdebug. The default value of xdebug.max_nesting_level will kill the test.
225 $alpha_parts = self::breakIntoPieces(3, $alpha);
226
227 $digits = '1234567890';
228
229 return [
230 'Accepts all digits.' => ['digit', $digits, true],
231 'Accepts no characters from a-z or A-Z.' => ['digit', $alpha, false],
232 'Accepts characters from a-z and A-Z (Part 1).' => ['alpha', $alpha_parts[0], true],
233 'Accepts characters from a-z and A-Z (Part 2).' => ['alpha', $alpha_parts[1], true],
234 'Accepts characters from a-z and A-Z (Part 3).' => ['alpha', $alpha_parts[2], true],
235 'Accepts no digits.' => ['alpha', $digits, false],
236 ];
237 }
238
239 #[DataProvider('emptyStringProvider')]
240 public function testEmptyString(string $input, bool $isOk): void
241 {
242 $brick = new Brick();
243
244 $parse = $brick->sequence(['']);
245 $result = $brick->apply($parse, $input);
246
247 $this->assertEquals($isOk, $result->isOk());
248 }
249
250 public static function emptyStringProvider(): array
251 {
252 return [
253 'Test empty input' => ['', true],
254 'Test non empty input' => ['x', false],
255 ];
256 }
257
258 public function testTransformation(): void
259 {
260 $brick = new Brick();
261
262 $intermediate = $this->getMockBuilder(Intermediate::class)->disableOriginalConstructor()->getMock();
263 $intermediate->method('done')->willReturn(true);
264
265 $ok = $this->getMockBuilder(Result::class)->getMock();
266 $ok->method('isOk')->willReturn(true);
267 $ok->method('value')->willReturn($intermediate);
268 $ok->method('map')->willReturn($ok);
269 $ok->method('then')->willReturn($ok);
270 $ok->method('except')->willReturn($ok);
271
272 $transformation = $this->getMockBuilder(Transformation::class)->getMock();
273 $transformation->expects(self::once())->method('applyTo')->willReturn($ok);
274
275 $parser = $brick->transformation($transformation, fn($x, $cc) => $cc(new Ok($x)));
276
277 $result = $brick->apply($parser, 'a');
278
279 $this->assertEquals($ok, $result);
280 }
281
282 private function string(): Closure
283 {
284 return static function (string $string): Ok {
285 return new Ok($string);
286 };
287 }
288}
A result encapsulates a value or an error and simplifies the handling of those.
Definition: Ok.php:31
@phpstan-type Continuation Closure(Result<Intermediate>): Result<Intermediate> @phpstan-type Parser C...
Definition: Brick.php:35
testEmptyString(string $input, bool $isOk)
Definition: BrickTest.php:240
static breakIntoPieces(int $x, string $break_me)
Definition: BrickTest.php:205
testCharacters(string $method, string $input, bool $isOk)
Definition: BrickTest.php:188
testRepeat(int $min, ?int $max, array $succeed, array $fail)
Definition: BrickTest.php:159
A result encapsulates a value or an error and simplifies the handling of those.
Definition: Result.php:29
A transformation is a function from one datatype to another.