ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncLockRemoveOverlayTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
25
29class MigratedLanguageFileSyncLockRemoveOverlayTest extends MigratedLanguageFileSyncLockTestCase
30{
35 public function testRemoveOverlayRunsAndRemovesTheLockFileWhenOnlyTheLockFileIsLeft(): void
36 {
37 $overlay_base = MigratedPoFixture::overlayBase($this->client_data_dir, $this->directory, 'de');
38 mkdir(dirname($overlay_base), 0775, true);
39 file_put_contents(MigratedPoFixture::overlayLock($overlay_base), '');
40
41 MigratedLanguageFileSync::removeOverlay($this->manager, 'de', self::MODULE, $this->client_data_dir);
42
43 $this->assertFileDoesNotExist(MigratedPoFixture::overlayLock($overlay_base));
44 }
45
50 public function testANestedRemoveOverlayInsideWithOverlayLockDoesNotDeleteTheLockFileOrRetry(): void
51 {
52 $overlay_base = MigratedPoFixture::overlayBase($this->client_data_dir, $this->directory, 'de');
53 mkdir(dirname($overlay_base), 0775, true);
54 file_put_contents($overlay_base . '.po', 'PO');
55 MigratedPoFixture::writeOverlayRevision($overlay_base, MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Servus']));
56 file_put_contents(MigratedPoFixture::overlayLock($overlay_base), '');
57 clearstatcache(true, MigratedPoFixture::overlayLock($overlay_base));
58 $lock_inode_before = lstat(MigratedPoFixture::overlayLock($overlay_base))['ino'];
59
60 // Run in a child process with a hard timeout: without the re-entrancy guard this scenario is
61 // the very same same-process self-deadlock testANestedWithOverlayLockForTheSameModuleReturns...
62 // above guards against - the nested removeOverlay() would block forever on the very lock its
63 // own enclosing withOverlayLock() call already holds.
64 $result = $this->runPhpWithTimeout($this->nestedRemoveOverlayChildCode(), 5.0);
65
66 $this->assertTrue($result['finished'], 'a nested removeOverlay() must return instead of hanging: ' . $result['output']);
67 $this->assertSame(0, $result['exit_code'], $result['output']);
68
69 clearstatcache(true, $overlay_base . '.po');
70 clearstatcache(true, MigratedPoFixture::overlayMo($overlay_base));
71 clearstatcache(true, MigratedPoFixture::overlayLock($overlay_base));
72 $this->assertFileDoesNotExist($overlay_base . '.po', 'the nested removeOverlay() must still actually remove the overlay files');
73 $this->assertFileDoesNotExist(MigratedPoFixture::overlayMo($overlay_base));
74 $this->assertStringContainsString(
75 'AFTER_NESTED:' . $lock_inode_before . "\n",
76 $result['output'],
77 'a removeOverlay() nested inside an already-held lock must NOT delete or replace the lock file - the outer caller still relies on it'
78 );
79 $this->assertFileExists(MigratedPoFixture::overlayLock($overlay_base));
80 $this->assertSame(
81 $lock_inode_before,
82 lstat(MigratedPoFixture::overlayLock($overlay_base))['ino'],
83 'the lock file must stay the exact same one throughout - no retry/replace cycle for the nested call'
84 );
85 }
86
87 private function nestedRemoveOverlayChildCode(): string
88 {
89 $template = <<<'PHP'
94
95 $root = __ROOT__;
96 require_once $root . '/vendor/composer/vendor/autoload.php';
97
98 $directory = new class implements LanguageFileDirectory {
99 public function getPrefix(): string
100 {
101 return 'stest';
102 }
103 public function getPath(): string
104 {
105 return 'x/';
106 }
107 public function getSuffix(): string
108 {
109 return '';
110 }
111 public function isLocal(): bool
112 {
113 return false;
114 }
115 };
116 $manager = new LanguageFileDirectoryManager(new CustomizingLanguageFileDirectory(), $directory);
117 $client_data_dir = __CLIENT_DATA_DIR__;
118
119 MigratedLanguageFileSync::withOverlayLock(
120 $manager,
121 'de',
122 'stest',
123 $client_data_dir,
124 function () use ($manager, $client_data_dir): void {
125 MigratedLanguageFileSync::removeOverlay($manager, 'de', 'stest', $client_data_dir);
126 clearstatcache();
127 $lock = @lstat($client_data_dir . '/lang/stest/de/lock');
128 echo 'AFTER_NESTED:' . ($lock === false ? 'missing' : $lock['ino']) . "\n";
129 }
130 );
131 echo "done\n";
132 PHP;
133
134 return str_replace(
135 ['__ROOT__', '__CLIENT_DATA_DIR__'],
136 [var_export(ILIAS_ABSOLUTE_PATH, true), var_export($this->client_data_dir, true)],
137 $template
138 );
139 }
140
144 public function testASyncAfterRemoveOverlayCreatesANewLockFile(): void
145 {
146 $relative_path = 'components/ILIAS/Language/tests/ComponentTranslation/tmp-lock-shipped-' . bin2hex(random_bytes(4)) . '/';
148 $relative_path,
149 self::MODULE,
150 'de',
151 MigratedPoFixture::catalog(self::MODULE, ['greeting' => 'Hallo'])
152 );
153 $directory = MigratedPoFixture::directory(self::MODULE, $relative_path);
154 $manager = new LanguageFileDirectoryManager(new CustomizingLanguageFileDirectory(), $directory);
155 $overlay_base = MigratedPoFixture::overlayBase($this->client_data_dir, $directory, 'de');
156
157 try {
158 // a local change: a shipped value writes no overlay, and without an overlay no lock file is kept
159 MigratedLanguageFileSync::sync($manager, ILIAS_ABSOLUTE_PATH, 'de', self::MODULE, ['greeting' => 'Moin'], $this->client_data_dir);
160 $this->assertFileExists(MigratedPoFixture::overlayLock($overlay_base), 'sync() must leave a .lock file behind for later callers to serialize on');
161
162 MigratedLanguageFileSync::removeOverlay($manager, 'de', self::MODULE, $this->client_data_dir);
163 $this->assertFileDoesNotExist(MigratedPoFixture::overlayLock($overlay_base), 'removeOverlay() (uninstall) must have removed it');
164
165 MigratedLanguageFileSync::sync($manager, ILIAS_ABSOLUTE_PATH, 'de', self::MODULE, ['greeting' => 'Servus'], $this->client_data_dir);
166 $this->assertFileExists(MigratedPoFixture::overlayLock($overlay_base), 'a reinstall/resync must create a fresh .lock file');
167 } finally {
169 }
170 }
171}
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
Fixture of the MigratedLanguageFileSyncLock*Test classes: a throwaway client data directory and the h...
runPhpWithTimeout(string $code, float $timeout_seconds)
Runs $code in a child PHP process and kills it after $timeout_seconds, so a regression that deadlocks...
static removeShippedDirectory(string $relative_path)
static directory(string $prefix, string $path, bool $local=false)
static catalog(string $module, array $entries)
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 writeShippedPo(string $relative_path, string $module, string $lang_key, TranslationCatalog $catalog)
Writes $catalog as the shipped .po: a module only counts as migrated while that exists.
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 writeOverlayRevision(string $base_path, TranslationCatalog $catalog)
Compiles $catalog into a revision of the overlay $base_path and points current to it,...
A directory of language files with a path and a prefix for them.