File: DoubleCheckedLockingTest.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 (155 lines) | stat: -rw-r--r-- 4,191 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
<?php

declare(strict_types=1);

namespace malkusch\lock\Tests\util;

use malkusch\lock\mutex\Mutex;
use malkusch\lock\util\DoubleCheckedLocking;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class DoubleCheckedLockingTest extends TestCase
{
    /** @var Mutex&MockObject */
    private $mutex;

    #[\Override]
    protected function setUp(): void
    {
        parent::setUp();

        $this->mutex = $this->createMock(Mutex::class);
    }

    /**
     * Tests that the lock will not be acquired for a failing test.
     */
    public function testCheckFailsAcquiresNoLock(): void
    {
        $this->mutex->expects(self::never())->method('synchronized');

        $checkedLocking = new DoubleCheckedLocking($this->mutex, static function (): bool {
            return false;
        });

        $result = $checkedLocking->then(static function (): void {
            self::fail();
        });

        // Failed check should return false.
        self::assertFalse($result); // @phpstan-ignore staticMethod.impossibleType
    }

    /**
     * Tests that the check and execution are in the same lock.
     */
    public function testLockedCheckAndExecution(): void
    {
        $lock = 0;
        $check = 0;

        $this->mutex->expects(self::once())
            ->method('synchronized')
            ->willReturnCallback(static function (\Closure $block) use (&$lock) {
                ++$lock;
                $result = $block();
                ++$lock;

                return $result;
            });

        $checkedLocking = new DoubleCheckedLocking($this->mutex, static function () use (&$lock, &$check): bool {
            if ($check === 1) {
                self::assertSame(1, $lock);
            }
            ++$check;

            return true;
        });

        $result = $checkedLocking->then(static function () use (&$lock) {
            self::assertSame(1, $lock);

            return 'test';
        });

        self::assertSame(2, $check);

        // Synchronized code should return a test string.
        self::assertSame('test', $result);
    }

    /**
     * Tests that the code is not executed if the first or second check fails.
     *
     * @param \Closure(): bool $check
     *
     * @dataProvider provideCodeNotExecutedCases
     */
    #[DataProvider('provideCodeNotExecutedCases')]
    public function testCodeNotExecuted(\Closure $check): void
    {
        $this->mutex->expects(self::any())
            ->method('synchronized')
            ->willReturnCallback(static function (\Closure $block) {
                return $block();
            });

        $checkedLocking = new DoubleCheckedLocking($this->mutex, $check);
        $result = $checkedLocking->then(static function (): void {
            self::fail();
        });

        // Each failed check should return false.
        self::assertFalse($result); // @phpstan-ignore staticMethod.impossibleType
    }

    /**
     * Returns checks for testCodeNotExecuted().
     *
     * @return iterable<list<mixed>>
     */
    public static function provideCodeNotExecutedCases(): iterable
    {
        yield [static function (): bool {
            return false;
        }];

        $checkCounter = 0;

        yield [static function () use (&$checkCounter): bool {
            $result = $checkCounter === 0;
            ++$checkCounter;

            return $result;
        }];
    }

    /**
     * Tests that the code executed if the checks are true.
     */
    public function testCodeExecuted(): void
    {
        $this->mutex->expects(self::once())
            ->method('synchronized')
            ->willReturnCallback(static function (\Closure $block) {
                return $block();
            });

        $checkedLocking = new DoubleCheckedLocking($this->mutex, static function (): bool {
            return true;
        });

        $executed = false;
        $result = $checkedLocking->then(static function () use (&$executed) {
            $executed = true;

            return 'test';
        });

        self::assertTrue($executed);
        self::assertSame('test', $result);
    }
}