19declare(strict_types=1);
27use PHPUnit\Framework\TestCase;
40 protected function setUp(): void
44 $this->root = sys_get_temp_dir() .
'/ilias_shipped_build_' . bin2hex(random_bytes(6));
45 mkdir($this->root, 0775,
true);
58 $directory = $this->root .
'/' . trim($relative_path,
'/');
73 $io = $this->createStub(AdminInteraction::class);
74 $io->method(
'inform')->willReturnCallback(
function (
string $message) use (&$messages):
void {
84 $environment = $this->createStub(Environment::class);
85 $environment->method(
'getResource')->willReturn($io);
86 $objective->achieve($environment);
94 $artifact_directory = MigratedLanguageFilePaths::shippedArtifactDirectory($this->root);
95 $build = ShippedTranslationsBuild::readPointer($artifact_directory)[
'build'] ??
null;
96 if ($build ===
null) {
97 return $artifact_directory .
'/no-build.mo';
100 return MigratedLanguageFilePaths::buildMoFile(MigratedLanguageFilePaths::buildDirectory($artifact_directory, $build),
$module, $lang_key);
105 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
110 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
112 [
'greeting' =>
'Hallo'],
122 $directory = new \ILIAS\Language\ComponentTranslation\MainLanguageFileDirectory();
123 mkdir($this->root .
'/lang', 0775,
true);
129 $this->assertStringContainsString(
'no migrated module, 0 compiled', $messages[array_key_last($messages)]);
137 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
141 $first_mtime = filemtime($this->
artifactPath(
'de',
'itest'));
147 $this->assertStringContainsString(
'unchanged, 0 compiled', $messages[array_key_last($messages)]);
148 $this->assertSame($first_mtime, filemtime($this->
artifactPath(
'de',
'itest')));
157 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
158 $this->
writeShippedPo(
'utest/',
'utest',
'de', [
'greeting' =>
'Servus']);
164 $previous = dirname($this->
artifactPath(
'de',
'utest'), 3);
167 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo geändert']);
172 $this->assertStringContainsString(
'2 compiled', $messages[array_key_last($messages)]);
173 $this->assertNotSame($previous, dirname($this->
artifactPath(
'de',
'utest'), 3));
174 $this->assertDirectoryExists($previous,
'the previous build is kept');
176 [
'greeting' =>
'Hallo geändert'],
186 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
190 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
192 unlink($this->root .
'/itest/itest_de.po');
197 $this->assertFileDoesNotExist($this->
artifactPath(
'de',
'itest'));
198 $this->assertStringContainsString(
'no migrated module, 0 compiled', $messages[array_key_last($messages)]);
206 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'<script>alert(1)</script>Hallo']);
213 $warning = implode(
"\n", $messages);
214 $this->assertStringContainsString(
'WARNING: Markup not allowed in a language value', $warning);
215 $this->assertStringContainsString(
'itest', $warning);
216 $this->assertStringContainsString(
'greeting', $warning);
217 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
219 $this->assertStringNotContainsString(
'<script', $translations[
'greeting']);
224 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'<script>x</script>Hallo']);
233 $output = implode(
"\n", $messages);
234 $this->assertStringContainsString(
'WARNING: 1 markup warning(s), 0 key collision(s), see ', $output);
235 $this->assertStringNotContainsString(
'WARNING: Markup not allowed', $output);
240 $this->
writeShippedPo(
'ita/',
'ita',
'de', [
'one' =>
'A1',
'two' =>
'A2']);
241 $this->
writeShippedPo(
'itb/',
'itb',
'de', [
'one' =>
'B1',
'two' =>
'B2']);
252 $grouped = array_values(array_filter($first,
static fn(
string $m):
bool => str_contains($m,
'different values')));
253 $this->assertCount(1,
$grouped,
'two keys of the same modules: one line');
254 $this->assertStringContainsString(
'WARNING: 2 key(s) with different values in the modules ita, itb',
$grouped[0]);
255 $this->assertStringEndsWith(
': one, two',
$grouped[0]);
257 $this->assertSame([], array_filter($second,
static fn(
string $m):
bool => str_contains($m,
'different values')));
258 $counted = array_values(array_filter($second,
static fn(
string $m):
bool => str_contains($m,
'key collision(s)')));
259 $this->assertCount(1, $counted);
260 $this->assertMatchesRegularExpression(
'~^WARNING: 0 markup warning\(s\), 2 key collision\(s\), see \S+/manifest\.json\.$~', $counted[0]);
268 $this->
writeShippedPo(
'broken/',
'broken',
'de', [
'greeting' =>
'Funktioniert']);
269 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
275 file_put_contents($this->root .
'/broken/broken_de.po',
"msgid \"kaputt\n");
276 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo, neu']);
282 $warning = implode(
"\n", $messages);
283 $this->assertStringContainsString(
'WARNING: Could not compile', $warning);
284 $this->assertStringContainsString(
'broken', $warning);
287 $this->assertStringContainsString(
', 1 compiled', $messages[array_key_last($messages)]);
293 mkdir($this->root .
'/broken', 0775,
true);
294 file_put_contents($this->root .
'/broken/broken_de.po',
"msgid \"kaputt\n");
295 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
305 $warning = implode(
"\n", $messages);
306 $this->assertStringContainsString(
'WARNING: Could not compile', $warning);
307 $this->assertStringContainsString(
'"broken" is not served for "de"', $warning);
308 $this->assertFileDoesNotExist($this->
artifactPath(
'de',
'broken'));
318 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
320 $outside = $this->root .
'/outside-de';
321 mkdir($outside, 0775,
true);
322 mkdir(MigratedLanguageFilePaths::shippedArtifactDirectory($this->root), 0775,
true);
323 symlink($outside, MigratedLanguageFilePaths::shippedArtifactDirectory($this->root) .
'/de');
329 $this->assertSame([], array_values(array_diff(scandir($outside) ?: [], [
'.',
'..'])));
340 $this->assertNotSame($objective_one->getHash(), $objective_two->getHash());
349 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
357 $full = (string) file_get_contents($this->
artifactPath(
'de',
'itest'));
358 file_put_contents($this->
artifactPath(
'de',
'itest'), substr($full, 0, 10));
364 $this->assertStringContainsString(
', 1 compiled', $messages[array_key_last($messages)]);
366 [
'greeting' =>
'Hallo'],
376 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
382 $original = (string) file_get_contents($this->
artifactPath(
'de',
'itest'));
383 $tampered = substr($original, 0, -1) . ($original[-1] ===
"\x00" ?
"\x01" :
"\x00");
384 file_put_contents($this->
artifactPath(
'de',
'itest'), $tampered);
385 $this->assertSame(strlen($original), strlen($tampered),
'precondition: same size, different content');
391 $this->assertStringContainsString(
', 1 compiled', $messages[array_key_last($messages)]);
399 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
404 file_put_contents(dirname($this->
artifactPath(
'de',
'itest'), 3) .
'/manifest.json',
'{not valid json');
410 $this->assertStringContainsString(
', 1 compiled', $messages[array_key_last($messages)]);
418 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
420 mkdir(MigratedLanguageFilePaths::shippedArtifactDirectory($this->root), 0775,
true);
421 file_put_contents(MigratedLanguageFilePaths::shippedArtifactDirectory($this->root) .
'/index.json',
'{not valid json');
427 $this->assertStringContainsString(
', 1 compiled', $messages[array_key_last($messages)]);
428 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
429 $this->assertFileDoesNotExist(MigratedLanguageFilePaths::shippedArtifactDirectory($this->root) .
'/index.json');
437 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
441 $artifact_inode_before = fileinode($this->
artifactPath(
'de',
'itest'));
444 touch($this->
artifactPath(
'de',
'itest'), time() - 1000);
450 $this->assertStringContainsString(
'unchanged, 0 compiled', $messages[array_key_last($messages)]);
451 clearstatcache(
true, $this->
artifactPath(
'de',
'itest'));
452 $this->assertSame($artifact_inode_before, fileinode($this->
artifactPath(
'de',
'itest')),
'the artifact is not rewritten (same inode)');
460 if (posix_getuid() === 0) {
461 $this->markTestSkipped(
'Cannot make a file unreadable while running as root; skipping.');
463 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
464 $this->
writeShippedPo(
'unreadable/',
'unreadable',
'de', [
'greeting' =>
'Lesbar']);
470 chmod($this->root .
'/unreadable/unreadable_de.po', 0000);
477 chmod($this->root .
'/unreadable/unreadable_de.po', 0664);
480 $warning = implode(
"\n", $messages);
481 $this->assertStringContainsString(
'WARNING: Could not compile', $warning);
482 $this->assertStringContainsString(
'unreadable', $warning);
484 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
493 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
495 $target_directory = MigratedLanguageFilePaths::shippedArtifactDirectory($this->root);
496 mkdir($target_directory .
'/de', 0775,
true);
497 file_put_contents($target_directory .
'/.index.json.ab12cd',
'leftover');
498 file_put_contents($target_directory .
'/de/.itest.mo.ab12cd',
'leftover');
502 $this->assertFileDoesNotExist($target_directory .
'/.index.json.ab12cd');
503 $this->assertFileDoesNotExist($target_directory .
'/de/.itest.mo.ab12cd');
512 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
514 $target_directory = MigratedLanguageFilePaths::shippedArtifactDirectory($this->root);
515 mkdir($target_directory, 0775,
true);
516 $outside = $this->root .
'/outside-language-dir';
517 mkdir($outside, 0775,
true);
518 file_put_contents($outside .
'/obsolete.mo',
'not tracked by the index at all');
519 symlink($outside, $target_directory .
'/xx');
523 $this->assertFileExists($outside .
'/obsolete.mo');
532 $catalog = new \ILIAS\Language\ComponentTranslation\Catalog\TranslationCatalog();
534 $catalog->add($entry);
542 $warning = implode(
"\n", $messages);
543 $this->assertStringNotContainsString(
"\x1b", $warning);
544 $this->assertStringContainsString(
'bad?key', $warning);
549 return new class ($prefix, $path,
$pattern) implements
550 \
ILIAS\Language\ComponentTranslation\LanguageFileDirectory,
551 \
ILIAS\Language\ComponentTranslation\NamesShippedLanguageFiles {
553 private string $prefix,
554 private string $path,
559 public function getPrefix():
string
561 return $this->prefix;
564 public function getPath():
string
569 public function getSuffix():
string
574 public function isLocal():
bool
579 public function getShippedFileNamePattern():
string
591 mkdir($this->root .
'/itest', 0775,
true);
593 $this->root .
'/itest/ilias_de.po',
600 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
602 [
'greeting' =>
'Hallo'],
613 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
620 $warning = implode(
"\n", $messages);
621 $this->assertStringContainsString(
'WARNING', $warning);
622 $this->assertStringContainsString(
'badmod', $warning);
623 $this->assertFileExists($this->
artifactPath(
'de',
'itest'));
629 if (!defined(
'ILIAS_VERSION_NUMERIC')) {
630 define(
'ILIAS_VERSION_NUMERIC',
'12.0');
637 $this->assertTrue($objective->isNotable());
638 $this->assertTrue($objective->isApplicable($this->createStub(Environment::class)));
639 $preconditions = $objective->getPreconditions($this->createStub(Environment::class));
640 $this->assertCount(1, $preconditions);
641 $this->assertInstanceOf(\
ILIAS\Setup\Condition\PHPExtensionLoadedCondition::class, $preconditions[0]);
642 $this->assertStringContainsString(
'gettext', $preconditions[0]->
getLabel());
647 $this->assertNull((
new \
ILIAS\Language\ComponentTranslation\TranslationGettextRepository())->getKeys(
'itest',
'de',
null),
'reads (and remembers) that there is no build');
648 $this->
writeShippedPo(
'itest/',
'itest',
'de', [
'greeting' =>
'Hallo']);
652 $this->assertSame([
'greeting'], array_keys((array) (
new \
ILIAS\Language\ComponentTranslation\TranslationGettextRepository())->getKeys(
'itest',
'de',
null)));
trait RestoresProcessState
Whatever a test does to the process (locale, LANGUAGE/LC_*, LOCPATH, the static state of the runtime ...
The single place that knows where the files of a module migrated to PO/MO live:
Builds what native gettext serves for the migrated modules (see TranslationGettextRepository) from ev...
Build step (php cli/setup.php build) for the migrated modules: builds what native gettext serves from...
static directory(string $prefix, string $path, bool $local=false)
static catalog(string $module, array $entries)
static readMo(string $file)
static writePo(string $file, TranslationCatalog $catalog)
static removeDirectory(string $directory)
static moveArtifactsToFreshDirectory()
Moves the builds to a directory this process has not looked at: the C library keeps what it read from...
static entry(?string $context, string $id, string $value)
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
ShippedLanguageFilesCompiledObjective builds every shipped .po for native gettext (see ShippedTransla...
writeShippedPo(string $relative_path, string $module, string $lang_key, array $entries)
testAnIndexJsonOfTheFormerLayoutIsRemoved()
The index.json of the former layout (even a broken one) does not abort the build; it is removed as ob...
testAnInvalidShippedFileNamePatternWarnsAndDoesNotAbortTheBuild()
An invalid shipped file name pattern is a warning that skips only that module.
testFindsAndCompilesAShippedPoNamedByTheIliasSchema()
A module using the "ilias_%s" schema has its shipped .po found and compiled like any other.
testAchieveMakesTheNewBuildVisibleToTheRuntimeOfTheSameProcess()
testSkipsADirectoryWithoutAPrefix()
The main lang/ directory (empty prefix) holds no .po and is skipped without error.
achieve(LanguageFileDirectoryManager $manager, ?AdminInteraction $io=null)
directoryManager(\ILIAS\Language\ComponentTranslation\LanguageFileDirectory ... $directories)
testATouchedButOtherwiseIdenticalPoKeepsTheBuild()
Identical content but a newer .po (e.g.
testDoesNotDeleteAnObsoleteArtifactReachedOnlyThroughASymlinkedLanguageDirectory()
removeObsoleteArtifacts() must not delete through a symlinked language directory pointing outside the...
directoryWithPattern(string $prefix, string $path, string $pattern)
testAChangedPoGivesANewBuildAndKeepsThePreviousOne()
A changed .po gives a new build (every .po compiled into a new directory, since a running process nev...
testAnUnreadablePoIsReportedAndServedFromTheFormerBuild()
A .po that cannot even be read is treated like a broken one.
testRemovesLeftoverTemporaryFilesFromAnAbortedPreviousBuild()
Leftover temporary files of AtomicFileWriter (".<name>.<random>") from a crashed build are cleaned up...
testGetHashChangesWhenAContributedDirectoryChanges()
testCountsTheMarkupWarningOnASubsequentUnchangedRun()
testATruncatedArtifactIsRebuiltEvenThoughItsShippedPoDidNotChange()
The index also records size and hash of the artifact itself: a truncated or corrupted artifact (e....
artifactPath(string $lang_key, string $module)
The catalog of $module/$lang_key in the build current.json names; a nonexistent path without one.
testABrokenPoIsReportedAndServedFromTheFormerBuild()
A broken .po is reported and served from the former build; the fallback is not counted as compiled.
testControlCharactersInABuildLogMessageAreReplaced()
The build log must not pass raw control characters (ANSI escape, bidi override in an identifier) to t...
testGroupsCollisionsOfAWrittenBuildAndCountsThemOnAnUnchangedRun()
testAnArtifactWithTheSameSizeButDifferentContentIsRebuilt()
A silently changed artifact (same size, e.g.
testRemovesAnArtifactWhoseShippedPoNoLongerExists()
A .po that no longer exists has its artifact removed on the next build.
testWarnsAboutDisallowedMarkupButStillCompilesTheCleanedArtifact()
Disallowed markup is compiled cleaned and warned about; the build does not abort.
testCompilesAShippedPoIntoAnArtifact()
testABrokenPoWithoutAFormerBuildIsReportedAndNotServed()
Without a former build a broken .po is only reported and its module not served for that language.
testNeverWritesOutsideTheArtifactDirectoryEvenViaASymlinkedLanguageDirectory()
Writes stay confined to the artifact directory (AtomicFileWriter::write() gets it as $confine_to_dire...
testABrokenManifestCausesARebuild()
A build whose manifest cannot be read is never trusted: it is rebuilt.
testASecondRunWithNoChangesReportsTheArtifactAsUnchanged()
A second run with nothing changed does not recompile (same hash, source, artifact present): "unchange...
testIsNotableAndNeedsTheGettextExtension()
informer(array &$messages)
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
This defines ways in which objectives may interact with admins during the setup.
An environment holds resources to be used in the setup process.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.