Skip to content

Commit 5232f87

Browse files
committed
test(lock): cover atomic lock behavior
Signed-off-by: memleakd <[email protected]>
1 parent d8b403b commit 5232f87

5 files changed

Lines changed: 282 additions & 0 deletions

File tree

tests/system/Cache/Handlers/MemcachedHandlerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ protected function setUp(): void
5151

5252
protected function tearDown(): void
5353
{
54+
if (! isset($this->handler)) {
55+
return;
56+
}
57+
5458
foreach (self::getKeyArray() as $key) {
5559
$this->handler->delete($key);
5660
}

tests/system/Cache/Handlers/PredisHandlerTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use CodeIgniter\Cache\CacheFactory;
1717
use CodeIgniter\CLI\CLI;
1818
use CodeIgniter\I18n\Time;
19+
use CodeIgniter\Lock\LockStoreInterface;
1920
use Config\Cache;
2021
use PHPUnit\Framework\Attributes\Group;
2122

@@ -45,10 +46,18 @@ protected function setUp(): void
4546

4647
$this->config = new Cache();
4748
$this->handler = CacheFactory::getHandler($this->config, 'predis');
49+
50+
if ($this->handler::class !== PredisHandler::class) {
51+
$this->markTestSkipped('Predis connection not available.');
52+
}
4853
}
4954

5055
protected function tearDown(): void
5156
{
57+
if (! isset($this->handler)) {
58+
return;
59+
}
60+
5261
foreach (self::getKeyArray() as $key) {
5362
$this->handler->delete($key);
5463
}
@@ -104,6 +113,21 @@ public function testSave(): void
104113
$this->assertTrue($this->handler->save(self::$key1, 'value'));
105114
}
106115

116+
public function testLockOperations(): void
117+
{
118+
$handler = $this->handler;
119+
120+
$this->assertInstanceOf(LockStoreInterface::class, $handler);
121+
$this->assertTrue($handler->acquireLock(self::$key1, 'owner1', 60));
122+
$this->assertFalse($handler->acquireLock(self::$key1, 'owner2', 60));
123+
$this->assertSame('owner1', $handler->getLockOwner(self::$key1));
124+
$this->assertFalse($handler->releaseLock(self::$key1, 'owner2'));
125+
$this->assertTrue($handler->refreshLock(self::$key1, 'owner1', 120));
126+
$this->assertTrue($handler->releaseLock(self::$key1, 'owner1'));
127+
$this->assertNull($handler->getLockOwner(self::$key1));
128+
$this->assertTrue($handler->forceReleaseLock(self::$key1));
129+
}
130+
107131
public function testSavePermanent(): void
108132
{
109133
$this->assertTrue($this->handler->save(self::$key1, 'value', 0));

tests/system/Cache/Handlers/RedisHandlerTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use CodeIgniter\Cache\CacheFactory;
1717
use CodeIgniter\CLI\CLI;
1818
use CodeIgniter\I18n\Time;
19+
use CodeIgniter\Lock\LockStoreInterface;
1920
use Config\Cache;
2021
use PHPUnit\Framework\Attributes\DataProvider;
2122
use PHPUnit\Framework\Attributes\Group;
@@ -54,6 +55,10 @@ protected function setUp(): void
5455

5556
protected function tearDown(): void
5657
{
58+
if (! isset($this->handler)) {
59+
return;
60+
}
61+
5762
foreach (self::getKeyArray() as $key) {
5863
$this->handler->delete($key);
5964
}
@@ -109,6 +114,21 @@ public function testSave(): void
109114
$this->assertTrue($this->handler->save(self::$key1, 'value'));
110115
}
111116

117+
public function testLockOperations(): void
118+
{
119+
$handler = $this->handler;
120+
121+
$this->assertInstanceOf(LockStoreInterface::class, $handler);
122+
$this->assertTrue($handler->acquireLock(self::$key1, 'owner1', 60));
123+
$this->assertFalse($handler->acquireLock(self::$key1, 'owner2', 60));
124+
$this->assertSame('owner1', $handler->getLockOwner(self::$key1));
125+
$this->assertFalse($handler->releaseLock(self::$key1, 'owner2'));
126+
$this->assertTrue($handler->refreshLock(self::$key1, 'owner1', 120));
127+
$this->assertTrue($handler->releaseLock(self::$key1, 'owner1'));
128+
$this->assertNull($handler->getLockOwner(self::$key1));
129+
$this->assertTrue($handler->forceReleaseLock(self::$key1));
130+
}
131+
112132
public function testSavePermanent(): void
113133
{
114134
$this->assertTrue($this->handler->save(self::$key1, 'value', 0));

tests/system/Config/ServicesTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use CodeIgniter\HTTP\URI;
3333
use CodeIgniter\Images\ImageHandlerInterface;
3434
use CodeIgniter\Language\Language;
35+
use CodeIgniter\Lock\LockManager;
3536
use CodeIgniter\Pager\Pager;
3637
use CodeIgniter\Router\RouteCollection;
3738
use CodeIgniter\Router\Router;
@@ -46,6 +47,7 @@
4647
use CodeIgniter\Validation\Validation;
4748
use CodeIgniter\View\Cell;
4849
use CodeIgniter\View\Parser;
50+
use Config\Cache;
4951
use Config\Database as DatabaseConfig;
5052
use Config\Exceptions;
5153
use Config\Security as SecurityConfig;
@@ -107,6 +109,32 @@ public function testNewFileLocator(): void
107109
$this->assertInstanceOf(FileLocator::class, $actual);
108110
}
109111

112+
public function testNewLocks(): void
113+
{
114+
$actual = Services::locks();
115+
$this->assertInstanceOf(LockManager::class, $actual);
116+
}
117+
118+
public function testLocksWithCustomCacheIsNotShared(): void
119+
{
120+
$config = new Cache();
121+
$config->file['storePath'] = WRITEPATH . 'cache/ServicesLockTest';
122+
123+
if (! is_dir($config->file['storePath'])) {
124+
mkdir($config->file['storePath'], 0777, true);
125+
}
126+
127+
try {
128+
$custom = Services::cache($config, false);
129+
130+
$this->assertInstanceOf(LockManager::class, Services::locks($custom));
131+
$this->assertNotSame(Services::locks($custom), Services::locks());
132+
} finally {
133+
delete_files($config->file['storePath'], false, true);
134+
rmdir($config->file['storePath']);
135+
}
136+
}
137+
110138
public function testNewUnsharedFileLocator(): void
111139
{
112140
$actual = Services::locator(false);

tests/system/Lock/LockTest.php

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Lock;
15+
16+
use CodeIgniter\Cache\CacheFactory;
17+
use CodeIgniter\Exceptions\InvalidArgumentException;
18+
use CodeIgniter\I18n\Time;
19+
use CodeIgniter\Lock\Exceptions\LockException;
20+
use CodeIgniter\Test\CIUnitTestCase;
21+
use Config\Cache;
22+
use PHPUnit\Framework\Attributes\Group;
23+
24+
/**
25+
* @internal
26+
*/
27+
#[Group('Others')]
28+
final class LockTest extends CIUnitTestCase
29+
{
30+
private Cache $config;
31+
private LockManager $locks;
32+
33+
protected function setUp(): void
34+
{
35+
parent::setUp();
36+
37+
helper('filesystem');
38+
39+
$this->config = new Cache();
40+
$this->config->file['storePath'] = WRITEPATH . 'cache/LockTest';
41+
42+
if (! is_dir($this->config->file['storePath'])) {
43+
mkdir($this->config->file['storePath'], 0777, true);
44+
}
45+
46+
$this->locks = new LockManager(CacheFactory::getHandler($this->config, 'file'));
47+
}
48+
49+
protected function tearDown(): void
50+
{
51+
parent::tearDown();
52+
53+
Time::setTestNow();
54+
55+
if (is_dir($this->config->file['storePath'])) {
56+
delete_files($this->config->file['storePath'], false, true);
57+
rmdir($this->config->file['storePath']);
58+
}
59+
}
60+
61+
public function testLockCanBeAcquiredAndReleased(): void
62+
{
63+
$lock = $this->locks->create('reports.daily-export', 60);
64+
65+
$this->assertTrue($lock->acquire());
66+
$this->assertFileExists($this->lockFile('reports.daily-export'));
67+
$this->assertTrue($lock->isAcquired());
68+
$this->assertTrue($lock->release());
69+
$this->assertFalse($lock->isAcquired());
70+
$this->assertTrue($this->locks->create('reports.daily-export', 60)->acquire());
71+
}
72+
73+
public function testCompetingLockCannotBeAcquiredUntilReleased(): void
74+
{
75+
$first = $this->locks->create('reports.daily-export', 60);
76+
$second = $this->locks->create('reports.daily-export', 60);
77+
78+
$this->assertTrue($first->acquire());
79+
$this->assertFalse($second->acquire());
80+
81+
$this->assertTrue($first->release());
82+
$this->assertTrue($second->acquire());
83+
}
84+
85+
public function testSameLockCannotBeAcquiredTwice(): void
86+
{
87+
$lock = $this->locks->create('reports.daily-export', 60);
88+
89+
$this->assertTrue($lock->acquire());
90+
$this->assertFalse($lock->acquire());
91+
}
92+
93+
public function testExpiredLockCanBeAcquiredByNewOwner(): void
94+
{
95+
Time::setTestNow('2026-01-01 12:00:00');
96+
97+
$first = $this->locks->create('imports.customer-feed', 10);
98+
99+
$this->assertTrue($first->acquire());
100+
101+
Time::setTestNow('2026-01-01 12:00:11');
102+
103+
$second = $this->locks->create('imports.customer-feed', 10);
104+
105+
$this->assertTrue($second->acquire());
106+
$this->assertFalse($first->isAcquired());
107+
}
108+
109+
public function testOnlyOwnerCanReleaseLock(): void
110+
{
111+
$first = $this->locks->create('payments.settlement', 60);
112+
$second = $this->locks->create('payments.settlement', 60);
113+
114+
$this->assertTrue($first->acquire());
115+
$this->assertFalse($second->release());
116+
$this->assertTrue($first->isAcquired());
117+
}
118+
119+
public function testForceReleaseIgnoresOwner(): void
120+
{
121+
$first = $this->locks->create('payments.settlement', 60);
122+
$second = $this->locks->create('payments.settlement', 60);
123+
124+
$this->assertTrue($first->acquire());
125+
$this->assertTrue($second->forceRelease());
126+
$this->assertTrue($second->acquire());
127+
}
128+
129+
public function testRestoreCanReleaseOwnedLock(): void
130+
{
131+
$lock = $this->locks->create('jobs.unique', 60);
132+
133+
$this->assertTrue($lock->acquire());
134+
135+
$restored = $this->locks->restore('jobs.unique', $lock->owner(), 60);
136+
137+
$this->assertTrue($restored->isAcquired());
138+
$this->assertTrue($restored->release());
139+
$this->assertFalse($lock->isAcquired());
140+
}
141+
142+
public function testRefreshRequiresOwner(): void
143+
{
144+
$first = $this->locks->create('cache.rebuild', 60);
145+
$second = $this->locks->create('cache.rebuild', 60);
146+
147+
$this->assertTrue($first->acquire());
148+
$this->assertTrue($first->refresh(120));
149+
$this->assertFalse($second->refresh(120));
150+
}
151+
152+
public function testRunReleasesLockAfterCallback(): void
153+
{
154+
$lock = $this->locks->create('notifications.send', 60);
155+
156+
$this->assertSame('sent', $lock->run(static fn (): string => 'sent'));
157+
$this->assertTrue($this->locks->create('notifications.send', 60)->acquire());
158+
}
159+
160+
public function testRunReturnsNullWhenLockCannotBeAcquired(): void
161+
{
162+
$first = $this->locks->create('notifications.send', 60);
163+
$second = $this->locks->create('notifications.send', 60);
164+
165+
$this->assertTrue($first->acquire());
166+
$this->assertNull($second->run(static fn (): string => 'sent'));
167+
}
168+
169+
public function testLogicalNamesCanContainReservedCacheCharacters(): void
170+
{
171+
$lock = $this->locks->create('tenant:1/payments/{settlement}', 60);
172+
173+
$this->assertTrue($lock->acquire());
174+
}
175+
176+
public function testEmptyLockNameIsRejected(): void
177+
{
178+
$this->expectException(InvalidArgumentException::class);
179+
$this->expectExceptionMessage('Lock name cannot be empty.');
180+
181+
$this->locks->create('');
182+
}
183+
184+
public function testNonPositiveTtlIsRejected(): void
185+
{
186+
$this->expectException(InvalidArgumentException::class);
187+
$this->expectExceptionMessage('Lock TTL must be a positive integer.');
188+
189+
$this->locks->create('reports.daily-export', 0);
190+
}
191+
192+
public function testUnsupportedCacheHandlerThrows(): void
193+
{
194+
$locks = new LockManager(CacheFactory::getHandler($this->config, 'dummy'));
195+
196+
$this->expectException(LockException::class);
197+
$this->expectExceptionMessage('does not support locks');
198+
199+
$locks->create('reports.daily-export');
200+
}
201+
202+
private function lockFile(string $name): string
203+
{
204+
return rtrim($this->config->file['storePath'], '\\/') . '/lock_' . hash('sha256', $name);
205+
}
206+
}

0 commit comments

Comments
 (0)