19declare(strict_types=1);
35 private function probeLock(
string $lock_file):
int
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)
45 $this->assertTrue($result[
'finished'],
'the non-blocking lock probe itself must never hang: ' . $result[
'output']);
47 return (
int) $result[
'exit_code'];
50 private function nestedLockChildCode(): string
59 require_once $root .
'/vendor/composer/vendor/autoload.php';
62 public function getPrefix(): string
66 public function getPath(): string
70 public function getSuffix(): string
74 public function isLocal(): bool
80 $client_data_dir = __CLIENT_DATA_DIR__;
83 $result = MigratedLanguageFileSync::withOverlayLock(
88 function () use ($manager, $client_data_dir, &
$log) {
91 return MigratedLanguageFileSync::withOverlayLock(
96 function () use (&
$log) {
99 return 'nested-return';
104 $log[
'result'] = $result;
105 foreach (
$log as $key => $value) {
106 echo $key .
':' . $value .
"\n";
111 [
'__ROOT__',
'__CLIENT_DATA_DIR__'],
112 [var_export(ILIAS_ABSOLUTE_PATH,
true), var_export($this->client_data_dir,
true)],
121 public function testANestedWithOverlayLockForTheSameModuleReturnsInsteadOfDeadlocking(): void
129 'a nested withOverlayLock() call for the same module must return instead of hanging: ' . $result[
'output']
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']);
140 public function testAConcurrentProcessCannotAcquireTheLockWhileTheCallbackRunsButCanAfterwards(): void
145 $probe_during =
null;
146 MigratedLanguageFileSync::withOverlayLock(
150 $this->client_data_dir,
151 function () use (&$probe_during, $lock_file):
void {
152 $probe_during = $this->probeLock($lock_file);
155 $probe_after = $this->probeLock($lock_file);
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');
165 public function testTheLockIsReleasedAfterAnExceptionInTheCallback(): void
171 MigratedLanguageFileSync::withOverlayLock(
175 $this->client_data_dir,
176 static function ():
void {
177 throw new RuntimeException(
'boom');
180 $this->fail(
'Expected a RuntimeException');
181 }
catch (RuntimeException
$e) {
182 $this->assertSame(
'boom',
$e->getMessage());
185 $this->assertSame(0, $this->probeLock($lock_file),
'the lock must be released even after an exception in the callback');
192 public function testAcquiresARealLockViaTheReadOnlyFallbackWhenTheLockFileCannotBeOpenedForWriting(): void
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.');
198 mkdir(dirname($lock_file), 0775,
true);
200 chmod($lock_file, 0444);
203 $probe_during =
null;
205 MigratedLanguageFileSync::withOverlayLock(
209 $this->client_data_dir,
210 function () use (&$probe_during, &$ran, $lock_file):
void {
212 $probe_during = $this->probeLock($lock_file);
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');
219 chmod($lock_file, 0664);
227 public function testRunsWithoutALockOrDirectoryWhenTheManagerIsNull(): void
230 $result = MigratedLanguageFileSync::withOverlayLock(
234 $this->client_data_dir,
235 static function () use (&$ran):
string {
242 $this->assertTrue($ran);
243 $this->assertSame(
'ok', $result);
244 $this->assertDirectoryDoesNotExist($this->client_data_dir .
'/lang');
251 public function testRunsWithoutALockWhenTheModuleHasNoContributedDirectory(): void
254 MigratedLanguageFileSync::withOverlayLock(
258 $this->client_data_dir,
259 static function () use (&$ran):
void {
264 $this->assertTrue($ran);
265 $this->assertDirectoryDoesNotExist($this->client_data_dir .
'/lang');
272 public function testRunsWithoutALockWhenTheModuleIsNotShippedAndHasNoExistingOverlayDirectory(): void
275 MigratedLanguageFileSync::withOverlayLock(
279 $this->client_data_dir,
280 static function () use (&$ran):
void {
286 $this->assertTrue($ran);
287 $this->assertDirectoryDoesNotExist($this->client_data_dir .
'/lang');
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.