1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
<?php
declare(strict_types=1);
namespace malkusch\lock\Tests\mutex;
use malkusch\lock\exception\ExecutionOutsideLockException;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\exception\LockReleaseException;
use malkusch\lock\exception\TimeoutException;
use malkusch\lock\mutex\SpinlockMutex;
use phpmock\environment\SleepEnvironmentBuilder;
use phpmock\MockEnabledException;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class SpinlockMutexTest extends TestCase
{
use PHPMock;
#[\Override]
protected function setUp(): void
{
parent::setUp();
$sleepBuilder = new SleepEnvironmentBuilder();
$sleepBuilder->addNamespace(__NAMESPACE__);
$sleepBuilder->addNamespace('malkusch\lock\mutex');
$sleepBuilder->addNamespace('malkusch\lock\util');
$sleep = $sleepBuilder->build();
try {
$sleep->enable();
$this->registerForTearDown($sleep);
} catch (MockEnabledException $e) {
// workaround for burn testing
\assert($e->getMessage() === 'microtime is already enabled.Call disable() on the existing mock.');
}
}
/**
* @return SpinlockMutex&MockObject
*/
private function createSpinlockMutexMock(float $timeout = 3): SpinlockMutex
{
return $this->getMockBuilder(SpinlockMutex::class)
->setConstructorArgs(['test', $timeout])
->onlyMethods(['acquire', 'release'])
->getMock();
}
/**
* Tests failing to acquire the lock.
*/
public function testFailAcquireLock(): void
{
$this->expectException(LockAcquireException::class);
$mutex = $this->createSpinlockMutexMock();
$mutex->expects(self::any())
->method('acquire')
->willThrowException(new LockAcquireException());
$mutex->synchronized(static function () {
self::fail('execution is not expected');
});
}
/**
* Tests failing to acquire the lock due to a timeout.
*/
public function testAcquireTimesOut(): void
{
$this->expectException(TimeoutException::class);
$this->expectExceptionMessage('Timeout of 3.0 seconds exceeded');
$mutex = $this->createSpinlockMutexMock();
$mutex->expects(self::atLeastOnce())
->method('acquire')
->willReturn(false);
$mutex->synchronized(static function () {
self::fail('execution is not expected');
});
}
/**
* Tests executing code which exceeds the timeout fails.
*/
public function testExecuteTooLong(): void
{
$mutex = $this->createSpinlockMutexMock(0.5);
$mutex->expects(self::any())
->method('acquire')
->willReturn(true);
$mutex->expects(self::any())
->method('release')
->willReturn(true);
$this->expectException(ExecutionOutsideLockException::class);
$this->expectExceptionMessageMatches(
'/The code executed for 0\.5\d+ seconds. But the timeout is 0\.5 ' .
'seconds. The last 0\.0\d+ seconds were executed outside of the lock./'
);
$mutex->synchronized(static function () {
usleep(501 * 1000);
});
}
/**
* Tests executing code which barely doesn't hit the timeout.
*/
public function testExecuteBarelySucceeds(): void
{
$mutex = $this->createSpinlockMutexMock(0.5);
$mutex->expects(self::any())->method('acquire')->willReturn(true);
$mutex->expects(self::once())->method('release')->willReturn(true);
$mutex->synchronized(static function () {
usleep(499 * 1000);
});
}
/**
* Tests failing to release a lock.
*/
public function testFailReleasingLock(): void
{
$this->expectException(LockReleaseException::class);
$mutex = $this->createSpinlockMutexMock();
$mutex->expects(self::any())->method('acquire')->willReturn(true);
$mutex->expects(self::any())->method('release')->willReturn(false);
$mutex->synchronized(static function () {});
}
/**
* Tests executing exactly until the timeout will leave the key one more second.
*/
public function testExecuteTimeoutLeavesOneSecondForKeyToExpire(): void
{
$mutex = $this->createSpinlockMutexMock(0.2);
$mutex->expects(self::once())
->method('acquire')
->with(self::anything(), 1.2)
->willReturn(true);
$mutex->expects(self::once())->method('release')->willReturn(true);
$mutex->synchronized(static function () {
usleep(199 * 1000);
});
}
}
|