ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
LanguageIdentifierEnumToolTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21use PHPUnit\Framework\Attributes\DataProvider;
22use PHPUnit\Framework\TestCase;
23
28class LanguageIdentifierEnumToolTest extends TestCase
29{
30 private const string TOOL = __DIR__ . '/../tools/language-identifier-enum/generate_language_identifier_enum.php';
31 private const string NAMESPACE = 'ILIAS\Demo';
32 private const string POT = 'components/ILIAS/Demo/lang/demo.pot';
33 private const string ENUM = 'components/ILIAS/Demo/src/DemoIdentifier.php';
34
35 private string $directory;
36 private string $root;
37
38 protected function setUp(): void
39 {
40 $this->directory = sys_get_temp_dir() . '/ilias_key_enum_tool_' . bin2hex(random_bytes(4));
41 $this->root = $this->directory . '/repo';
42 $tool_dir = $this->root . '/components/ILIAS/Language/tools/language-identifier-enum';
43 mkdir($tool_dir, 0775, true);
44 mkdir($this->root . '/components/ILIAS/Demo/lang', 0775, true);
45 mkdir($this->root . '/components/ILIAS/Demo/src', 0775, true);
46 mkdir($this->directory . '/outside', 0775, true);
47 copy(self::TOOL, $tool_dir . '/generate_language_identifier_enum.php');
48 mkdir($this->root . '/vendor/composer/vendor', 0775, true);
49 $real_autoload = realpath(__DIR__ . '/../../../../vendor/composer/vendor/autoload.php');
50 $this->assertIsString($real_autoload);
51 symlink($real_autoload, $this->root . '/vendor/composer/vendor/autoload.php');
52 }
53
54 protected function tearDown(): void
55 {
56 MigratedPoFixture::removeDirectory($this->directory);
57 }
58
60 private function runTool(string ...$arguments): array
61 {
62 $result = ChildPhp::run(
63 [
64 '-d', 'error_reporting=-1', '-d', 'display_errors=stderr',
65 $this->root . '/components/ILIAS/Language/tools/language-identifier-enum/generate_language_identifier_enum.php',
66 ...$arguments,
67 ],
68 $this->root
69 );
70 $this->assertNotNull($result, 'could not start the child PHP process');
71
72 return $result;
73 }
74
76 private function writePot(array $messages, string $header = "\"X-Domain: demo\\n\"\n", string $path = self::POT): void
77 {
78 $pot = "msgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n" . $header . "\n";
79 foreach ($messages as $message) {
80 if (str_starts_with($message, 'plural:')) {
81 $id = substr($message, 7);
82 $pot .= "msgid \"$id\"\nmsgid_plural \"{$id}s\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n";
83 } elseif (str_starts_with($message, 'ctx:')) {
84 [, $context, $id] = explode(':', $message, 3);
85 $pot .= "msgctxt \"$context\"\nmsgid \"$id\"\nmsgstr \"\"\n\n";
86 } else {
87 $pot .= "msgid \"$message\"\nmsgstr \"\"\n\n";
88 }
89 }
90 file_put_contents($this->root . '/' . $path, $pot);
91 }
92
94 private function generate(string ...$options): array
95 {
96 return $this->runTool(...$options, ...['--namespace=' . self::NAMESPACE, self::POT, self::ENUM]);
97 }
98
100 private function loadEnum(string $file = self::ENUM, string $class = self::NAMESPACE . '\DemoIdentifier'): array
101 {
102 $code = sprintf(
103 'require_once %s; require %s; $m = []; foreach (%s::cases() as $c) { $m[$c->name] = $c->value; } '
104 . 'echo json_encode(["module" => %s::cases() === [] ? "" : %s::cases()[0]->module(), "cases" => $m, "is_key" => is_a(%s::class, "ILIAS\\\\Language\\\\LanguageIdentifier", true)]);',
105 var_export((string) realpath(__DIR__ . '/../../../../vendor/composer/vendor/autoload.php'), true),
106 var_export($this->root . '/' . $file, true),
107 $class,
108 $class,
109 $class,
110 $class
111 );
112 file_put_contents($this->directory . '/load.php', '<?php ' . $code);
113 $result = ChildPhp::run(
114 ['-d', 'error_reporting=-1', '-d', 'display_errors=stderr', $this->directory . '/load.php']
115 );
116 $this->assertNotNull($result, 'could not start the child PHP process');
117 [$exit, $stdout, $stderr] = $result;
118 $this->assertSame(0, $exit, $stderr);
119 $this->assertSame('', $stderr, 'no warning or deprecation');
120 $decoded = json_decode($stdout, true, 512, JSON_THROW_ON_ERROR);
121 $this->assertTrue($decoded['is_key']);
122
123 return ['module' => $decoded['module'], 'cases' => $decoded['cases']];
124 }
125
127 {
128 $this->writePot([
129 'poll_answers', 'a-b.c', '1st', 'class', 'Class', 'style', 'Style', 'STYLE', 'a b', 'a-b',
130 'plural:item', 'ctx:demo:with_context', 'Ünï', 'x_2', 'x', 'X',
131 ]);
132
133 [$exit, $stdout, $stderr] = $this->generate();
134
135 $this->assertSame(0, $exit, $stderr);
136 $this->assertStringContainsString('Written:', $stdout);
137 $this->assertStringContainsString('NOTE: Case name STYLE: "style" -> STYLE, "STYLE" -> STYLE_2, "Style" -> STYLE_3', $stderr);
138 $this->assertStringContainsString('NOTE: Case name KEY_CLASS: "class" -> KEY_CLASS, "Class" -> KEY_CLASS_2', $stderr);
139 $loaded = $this->loadEnum();
140 $this->assertSame('demo', $loaded['module']);
141 $this->assertEquals([
142 'POLL_ANSWERS' => 'poll_answers',
143 'A_B_C' => 'a-b.c',
144 'KEY_1ST' => '1st',
145 'KEY_CLASS' => 'class',
146 'KEY_CLASS_2' => 'Class',
147 'STYLE' => 'style',
148 'STYLE_2' => 'STYLE',
149 'STYLE_3' => 'Style',
150 'A_B' => 'a b',
151 'A_B_2' => 'a-b',
152 'ITEM' => 'item',
153 'WITH_CONTEXT' => 'with_context',
154 '_N_' => 'Ünï',
155 'X_2' => 'x_2',
156 'X' => 'x',
157 'X_3' => 'X',
158 ], $loaded['cases']);
159 }
160
162 {
163 $identifiers = ['one', 'Two', 'three_3', '4four', 'class', 'CLASS', 'a-a', 'a_a', 'A.A', 'plural_message'];
164 $this->writePot(array_map(static fn(string $i): string => $i === 'plural_message' ? 'plural:plural_message' : $i, $identifiers));
165
166 [$exit, , $stderr] = $this->generate();
167
168 $this->assertSame(0, $exit, $stderr);
169 $cases = $this->loadEnum()['cases'];
170 $this->assertCount(count($identifiers), $cases);
171 $values = array_values($cases);
172 sort($values);
173 $expected = $identifiers;
174 sort($expected);
175 $this->assertSame($expected, $values, 'no identifier lost, none doubled, the msgid_plural is no case');
176 foreach (array_keys($cases) as $name) {
177 $this->assertMatchesRegularExpression('/^[A-Z_][A-Z0-9_]*$/', $name);
178 }
179 }
180
182 {
183 $this->writePot(['one', 'two']);
184 [, $stdout] = $this->generate('--stdout');
185 $this->assertFileDoesNotExist($this->root . '/' . self::ENUM, '--stdout writes nothing');
186
187 $this->assertSame(0, $this->generate()[0]);
188 $this->assertSame($stdout, (string) file_get_contents($this->root . '/' . self::ENUM));
189 $before = fileperms($this->root . '/' . self::ENUM);
190 [$exit, $again] = $this->generate();
191
192 $this->assertSame(0, $exit);
193 $this->assertStringContainsString('Unchanged:', $again);
194 $this->assertSame($before, fileperms($this->root . '/' . self::ENUM));
195 $this->assertSame(['.', '..', 'DemoIdentifier.php'], scandir($this->root . '/components/ILIAS/Demo/src'), 'no temporary file left');
196 }
197
199 public static function modules(): iterable
200 {
201 yield 'option wins' => ['--module=from_option', "\"X-Domain: from_header\\n\"\n", 'from_option'];
202 yield 'X-Domain header' => [null, "\"X-Domain: from_header\\n\"\n", 'from_header'];
203 yield 'file name' => [null, '', 'demo'];
204 }
205
206 #[DataProvider('modules')]
207 public function testModuleComesFromTheOptionTheHeaderOrTheFileName(?string $option, string $header, string $expected): void
208 {
209 $this->writePot(['one'], $header);
210
211 [$exit, , $stderr] = $this->runTool(...array_filter([$option, '--namespace=' . self::NAMESPACE, self::POT, self::ENUM]));
212
213 $this->assertSame(0, $exit, $stderr);
214 $this->assertSame($expected, $this->loadEnum()['module']);
215 }
216
218 {
219 $this->writePot([]);
220
221 [$exit, , $stderr] = $this->generate();
222
223 $this->assertSame(0, $exit, $stderr);
224 $this->assertSame([], $this->loadEnum()['cases']);
225 $this->assertSame(0, $this->generate('--check')[0]);
226 }
227
229 {
230 $this->writePot(['one']);
231
232 [$exit, $stdout] = $this->runTool('--stdout', '--namespace=ILIAS\Language', self::POT, 'components/ILIAS/Demo/src/DemoIdentifier.php');
233
234 $this->assertSame(0, $exit);
235 $this->assertStringContainsString("namespace ILIAS\\Language;\n", $stdout);
236 $this->assertStringNotContainsString('use ILIAS\\Language\\LanguageIdentifier;', $stdout, 'no import of its own namespace');
237 $this->assertStringContainsString('enum DemoIdentifier: string implements LanguageIdentifier', $stdout);
238 }
239
240 public function testCheckPassesOnWhatWasGenerated(): void
241 {
242 $this->writePot(['one', 'plural:two', 'Style', 'style']);
243 $this->generate();
244
245 [$exit, $stdout, $stderr] = $this->generate('--check');
246
247 $this->assertSame(0, $exit, $stderr);
248 $this->assertStringContainsString('OK:', $stdout);
249 }
250
252 public static function deviations(): iterable
253 {
254 yield 'surplus case' => [static fn(string $code): string => str_replace(" public function module", " case EXTRA = 'extra';\n\n public function module", $code), 'Surplus case EXTRA'];
255 yield 'missing case' => [static fn(string $code): string => str_replace(" case TWO = 'two';\n", '', $code), 'Missing case TWO'];
256 yield 'wrong value' => [static fn(string $code): string => str_replace("case TWO = 'two'", "case TWO = 'zwei'", $code), 'Case TWO has the value'];
257 yield 'wrong module' => [static fn(string $code): string => str_replace("return 'demo';", "return 'other';", $code), 'module() returns'];
258 yield 'not a LanguageIdentifier' => [static fn(string $code): string => str_replace(' implements LanguageIdentifier', '', $code), 'does not implement'];
259 yield 'not string backed' => [static fn(string $code): string => (string) preg_replace("/case (\w+) = '[^']*';/", 'case $1 = 1;', str_replace('enum DemoIdentifier: string', 'enum DemoIdentifier: int', $code)), 'not backed by string'];
260 }
261
263 #[DataProvider('deviations')]
264 public function testCheckReportsADeviationWithExitCodeOne(callable $break, string $message): void
265 {
266 $this->writePot(['one', 'two']);
267 $this->generate();
268 $file = $this->root . '/' . self::ENUM;
269 $broken = $break((string) file_get_contents($file));
270 $this->assertNotSame(file_get_contents($file), $broken, 'the fixture really breaks the enum');
271 file_put_contents($file, $broken);
272
273 [$exit, , $stderr] = $this->generate('--check');
274
275 $this->assertSame(1, $exit, $stderr);
276 $this->assertStringContainsString('DEVIATION: ', $stderr);
277 $this->assertStringContainsString($message, $stderr);
278 }
279
280 public function testCheckNoticesAChangedPot(): void
281 {
282 $this->writePot(['one', 'two']);
283 $this->generate();
284 $this->writePot(['one', 'two', 'three']);
285
286 [$exit, , $stderr] = $this->generate('--check');
287
288 $this->assertSame(1, $exit);
289 $this->assertStringContainsString('Missing case THREE', $stderr);
290 }
291
293 {
294 $this->writePot(['one']);
295
296 [$exit, , $stderr] = $this->generate('--check');
297
298 $this->assertSame(2, $exit);
299 $this->assertStringContainsString('does not exist', $stderr);
300 $this->assertFileDoesNotExist($this->root . '/' . self::ENUM);
301 }
302
304 {
305 $this->writePot(['one']);
306 file_put_contents($this->root . '/' . self::ENUM, "<?php\nnamespace Other;\nenum DemoIdentifier: string { case ONE = 'one'; }\n");
307
308 [$exit, , $stderr] = $this->generate('--check');
309
310 $this->assertSame(2, $exit);
311 $this->assertStringContainsString('does not declare the enum', $stderr);
312 }
313
315 public static function invalidInput(): iterable
316 {
317 yield 'no namespace' => [['POT', 'ENUM'], 'Usage:'];
318 yield 'one path missing' => [['NS', 'POT'], 'Usage:'];
319 yield 'three paths' => [['NS', 'POT', 'ENUM', 'ENUM'], 'Usage:'];
320 yield 'unknown option' => [['NS', '--force', 'POT', 'ENUM'], 'Unknown option'];
321 yield 'check and stdout' => [['NS', '--check', '--stdout', 'POT', 'ENUM'], 'exclude each other'];
322 yield 'leading backslash' => [['--namespace=\ILIAS\Demo', 'POT', 'ENUM'], 'Invalid namespace'];
323 yield 'empty namespace' => [['--namespace=', 'POT', 'ENUM'], 'Invalid namespace'];
324 yield 'digit namespace' => [['--namespace=ILIAS\1Demo', 'POT', 'ENUM'], 'Invalid namespace'];
325 yield 'namespace with space' => [['--namespace=ILIAS Demo', 'POT', 'ENUM'], 'Invalid namespace'];
326 yield 'namespace with code' => [['--namespace=A;eval($x)', 'POT', 'ENUM'], 'Invalid namespace'];
327 yield 'double backslash' => [['--namespace=ILIAS\\\\Demo', 'POT', 'ENUM'], 'Invalid namespace'];
328 yield 'module with path' => [['NS', '--module=../x', 'POT', 'ENUM'], 'Invalid module'];
329 yield 'module with space' => [['NS', '--module=a b', 'POT', 'ENUM'], 'Invalid module'];
330 yield 'empty module' => [['NS', '--module=', 'POT', 'ENUM'], 'Invalid module'];
331 yield 'enum name with dash' => [['NS', 'POT', 'components/ILIAS/Demo/src/a-b.php'], 'no valid enum name'];
332 yield 'enum name with digit first' => [['NS', 'POT', 'components/ILIAS/Demo/src/1Key.php'], 'no valid enum name'];
333 yield 'enum no php' => [['NS', 'POT', 'components/ILIAS/Demo/src/DemoIdentifier.txt'], 'no .php file'];
334 yield 'enum directory missing' => [['NS', 'POT', 'components/ILIAS/Demo/nowhere/DemoIdentifier.php'], 'does not exist'];
335 yield 'pot missing' => [['NS', 'components/ILIAS/Demo/lang/none.pot', 'ENUM'], 'no .pot file'];
336 yield 'pot is no pot' => [['NS', 'components/ILIAS/Demo/lang/demo.txt', 'ENUM'], 'no .pot file'];
337 yield 'pot outside by ..' => [['NS', 'components/../../outside/x.pot', 'ENUM'], 'no .pot file'];
338 yield 'enum outside by ..' => [['NS', 'POT', 'components/../../outside/DemoIdentifier.php'], 'no .php file path'];
339 yield 'enum with .. inside missing part' => [['NS', 'POT', 'components/ILIAS/Demo/new/../src/DemoIdentifier.php'], 'no .php file path'];
340 yield 'enum absolute outside' => [['NS', 'POT', '/tmp/DemoIdentifier.php'], 'no .php file path'];
341 yield 'enum is the root' => [['NS', 'POT', '.php'], 'is no valid enum name'];
342 yield 'pot through a symlink out' => [['NS', 'linked/outside.pot', 'ENUM'], 'no .pot file'];
343 yield 'enum through a symlink out' => [['NS', 'POT', 'linked/DemoIdentifier.php'], 'no .php file path'];
344 yield 'enum is a dangling symlink out' => [['NS', 'POT', 'components/ILIAS/Demo/src/Dangling.php'], 'Refusing|no .php file path'];
345 }
346
348 #[DataProvider('invalidInput')]
349 public function testInvalidInputIsRefusedWithExitCodeTwoAndNothingIsWritten(array $arguments, string $stderr_pattern): void
350 {
351 $this->writePot(['one']);
352 file_put_contents($this->directory . '/outside/outside.pot', "msgid \"\"\nmsgstr \"\"\n\nmsgid \"x\"\nmsgstr \"\"\n");
353 symlink($this->directory . '/outside', $this->root . '/linked');
354 symlink($this->directory . '/outside/target.php', $this->root . '/components/ILIAS/Demo/src/Dangling.php');
355 $arguments = array_map(static fn(string $a): string => match ($a) {
356 'POT' => self::POT,
357 'ENUM' => self::ENUM,
358 'NS' => '--namespace=' . self::NAMESPACE,
359 default => $a,
360 }, $arguments);
361
362 [$exit, $stdout, $stderr] = $this->runTool(...$arguments);
363
364 $this->assertSame(2, $exit, $stdout . $stderr);
365 $this->assertMatchesRegularExpression('/ERROR: .*(' . $stderr_pattern . ')/', $stderr);
366 $this->assertSame([], glob($this->directory . '/outside/*.php') ?: []);
367 $this->assertSame(
368 [$this->root . '/components/ILIAS/Demo/src/Dangling.php'],
369 glob($this->root . '/components/ILIAS/Demo/src/*') ?: [],
370 'no enum written'
371 );
372 }
373
374 public function testAnUnsupportedMsgctxtIsRefused(): void
375 {
376 $this->writePot(['one', 'ctx:other:two']);
377
378 [$exit, , $stderr] = $this->generate();
379
380 $this->assertSame(2, $exit);
381 $this->assertStringContainsString('msgctxt "other"', $stderr);
382 $this->assertFileDoesNotExist($this->root . '/' . self::ENUM);
383 }
384
386 {
387 $this->writePot(['one'], "\"X-Domain: ../../x\\n\"\n");
388
389 [$exit, , $stderr] = $this->generate();
390
391 $this->assertSame(2, $exit);
392 $this->assertStringContainsString('Invalid module', $stderr);
393 }
394
396 {
397 $this->writePot(['one']);
398
399 [$exit, , $stderr] = $this->runTool("--namespace=A\x1b[31mB", self::POT, self::ENUM);
400
401 $this->assertSame(2, $exit);
402 $this->assertStringNotContainsString("\x1b", $stderr);
403 $this->assertStringContainsString('\x1B', $stderr);
404 }
405
407 public static function unsafePotPaths(): iterable
408 {
409 yield 'asterisk' => ['a*b/demo.pot'];
410 yield 'space' => ['a b/demo.pot'];
411 yield 'comment end' => ['x*/y/demo.pot'];
412 yield 'code in directory names' => ["x*/system('echo PWNED');/*/demo.pot"];
413 yield 'quote' => ["it's/demo.pot"];
414 }
415
416 #[DataProvider('unsafePotPaths')]
417 public function testAPotPathWithOtherThanPlainPathCharactersIsRefused(string $relative): void
418 {
419 $path = 'components/ILIAS/Demo/lang/' . $relative;
420 mkdir(dirname($this->root . '/' . $path), 0775, true);
421 $this->writePot(['one'], "\"X-Domain: demo\\n\"\n", $path);
422
423 [$exit, $stdout, $stderr] = $this->runTool('--namespace=' . self::NAMESPACE, $path, self::ENUM);
424
425 $this->assertSame(2, $exit, $stdout . $stderr);
426 $this->assertStringContainsString('may only contain', $stderr);
427 $this->assertStringNotContainsString("PWNED\n", $stdout, 'nothing is executed');
428 $this->assertFileDoesNotExist($this->root . '/' . self::ENUM);
429 [$exit_stdout, $out] = $this->runTool('--stdout', '--namespace=' . self::NAMESPACE, $path, self::ENUM);
430 $this->assertSame(2, $exit_stdout);
431 $this->assertSame('', $out, '--stdout prints nothing either');
432 }
433
435 {
436 $this->writePot(['a*/b', '*/', "/* x", 'it\'s', 'ok']);
437
438 [$exit, , $stderr] = $this->generate();
439
440 $this->assertSame(0, $exit, $stderr);
441 $file = $this->root . '/' . self::ENUM;
442 $lint = ChildPhp::run(['-l', $file]);
443 $this->assertNotNull($lint, 'could not start the child PHP process');
444 $this->assertSame(0, $lint[0], $lint[1] . $lint[2]);
445 $this->assertEqualsCanonicalizing(['a*/b', '*/', '/* x', "it's", 'ok'], array_values($this->loadEnum()['cases']));
446 [$check_exit, , $check_stderr] = $this->generate('--check');
447 $this->assertSame(0, $check_exit, $check_stderr);
448 }
449
450 public function testReservedCaseNamesGetThePrefix(): void
451 {
452 $this->writePot(['__halt_compiler', 'class', 'CLASS', 'Class']);
453
454 [$exit, , $stderr] = $this->generate();
455
456 $this->assertSame(0, $exit, $stderr);
457 $cases = $this->loadEnum()['cases'];
458 $this->assertSame('__halt_compiler', $cases['KEY___HALT_COMPILER'] ?? null);
459 $this->assertSame('class', $cases['KEY_CLASS'] ?? null);
460 $this->assertEqualsCanonicalizing(['KEY___HALT_COMPILER', 'KEY_CLASS', 'KEY_CLASS_2', 'KEY_CLASS_3'], array_keys($cases));
461 }
462
467 public static function modulesOfAnEnumWithoutCases(): iterable
468 {
469 yield 'the module' => ["return 'demo';", 0, 'OK:'];
470 yield 'a comment in between' => ["/* c */ return 'demo'; // x", 0, 'OK:'];
471 yield 'another module' => ["return 'other';", 1, 'module() returns'];
472 yield 'concatenation' => ["return 'de' . 'mo';", 1, 'does not just return'];
473 yield 'call' => ["return strtolower('DEMO');", 1, 'does not just return'];
474 yield 'double quoted' => ['return "demo";', 1, 'does not just return'];
475 yield 'constant' => ['return self::class;', 1, 'does not just return'];
476 yield 'more than the return' => ["echo 'x'; return 'demo';", 1, 'does not just return'];
477 yield 'second statement' => ["return 'demo'; echo 'x';", 1, 'does not just return'];
478 }
479
480 #[DataProvider('modulesOfAnEnumWithoutCases')]
481 public function testCheckReadsTheModuleOfAnEnumWithoutCasesFromItsBody(string $body, int $expected_exit, string $message): void
482 {
483 $this->writePot([]);
484 $this->generate();
485 $file = $this->root . '/' . self::ENUM;
486 $code = (string) file_get_contents($file);
487 $changed = str_replace("return 'demo';", $body, $code);
488 $this->assertNotSame('', $changed);
489 file_put_contents($file, $changed);
490
491 [$exit, $stdout, $stderr] = $this->generate('--check');
492
493 $this->assertSame($expected_exit, $exit, $stdout . $stderr);
494 $this->assertStringContainsString($message, $stdout . $stderr);
495 }
496
498 {
499 $this->writePot([]);
500 $this->generate();
501 $file = $this->root . '/' . self::ENUM;
502 file_put_contents($file, str_replace("return 'demo';", "echo 'PWNED'; return 'demo';", (string) file_get_contents($file)));
503
504 [$exit, $stdout] = $this->generate('--check');
505
506 $this->assertSame(1, $exit);
507 $this->assertStringNotContainsString('PWNED', $stdout);
508 }
509
511 {
512 $this->writePot(["a\x1b[31mb"]);
513
514 [$exit, $stdout, $stderr] = $this->generate();
515
516 $this->assertSame(2, $exit);
517 $this->assertStringNotContainsString("\x1b", $stdout . $stderr);
518 $this->assertFileDoesNotExist($this->root . '/' . self::ENUM);
519 }
520}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$out
Definition: buildRTE.php:24
static run(array $arguments, ?string $working_directory=null)
Definition: ChildPhp.php:60
tools/language-identifier-enum/generate_language_identifier_enum.php as a process,...
testInvalidInputIsRefusedWithExitCodeTwoAndNothingIsWritten(array $arguments, string $stderr_pattern)
testAPotPathWithOtherThanPlainPathCharactersIsRefused(string $relative)
testCheckReportsADeviationWithExitCodeOne(callable $break, string $message)
loadEnum(string $file=self::ENUM, string $class=self::NAMESPACE . '\DemoIdentifier')
testModuleComesFromTheOptionTheHeaderOrTheFileName(?string $option, string $header, string $expected)
testCheckReadsTheModuleOfAnEnumWithoutCasesFromItsBody(string $body, int $expected_exit, string $message)
writePot(array $messages, string $header="\"X-Domain: demo\\n\"\n", string $path=self::POT)
static removeDirectory(string $directory)
$body
Definition: ltiauth.php:41
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples