diff --git a/components/ILIAS/Language/README.md b/components/ILIAS/Language/README.md index fdf90ff455a7..4ff6fbb8458f 100755 --- a/components/ILIAS/Language/README.md +++ b/components/ILIAS/Language/README.md @@ -172,6 +172,12 @@ Only other modules (plugins) are read from `lng_data`/`lng_modules`. * Without a build, for a language the build lacks, or for a language that is not installed a shipped module has no texts (`-identifier-`); a missing build or a shipped `.po` that is not built is reported. +* A real locale in `LC_MESSAGES` (e.g. from the setting "Locale" of the + general settings) stays, native gettext sets one only where `LC_MESSAGES` + is `C`, `POSIX` or `C.`. ICU takes its default locale from + `LC_MESSAGES` once per process: a PHP-FPM worker whose first ICU call comes + before that setting or without it keeps `ilias__MESSAGES` (glibc) for its + lifetime. * Requirements: the PHP extension `gettext` for CLI and web server, and * glibc: the locale `C.utf8`, with the same C library for both; native gettext activates the build's copy of it through `LOCPATH`; @@ -180,7 +186,8 @@ Only other modules (plugins) are read from `lng_data`/`lng_modules`. the libc there ignores `LOCPATH`, no locale is shipped (`C.UTF-8` does not work: GNU libintl ignores `LANGUAGE` there); locale variables of the environment (e.g. `LANG=C.UTF-8`) do not matter: native gettext sets the - environment variable `LC_ALL` to that locale (child processes inherit it); + environment variable `LC_ALL` to the active locale, a kept one too (child + processes inherit it); * musl is not supported (ignores `LOCPATH`). Build on the platform that serves ILIAS (with Docker in the container): the diff --git a/components/ILIAS/Language/src/ComponentTranslation/NativeGettext.php b/components/ILIAS/Language/src/ComponentTranslation/NativeGettext.php index 4dadb768d912..3372aae7506e 100644 --- a/components/ILIAS/Language/src/ComponentTranslation/NativeGettext.php +++ b/components/ILIAS/Language/src/ComponentTranslation/NativeGettext.php @@ -25,7 +25,8 @@ * migrated modules, and the one place that calls it. * * Native gettext only translates while LC_MESSAGES is not "C"/"POSIX" and finds a catalog as - * `//LC_MESSAGES/.mo`. ILIAS does not depend on the locale's language: + * `//LC_MESSAGES/.mo`. ILIAS does not depend on the locale's language, so a + * real locale in LC_MESSAGES (e.g. of ilInitialisation::initLocale()) stays. Only for "C"/"POSIX"/"C.": * - glibc: the build ships its own locale (a copy of "C.utf8", see ShippedTranslationsBuild) named * LOCALE_NAME; activate() loads it via LOCPATH and removes LOCPATH again right away (while it is set, * setlocale() finds no system locale); @@ -42,7 +43,8 @@ * from translating). In a thread-safe (ZTS) PHP the end of another thread's request can take them back * mid-request: isActive() checks, reactivateIfLost() activates again (the administration shows a hint, * see isThreadSafe()). Child processes inherit LANGUAGE=messages, which is harmless, and on macOS/BSD - * LC_ALL=, which overrides the locale of the environment for them. + * LC_ALL= (a kept one too, e.g. the locale of the administration), which overrides the + * locale of the environment for them. * * The C library never reloads a catalog file it has read once (also not a replaced one), so every new * state (a build, an overlay revision) gets a directory of its own, and binding the domain loads it. @@ -85,6 +87,8 @@ final class NativeGettext private static ?string $problem = null; + private static bool $messages_set = false; + public static function isExtensionLoaded(): bool { return extension_loaded('gettext'); @@ -126,8 +130,8 @@ public static function useSystemLocaleForTests(?bool $system_locale): void } /** - * Activates the locale LOCALE_NAME of $locale_directory (or, see usesSystemLocale(), a system locale) - * for LC_MESSAGES and sets LANGUAGE; if it fails, getProblem() tells why. + * Sets LANGUAGE and, if LC_MESSAGES holds no real locale, the locale LOCALE_NAME of $locale_directory (or, see + * usesSystemLocale(), a system locale) for it; if it fails, getProblem() tells why. */ public static function activate(string $locale_directory): bool { @@ -141,12 +145,17 @@ public static function activate(string $locale_directory): bool if (self::usesSystemLocale()) { return self::activateSystemLocale($locale_directory); } + // also checked when a real locale stays: a build without its locale is broken if (!is_dir($locale_directory . '/' . self::LOCALE_NAME)) { self::$problem = sprintf('the locale "%s" is missing in "%s"', self::LOCALE_NAME, $locale_directory); return false; } self::$previous ??= self::processState(); + $kept = self::keptLocale(); + if ($kept !== null) { + return self::activated($locale_directory, $kept, false); + } $locpath = getenv('LOCPATH'); putenv('LOCPATH=' . $locale_directory); $locale = setlocale(LC_MESSAGES, self::LOCALE_NAME); @@ -157,13 +166,14 @@ public static function activate(string $locale_directory): bool return false; } - return self::activated($locale_directory, $locale); + return self::activated($locale_directory, $locale, true); } private static function activateSystemLocale(string $locale_directory): bool { self::$previous ??= self::processState(); - $locale = setlocale(LC_MESSAGES, self::SYSTEM_LOCALES); + $kept = self::keptLocale(); + $locale = $kept ?? setlocale(LC_MESSAGES, self::SYSTEM_LOCALES); if ($locale === false) { self::$problem = sprintf( 'none of the system locales %s exists for LC_MESSAGES (needed on %s, where native gettext cannot use the locale of the build)', @@ -175,7 +185,25 @@ private static function activateSystemLocale(string $locale_directory): bool // GNU libintl on macOS reads the locale of the environment before setlocale()'s, and "C" there ignores LANGUAGE putenv('LC_ALL=' . $locale); - return self::activated($locale_directory, $locale); + return self::activated($locale_directory, $locale, $kept === null); + } + + /** A real LC_MESSAGES locale stays, so ICU & Co. keep seeing it (e.g. the one of ilInitialisation::initLocale()). */ + private static function keptLocale(): ?string + { + $locale = setlocale(LC_MESSAGES, '0'); + if ( + $locale === false + || $locale === 'C' + || $locale === 'POSIX' + || str_starts_with($locale, 'C.') + || $locale === self::LOCALE_NAME + || (self::$messages_set && $locale === self::$active_locale) + ) { + return null; + } + + return $locale; } /** @@ -186,11 +214,12 @@ private static function processState(): array return ['language' => getenv('LANGUAGE'), 'messages' => setlocale(LC_MESSAGES, '0'), 'all' => getenv('LC_ALL')]; } - private static function activated(string $locale_directory, string $locale): bool + private static function activated(string $locale_directory, string $locale, bool $messages_set): bool { putenv('LANGUAGE=' . self::CATALOG_LANGUAGE); self::$locale_directory = $locale_directory; self::$active_locale = $locale; + self::$messages_set = $messages_set; self::$problem = null; return true; @@ -224,17 +253,17 @@ public static function reactivateIfLost(): bool } /** - * Takes LANGUAGE, LC_MESSAGES and LC_ALL back to the state before the first activate(); settings changed by - * other code since stay. Only for long-running processes (e.g. Setup's build after its check), a web - * request needs no call. No-op in a thread-safe PHP, where other threads may use them. The activated - * directory is kept, so reactivateIfLost() can activate it again. + * Takes LANGUAGE, LC_MESSAGES (if activate() set it) and LC_ALL back to the state before the first + * activate(); settings changed by other code since stay. Only for long-running processes (e.g. Setup's build + * after its check), a web request needs no call. No-op in a thread-safe PHP, where other threads may use them. + * The activated directory is kept, so reactivateIfLost() can activate it again. */ public static function restore(): void { if (self::$previous === null || self::isThreadSafe()) { return; } - if (self::$previous['messages'] !== false && setlocale(LC_MESSAGES, '0') === self::$active_locale) { + if (self::$messages_set && self::$previous['messages'] !== false && setlocale(LC_MESSAGES, '0') === self::$active_locale) { setlocale(LC_MESSAGES, self::$previous['messages']); } if (getenv('LANGUAGE') === self::CATALOG_LANGUAGE) { @@ -316,6 +345,7 @@ public static function resetForTests(): void self::$system_locale_for_tests = null; self::$locale_directory = null; self::$active_locale = null; + self::$messages_set = false; self::$previous = null; self::$problem = null; } diff --git a/components/ILIAS/Language/src/ComponentTranslation/ShippedTranslationsBuild.php b/components/ILIAS/Language/src/ComponentTranslation/ShippedTranslationsBuild.php index dbe6940e5f4a..bc3dbd3d6854 100644 --- a/components/ILIAS/Language/src/ComponentTranslation/ShippedTranslationsBuild.php +++ b/components/ILIAS/Language/src/ComponentTranslation/ShippedTranslationsBuild.php @@ -573,6 +573,23 @@ private function findCollisions(array $values_per_language): array * @throws RuntimeException if native gettext does not serve the expected value */ private function assertNativeGettextServes(string $build_directory, array $sample): void + { + $messages = setlocale(LC_MESSAGES, '0'); + // with "C" activate() loads the shipped locale instead of keeping a real one, so the check covers it + setlocale(LC_MESSAGES, 'C'); + try { + $this->assertActivatedNativeGettextServes($build_directory, $sample); + } finally { + if ($messages !== false) { + setlocale(LC_MESSAGES, $messages); + } + } + } + + /** + * @param array{module: string, lang: string, key: string, value: string} $sample + */ + private function assertActivatedNativeGettextServes(string $build_directory, array $sample): void { if (!NativeGettext::activate(MigratedLanguageFilePaths::buildLocaleDirectory($build_directory))) { throw new RuntimeException(sprintf('Native gettext is not available: %s.', (string) NativeGettext::getProblem())); diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFilePathsTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFilePathsTest.php index 4a112d20a33c..7d01b0e61773 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFilePathsTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFilePathsTest.php @@ -33,6 +33,8 @@ */ class MigratedLanguageFilePathsTest extends TestCase { + use RestoresProcessState; + private string $root; protected function setUp(): void diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncLockTestCase.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncLockTestCase.php index 68679cb87a43..f72e8a6a8ef2 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncLockTestCase.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncLockTestCase.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -29,6 +29,8 @@ */ abstract class MigratedLanguageFileSyncLockTestCase extends TestCase { + use RestoresProcessState; + protected const string MODULE = 'stest'; protected string $client_data_dir; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncPluralOverlayTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncPluralOverlayTest.php index 7767ad435f78..b9e9fe92326f 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncPluralOverlayTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncPluralOverlayTest.php @@ -34,6 +34,8 @@ */ class MigratedLanguageFileSyncPluralOverlayTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'ptest'; private string $fixture_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRemarksTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRemarksTest.php index ffec8d2cdd6b..1678e1877ca5 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRemarksTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRemarksTest.php @@ -34,6 +34,8 @@ */ class MigratedLanguageFileSyncRemarksTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'rtest'; private string $fixture_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRevisionTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRevisionTest.php index 357c6b21d236..e32668944c5b 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRevisionTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncRevisionTest.php @@ -34,6 +34,8 @@ */ class MigratedLanguageFileSyncRevisionTest extends TestCase { + use \RestoresProcessState; + private const string MODULE = 'rvs'; private string $root; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncSymlinkTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncSymlinkTest.php index ed608c058f8d..aed72fbe56a5 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncSymlinkTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncSymlinkTest.php @@ -37,6 +37,8 @@ */ class MigratedLanguageFileSyncSymlinkTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'stest'; private string $fixture_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncTestCase.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncTestCase.php index cf4af282ec5c..a24683419191 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncTestCase.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedLanguageFileSyncTestCase.php @@ -35,6 +35,8 @@ */ abstract class MigratedLanguageFileSyncTestCase extends TestCase { + use RestoresProcessState; + protected const string MODULE = 'stest'; protected string $fixture_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/MigratedTranslationsTest.php b/components/ILIAS/Language/tests/ComponentTranslation/MigratedTranslationsTest.php index 677b06aa899c..9987dd969ddd 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/MigratedTranslationsTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/MigratedTranslationsTest.php @@ -33,6 +33,8 @@ */ class MigratedTranslationsTest extends TestCase { + use \RestoresProcessState; + private string $root; private string $client_data_dir; private string $build_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/NativeGettextTest.php b/components/ILIAS/Language/tests/ComponentTranslation/NativeGettextTest.php index ecf5ab20a1c9..c7225c04c20d 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/NativeGettextTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/NativeGettextTest.php @@ -24,6 +24,7 @@ use ILIAS\Language\ComponentTranslation\Catalog\TranslationEntry; use MigratedPoFixture; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; /** @@ -35,26 +36,18 @@ */ class NativeGettextTest extends TestCase { + use \RestoresProcessState; + private const string DOMAIN = 'ntg.de'; + private const string REAL_LOCALE = 'de_DE.UTF-8'; private string $root; private string $build_directory; private string $locale_directory; - /** - * @var array{language: string|false, locpath: string|false, all: string|false, messages: string|false} - */ - private array $process_state; - protected function setUp(): void { MigratedPoFixture::resetRuntime(); - $this->process_state = [ - 'language' => getenv('LANGUAGE'), - 'locpath' => getenv('LOCPATH'), - 'all' => getenv('LC_ALL'), - 'messages' => setlocale(LC_MESSAGES, '0'), - ]; $this->root = sys_get_temp_dir() . '/ilias_native_gettext_' . bin2hex(random_bytes(6)); mkdir($this->root, 0775, true); @@ -76,13 +69,6 @@ protected function setUp(): void protected function tearDown(): void { NativeGettext::resetForTests(); - // whatever a test did to the process, the next one starts as this one did - putenv($this->process_state['language'] === false ? 'LANGUAGE' : 'LANGUAGE=' . $this->process_state['language']); - putenv($this->process_state['locpath'] === false ? 'LOCPATH' : 'LOCPATH=' . $this->process_state['locpath']); - putenv($this->process_state['all'] === false ? 'LC_ALL' : 'LC_ALL=' . $this->process_state['all']); - if ($this->process_state['messages'] !== false) { - setlocale(LC_MESSAGES, $this->process_state['messages']); - } MigratedPoFixture::removeDirectory($this->root); MigratedPoFixture::resetRuntime(); } @@ -186,6 +172,7 @@ public static function processSettingsBeforeSystemLocaleActivation(): iterable { yield 'C locale' => ['C', 'de']; yield 'the system locale itself, LANGUAGE unset' => ['system', false]; + yield 'a real locale, which stays' => [self::REAL_LOCALE, 'de']; } #[DataProvider('processSettingsBeforeSystemLocaleActivation')] @@ -193,12 +180,19 @@ public function testSystemLocaleIsOnlyActiveAfterActivateAndRestoreGivesTheProce { $this->requireSystemLocale(); NativeGettext::useSystemLocaleForTests(true); + $kept = $locale === self::REAL_LOCALE; + if ($kept) { + $this->requireRealLocale(); + } $locale = $locale === 'system' ? (string) setlocale(LC_MESSAGES, NativeGettext::SYSTEM_LOCALES) : (string) setlocale(LC_MESSAGES, $locale); putenv($language === false ? 'LANGUAGE' : 'LANGUAGE=' . $language); $this->assertFalse(NativeGettext::isActive(), 'a locale set by someone else is not our activation'); $this->activateSystemLocale(); $this->assertTrue(NativeGettext::isActive()); + if ($kept) { + $this->assertSame($locale, setlocale(LC_MESSAGES, '0'), 'a real locale is not replaced'); + } NativeGettext::restore(); @@ -398,6 +392,182 @@ public function testReactivateIfLostRepairsWhatOtherCodeChanged(\Closure $change $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); } + private function requireRealLocale(): void + { + $before = setlocale(LC_MESSAGES, '0'); + $found = setlocale(LC_MESSAGES, self::REAL_LOCALE); + setlocale(LC_MESSAGES, $before); + if ($found === false) { + $this->markTestSkipped('the locale "' . self::REAL_LOCALE . '" is not installed'); + } + } + + #[DataProvider('previousProcessSettings')] + public function testActivateKeepsARealLocaleAndRestoreLeavesItWhileTakingLanguageBack(string|false $language, string|false $locpath): void + { + MigratedPoFixture::requireGlibcLocale($this); + $this->requireRealLocale(); + setlocale(LC_MESSAGES, self::REAL_LOCALE); + putenv($language === false ? 'LANGUAGE' : 'LANGUAGE=' . $language); + putenv($locpath === false ? 'LOCPATH' : 'LOCPATH=' . $locpath); + + $this->activateBuild(); + + $this->assertTrue(NativeGettext::isActive()); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0')); + $this->assertSame(NativeGettext::CATALOG_LANGUAGE, getenv('LANGUAGE')); + $this->assertSame($locpath, getenv('LOCPATH'), 'LOCPATH is not touched when the locale stays'); + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + + NativeGettext::restore(); + + $this->assertFalse(NativeGettext::isActive()); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0'), 'restore() leaves a locale it did not set'); + $this->assertSame($language, getenv('LANGUAGE')); + $this->assertSame($locpath, getenv('LOCPATH')); + } + + #[DataProvider('lcAllBeforeSystemLocaleActivation')] + public function testSystemLocaleBranchKeepsARealLocaleSetsLcAllToItAndRestoreGivesLcAllBack(string|false $lc_all): void + { + $this->requireRealLocale(); + NativeGettext::useSystemLocaleForTests(true); + setlocale(LC_MESSAGES, self::REAL_LOCALE); + putenv($lc_all === false ? 'LC_ALL' : 'LC_ALL=' . $lc_all); + + $this->activateSystemLocale(); + + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0')); + $this->assertSame(self::REAL_LOCALE, getenv('LC_ALL')); + $this->assertTrue(NativeGettext::isActive()); + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + + NativeGettext::restore(); + + $this->assertSame($lc_all, getenv('LC_ALL')); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0')); + $this->assertFalse(NativeGettext::isActive()); + } + + /** + * @return iterable + */ + public static function branches(): iterable + { + yield 'glibc branch (own locale)' => [false]; + yield 'system locale branch' => [true]; + } + + #[DataProvider('branches')] + public function testReactivateIfLostKeepsALocaleOtherCodeSetForEverything(bool $system_locale): void + { + $system_locale ? $this->requireSystemLocale() : MigratedPoFixture::requireGlibcLocale($this); + $this->requireRealLocale(); + NativeGettext::useSystemLocaleForTests($system_locale); + $this->activateBuildInBranch($system_locale); + $locpath = getenv('LOCPATH'); + setlocale(LC_ALL, self::REAL_LOCALE); + $this->assertFalse(NativeGettext::isActive(), 'precondition'); + + $this->assertTrue(NativeGettext::reactivateIfLost()); + + $this->assertTrue(NativeGettext::isActive()); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0')); + $this->assertSame($locpath, getenv('LOCPATH')); + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + $this->assertFalse(NativeGettext::reactivateIfLost(), 'nothing left to repair'); + + NativeGettext::restore(); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0'), 'the locale of the other code is not ours to take back'); + } + + private function activateBuildInBranch(bool $system_locale): void + { + $system_locale ? $this->activateSystemLocale() : $this->activateBuild(); + } + + public function testActivateStillFailsWithoutLocaleDirectoryWhenARealLocaleStays(): void + { + MigratedPoFixture::requireGlibcLocale($this); + $this->requireRealLocale(); + setlocale(LC_MESSAGES, self::REAL_LOCALE); + putenv('LANGUAGE=de'); + $this->assertDirectoryDoesNotExist($this->root . '/locale-none'); + + $this->assertFalse(NativeGettext::activate($this->root . '/locale-none')); + + $this->assertStringContainsString(NativeGettext::LOCALE_NAME, (string) NativeGettext::getProblem()); + $this->assertFalse(NativeGettext::isActive()); + $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES, '0')); + $this->assertSame('de', getenv('LANGUAGE')); + } + + /** + * @return iterable + */ + public static function localesThatAreNotReal(): iterable + { + yield 'C.UTF-8' => ['C.UTF-8']; + yield 'C.utf8' => ['C.utf8']; + yield 'POSIX' => ['POSIX']; + yield 'C' => ['C']; + } + + #[DataProvider('localesThatAreNotReal')] + public function testALocaleOfTheCFamilyIsNotRealSoTheOwnLocaleIsActivatedAndTakenBackAgain(string $locale): void + { + MigratedPoFixture::requireGlibcLocale($this); + if (setlocale(LC_MESSAGES, $locale) === false) { + $this->markTestSkipped('the locale "' . $locale . '" is not installed'); + } + $before = setlocale(LC_MESSAGES, '0'); + + $this->activateBuild(); + + $this->assertSame(NativeGettext::LOCALE_NAME, setlocale(LC_MESSAGES, '0')); + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + + NativeGettext::restore(); + + $this->assertSame($before, setlocale(LC_MESSAGES, '0')); + } + + public function testTheOwnLocaleIsNeverTakenForARealOneAfterLanguageWasLost(): void + { + MigratedPoFixture::requireGlibcLocale($this); + $before = setlocale(LC_MESSAGES, '0'); + $this->activateBuild(); + putenv('LANGUAGE'); + $this->assertFalse(NativeGettext::isActive(), 'precondition'); + + $this->assertTrue(NativeGettext::reactivateIfLost()); + + $this->assertSame(NativeGettext::LOCALE_NAME, setlocale(LC_MESSAGES, '0')); + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + + NativeGettext::restore(); + + $this->assertSame($before, setlocale(LC_MESSAGES, '0'), 'the own locale is ours, so it is taken back'); + $this->assertNotSame(NativeGettext::CATALOG_LANGUAGE, getenv('LANGUAGE')); + } + + #[RunInSeparateProcess] + public function testTheIcuDefaultLocaleStaysTheOneOfTheAdministrator(): void + { + MigratedPoFixture::requireGlibcLocale($this); + $this->requireRealLocale(); + if (!extension_loaded('intl')) { + $this->markTestSkipped('needs ext-intl'); + } + setlocale(LC_ALL, [self::REAL_LOCALE]); + setlocale(LC_NUMERIC, 'C'); + + $this->activateBuild(); + + $this->assertSame('Hallo', NativeGettext::translate(self::DOMAIN, 'greeting')); + $this->assertSame('de_DE', \locale_get_default()); + } + public function testReactivateIfLostDoesNothingWithoutAnActivationOrWhileActive(): void { // setUp()'s build activated native gettext - restore() keeps that locale for a reactivation diff --git a/components/ILIAS/Language/tests/ComponentTranslation/OverlayDirectoryOwnershipTest.php b/components/ILIAS/Language/tests/ComponentTranslation/OverlayDirectoryOwnershipTest.php index 978544ccc0da..240faa784ccb 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/OverlayDirectoryOwnershipTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/OverlayDirectoryOwnershipTest.php @@ -32,6 +32,8 @@ */ class OverlayDirectoryOwnershipTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'stest'; private string $fixture_directory; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/ShippedPoMergerTest.php b/components/ILIAS/Language/tests/ComponentTranslation/ShippedPoMergerTest.php index 5239c90203f2..e45380fef9cf 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/ShippedPoMergerTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/ShippedPoMergerTest.php @@ -39,6 +39,8 @@ */ class ShippedPoMergerTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'mtest'; private const string LANG = 'de'; diff --git a/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsBuildTest.php b/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsBuildTest.php index 356fe38d92f9..ab4802a339fa 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsBuildTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsBuildTest.php @@ -34,19 +34,15 @@ */ class ShippedTranslationsBuildTest extends TestCase { + use \RestoresProcessState; + private string $root; private string $artifacts; - private string|false $messages_locale; - private string|false $language; - private string|false $lc_all; protected function setUp(): void { // never the build of the installation, see MigratedPoFixture::resetRuntime() MigratedPoFixture::resetRuntime(); - $this->messages_locale = setlocale(LC_MESSAGES, '0'); - $this->language = getenv('LANGUAGE'); - $this->lc_all = getenv('LC_ALL'); $this->root = sys_get_temp_dir() . '/ilias_shipped_build_test_' . bin2hex(random_bytes(6)); mkdir($this->root, 0775, true); $this->artifacts = MigratedLanguageFilePaths::shippedArtifactDirectory($this->root); @@ -54,12 +50,6 @@ protected function setUp(): void protected function tearDown(): void { - NativeGettext::resetForTests(); - if ($this->messages_locale !== false) { - setlocale(LC_MESSAGES, $this->messages_locale); - } - putenv($this->language === false ? 'LANGUAGE' : 'LANGUAGE=' . $this->language); - putenv($this->lc_all === false ? 'LC_ALL' : 'LC_ALL=' . $this->lc_all); MigratedPoFixture::removeDirectory($this->root); MigratedPoFixture::resetRuntime(); } @@ -254,6 +244,49 @@ public function testABuildNativeGettextCannotServeLeavesCurrentAndThePreviousBui $this->assertDirectoryExists($this->buildDirectory((string) $good['build'])); } + private function useRealLocaleForMessages(): string + { + MigratedPoFixture::requireGlibcLocale($this); + $before = setlocale(LC_MESSAGES, '0'); + $real = setlocale(LC_MESSAGES, 'de_DE.UTF-8'); + if ($real === false) { + setlocale(LC_MESSAGES, $before); + $this->markTestSkipped('the locale "de_DE.UTF-8" is not installed'); + } + // as in a fresh Setup process, started with the locale of an administrator + NativeGettext::resetForTests(); + NativeGettext::useSystemLocaleForTests(false); + setlocale(LC_MESSAGES, $real); + + return $real; + } + + public function testABuildNativeGettextCannotServeIsRefusedEvenWhenARealLocaleWasActiveBeforeAndItStaysUnchanged(): void + { + $real = $this->useRealLocaleForMessages(); + $this->ship('mfa', 'de', ['greeting' => 'Hallo']); + + try { + $this->build(['mfa'], $this->brokenLocaleSource()); + $this->fail('the build must not be accepted'); + } catch (RuntimeException $e) { + $this->assertStringContainsString('cannot be activated', $e->getMessage()); + } + + $this->assertSame($real, setlocale(LC_MESSAGES, '0')); + } + + public function testAnIntactBuildIsCheckedWithItsOwnLocaleAndLeavesARealLocaleOfTheProcessUnchanged(): void + { + $real = $this->useRealLocaleForMessages(); + $this->ship('mfa', 'de', ['greeting' => 'Hallo']); + + $result = $this->build(['mfa']); + + $this->assertNotNull($result['build']); + $this->assertSame($real, setlocale(LC_MESSAGES, '0')); + } + public function testABuildWithoutAnyTranslatedValueCannotBeCheckedAndIsRefused(): void { $this->ship('mfa', 'de', ['greeting' => 'Hallo']); diff --git a/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsTest.php b/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsTest.php index ca99599517fb..1260cf3d88ec 100644 --- a/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsTest.php +++ b/components/ILIAS/Language/tests/ComponentTranslation/ShippedTranslationsTest.php @@ -32,6 +32,8 @@ */ class ShippedTranslationsTest extends TestCase { + use \RestoresProcessState; + private string $root; private ShippedTranslations $shipped_translations; diff --git a/components/ILIAS/Language/tests/MigratedPoFixture.php b/components/ILIAS/Language/tests/MigratedPoFixture.php index 53951e22c3e1..bb2edd15bc11 100644 --- a/components/ILIAS/Language/tests/MigratedPoFixture.php +++ b/components/ILIAS/Language/tests/MigratedPoFixture.php @@ -37,6 +37,8 @@ */ final class MigratedPoFixture { + private const array PROCESS_ENVIRONMENT = ['LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LOCPATH', 'LANG']; + private static bool $artifact_cleanup_registered = false; /** The C library never forgets a catalog path it has looked at, so every resetRuntime() gets a directory of its own. */ @@ -298,6 +300,36 @@ public static function resetRuntime(?string $artifact_directory = null): void NativeGettext::resetForTests(); } + /** + * @return array{locale: string|false, env: array} + */ + public static function captureProcessState(): array + { + $env = []; + foreach (self::PROCESS_ENVIRONMENT as $name) { + $env[$name] = getenv($name); + } + + return ['locale' => setlocale(LC_ALL, '0'), 'env' => $env]; + } + + /** + * Gives back what captureProcessState() saw and forgets the static state of the runtime read path. + * + * @param array{locale: string|false, env: array} $state + */ + public static function restoreProcessState(array $state): void + { + MigratedTranslations::resetForTests(); + NativeGettext::resetForTests(); + foreach ($state['env'] as $name => $value) { + putenv($value === false ? $name : $name . '=' . $value); + } + if ($state['locale'] !== false && setlocale(LC_ALL, $state['locale']) === false) { + setlocale(LC_ALL, 'C'); + } + } + /** Moves the builds to a directory this process has not looked at: the C library keeps what it read from a path (mapped, not copied), so a file damaged in place must not be one it read. */ public static function moveArtifactsToFreshDirectory(): void { diff --git a/components/ILIAS/Language/tests/RestoresProcessState.php b/components/ILIAS/Language/tests/RestoresProcessState.php new file mode 100644 index 000000000000..799cd8fc4929 --- /dev/null +++ b/components/ILIAS/Language/tests/RestoresProcessState.php @@ -0,0 +1,46 @@ +}|null */ + private ?array $process_state_before_test = null; + + #[Before] + protected function captureProcessStateBeforeTest(): void + { + $this->process_state_before_test = MigratedPoFixture::captureProcessState(); + } + + #[After] + protected function restoreProcessStateAfterTest(): void + { + if ($this->process_state_before_test !== null) { + MigratedPoFixture::restoreProcessState($this->process_state_before_test); + } + } +} diff --git a/components/ILIAS/Language/tests/Setup/InstallLanguagesRemovesOverlaysTest.php b/components/ILIAS/Language/tests/Setup/InstallLanguagesRemovesOverlaysTest.php index d08011499622..66b645d52154 100644 --- a/components/ILIAS/Language/tests/Setup/InstallLanguagesRemovesOverlaysTest.php +++ b/components/ILIAS/Language/tests/Setup/InstallLanguagesRemovesOverlaysTest.php @@ -35,6 +35,8 @@ */ class InstallLanguagesRemovesOverlaysTest extends TestCase { + use RestoresProcessState; + private const string MODULE = 'pilot'; private string $root; diff --git a/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerDatabaseRowsOfMigratedModuleTest.php b/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerDatabaseRowsOfMigratedModuleTest.php index 771ed1208103..0b0fdcb62e46 100644 --- a/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerDatabaseRowsOfMigratedModuleTest.php +++ b/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerDatabaseRowsOfMigratedModuleTest.php @@ -28,6 +28,8 @@ */ class LanguageInstallationManagerDatabaseRowsOfMigratedModuleTest extends TestCase { + use RestoresProcessState; + /** * @param array $rows * @param array $final_entries diff --git a/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerMigratedModulesTestCase.php b/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerMigratedModulesTestCase.php index b91559e0095d..695f4f4060bf 100644 --- a/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerMigratedModulesTestCase.php +++ b/components/ILIAS/Language/tests/Setup/LanguageInstallationManagerMigratedModulesTestCase.php @@ -41,6 +41,8 @@ */ abstract class LanguageInstallationManagerMigratedModulesTestCase extends TestCase { + use RestoresProcessState; + protected const string NOW = '2026-01-02 03:04:05'; protected const string MODULE = 'pilot'; diff --git a/components/ILIAS/Language/tests/Setup/ShippedLanguageFilesCompiledObjectiveTest.php b/components/ILIAS/Language/tests/Setup/ShippedLanguageFilesCompiledObjectiveTest.php index c1bf4adcfe53..dcfc391c2f07 100644 --- a/components/ILIAS/Language/tests/Setup/ShippedLanguageFilesCompiledObjectiveTest.php +++ b/components/ILIAS/Language/tests/Setup/ShippedLanguageFilesCompiledObjectiveTest.php @@ -33,6 +33,8 @@ */ class ShippedLanguageFilesCompiledObjectiveTest extends TestCase { + use RestoresProcessState; + private string $root; protected function setUp(): void diff --git a/components/ILIAS/Language/tests/Setup/ilLanguagesInstalledAndUpdatedObjectiveTest.php b/components/ILIAS/Language/tests/Setup/ilLanguagesInstalledAndUpdatedObjectiveTest.php index 6435c58623ee..ca32727eb128 100644 --- a/components/ILIAS/Language/tests/Setup/ilLanguagesInstalledAndUpdatedObjectiveTest.php +++ b/components/ILIAS/Language/tests/Setup/ilLanguagesInstalledAndUpdatedObjectiveTest.php @@ -35,6 +35,8 @@ */ class ilLanguagesInstalledAndUpdatedObjectiveTest extends TestCase { + use RestoresProcessState; + private bool $had_global_db; private mixed $previous_global_db = null; diff --git a/components/ILIAS/Language/tests/WithPluralFormKeysTest.php b/components/ILIAS/Language/tests/WithPluralFormKeysTest.php index 41377d5dc108..25b4189e53cc 100644 --- a/components/ILIAS/Language/tests/WithPluralFormKeysTest.php +++ b/components/ILIAS/Language/tests/WithPluralFormKeysTest.php @@ -26,6 +26,8 @@ */ class WithPluralFormKeysTest extends TestCase { + use RestoresProcessState; + private const string SEPARATOR = '#:#'; /** diff --git a/components/ILIAS/Language/tests/ilLanguageBaseTestCase.php b/components/ILIAS/Language/tests/ilLanguageBaseTestCase.php index bd24f0f574a5..9c304fc6265b 100644 --- a/components/ILIAS/Language/tests/ilLanguageBaseTestCase.php +++ b/components/ILIAS/Language/tests/ilLanguageBaseTestCase.php @@ -29,6 +29,8 @@ */ abstract class ilLanguageBaseTestCase extends TestCase { + use RestoresProcessState; + protected function setUp(): void { // never the build of the installation, see MigratedPoFixture::resetRuntime() diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUIDispatchTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUIDispatchTest.php index 29093a771777..e4b1edbe1bfc 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUIDispatchTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUIDispatchTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -24,6 +24,8 @@ class ilObjLanguageExtGUIDispatchTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageGuiTestSupport; private const array COMMANDS = [ diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUIMaintenanceTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUIMaintenanceTest.php index 9d6cb20b97e3..681fd03347e1 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUIMaintenanceTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUIMaintenanceTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -28,6 +28,8 @@ class ilObjLanguageExtGUIMaintenanceTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; private ?string $maintenance_directory = null; diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveNewEntryTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveNewEntryTest.php index d3f0478443ff..ba8df6a0e54e 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveNewEntryTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveNewEntryTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -27,6 +27,8 @@ class ilObjLanguageExtGUISaveNewEntryTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; /** @return ilObjLanguageExtGUI&MockObject */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveObjectTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveObjectTest.php index d7d1404e0706..8513b5ea7637 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveObjectTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveObjectTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -25,6 +25,8 @@ class ilObjLanguageExtGUISaveObjectTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; /** @var array|null */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveSettingsTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveSettingsTest.php index 3b01dc06257c..5497ed7c5b9d 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveSettingsTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUISaveSettingsTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -27,6 +27,8 @@ class ilObjLanguageExtGUISaveSettingsTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; /** @return ilObjLanguageExtGUI&MockObject */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUITest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUITest.php index 4c00063644fb..9b7c5b229554 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUITest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUITest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -23,6 +23,8 @@ class ilObjLanguageExtGUITest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; /** A data-changing command missing here would be triggerable via GET without the CSRF token. */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageExtGUIUploadTest.php b/components/ILIAS/Language/tests/ilObjLanguageExtGUIUploadTest.php index 4d8942a93d34..27e5f25914b1 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageExtGUIUploadTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageExtGUIUploadTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -28,6 +28,8 @@ */ class ilObjLanguageExtGUIUploadTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageExtGUITestFixture; /** diff --git a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIIdValidationTest.php b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIIdValidationTest.php index 64bc0ea3f10f..2ae8b9e2cc19 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIIdValidationTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIIdValidationTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -27,6 +27,8 @@ class ilObjLanguageFolderGUIIdValidationTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageFolderGUITestFixture; /** One non-"lng" id aborts the whole request: failure message, redirect to "view", InstallLanguage never called. */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageFolderGUILanguageDetectionTest.php b/components/ILIAS/Language/tests/ilObjLanguageFolderGUILanguageDetectionTest.php index e0db4dc88b59..135b91634cfd 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageFolderGUILanguageDetectionTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageFolderGUILanguageDetectionTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -30,6 +30,8 @@ #[AllowMockObjectsWithoutExpectations] class ilObjLanguageFolderGUILanguageDetectionTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageFolderGUITestFixture; /** diff --git a/components/ILIAS/Language/tests/ilObjLanguageFolderGUITest.php b/components/ILIAS/Language/tests/ilObjLanguageFolderGUITest.php index f3255fa5cc51..7334727004e4 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageFolderGUITest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageFolderGUITest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -28,6 +28,8 @@ class ilObjLanguageFolderGUITest extends TestCase { + use RestoresProcessState; + use ilObjLanguageFolderGUITestFixture; /** @return ilObjLanguageFolderGUI&\PHPUnit\Framework\MockObject\MockObject */ diff --git a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallChangesTest.php b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallChangesTest.php index 3716eac8ddef..e37c054e0141 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallChangesTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallChangesTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -25,6 +25,8 @@ class ilObjLanguageFolderGUIUninstallChangesTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageFolderGUITestFixture; public function testUninstallChangesObjectEmbedsTheThrowableMessageFromAnErrorResultIntoTheFailureMessage(): void diff --git a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallTest.php b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallTest.php index 82d7dabcb564..2f84a52a18cf 100644 --- a/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallTest.php +++ b/components/ILIAS/Language/tests/ilObjLanguageFolderGUIUninstallTest.php @@ -6,8 +6,8 @@ * * ILIAS is licensed with the GPL-3.0, * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with - * the source code, too. + * You should have received a copy of said license along with the + * source code, too. * * If this is not the case or you just want to try ILIAS, you'll find * us at: @@ -25,6 +25,8 @@ class ilObjLanguageFolderGUIUninstallTest extends TestCase { + use RestoresProcessState; + use ilObjLanguageFolderGUITestFixture; public function testUninstallObjectEmbedsTheThrowableMessageFromAnErrorResultIntoTheFailureMessage(): void