ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
LanguageInstallationManagerMigratedAfterWriteTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
31use PHPUnit\Framework\Attributes\DataProvider;
32use PHPUnit\Framework\TestCase;
33
35{
37 {
38 $this->shipPo(['greeting' => 'Hallo']);
39 $this->collation_rows = [
40 ['module' => 'common', 'lang_array' => serialize(['yes' => 'Ja'])],
41 ['module' => self::MODULE, 'lang_array' => 'a:1:{s:8:"greeting";s:5:"Hal'],
42 ];
43 $invalidated = [];
44 $cwd = getcwd();
45
46 try {
47 $this->manager(null, static function (string $lang_key) use (&$invalidated): void {
48 $invalidated[] = $lang_key;
49 })->insertLanguageForInstallation('de');
50 $this->fail('Expected a LanguageDataNotSavedException');
52 $this->assertSame(409, $e->getCode());
53 $this->assertStringContainsString("'pilot'", $e->getMessage());
54 $this->assertStringContainsString("'de'", $e->getMessage());
55 }
56
57 $this->assertSame([], $invalidated, 'no cache invalidation for data that was not saved correctly');
58 $this->assertFileDoesNotExist($this->overlayBase() . '.po', 'no overlay for data that was not saved correctly');
59 $this->assertSame($cwd, getcwd(), 'the working directory is restored on the exception path');
60 // counts only the collation query of the lng_modules write; migratedModuleRemarks() issues its own lng_data SELECT beforehand
61 $collation_query = array_values(array_filter(
62 $this->queries,
63 static fn(string $q): bool => str_starts_with($q, 'SELECT') && str_contains($q, 'lang_array') && str_contains($q, 'lng_modules')
64 ));
65 $this->assertCount(1, $collation_query);
66 $this->assertStringContainsString("module IN ('pilot')", $collation_query[0]);
67 }
68
70 {
71 $this->writeLang('lang/ilias_de.lang', ['common#:#yes#:#Ja']);
72 $this->collation_rows = [['module' => 'common', 'lang_array' => serialize(false)]];
73
74 $this->expectException(LanguageDataNotSavedException::class);
75 $this->manager()->insertLanguageForInstallation('de');
76 }
77
79 {
80 $this->shipPo(['greeting' => 'Hallo']);
81 $overlay_po = $this->overlayBase() . '.po';
82 $calls = [];
83
84 $this->manager(null, function (string $lang_key) use (&$calls, $overlay_po): void {
85 $calls[] = [$lang_key, count(array_filter($this->queries, static fn(string $q): bool => str_starts_with($q, 'INSERT INTO lng_modules'))), is_file($overlay_po)];
86 })->insertLanguageForInstallation('de');
87
88 $this->assertSame([['de', 1, false]], $calls, 'once, after the lng_modules write, before the overlay sync');
89 }
90
92 {
93 $this->expectManagerWarning();
94 $this->shipPo(['greeting' => 'Hallo']);
95 $this->seedOverlay(['greeting' => ['value' => 'Hallo', 'original' => 'Hallo']]);
96
97 $this->manager(null, static function (): void {
98 throw new RuntimeException('cache is down');
99 })->insertLanguageForInstallation('de');
100
101 $this->assertSame(['greeting' => 'Hallo'], $this->lngModules()[self::MODULE]);
102 // a shipped value writes no file; the sync still running shows in the removal of an outdated overlay
103 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($this->overlayBase()), 'the overlay sync still ran: it removed the outdated overlay');
104 $this->assertStringContainsString('cache is down', $this->loggedWarnings());
105 }
106
108 {
109 $this->expectErrorLog();
110 $this->shipPo(['greeting' => 'Hallo']);
111
112 $this->manager(null, static function (): void {
113 throw new RuntimeException('cache is down');
114 }, true, false)->insertLanguageForInstallation('de');
115
116 $this->assertStringContainsString('cache is down', $this->errorLog());
117 $this->assertSame([], $this->logged_warnings);
118 }
119
121 {
122 $this->expectErrorLog();
123 $this->shipPo(['greeting' => 'Hallo']);
124 $logger = $this->createStub(\ILIAS\Logging\Logger\LoggerInterface::class);
125 $logger->method('warning')->willThrowException(new RuntimeException('logger is down'));
126
127 $this->manager(null, static function (): void {
128 throw new RuntimeException('cache is down');
129 }, true, true, $logger)->insertLanguageForInstallation('de');
130
131 $this->assertSame(['greeting' => 'Hallo'], $this->lngModules()[self::MODULE]);
132 $this->assertStringContainsString('cache is down', $this->errorLog());
133 }
134
140 {
141 $this->expectManagerWarning();
142 $this->shipPo(['greeting' => 'Hallo']);
143 // a local change: only then an overlay has to be written (delta)
144 $this->db_local_changes = [self::MODULE => ['greeting' => 'Servus']];
145 rmdir($this->root . '/client-data');
146 mkdir($this->root . '/client-data');
147 file_put_contents($this->root . '/client-data/lang', 'not a directory');
148
149 set_error_handler(static fn(): bool => true, E_WARNING);
150 try {
151 $unwritten = $this->manager()->insertLanguageForInstallation('de');
152 } finally {
153 restore_error_handler();
154 }
155
156 $this->assertSame(['greeting' => 'Servus'], $this->lngModules()[self::MODULE]);
157 $this->assertStringContainsString('Could not sync migrated language file for module "pilot"', $this->loggedWarnings());
158 $this->assertSame([self::MODULE], $unwritten, 'the failed module is returned so callers can report it');
159 }
160
165 #[DataProvider('writePaths')]
166 public function testEveryWritePathReturnsOnlyTheModulesWhoseOverlayCouldNotBeWritten(string $method, bool $writes_overlay): void
167 {
168 if ($writes_overlay) {
169 $this->expectManagerWarning();
170 }
171 $this->shipPo(['greeting' => 'Hallo']);
172 $this->writeLang('lang/ilias_de.lang', ['common#:#yes#:#Ja']);
173 $this->writeLang('lang/customizing/ilias_de.lang.local', ['pilot#:#greeting#:#Servus']);
174 $this->db_entries = [self::MODULE => ['greeting' => 'Hallo'], 'common' => ['yes' => 'Ja']];
175 file_put_contents($this->root . '/client-data/lang', 'not a directory');
176
177 set_error_handler(static fn(): bool => true, E_WARNING);
178 try {
179 $unwritten = $this->manager()->$method('de');
180 } finally {
181 restore_error_handler();
182 }
183
184 // "remove local changes" leaves only shipped values: no overlay to write, nothing can fail
185 $this->assertSame($writes_overlay ? [self::MODULE] : [], $unwritten);
186 }
187
188 #[DataProvider('writePaths')]
189 public function testEveryWritePathReturnsNothingWhenTheOverlayWasWritten(string $method, bool $writes_overlay): void
190 {
191 $this->shipPo(['greeting' => 'Hallo']);
192 $this->writeLang('lang/ilias_de.lang', ['common#:#yes#:#Ja']);
193 $this->writeLang('lang/customizing/ilias_de.lang.local', ['pilot#:#greeting#:#Servus']);
194 $this->db_entries = [self::MODULE => ['greeting' => 'Hallo'], 'common' => ['yes' => 'Ja']];
195
196 $this->assertSame([], $this->manager()->$method('de'));
197 // only a path that keeps the customizing value writes an overlay
198 $this->assertSame($writes_overlay, is_file(MigratedPoFixture::overlayMo($this->overlayBase())));
199 }
200
204 public static function writePaths(): array
205 {
206 return [
207 'install/update' => ['insertLanguageForInstallation', true],
208 'remove local changes' => ['insertLanguageForRemovingLocalChanges', false],
209 'apply local changes' => ['insertLanguageForApplyingLocalChanges', true],
210 ];
211 }
212
217 {
218 $this->shipPo(['greeting' => 'Hallo']);
219
220 $this->assertSame([], $this->manager(static fn(): ?string => null)->insertLanguageForInstallation('de'));
221 }
222
228 {
229 $this->shipPo(['greeting' => 'Hallo']);
230 $blocked = $this->root . '/blocked';
231 mkdir($blocked);
232 file_put_contents($blocked . '/lang', 'not a directory');
233 $target = $this->root . '/client-data';
234 $manager = $this->manager(static function () use (&$target): ?string {
235 return $target;
236 });
237
238 $this->assertSame([], $manager->findUnwritableOverlayDirectories(['de']));
239
240 $target = $blocked;
241 $this->assertSame([$blocked . '/lang/pilot/de'], $manager->findUnwritableOverlayDirectories(['de']));
242 $this->assertSame([], $manager->findUnwritableOverlayDirectories(['en']), 'pilot is not migrated for en');
243
244 $target = null;
245 $this->assertSame([], $manager->findUnwritableOverlayDirectories(['de']));
246 }
247
249 {
250 $this->shipPo(['greeting' => 'Hallo']);
251 $elsewhere = $this->root . '/elsewhere';
252 mkdir($elsewhere);
253 $target = null;
254 $manager = $this->manager(static function () use (&$target): ?string {
255 return $target;
256 });
257
258 $target = $elsewhere;
259 // a local change, so there is an overlay to write (delta)
260 $this->db_local_changes = [self::MODULE => ['greeting' => 'Servus']];
261 $manager->insertLanguageForInstallation('de');
262
263 $this->assertFileExists(MigratedPoFixture::overlayMo($elsewhere . '/lang/pilot/de/pilot_de'));
264 $this->assertDirectoryDoesNotExist($this->root . '/client-data/lang');
265 }
266
267 #[DataProvider('noClientDataDir')]
268 public function testWithoutClientDataDirTheDatabaseIsWrittenButNoOverlay(bool $with_resolver): void
269 {
270 $this->shipPo(['greeting' => 'Hallo']);
271
272 $this->manager(static fn(): ?string => null, null, $with_resolver)->insertLanguageForInstallation('de');
273
274 $this->assertSame(['greeting' => 'Hallo'], $this->lngModules()[self::MODULE]);
275 $this->assertDirectoryDoesNotExist($this->root . '/client-data/lang');
276 }
277
281 public static function noClientDataDir(): array
282 {
283 return ['resolver returns null' => [true], 'no resolver' => [false]];
284 }
285
291 {
292 $this->expectManagerWarning();
293 $this->writeLang('lang/ilias_de.lang', [
294 'common#:#yes#:#Ja',
295 'common#:#yes#:#Doppelt',
296 'common#:#no#:#Nein',
297 ]);
298
299 $this->manager()->insertLanguageForInstallation('de');
300
301 $this->assertSame(['yes' => 'Ja', 'no' => 'Nein'], $this->lngModules()['common']);
302 $this->assertSame(['value' => 'Ja', 'local_change' => null], $this->lngData()['common|yes']);
303 $this->assertStringContainsString('Duplicate language entries for language "de"', $this->loggedWarnings());
304 $this->assertStringContainsString('common#:#yes', $this->loggedWarnings());
305 }
306
308 {
309 $this->writeLang('lang/ilias_de.lang', ['common#:#yes#:#Ja']);
310 $this->writeLang('lang/customizing/ilias_de.lang.local', ['common#:#yes#:#Jawohl']);
311
312 $this->manager()->insertLanguageForInstallation('de');
313
314 $this->assertSame(['yes' => 'Jawohl'], $this->lngModules()['common']);
315 $this->assertStringNotContainsString('Duplicate', $this->loggedWarnings());
316 }
317}
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 ....
The module array just written to lng_modules could not be read back - almost always a wrong collation...
Write access to the language installation: installing, flushing and registering languages in the data...
testFindUnwritableOverlayDirectoriesChecksTheResolvedClientDataDir()
Checked against the client data directory the instance maintains the overlay in (resolved at call tim...
testEveryWritePathReturnsOnlyTheModulesWhoseOverlayCouldNotBeWritten(string $method, bool $writes_overlay)
The same failure is returned by all three write paths - and only the migrated module whose overlay fa...
testADuplicateInOneFileKeepsTheFirstOccurrenceAndIsReported()
Shipped ilias_fa.lang/ilias_nl.lang contain duplicates: the first occurrence wins,...
testWithoutClientDataDirNothingIsReturnedAsUnwritten()
No overlay is maintained without client data directory - that is not a failure to report.
testAnOverlaySyncFailureIsLoggedAndTheDatabaseWriteStays()
Root-proof overlay failure (a file where the overlay's "lang" directory must be created): logged,...
Fixtures of the LanguageInstallationManager tests for modules migrated to PO/MO ("pilot").
errorLog()
What error_log() wrote during this test so far - PHPUnit redirects error_log to a capture file per te...
manager(?\Closure $client_data_dir_resolver=null, ?\Closure $language_cache_invalidator=null, bool $with_resolver=true, bool $with_logger=true, ?LoggerInterface $logger=null)
static overlayMo(string $base_path)
The compiled catalog of the revision current names; a non-existing path if there is none.
A directory of language files with a path and a prefix for them.
Read-only access to the installed languages (database) and the available or valid language files (fil...
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.