ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncDeltaTest.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{
40 #[DataProvider('refreshFlags')]
41 public function testSyncingOnlyShippedValuesWritesNoOverlay(bool $refresh): void
42 {
43 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
44 $shipped_before = file_get_contents($this->shippedPo());
45
46 $this->sync(['greeting' => 'Hallo', 'farewell' => 'Tschüss'], $refresh);
47
48 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
49 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($this->overlayBase()));
50 $this->assertFileDoesNotExist(MigratedPoFixture::overlayLock($this->overlayBase()));
51 $this->assertDirectoryDoesNotExist(dirname($this->overlayBase()));
52 $this->assertSame($shipped_before, file_get_contents($this->shippedPo()), 'the shipped .po is never written');
53 }
54
55 public static function refreshFlags(): array
56 {
57 return ['admin edit (refresh = false)' => [false], 'reconciling write (refresh = true)' => [true]];
58 }
59
61 {
62 $this->seedShipped(['greeting' => 'Hallo']);
63
64 $this->sync(['greeting' => 'Servus']);
65
66 $greeting = $this->overlayPo()->find(null, 'greeting');
67 $this->assertSame('Servus', $greeting->getTranslation());
68 $this->assertSame('Hallo', LocalChangeComments::getOriginal($greeting));
69 $this->assertMatchesRegularExpression(
70 '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/',
71 (string) LocalChangeComments::getLocalChange($greeting)
72 );
73 }
74
76 {
77 $this->seedShipped(['greeting' => 'Hallo']);
78
79 $this->sync(['greeting' => 'Hallo', 'custom' => 'Eigener Wert']);
80
81 $custom = $this->overlayPo()->find(null, 'custom');
82 $this->assertSame('Eigener Wert', $custom->getTranslation());
83 $this->assertNull(LocalChangeComments::getOriginal($custom));
84 $this->assertNotNull(LocalChangeComments::getLocalChange($custom));
85 }
86
93 {
94 $this->seedShipped([
95 'unchanged' => ['value' => 'Hello', 'fuzzy' => true],
96 'changed' => ['value' => 'Bye', 'fuzzy' => true],
97 'reviewed' => ['value' => 'Hallo'],
98 ]);
99
100 $this->sync(['unchanged' => 'Hello', 'changed' => 'Tschüss', 'reviewed' => 'Hallo']);
101
102 // the shipped values (and their fuzzy flag) stay in the shipped .po, only the changed value enters the overlay
103 // - never fuzzy
104 $overlay = $this->overlayPo();
105 $this->assertNull($overlay->find(null, 'unchanged'));
106 $this->assertFalse($overlay->find(null, 'changed')->hasFlag('fuzzy'));
107 $this->assertNull($overlay->find(null, 'reviewed'));
108 }
109
116 {
117 $this->seedShipped(['greeting' => '<script>alert(1)</script>Hallo']);
118
119 $this->sync(['greeting' => '<script>alert(1)</script>Hallo']);
120
121 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
122 }
123
129 {
130 $catalog = MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Hallo']);
131 $catalog->find(self::MODULE, 'greeting')->addExtractedComment('shown on the login page');
132 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
133
134 $this->sync(['greeting' => 'Servus']);
135
136 $this->assertSame(['shown on the login page'], $this->overlayPo()->find(null, 'greeting')->getExtractedComments());
137 }
138
140 {
141 $catalog = MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Hallo']);
142 $catalog->setHeader('Language', 'de');
143 $catalog->setHeader('Plural-Forms', 'nplurals=2; plural=(n != 1);');
144 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
145
146 // a local change - with only shipped values there is no overlay (delta)
147 $this->sync(['greeting' => 'Servus']);
148
149 $this->assertSame('de', $this->overlayPo()->getHeader('Language'));
150 $this->assertSame('nplurals=2; plural=(n != 1);', $this->overlayPo()->getHeader('Plural-Forms'));
151 }
152
158 {
159 $this->seedShipped(['greeting' => 'Hallo, neu']);
160 $this->seedOverlay(['greeting' => ['value' => 'Hallo', 'original' => 'Hallo']]);
161
162 $this->sync(['greeting' => 'Servus']);
163
164 $greeting = $this->overlayPo()->find(null, 'greeting');
165 $this->assertSame('Servus', $greeting->getTranslation());
166 $this->assertSame('Hallo', LocalChangeComments::getOriginal($greeting));
167 $this->assertNotNull(LocalChangeComments::getLocalChange($greeting));
168 }
169
171 {
172 $this->seedShipped([
173 'kept' => ['value' => 'Hello', 'fuzzy' => true],
174 'edited' => ['value' => 'Bye', 'fuzzy' => true],
175 ]);
176 $this->seedOverlay([
177 'kept' => ['value' => 'Hello', 'original' => 'Hello', 'fuzzy' => true],
178 'edited' => ['value' => 'Bye', 'original' => 'Bye', 'fuzzy' => true],
179 ]);
180
181 $this->sync(['kept' => 'Hello', 'edited' => 'Tschüss']);
182
183 // the kept shipped value (fuzzy in the shipped .po) leaves the overlay, the edited one is no longer fuzzy
184 $overlay = $this->overlayPo();
185 $this->assertNull($overlay->find(null, 'kept'));
186 $this->assertFalse($overlay->find(null, 'edited')->hasFlag('fuzzy'));
187 }
188
190 {
191 $this->seedShipped(['greeting' => 'Hallo']);
192 $this->seedOverlay([
193 'greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z'],
194 ]);
195
196 $this->sync(['greeting' => 'Hallo']);
197
198 // the shipped value again is no local change: the entry, and with it the now empty overlay, is removed
199 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
200 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($this->overlayBase()));
201 $this->assertFalse(MigratedLanguageFileSync::loadModuleTranslations(
202 $this->manager,
203 'de',
204 self::MODULE,
205 $this->client_data_dir
206 )['greeting']['local_change']);
207 }
208
214 {
215 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
216 $this->seedOverlay([
217 'greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z'],
218 'farewell' => ['value' => 'Tschüss', 'original' => 'Tschüss'],
219 ]);
220
221 $this->sync(['greeting' => 'Servus', 'farewell' => 'Pfiat di']);
222
223 $overlay = $this->overlayPo();
224 $this->assertSame('2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($overlay->find(null, 'greeting')));
225 $farewell_change = LocalChangeComments::getLocalChange($overlay->find(null, 'farewell'));
226 $this->assertNotNull($farewell_change);
227 $this->assertNotSame('2020-01-01T00:00:00Z', $farewell_change);
228 }
229
235 #[DataProvider('refreshFlags')]
237 {
238 $this->seedShipped(['greeting' => 'Hallo']);
239 $legacy_overlay = MigratedPoFixture::catalog(self::MODULE, [
240 'greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z'],
241 ]);
242 $legacy_overlay->find(self::MODULE, 'greeting')->addExtractedComment('shown on the login page');
243 MigratedPoFixture::writePair($this->overlayBase(), $legacy_overlay);
244 $this->assertNotNull($this->overlayPo()->find(self::MODULE, 'greeting'), 'precondition: seeded with the module as msgctxt');
245
246 // a genuine local change is kept alive by re-sending the exact same value
247 $this->sync(['greeting' => 'Servus'], $refresh);
248
249 $this->assertNull($this->overlayPo()->find(self::MODULE, 'greeting'), 'no longer stored under the module context');
250 $migrated = $this->overlayPo()->find(null, 'greeting');
251 $this->assertNotNull($migrated);
252 $this->assertSame('Servus', $migrated->getTranslation());
253 $this->assertSame('Hallo', LocalChangeComments::getOriginal($migrated));
254 $this->assertSame('2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($migrated));
255 if (!$refresh) {
256 // refresh=true re-baselines extracted comments from the shipped entry (which has none
257 // here) - only an ordinary admin edit (refresh=false) keeps the overlay's own comment
258 $this->assertSame(['shown on the login page'], $migrated->getExtractedComments());
259 }
260 }
261
268 {
269 $this->seedShipped(['unchanged_one' => 'A', 'unchanged_two' => 'B', 'changed' => 'C']);
270 // A full overlay as an old version would have written it: every entry present, most of them
271 // carrying exactly the shipped value with no local_change at all.
272 $this->seedOverlay([
273 'unchanged_one' => ['value' => 'A', 'original' => 'A'],
274 'unchanged_two' => ['value' => 'B', 'original' => 'B'],
275 'changed' => ['value' => 'C, lokal', 'original' => 'C', 'local_change' => '2020-01-01T00:00:00Z'],
276 ]);
277
278 $this->sync(['unchanged_one' => 'A', 'unchanged_two' => 'B', 'changed' => 'C, lokal'], true);
279
280 $this->assertSame(['changed'], $this->overlayIdentifiers());
281 $this->assertSame(
282 '2020-01-01T00:00:00Z',
283 LocalChangeComments::getLocalChange($this->overlayPo()->find(null, 'changed'))
284 );
285 }
286
288 {
289 $this->seedShipped(['greeting' => 'Hallo, überarbeitet']);
290 $this->seedOverlay(['greeting' => ['value' => 'Hallo', 'original' => 'Hallo']]);
291
292 $this->sync(['greeting' => 'Hallo, überarbeitet'], true);
293
294 // the new shipped value is served from the shipped .po, the outdated overlay entry is removed
295 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
296 $this->assertSame(
297 ['value' => 'Hallo, überarbeitet', 'local_change' => false, 'local_change_date' => null, 'original' => 'Hallo, überarbeitet'],
298 MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir)['greeting']
299 );
300 }
301
303 {
304 $this->seedShipped(['greeting' => 'Hallo, überarbeitet']);
305 $this->seedOverlay([
306 'greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z'],
307 ]);
308
309 $this->sync(['greeting' => 'Servus'], true);
310
311 $greeting = $this->overlayPo()->find(null, 'greeting');
312 $this->assertSame('Servus', $greeting->getTranslation());
313 $this->assertSame('Hallo, überarbeitet', LocalChangeComments::getOriginal($greeting));
314 $this->assertSame('2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($greeting));
315 }
316
323 {
324 $this->seedShipped(['greeting' => 'Hallo']);
325 $this->seedOverlay([
326 'greeting' => ['value' => 'Hallo', 'original' => 'Hallo'],
327 'dropped' => ['value' => 'Neu', 'original' => 'Alt', 'local_change' => '2020-01-01T00:00:00Z'],
328 ]);
329
330 $this->sync(['greeting' => 'Hallo', 'dropped' => 'Neu'], true);
331
332 $dropped = $this->overlayPo()->find(null, 'dropped');
333 $this->assertSame('Alt', LocalChangeComments::getOriginal($dropped));
334 $this->assertSame('2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($dropped));
335 }
336
344 #[DataProvider('refreshFuzzyCases')]
346 array $shipped,
347 array $overlay,
348 string $value,
349 ?bool $expected_fuzzy
350 ): void {
351 $this->seedShipped(['greeting' => $shipped]);
352 $this->seedOverlay(['greeting' => $overlay]);
353
354 $this->sync(['greeting' => $value], true);
355
356 // `null`: the value is the shipped one, so the entry (and its fuzzy flag) lives in the shipped .po only and the
357 // overlay is removed
358 if ($expected_fuzzy === null) {
359 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
360 return;
361 }
362 $this->assertSame($expected_fuzzy, $this->overlayPo()->find(null, 'greeting')->hasFlag('fuzzy'));
363 }
364
368 public static function refreshFuzzyCases(): array
369 {
370 return [
371 'shipped fuzzy, value is shipped' => [
372 ['value' => 'Hello', 'fuzzy' => true], ['value' => 'Hello', 'original' => 'Hello'], 'Hello', null,
373 ],
374 'shipped fuzzy, value differs' => [
375 ['value' => 'Hello', 'fuzzy' => true], ['value' => 'Hallo', 'original' => 'Hello', 'fuzzy' => true], 'Hallo', false,
376 ],
377 'shipped reviewed now, overlay still fuzzy' => [
378 ['value' => 'Hallo'], ['value' => 'Hallo', 'original' => 'Hello', 'fuzzy' => true], 'Hallo', null,
379 ],
380 ];
381 }
382
384 {
385 $this->seedShipped(['greeting' => 'Hallo', 'farewell' => 'Tschüss']);
386 // local changes, since shipped values are never in the overlay
387 $this->seedOverlay([
388 'greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z'],
389 'farewell' => ['value' => 'Pfiat di', 'original' => 'Tschüss', 'local_change' => '2020-01-01T00:00:00Z'],
390 ]);
391
392 $this->sync(['greeting' => 'Servus']);
393
394 $this->assertSame(['greeting'], $this->overlayIdentifiers());
395 $this->assertSame(['greeting' => 'Servus'], MigratedPoFixture::readMo(MigratedPoFixture::overlayMo($this->overlayBase())));
396 }
397
400 {
401 $this->seedShipped(['greeting' => 'Hallo']);
402 $this->seedOverlay(['greeting' => ['value' => 'Servus', 'original' => 'Hallo', 'local_change' => '2020-01-01T00:00:00Z']]);
403
404 $this->sync([]);
405
406 $this->assertFileDoesNotExist($this->overlayBase() . '.po');
407 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($this->overlayBase()));
408 $this->assertFileExists(MigratedPoFixture::overlayLock($this->overlayBase()), 'the lock file is only removed on uninstall');
409 $this->assertNotNull(MigratedPoFixture::readPo($this->shippedPo())->find(self::MODULE, 'greeting'));
410 }
411
417 {
418 $this->seedShipped(['greeting' => 'Hallo']);
419 $this->seedOverlay([
420 'greeting' => ['value' => 'Hallo', 'original' => 'Hallo'],
421 'foreign' => ['value' => 'Fremd', 'context' => 'other_module'],
422 ]);
423
424 // a local change keeps an overlay to look into (delta)
425 $this->sync(['greeting' => 'Servus']);
426
427 $this->assertNull($this->overlayPo()->find('other_module', 'foreign'));
428 }
429
435 {
436 $this->seedShipped(['b' => 'B']);
437
438 $this->sync(['b' => 'B', 'a' => 'A', 'C' => 'C', '9' => 'neun', '10' => 'zehn']);
439
440 // "b" carries the shipped value and is not written
441 $this->assertSame(['10', '9', 'C', 'a'], $this->overlayIdentifiers());
442 }
443}
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 ....
sync() computing the delta overlay: creation, admin edit, reconciling write (refresh),...
testEntriesOfAnotherContextInTheOldOverlayAreNotCarriedOver()
Entries of a different context (another module) are never taken over into this module's overlay: the ...
testSyncingNoEntriesRemovesTheOverlayButKeepsTheShippedFile()
An empty delta removes the overlay files - the shipped file and the lock file are kept.
testTheOverlayTakesTheExtractedCommentsOfTheShippedEntry()
Extracted comments ("#. ...", context for translators) are part of the shipped file and are carried i...
testEntriesAreWrittenSortedByIdentifier()
Sorted byte-wise by identifier, independent of the order of $entries - keeps the file diff-stable.
testTheDeltaIsComparedAgainstTheRawShippedValueNotTheMarkupCleanedOne()
The delta is compared against the RAW shipped .po value (TranslationCatalog::fromPoFile()),...
testRefreshRecomputesFuzzyFromTheShippedEntry(array $shipped, array $overlay, string $value, ?bool $expected_fuzzy)
On refresh fuzzy is recomputed from the shipped file: set exactly when the value is the shipped value...
testTheLocalChangeTimestampOfAnUntouchedEntrySurvivesAnUnrelatedWrite()
A full-replace write re-sends every value; an entry whose value did not change must keep its existing...
testSyncingOnlyShippedValuesWritesNoOverlay(bool $refresh)
A module whose values are all shipped ones needs no overlay: neither file, lock nor directory is crea...
testALegacyEntryWithTheModuleAsMsgctxtIsMigratedToNoContextPreservingItsBookkeeping(bool $refresh)
An overlay of an earlier version carried the module as msgctxt on every entry (see seedOverlay()); th...
testCreatingTheOverlayKeepsShippedFuzzyOnlyForTheUnchangedShippedValue()
A shipped "fuzzy" (an unreviewed value, usually the English fallback) must survive the very first syn...
testRefreshKeepsTheOriginalOfAnEntryTheShippedPoNoLongerContains()
An entry the shipped .po no longer contains (kept by the caller as a real local change,...
testAnAdminEditNeverMovesTheOriginal()
refresh = false (an ordinary admin edit): the baseline "original" must never move - even when the shi...
testALegacyFullOverlayShrinksToTheDeltaOnTheNextReconcilingWrite()
An overlay written by the pre-delta design carried EVERY entry of the module.
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 writePair(string $base_path, TranslationCatalog $catalog)
Writes "<base>.po" and, like MigratedLanguageFileSync::sync(), its compiled revision named by current...
static catalog(string $module, array $entries)
static readMo(string $file)
static writePo(string $file, TranslationCatalog $catalog)
static overlayLock(string $base_path)
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)
A directory of language files with a path and a prefix for them.