ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncLockTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
25
29class MigratedLanguageFileSyncLockTest extends MigratedLanguageFileSyncLockTestCase
30{
35 private function probeLock(string $lock_file): int
36 {
37 // Mirrors acquireLock()'s own 'c' ?: 'r' fallback: a lock file that is only readable to this
38 // process (see the read-only-fallback test) must not make the probe report "could not even
39 // open it" (2) when the real question is only whether the lock itself is held (1) or free (0).
40 $code = sprintf(
41 '$h = @fopen(%1$s, "c") ?: @fopen(%1$s, "r"); if ($h === false) { exit(2); } exit(flock($h, LOCK_EX | LOCK_NB) ? 0 : 1);',
42 var_export($lock_file, true)
43 );
44 $result = $this->runPhpWithTimeout($code, 3.0);
45 $this->assertTrue($result['finished'], 'the non-blocking lock probe itself must never hang: ' . $result['output']);
46
47 return (int) $result['exit_code'];
48 }
49
50 private function nestedLockChildCode(): string
51 {
52 $template = <<<'PHP'
57
58 $root = __ROOT__;
59 require_once $root . '/vendor/composer/vendor/autoload.php';
60
61 $directory = new class implements LanguageFileDirectory {
62 public function getPrefix(): string
63 {
64 return 'stest';
65 }
66 public function getPath(): string
67 {
68 return 'x/';
69 }
70 public function getSuffix(): string
71 {
72 return '';
73 }
74 public function isLocal(): bool
75 {
76 return false;
77 }
78 };
79 $manager = new LanguageFileDirectoryManager(new CustomizingLanguageFileDirectory(), $directory);
80 $client_data_dir = __CLIENT_DATA_DIR__;
81
82 $log = [];
83 $result = MigratedLanguageFileSync::withOverlayLock(
84 $manager,
85 'de',
86 'stest',
87 $client_data_dir,
88 function () use ($manager, $client_data_dir, &$log) {
89 $log['outer'] = 1;
90
91 return MigratedLanguageFileSync::withOverlayLock(
92 $manager,
93 'de',
94 'stest',
95 $client_data_dir,
96 function () use (&$log) {
97 $log['inner'] = 1;
98
99 return 'nested-return';
100 }
101 );
102 }
103 );
104 $log['result'] = $result;
105 foreach ($log as $key => $value) {
106 echo $key . ':' . $value . "\n";
107 }
108 PHP;
109
110 return str_replace(
111 ['__ROOT__', '__CLIENT_DATA_DIR__'],
112 [var_export(ILIAS_ABSOLUTE_PATH, true), var_export($this->client_data_dir, true)],
113 $template
114 );
115 }
116
121 public function testANestedWithOverlayLockForTheSameModuleReturnsInsteadOfDeadlocking(): void
122 {
123 $this->seedLockDirectory();
124
125 $result = $this->runPhpWithTimeout($this->nestedLockChildCode(), 5.0);
126
127 $this->assertTrue(
128 $result['finished'],
129 'a nested withOverlayLock() call for the same module must return instead of hanging: ' . $result['output']
130 );
131 $this->assertSame(0, $result['exit_code'], $result['output']);
132 $this->assertStringContainsString('outer:1', $result['output']);
133 $this->assertStringContainsString('inner:1', $result['output']);
134 $this->assertStringContainsString('result:nested-return', $result['output']);
135 }
136
140 public function testAConcurrentProcessCannotAcquireTheLockWhileTheCallbackRunsButCanAfterwards(): void
141 {
142 $this->seedLockDirectory();
143 $lock_file = $this->lockFile();
144
145 $probe_during = null;
146 MigratedLanguageFileSync::withOverlayLock(
147 $this->manager,
148 'de',
149 self::MODULE,
150 $this->client_data_dir,
151 function () use (&$probe_during, $lock_file): void {
152 $probe_during = $this->probeLock($lock_file);
153 }
154 );
155 $probe_after = $this->probeLock($lock_file);
156
157 $this->assertSame(1, $probe_during, 'a concurrent process must not be able to lock the file while the callback runs');
158 $this->assertSame(0, $probe_after, 'the lock must be released once the callback returns');
159 }
160
165 public function testTheLockIsReleasedAfterAnExceptionInTheCallback(): void
166 {
167 $this->seedLockDirectory();
168 $lock_file = $this->lockFile();
169
170 try {
171 MigratedLanguageFileSync::withOverlayLock(
172 $this->manager,
173 'de',
174 self::MODULE,
175 $this->client_data_dir,
176 static function (): void {
177 throw new RuntimeException('boom');
178 }
179 );
180 $this->fail('Expected a RuntimeException');
181 } catch (RuntimeException $e) {
182 $this->assertSame('boom', $e->getMessage());
183 }
184
185 $this->assertSame(0, $this->probeLock($lock_file), 'the lock must be released even after an exception in the callback');
186 }
187
192 public function testAcquiresARealLockViaTheReadOnlyFallbackWhenTheLockFileCannotBeOpenedForWriting(): void
193 {
194 if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
195 $this->markTestSkipped('Root can always open the lock file for writing; the read-only fallback cannot be exercised.');
196 }
197 $lock_file = $this->lockFile();
198 mkdir(dirname($lock_file), 0775, true);
199 touch($lock_file);
200 chmod($lock_file, 0444);
201
202 try {
203 $probe_during = null;
204 $ran = false;
205 MigratedLanguageFileSync::withOverlayLock(
206 $this->manager,
207 'de',
208 self::MODULE,
209 $this->client_data_dir,
210 function () use (&$probe_during, &$ran, $lock_file): void {
211 $ran = true;
212 $probe_during = $this->probeLock($lock_file);
213 }
214 );
215
216 $this->assertTrue($ran, 'the callback must still run under the read-only-fallback lock');
217 $this->assertSame(1, $probe_during, 'the read-only fallback must still take a real, exclusive lock');
218 } finally {
219 chmod($lock_file, 0664);
220 }
221 }
222
227 public function testRunsWithoutALockOrDirectoryWhenTheManagerIsNull(): void
228 {
229 $ran = false;
230 $result = MigratedLanguageFileSync::withOverlayLock(
231 null,
232 'de',
233 self::MODULE,
234 $this->client_data_dir,
235 static function () use (&$ran): string {
236 $ran = true;
237
238 return 'ok';
239 }
240 );
241
242 $this->assertTrue($ran);
243 $this->assertSame('ok', $result);
244 $this->assertDirectoryDoesNotExist($this->client_data_dir . '/lang');
245 }
246
251 public function testRunsWithoutALockWhenTheModuleHasNoContributedDirectory(): void
252 {
253 $ran = false;
254 MigratedLanguageFileSync::withOverlayLock(
255 $this->manager,
256 'de',
257 'unknown_module',
258 $this->client_data_dir,
259 static function () use (&$ran): void {
260 $ran = true;
261 }
262 );
263
264 $this->assertTrue($ran);
265 $this->assertDirectoryDoesNotExist($this->client_data_dir . '/lang');
266 }
267
272 public function testRunsWithoutALockWhenTheModuleIsNotShippedAndHasNoExistingOverlayDirectory(): void
273 {
274 $ran = false;
275 MigratedLanguageFileSync::withOverlayLock(
276 $this->manager,
277 'de',
278 self::MODULE,
279 $this->client_data_dir,
280 static function () use (&$ran): void {
281 $ran = true;
282 },
283 ILIAS_ABSOLUTE_PATH
284 );
285
286 $this->assertTrue($ran);
287 $this->assertDirectoryDoesNotExist($this->client_data_dir . '/lang');
288 }
289}
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...
seedLockDirectory()
Forces withOverlayLock() to take a real lock without needing a shipped .po: the overlay directory alr...
A directory of language files with a path and a prefix for them.
$log
Definition: ltiauth.php:50