File: SpinlockMutexTest.php

package info (click to toggle)
php-malkusch-lock 2.3.0%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 452 kB
  • sloc: php: 2,124; makefile: 19
file content (157 lines) | stat: -rw-r--r-- 4,815 bytes parent folder | download | duplicates (2)
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);
        });
    }
}