ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncRemarksTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28use PHPUnit\Framework\TestCase;
29
36{
38
39 private const string MODULE = 'rtest';
40
41 private string $fixture_directory;
42 private string $client_data_dir;
45
46 protected function setUp(): void
47 {
48 // never the build of the installation, see MigratedPoFixture::resetRuntime()
51
52 if (!defined('ILIAS_ABSOLUTE_PATH')) {
53 define('ILIAS_ABSOLUTE_PATH', realpath(__DIR__ . '/../../../../../'));
54 }
55
56 $this->fixture_directory = __DIR__ . '/tmp-remarks-sync-fixtures-' . bin2hex(random_bytes(4));
57 mkdir($this->fixture_directory, 0775, true);
58 $this->client_data_dir = sys_get_temp_dir() . '/ilias_mlfs_remarks_test_' . bin2hex(random_bytes(4));
59 mkdir($this->client_data_dir, 0775, true);
60
61 $this->directory = MigratedPoFixture::directory(
62 self::MODULE,
63 'components/ILIAS/Language/tests/ComponentTranslation/' . basename($this->fixture_directory) . '/'
64 );
65 $this->manager = new LanguageFileDirectoryManager(new CustomizingLanguageFileDirectory(), $this->directory);
66 }
67
68 protected function tearDown(): void
69 {
70 MigratedPoFixture::removeDirectory($this->fixture_directory);
71 MigratedPoFixture::removeDirectory($this->client_data_dir);
72
74 }
75
76 private function shippedPo(string $lang_key = 'de'): string
77 {
78 return $this->fixture_directory . '/' . self::MODULE . '_' . $lang_key . '.po';
79 }
80
81 private function overlayBase(string $lang_key = 'de'): string
82 {
83 return MigratedPoFixture::overlayBase($this->client_data_dir, $this->directory, $lang_key);
84 }
85
86 private function overlayPo(): TranslationCatalog
87 {
88 return MigratedPoFixture::readPo($this->overlayBase() . '.po');
89 }
90
91 private function overlayEntry(string $identifier): ?TranslationEntry
92 {
93 return $this->overlayPo()->find(null, $identifier);
94 }
95
100 private function seedShipped(array $entries, string $header = 'nplurals=2; plural=(n != 1);'): void
101 {
102 $catalog = new TranslationCatalog();
103 $catalog->setHeader('Content-Type', 'text/plain; charset=UTF-8');
104 $catalog->setHeader('Plural-Forms', $header);
105 foreach ($entries as $identifier => $value) {
106 $entry = new TranslationEntry(self::MODULE, (string) $identifier);
107 if (is_array($value)) {
108 $entry->setPlural($identifier . '_plural', $value);
109 } else {
110 $entry->translate($value);
111 }
112 $catalog->add($entry);
113 }
114 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
115 }
116
121 private function sync(array $entries, ?array $remarks = null): void
122 {
123 MigratedLanguageFileSync::sync(
124 $this->manager,
125 ILIAS_ABSOLUTE_PATH,
126 'de',
127 self::MODULE,
128 $entries,
129 $this->client_data_dir,
130 false,
131 null,
132 $remarks
133 );
134 }
135
142 {
143 $this->seedShipped(['greeting' => 'Hallo']);
144
145 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
146
147 $entry = $this->overlayEntry('greeting');
148 $this->assertNotNull($entry, 'the remark-only entry must be in the overlay .po');
149 $this->assertSame('', $entry->getTranslation(), 'empty msgstr - the value itself is unchanged');
150 $this->assertSame('Bitte prüfen', LocalChangeComments::getRemark($entry));
151 $this->assertArrayNotHasKey('greeting', MigratedPoFixture::readMo(MigratedPoFixture::overlayMo($this->overlayBase())));
152 }
153
159 {
160 $this->seedShipped(['greeting' => 'Hallo']);
161 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
162
163 $translations = MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir);
164 $this->assertSame('Hallo', $translations['greeting']['value'] ?? null);
165 $this->assertFalse($translations['greeting']['local_change']);
166
167 $local_changes = MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
168 $this->assertArrayNotHasKey('greeting', $local_changes, 'a remark-only entry is not a local change of the value');
169 }
170
172 {
173 $this->seedShipped(['greeting' => 'Hallo']);
174 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
175
176 $remarks = MigratedLanguageFileSync::loadRemarks($this->manager, 'de', self::MODULE, $this->client_data_dir);
177
178 $this->assertSame(['greeting' => 'Bitte prüfen'], $remarks);
179 }
180
182 {
183 $this->seedShipped(['greeting' => 'Hallo']);
184
185 $this->assertSame([], MigratedLanguageFileSync::loadRemarks($this->manager, 'de', self::MODULE, $this->client_data_dir));
186 }
187
193 {
194 $this->seedShipped(['greeting' => 'Hallo']);
195 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
196
197 $this->sync(['greeting' => 'Servus']); // remarks omitted (null) - only the value changes
198
199 $entry = $this->overlayEntry('greeting');
200 $this->assertNotNull($entry);
201 $this->assertSame('Servus', $entry->getTranslation());
202 $this->assertSame('Bitte prüfen', LocalChangeComments::getRemark($entry), 'the remark must survive a remarks-less sync()');
203 }
204
210 {
211 $this->seedShipped(['greeting' => 'Hallo']);
212 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
213 $this->assertNotNull($this->overlayEntry('greeting'), 'precondition');
214
215 $this->sync(['greeting' => 'Hallo'], []);
216
217 $this->assertFalse(is_file($this->overlayBase() . '.po'), 'nothing left to keep - the whole overlay is removed');
218 }
219
225 {
226 $this->seedShipped(['greeting' => 'Hallo']);
227 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Bitte prüfen']);
228
229 $this->sync(['greeting' => 'Servus'], ['greeting' => 'Bitte prüfen']);
230
231 $entry = $this->overlayEntry('greeting');
232 $this->assertNotNull($entry);
233 $this->assertSame('Servus', $entry->getTranslation());
234 $this->assertSame('Bitte prüfen', LocalChangeComments::getRemark($entry));
235 $this->assertSame('Servus', MigratedPoFixture::readMo(MigratedPoFixture::overlayMo($this->overlayBase()))['greeting'] ?? null);
236 }
237
243 {
244 $this->seedShipped(['item' => ['Eintrag', 'Einträge']]);
245
246 $this->sync(
247 ['item [0]' => 'Eintrag', 'item [1]' => 'Einträge'],
248 ['item' => 'Bitte prüfen']
249 );
250
251 $entry = $this->overlayEntry('item');
252 $this->assertNotNull($entry);
253 $this->assertTrue($entry->isPlural());
254 $this->assertSame(['', ''], $entry->getPluralTranslations(), 'remark only - both forms stay empty');
255 $this->assertSame('Bitte prüfen', LocalChangeComments::getRemark($entry));
256 $this->assertArrayNotHasKey('item', MigratedPoFixture::readMo(MigratedPoFixture::overlayMo($this->overlayBase())));
257 }
258
264 {
265 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
266 $this->sync(['greeting' => 'Hallo', 'farewell' => 'Tschüss'], ['greeting' => 'Alte Bemerkung']);
267
268 MigratedLanguageFileSync::setRemarks(
269 $this->manager,
270 'de',
271 self::MODULE,
272 ['greeting' => 'Neue Bemerkung', 'farewell' => 'Auch geprüft'],
273 $this->client_data_dir
274 );
275
276 $remarks = MigratedLanguageFileSync::loadRemarks($this->manager, 'de', self::MODULE, $this->client_data_dir);
277 // order is not a guaranteed contract of loadRemarks() - only the content is
278 ksort($remarks);
279 $this->assertSame(['farewell' => 'Auch geprüft', 'greeting' => 'Neue Bemerkung'], $remarks);
280 }
281
283 {
284 $this->seedShipped(['greeting' => 'Hallo']);
285 $this->sync(['greeting' => 'Hallo'], ['greeting' => 'Alte Bemerkung']);
286
287 MigratedLanguageFileSync::setRemarks($this->manager, 'de', self::MODULE, ['greeting' => null], $this->client_data_dir);
288
289 $this->assertSame([], MigratedLanguageFileSync::loadRemarks($this->manager, 'de', self::MODULE, $this->client_data_dir));
290 }
291
298 {
299 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
300
301 // no logger is registered in this test process - the warning falls back to error_log(),
302 // which would otherwise print to this test's own output and mark it risky
303 $previous_error_log = ini_set('error_log', sys_get_temp_dir() . '/ilias_mlfs_remarks_test_error.log');
304 try {
305 $this->sync(
306 ['greeting' => 'Hallo', 'farewell' => 'Tschüss'],
307 ['greeting' => "Ung\xFCltig", 'farewell' => 'Gültige Bemerkung']
308 );
309 } finally {
310 ini_set('error_log', (string) $previous_error_log);
311 }
312
313 $remarks = MigratedLanguageFileSync::loadRemarks($this->manager, 'de', self::MODULE, $this->client_data_dir);
314 $this->assertArrayNotHasKey('greeting', $remarks);
315 $this->assertSame('Gültige Bemerkung', $remarks['farewell'] ?? null);
316 }
317
325 {
326 $this->seedShipped(['greeting' => 'Hallo']);
327
328 $this->expectException(RuntimeException::class);
329 $this->expectExceptionMessageMatches('/would not be valid UTF-8/');
330 $this->sync(['greeting' => "Ung\xFCltig"]);
331 }
332
338 {
339 $catalog = new TranslationCatalog();
340 $catalog->setHeader('Plural-Forms', 'nplurals=2; plural=(n != 1);');
341 $fuzzy_singular = new TranslationEntry(self::MODULE, 'fuzzy_one');
342 $fuzzy_singular->translate('Noch nicht übersetzt');
343 $fuzzy_singular->addFlag('fuzzy');
344 $catalog->add($fuzzy_singular);
345 $clean_singular = new TranslationEntry(self::MODULE, 'clean');
346 $clean_singular->translate('Übersetzt');
347 $catalog->add($clean_singular);
348 $fuzzy_plural = new TranslationEntry(self::MODULE, 'item');
349 $fuzzy_plural->setPlural('items', ['Eintrag', 'Einträge']);
350 $fuzzy_plural->addFlag('fuzzy');
351 $catalog->add($fuzzy_plural);
352 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
353
354 $fuzzy = MigratedLanguageFileSync::loadShippedFuzzyIdentifiers($this->shippedPo(), self::MODULE);
355
356 $this->assertSame(
357 ['fuzzy_one' => true, 'item [0]' => true, 'item [1]' => true],
358 $fuzzy
359 );
360 }
361
363 {
364 $this->seedShipped(['greeting' => 'Hallo']);
365
366 $this->assertSame([], MigratedLanguageFileSync::loadShippedFuzzyIdentifiers($this->shippedPo(), self::MODULE));
367 }
368}
trait RestoresProcessState
Whatever a test does to the process (locale, LANGUAGE/LC_*, LOCPATH, the static state of the runtime ...
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
One message of a TranslationCatalog (a msgctxt/msgid/msgstr block of a .po file).
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 ....
MigratedLanguageFileSync's admin remarks (sync()'s $remarks, loadRemarks(), setRemarks()): a remark i...
testNullRemarksKeepsTheExistingOverlayRemark()
$remarks = null keeps the overlay's existing remarks: a value-only sync() (ordinary GUI save,...
testSyncDropsAnInvalidUtf8RemarkButWritesEveryOtherEntry()
A remark that is not valid UTF-8 (e.g.
testAPluralMessagesRemarkBelongsToItsBaseIdentifier()
A plural message's remark belongs to its base identifier (never a form key) - even when only the rema...
testEmptyArrayRemarksRemovesEveryRemarkAndDropsTheOverlayIfNothingElseIsLeft()
$remarks = [] removes every remark of the module - and, once the value is back to shipped too,...
testLoadShippedFuzzyIdentifiersListsFuzzyEntriesOnly()
Only entries actually flagged "fuzzy" in the shipped .po are listed - a plural message's every form k...
testSyncThrowsInsteadOfWritingAnOverlayThatWouldNotBeValidUtf8()
A safety net independent of the remark check above: if the compiled overlay content would not be vali...
testARemarkOnAnUnchangedValueCreatesARemarkOnlyEntryAbsentFromTheCompiledMo()
A remark on a key whose value is exactly the shipped one still belongs in the overlay - as a "remark ...
seedShipped(array $entries, string $header='nplurals=2;plural=(n !=1);')
testSetRemarksChangesOnlyTheGivenIdentifiersRemarks()
setRemarks() changes exactly the given identifiers' remarks under the overlay lock,...
testARealValueChangeOnTheSameKeyKeepsItsRemark()
A later real value change on the very same key keeps its remark - the "remark only" entry is simply r...
testARemarkOnlyEntryIsSkippedByLoadModuleTranslationsAndLoadLocalChanges()
loadModuleTranslations() (what ilLanguage ultimately reads) and loadLocalChanges() (the admin GUI's p...
static directory(string $prefix, string $path, bool $local=false)
static readMo(string $file)
static writePo(string $file, TranslationCatalog $catalog)
static removeDirectory(string $directory)
static overlayMo(string $base_path)
The compiled catalog of the revision current names; a non-existing path if there is none.
static readPo(string $file)
static overlayBase(string $client_data_dir, LanguageFileDirectory $directory, string $lang_key)
The overlay of $directory's module for $lang_key without the .po extension.
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
A directory of language files with a path and a prefix for them.