ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncReadersTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28use PHPUnit\Framework\Attributes\DataProvider;
29use PHPUnit\Framework\TestCase;
30
35{
43 {
44 file_put_contents($this->shippedPo(), "msgid \"kaputt\n");
45 $this->seedOverlay(['greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z']]);
46
47 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
48
49 $this->assertSame(
50 ['value' => 'Servus', 'local_change' => true, 'local_change_date' => '2020-01-01 00:00:00', 'original' => 'Hallo'],
51 $result['greeting']
52 );
53 }
54
56 {
57 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
58 $this->seedOverlay(['greeting' => ['value' => 'Servus', 'original' => 'Hallo']]);
59
60 $this->assertSame(
61 ['greeting' => ['value' => 'Servus', 'local_change' => false, 'local_change_date' => null, 'original' => 'Hallo']],
62 MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir)
63 );
64 }
65
67 {
68 $this->seedShipped(['greeting' => 'Hallo']);
69
70 $this->assertSame([], MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir));
71 }
72
74 {
75 $this->seedOverlay(['greeting' => 'Servus']);
76
77 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir));
78 }
79
81 {
82 $this->seedShipped(['greeting' => 'Hallo']);
83
84 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', 'other_module', $this->client_data_dir));
85 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, null));
86 }
87
95 {
96 $this->seedShipped(['greeting' => 'Hallo']);
97 MigratedPoFixture::writeOverlayRevision($this->overlayBase(), MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Servus']));
98
99 $this->expectException(RuntimeException::class);
100 $this->expectExceptionMessageMatches('/has no/');
101 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir);
102 }
103
109 {
110 $this->seedShipped(['greeting' => 'Hallo']);
111 MigratedPoFixture::writeOverlayRevision($this->overlayBase(), MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Servus']));
112
113 $this->expectException(RuntimeException::class);
114 MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
115 }
116
122 {
123 $this->seedShipped(['greeting' => 'Hallo']);
124 mkdir($this->overlayBase() . '.po', 0775, true);
125
126 $this->expectException(RuntimeException::class);
127 $this->expectExceptionMessageMatches('/no regular file/');
128 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir);
129 }
130
136 {
137 $this->seedShipped(['greeting' => 'Hallo']);
138 MigratedPoFixture::writeOverlayRevision($this->overlayBase(), MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Servus']));
139
140 $this->expectException(RuntimeException::class);
141 $this->sync(['greeting' => 'Hallo']);
142 }
143
145 {
146 $second_directory = MigratedPoFixture::directory('stwo', $this->directory->getPath());
147 $not_migrated = MigratedPoFixture::directory('nomig', $this->directory->getPath());
151 $this->directory,
152 $second_directory,
153 $not_migrated
154 );
155 $this->seedShipped([
156 'greeting' => 'Hallo',
157 'foreign' => ['value' => 'Fremd', 'context' => 'other_module'],
158 ]);
159 $this->seedShipped(['farewell' => 'Tschüss'], 'de', 'stwo');
160 $this->seedShipped(['farewell' => 'Bye'], 'en', 'stwo');
161 $this->seedShipped(['x' => 'only english'], 'en', 'nomig');
162
163 $this->assertSame(
164 [self::MODULE => ['greeting' => 'Hallo'], 'stwo' => ['farewell' => 'Tschüss']],
165 MigratedLanguageFileSync::loadShippedModules($manager, $this->ilias_root, 'de')
166 );
167 }
168
170 {
171 $this->seedShipped([]);
172
173 $this->assertSame(
174 [self::MODULE => []],
175 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root, 'de')
176 );
177 }
178
185 {
186 $this->seedShipped(['greeting' => 'Hallo']);
187 $this->assertSame(
188 ['greeting' => 'Hallo'],
189 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root, 'de')[self::MODULE]
190 );
191
192 $this->seedShipped(['greeting' => 'Servus']);
193
194 $this->assertSame(
195 ['greeting' => 'Servus'],
196 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root, 'de')[self::MODULE]
197 );
198 }
199
207 {
208 $cache = new ReflectionProperty(MigratedLanguageFileSync::class, 'shipped_catalogs');
209 $cache->setValue(null, []);
210
211 try {
212 for ($i = 0; $i < 520; $i++) {
213 $module = 'cachetest' . $i;
214 $directory = MigratedPoFixture::directory($module, $this->directory->getPath());
216 $this->fixture_directory . '/' . $module . '_de.po',
218 );
219 MigratedLanguageFileSync::loadShippedModuleEntries($this->fixture_directory . '/' . $module . '_de.po', $module);
220 unset($directory);
221 }
222
223 $this->assertLessThanOrEqual(512, count($cache->getValue()));
224 } finally {
225 $cache->setValue(null, []);
226 }
227 }
228
235 {
236 $corrupt = MigratedPoFixture::directory('scorrupt', $this->directory->getPath());
237 $english_only = MigratedPoFixture::directory('senonly', $this->directory->getPath());
241 $this->directory,
242 $corrupt,
243 $english_only
244 );
245 $this->seedShipped(['greeting' => 'Hallo']);
246 file_put_contents($this->shippedPo('de', 'scorrupt'), "msgid \"kaputt\n");
247 $this->seedShipped(['x' => 'only english'], 'en', 'senonly');
248
249 $this->assertSame(
250 [self::MODULE => $this->shippedPo(), 'scorrupt' => $this->shippedPo('de', 'scorrupt')],
251 MigratedLanguageFileSync::findShippedModuleFiles($manager, $this->ilias_root, 'de')
252 );
253 }
254
256 {
257 $this->seedShipped(['greeting' => 'Hallo']);
258
259 $this->assertFalse(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, []));
260 $this->assertFalse(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, ['en']));
261 $this->assertTrue(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, ['en', 'de']));
262 }
263
269 {
270 $catalog = MigratedPoFixture::catalog(self::MODULE, [
271 'greeting' => "Hallo\nWelt",
272 'farewell' => 'Tschüss',
273 'foreign' => ['value' => 'Fremd', 'context' => 'other_module'],
274 ]);
275 $catalog->find(self::MODULE, 'greeting')->addExtractedComment('shown on the login page');
276 $catalog->find(self::MODULE, 'greeting')->addExtractedComment('keep it short');
277 $catalog->find('other_module', 'foreign')->addExtractedComment('not returned');
278 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
279
280 $entries = MigratedLanguageFileSync::loadShippedModuleEntries($this->shippedPo(), self::MODULE);
281 ksort($entries);
282
283 $this->assertSame(
284 [
285 'farewell' => ['value' => 'Tschüss', 'comment' => null],
286 'greeting' => ['value' => "Hallo\nWelt", 'comment' => 'shown on the login page keep it short'],
287 ],
288 $entries
289 );
290 }
291
294 {
295 $catalog = MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Hej', 'farewell' => 'Hejdå']);
296 $catalog->find(self::MODULE, 'greeting')->addExtractedComment('en note');
297 $catalog->find(self::MODULE, 'greeting')->addTranslatorNote('egen anteckning');
298 $catalog->find(self::MODULE, 'farewell')->addExtractedComment('en note');
299 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
300
301 $entries = MigratedLanguageFileSync::loadShippedModuleEntries($this->shippedPo(), self::MODULE);
302
303 $this->assertSame('egen anteckning', $entries['greeting']['comment']);
304 $this->assertSame('en note', $entries['farewell']['comment']);
305 $this->assertSame(
306 ['greeting' => ['egen anteckning'], 'farewell' => ['en note']],
307 MigratedLanguageFileSync::loadShippedNotes($this->shippedPo(), self::MODULE)
308 );
309 }
310
312 {
313 file_put_contents($this->shippedPo(), "msgid \"kaputt\n");
314
315 $this->expectException(RuntimeException::class);
316 MigratedLanguageFileSync::loadShippedModuleEntries($this->shippedPo(), self::MODULE);
317 }
318
320 {
321 $this->seedShipped(['changed' => 'Hallo']);
322 $this->seedOverlay([
323 'changed' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2026-09-21T10:11:12Z'],
324 'unchanged' => ['value' => 'Tschüss', 'original' => 'Tschüss'],
325 'foreign' => ['value' => 'x', 'context' => 'other_module'],
326 ]);
327
328 $this->assertSame(
329 [
330 'changed' => [
331 'value' => 'Servus',
332 'local_change' => true,
333 'local_change_date' => '2026-09-21 10:11:12',
334 'original' => 'Hallo',
335 ],
336 'unchanged' => [
337 'value' => 'Tschüss',
338 'local_change' => false,
339 'local_change_date' => null,
340 'original' => 'Tschüss',
341 ],
342 ],
343 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir)
344 );
345 }
346
349 {
350 $this->seedShipped(['greeting' => 'Hallo']);
351 $this->seedOverlay(['greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z']]);
353
354 $this->assertSame(
355 ['value' => 'Servus', 'local_change' => true, 'local_change_date' => '2020-01-01 00:00:00', 'original' => 'Hallo'],
356 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir)['greeting']
357 );
358 }
359
372 {
373 $this->seedShipped(['greeting' => 'Hallo']);
374 MigratedPoFixture::writeOverlayRevision($this->overlayBase(), MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Stray']));
375
376 try {
377 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir);
378 $this->fail('Expected a RuntimeException');
379 } catch (RuntimeException $e) {
380 $this->assertStringContainsString('has no', $e->getMessage());
381 }
382 try {
383 $this->sync(['greeting' => 'Hallo']);
384 $this->fail('Expected a RuntimeException');
385 } catch (RuntimeException) {
386 }
387
388 $this->assertSame(['greeting' => 'Stray'], MigratedPoFixture::readMo(MigratedPoFixture::overlayMo($this->overlayBase())));
389 }
390
392 {
393 $this->seedShipped(['greeting' => 'Hallo']);
394 $this->seedOverlay(['greeting' => 'Servus']);
395
396 $this->assertSame([self::MODULE], MigratedLanguageFileSync::getMigratedModules($this->manager, 'de', $this->client_data_dir));
397 $this->assertSame([], MigratedLanguageFileSync::getMigratedModules($this->manager, 'en', $this->client_data_dir));
398 $this->assertSame([], MigratedLanguageFileSync::getMigratedModules($this->manager, 'de', null));
399 }
400
406 {
407 $this->seedShipped(['greeting' => 'Hallo']);
408
409 $this->assertDirectoryDoesNotExist(dirname($this->overlayBase()));
410 $this->assertSame([self::MODULE], MigratedLanguageFileSync::getMigratedModules($this->manager, 'de', $this->client_data_dir));
411 }
412
414 {
415 $this->seedShipped(['greeting' => 'Hallo']);
416 $this->seedOverlay(['greeting' => 'Hallo']);
417 $shipped_before = file_get_contents($this->shippedPo());
418
419 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, $this->client_data_dir);
420
421 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
422 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($this->overlayBase()));
423 $this->assertSame($shipped_before, file_get_contents($this->shippedPo()));
424 }
425
427 {
428 $this->seedOverlay(['greeting' => 'Hallo'], 'de');
429 $this->seedOverlay(['greeting' => 'Hello'], 'en');
430
431 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, $this->client_data_dir);
432
433 $this->assertFileExists($this->overlayBase('en') . '.po');
434 $this->assertFileExists(MigratedPoFixture::overlayMo($this->overlayBase('en')));
435 }
436
438 {
439 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, $this->client_data_dir);
440 $this->seedOverlay(['greeting' => 'Hallo']);
441
442 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', 'other_module', $this->client_data_dir);
443 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, null);
444
445 $this->assertFileExists($this->overlayBase() . '.po');
446 $this->assertFileExists(MigratedPoFixture::overlayMo($this->overlayBase()));
447 }
448
450 {
451 if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
452 $this->markTestSkipped('A non-removable file cannot be forced for root (no root-proof variant for unlink()).');
453 }
454 $this->seedOverlay(['greeting' => 'Hallo']);
455 $overlay_directory = dirname($this->overlayBase());
456 chmod($overlay_directory, 0555);
457
458 // the lock file cannot be created in the read-only directory either - logged (error_log)
459 $this->expectErrorLog();
460 set_error_handler(static fn(): bool => true, E_WARNING);
461 try {
462 $this->expectException(RuntimeException::class);
463 $this->expectExceptionMessageMatches('/overlay file/i');
464 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, $this->client_data_dir);
465 } finally {
466 restore_error_handler();
467 chmod($overlay_directory, 0775);
468 }
469 }
470}
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
Tracks per overlay entry whether the current value still matches the value of the shipped ....
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
Read side of MigratedLanguageFileSync: local changes, shipped modules and entries,...
testLoadLocalChangesNeverParsesTheShippedPoOnlyChecksItExists()
loadLocalChanges() is loadModuleTranslations()'s pure overlay-delta half: only the shipped ....
testTheTranslatorCommentIsTheNoteAndBothKindsAreShippedComments()
The own note (translator comment) wins over the reference note ("#."); loadShippedNotes() agrees.
testAnOverlayMoWithoutItsPoIsUnreadableAndNotRemovedByAWrite()
Without an overlay .po at all, loadModuleTranslations() falls back to the shipped-only result (never ...
testFindShippedModuleFilesListsEveryModuleWithAShippedPoForTheLanguage()
Every module with a shipped .po for the language - also one whose .po cannot be parsed (the callers n...
testAnIrregularFileAtTheOverlayPoPositionThrows()
An irregular file (here: a directory) at the overlay .po position must not be silently skipped either...
testLoadLocalChangesThrowsForAnOverlayMoWithoutItsPo()
The same guard for loadLocalChanges(), which LanguageInstallationManager's loadOverlay() relies on to...
testGetMigratedModulesListsAModuleWithNoOverlayFileWhatsoever()
The essence of "migrated == has a shipped .po": a module without ANY overlay file at all (never had a...
testAChangedShippedPoWithinTheSameRequestIsReReadNotServedFromTheCache()
readShippedPo() caches a parsed shipped .po per request, keyed by its content hash - so a ....
testAnOverlayMoWithoutItsPoThrowsInsteadOfBeingTreatedAsAbsent()
An overlay .mo without its .po (which carries the bookkeeping) must never be treated as "no overlay" ...
testTheShippedPoCacheNeverGrowsBeyondItsConfiguredLimit()
The cache is bounded (SHIPPED_CATALOG_CACHE_SIZE = 512, see the class docblock) - installing all ship...
testSyncThrowsForAnOverlayMoWithoutItsPoInsteadOfRemovingOrRebuildingIt()
sync() itself must refuse to touch such an overlay too - not silently rebuild or remove it (see the c...
testLoadModuleTranslationsIgnoresAMissingOverlayMoAndStillReadsTheOverlayPo()
Only the overlay .po is read: a missing overlay .mo does not make the module "not migrated".
testLoadShippedModuleEntriesReturnsValueAndJoinedExtractedComment()
The extracted comments ("#.", the counterpart of a .lang "###" comment) of an entry are joined into o...
Fixtures of the MigratedLanguageFileSync tests.
sync(array $entries, bool $refresh=false, ?string $client_data_dir='default')
seedOverlay(array $entries, string $lang_key='de')
seedShipped(array $entries, string $lang_key='de', string $module=self::MODULE)
shippedPo(string $lang_key='de', string $module=self::MODULE)
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 overlayMo(string $base_path)
The compiled catalog of the revision current names; a non-existing path if there is none.
static writeOverlayRevision(string $base_path, TranslationCatalog $catalog)
Compiles $catalog into a revision of the overlay $base_path and points current to it,...
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
A directory of language files with a path and a prefix for them.