From 1a7969d445d3bb3fe0b81f639216452cb9dc4064 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:26:43 +0200 Subject: [PATCH] refactor: refactored critical sections --- README.md | 10 +-- src/CriticalSection.php | 54 ++++++++---- src/CriticalSectionInterface.php | 27 ------ src/CriticalSectionStaticFactory.php | 56 +++++++++++++ .../CouldNotEnterCriticalSection.php | 2 + .../CouldNotLeaveCriticalSection.php | 2 + src/Exception/CriticalSectionException.php | 4 +- src/NonCriticalSection.php | 40 --------- .../Lock/CriticalSection.php} | 27 +++--- src/WrappingCriticalSection.php | 43 ++-------- tests/CriticalSectionTest.php | 46 +++++++---- tests/CriticalSectionTestCase.php | 82 +++++++++++++++++++ tests/NonCriticalSectionTest.php | 38 --------- .../Lock/CriticalSectionTest.php} | 60 ++++++-------- tests/WrappingCriticalSectionTest.php | 69 +++++++--------- 15 files changed, 293 insertions(+), 267 deletions(-) delete mode 100644 src/CriticalSectionInterface.php create mode 100644 src/CriticalSectionStaticFactory.php delete mode 100644 src/NonCriticalSection.php rename src/{SymfonyLockCriticalSection.php => Symfony/Lock/CriticalSection.php} (50%) create mode 100644 tests/CriticalSectionTestCase.php delete mode 100644 tests/NonCriticalSectionTest.php rename tests/{SymfonyLockCriticalSectionTest.php => Symfony/Lock/CriticalSectionTest.php} (54%) diff --git a/README.md b/README.md index e0ef766..ccac319 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Critical section based on `symfony/lock` -The [CriticalSection](./src/CriticalSection.php) is a simple object that handles the critical section overhead for you +[The `CriticalSection`](./src/CriticalSection.php) is a simple object that handles the critical section overhead for you and lets you focus on the actual code. ```php @@ -14,7 +14,7 @@ $criticalOutput = CriticalSection::withLock($lock)(fn () => 'This was critical.' var_dump($criticalOutput); ``` -You can wrap critical sections one inside the other thanks to the [WrappingCriticalSection](./src/WrappingCriticalSection.php). +You can wrap critical sections one inside the other thanks to [the `WrappingCriticalSection`](./src/WrappingCriticalSection.php). This makes it easy to combine multiple locks, for example. ```php @@ -45,7 +45,7 @@ var_dump($criticalOutput); ## Do you need to accept only locked resources? -Use [`LockedResource`](./src/LockedResource.php) if you need to be sure that you are not processing resource outside it's critical section. +Use [the `LockedResource`](./src/LockedResource.php) if you need to be sure that you are not processing resource outside it's critical section. ```php namespace PetrKnap\CriticalSection; @@ -53,7 +53,7 @@ namespace PetrKnap\CriticalSection; use Symfony\Component\Lock\NoLock; /** @param Locked $resource */ -function f(Locked $resource) { +function f(LockedResource $resource) { echo $resource->value; } @@ -64,7 +64,7 @@ CriticalSection::withLock($lock)(fn () => f($resource)); ## Does your critical section work with database? -Use [`doctrine/dbal`](https://packagist.org/packages/doctrine/dbal) and its `transactional` method. +Use [the `doctrine/dbal`](https://packagist.org/packages/doctrine/dbal) and its `transactional` method. ```php /** @var PetrKnap\CriticalSection\CriticalSectionInterface $criticalSection */ diff --git a/src/CriticalSection.php b/src/CriticalSection.php index c6fada7..0c43d13 100644 --- a/src/CriticalSection.php +++ b/src/CriticalSection.php @@ -4,30 +4,52 @@ namespace PetrKnap\CriticalSection; -use Symfony\Component\Lock\LockInterface; +use Throwable; -/** @template T */ -final class CriticalSection +abstract class CriticalSection { - /** @return NonCriticalSection */ - public static function create(): NonCriticalSection - { - return new NonCriticalSection(); - } + use CriticalSectionStaticFactory; - /** @return WrappingCriticalSection */ - public static function withLock(LockInterface $lock, bool $isBlocking = true): WrappingCriticalSection - { - return self::create()->withLock($lock, $isBlocking); + protected function __construct( + protected readonly bool $isBlocking, + ) { } /** - * @param array $locks + * @phpstan-ignore-next-line Template type T ... is not referenced in a parameter. + * + * @template T of mixed * - * @return WrappingCriticalSection + * @param (callable(mixed ...$args): T)|callable $criticalSection + * @param mixed ...$args will be forwarded to {@link $criticalSection} + * + * @return T|null returned by {@link $criticalSection} or null when it is occupied (non-blocking mode only) + * + * @throws Exception\CouldNotEnterCriticalSection + * @throws Exception\CouldNotLeaveCriticalSection + * @throws Throwable from {@link $criticalSection} */ - public static function withLocks(array $locks, bool $isBlocking = true): WrappingCriticalSection + public function __invoke(callable $criticalSection, mixed ...$args): mixed { - return self::create()->withLocks($locks, $isBlocking); + if ($this->enter() === false) { + return null; + } + try { + return $criticalSection(...$args); + } finally { + $this->leave(); + } } + + /** + * @return bool false if it is occupied (non-blocking mode only) + * + * @throws Exception\CouldNotEnterCriticalSection + */ + abstract protected function enter(): bool; + + /** + * @throws Exception\CouldNotLeaveCriticalSection + */ + abstract protected function leave(): void; } diff --git a/src/CriticalSectionInterface.php b/src/CriticalSectionInterface.php deleted file mode 100644 index b366078..0000000 --- a/src/CriticalSectionInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - $arguments + */ + public static function __callStatic(string $name, array $arguments): mixed + { + return match ($name) { + 'withLock', 'withLocks' => self::nonCritical(canEnter: true)->$name(...$arguments), + default => NotImplemented::throw("Static method `{$name}`"), + }; + } + + private static function nonCritical(CriticalSection $wrappedCriticalSection = null, bool $canEnter = null): WrappingCriticalSection + { + return new class ($wrappedCriticalSection, (bool) $canEnter) extends WrappingCriticalSection { + public function __construct( + private readonly CriticalSection|null $wrappedCriticalSection, + private readonly bool $canEnter, + ) { + parent::__construct($wrappedCriticalSection, false); + } + + public function enter(): bool + { + return $this->canEnter; + } + + public function leave(): void + { + } + + protected function getWrappingReferenceOrNull(): CriticalSection|null + { + return $this->wrappedCriticalSection; + } + }; + } +} diff --git a/src/Exception/CouldNotEnterCriticalSection.php b/src/Exception/CouldNotEnterCriticalSection.php index 13c775a..bbba78b 100644 --- a/src/Exception/CouldNotEnterCriticalSection.php +++ b/src/Exception/CouldNotEnterCriticalSection.php @@ -4,8 +4,10 @@ namespace PetrKnap\CriticalSection\Exception; +use PetrKnap\Shorts\ExceptionWrapper; use RuntimeException; final class CouldNotEnterCriticalSection extends RuntimeException implements CriticalSectionException { + use ExceptionWrapper; } diff --git a/src/Exception/CouldNotLeaveCriticalSection.php b/src/Exception/CouldNotLeaveCriticalSection.php index dea1756..cb25449 100644 --- a/src/Exception/CouldNotLeaveCriticalSection.php +++ b/src/Exception/CouldNotLeaveCriticalSection.php @@ -4,8 +4,10 @@ namespace PetrKnap\CriticalSection\Exception; +use PetrKnap\Shorts\ExceptionWrapper; use RuntimeException; final class CouldNotLeaveCriticalSection extends RuntimeException implements CriticalSectionException { + use ExceptionWrapper; } diff --git a/src/Exception/CriticalSectionException.php b/src/Exception/CriticalSectionException.php index 9e72f12..a3f03a5 100644 --- a/src/Exception/CriticalSectionException.php +++ b/src/Exception/CriticalSectionException.php @@ -4,6 +4,8 @@ namespace PetrKnap\CriticalSection\Exception; -interface CriticalSectionException +use Throwable; + +interface CriticalSectionException extends Throwable { } diff --git a/src/NonCriticalSection.php b/src/NonCriticalSection.php deleted file mode 100644 index 73d04b4..0000000 --- a/src/NonCriticalSection.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -final class NonCriticalSection extends WrappingCriticalSection -{ - /** - * @internal Use {@link CriticalSection::create()} - * - * @param CriticalSectionInterface|null $wrappedCriticalSection - */ - public function __construct( - private ?CriticalSectionInterface $wrappedCriticalSection = null, - private bool $canEnter = true, - ) { - parent::__construct($wrappedCriticalSection); - } - - protected function enter(): bool - { - return $this->canEnter; - } - - protected function leave(): void - { - } - - /** @return CriticalSectionInterface|null */ - protected function getWrappingReferenceOrNull(): ?CriticalSectionInterface - { - return $this->wrappedCriticalSection; - } -} diff --git a/src/SymfonyLockCriticalSection.php b/src/Symfony/Lock/CriticalSection.php similarity index 50% rename from src/SymfonyLockCriticalSection.php rename to src/Symfony/Lock/CriticalSection.php index 25655c4..25a9ef5 100644 --- a/src/SymfonyLockCriticalSection.php +++ b/src/Symfony/Lock/CriticalSection.php @@ -2,29 +2,24 @@ declare(strict_types=1); -namespace PetrKnap\CriticalSection; +namespace PetrKnap\CriticalSection\Symfony\Lock; -use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection; -use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection; +use PetrKnap\CriticalSection\CriticalSection as Base; +use PetrKnap\CriticalSection\Exception; +use PetrKnap\CriticalSection\WrappingCriticalSection; use Symfony\Component\Lock\Exception\LockAcquiringException; use Symfony\Component\Lock\Exception\LockConflictedException; use Symfony\Component\Lock\Exception\LockReleasingException; use Symfony\Component\Lock\LockInterface; -/** - * @template T - * - * @extends WrappingCriticalSection - */ -final class SymfonyLockCriticalSection extends WrappingCriticalSection +final class CriticalSection extends WrappingCriticalSection { - /** @param CriticalSectionInterface|null $wrappedCriticalSection */ protected function __construct( - ?CriticalSectionInterface $wrappedCriticalSection, - private LockInterface $lock, - private bool $isBlocking, + private readonly LockInterface $lock, + Base|null $wrappedCriticalSection, + bool $isBlocking, ) { - parent::__construct($wrappedCriticalSection); + parent::__construct($wrappedCriticalSection, $isBlocking); } protected function enter(): bool @@ -32,7 +27,7 @@ protected function enter(): bool try { return $this->lock->acquire(blocking: $this->isBlocking); } catch (LockConflictedException | LockAcquiringException $reason) { - throw new CouldNotEnterCriticalSection(previous: $reason); + throw new Exception\CouldNotEnterCriticalSection($reason); } } @@ -41,7 +36,7 @@ protected function leave(): void try { $this->lock->release(); } catch (LockReleasingException $reason) { - throw new CouldNotLeaveCriticalSection(previous: $reason); + throw new Exception\CouldNotLeaveCriticalSection($reason); } } } diff --git a/src/WrappingCriticalSection.php b/src/WrappingCriticalSection.php index 48b8e63..632a335 100644 --- a/src/WrappingCriticalSection.php +++ b/src/WrappingCriticalSection.php @@ -4,48 +4,34 @@ namespace PetrKnap\CriticalSection; -use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection; -use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection; use Symfony\Component\Lock\LockInterface; -/** - * @template T - * - * @implements CriticalSectionInterface - */ -abstract class WrappingCriticalSection implements CriticalSectionInterface +abstract class WrappingCriticalSection extends CriticalSection { - /** @param CriticalSectionInterface|null $wrappedCriticalSection */ protected function __construct( - private ?CriticalSectionInterface $wrappedCriticalSection, + private readonly CriticalSection|null $wrappedCriticalSection, + bool $isBlocking, ) { + parent::__construct($isBlocking); } - public function __invoke(callable $criticalSection, mixed ...$args) + public function __invoke(callable $criticalSection, mixed ...$args): mixed { - if ($this->enter() === false) { - return null; - } - try { + return parent::__invoke(function (mixed ...$args) use ($criticalSection) { if ($this->wrappedCriticalSection) { return ($this->wrappedCriticalSection)(static fn () => $criticalSection(...$args)); } return $criticalSection(...$args); - } finally { - $this->leave(); - } + }, ...$args); } - /** @return WrappingCriticalSection */ public function withLock(LockInterface $lock, bool $isBlocking = true): WrappingCriticalSection { - return new SymfonyLockCriticalSection($this->getWrappingReferenceOrNull(), $lock, $isBlocking); + return new Symfony\Lock\CriticalSection($lock, $this->getWrappingReferenceOrNull(), $isBlocking); } /** * @param array $locks - * - * @return WrappingCriticalSection */ public function withLocks(array $locks, bool $isBlocking = true): WrappingCriticalSection { @@ -57,18 +43,7 @@ public function withLocks(array $locks, bool $isBlocking = true): WrappingCritic return $instance; } - /** - * @return bool false if it is occupied (non-blocking mode only) - * - * @throws CouldNotEnterCriticalSection - */ - abstract protected function enter(): bool; - - /** @throws CouldNotLeaveCriticalSection */ - abstract protected function leave(): void; - - /** @return CriticalSectionInterface|null */ - protected function getWrappingReferenceOrNull(): ?CriticalSectionInterface + protected function getWrappingReferenceOrNull(): CriticalSection|null { return $this; } diff --git a/tests/CriticalSectionTest.php b/tests/CriticalSectionTest.php index 6ea551c..2574145 100644 --- a/tests/CriticalSectionTest.php +++ b/tests/CriticalSectionTest.php @@ -4,20 +4,15 @@ namespace PetrKnap\CriticalSection; -use PHPUnit\Framework\TestCase; use Symfony\Component\Lock\LockInterface; -final class CriticalSectionTest extends TestCase +final class CriticalSectionTest extends CriticalSectionTestCase { - public function testCreatesCriticalSection(): void - { - self::assertInstanceOf( - WrappingCriticalSection::class, - CriticalSection::create() - ); - } + use CriticalSectionStaticFactory; - /** @dataProvider dataCreatesCriticalSectionWithLock */ + /** + * @dataProvider dataCreatesCriticalSectionWithLock + */ public function testCreatesCriticalSectionWithLock(bool $isBlocking): void { $lock = self::createMock(LockInterface::class); @@ -29,12 +24,11 @@ public function testCreatesCriticalSectionWithLock(bool $isBlocking): void ->method('release'); $criticalSection = CriticalSection::withLock($lock, $isBlocking); - $criticalSection(fn () => null); - self::assertInstanceOf( - SymfonyLockCriticalSection::class, + Symfony\Lock\CriticalSection::class, $criticalSection ); + $criticalSection(static fn () => null); } public static function dataCreatesCriticalSectionWithLock(): array @@ -45,7 +39,9 @@ public static function dataCreatesCriticalSectionWithLock(): array ]; } - /** @dataProvider dataCreatesCriticalSectionWithLock */ + /** + * @dataProvider dataCreatesCriticalSectionWithLock + */ public function testCreatesCriticalSectionWithLocks(bool $isBlocking): void { $lock = self::createMock(LockInterface::class); @@ -56,6 +52,26 @@ public function testCreatesCriticalSectionWithLocks(bool $isBlocking): void $lock->expects(self::exactly(3)) ->method('release'); - CriticalSection::withLocks([$lock, $lock, $lock], $isBlocking)(fn () => null); + $criticalSection = CriticalSection::withLocks([$lock, $lock, $lock], $isBlocking); + self::assertInstanceOf( + Symfony\Lock\CriticalSection::class, + $criticalSection + ); + $criticalSection(static fn () => null); + } + + protected function createCriticalSection(bool $isBlocking): CriticalSection|null + { + return $isBlocking ? null : self::nonCritical(canEnter: true); + } + + protected function createUnenterableCriticalSection(bool $isBlocking): CriticalSection|null + { + return $isBlocking ? null : self::nonCritical(canEnter: false); + } + + protected function createUnleavableCriticalSection(): CriticalSection|null + { + return null; } } diff --git a/tests/CriticalSectionTestCase.php b/tests/CriticalSectionTestCase.php new file mode 100644 index 0000000..f8d9523 --- /dev/null +++ b/tests/CriticalSectionTestCase.php @@ -0,0 +1,82 @@ +createCriticalSection($isBlocking))( + function (string $s, int $i, mixed $n) use (&$receivedArgs): void { + $receivedArgs = func_get_args(); + }, + ...$expectedArgs, + ); + + self::assertEquals( + $expectedArgs, + $receivedArgs, + ); + } + + /** + * @dataProvider isBlocking + */ + public function testForwardsReturnFromCriticalSection(bool $isBlocking): void + { + $expectedReturn = new Some\Resource(); + + self::assertSame( + $expectedReturn, + self::skipOnNull($this->createCriticalSection($isBlocking))(static fn (): Some\Resource => $expectedReturn), + ); + } + + /** + * @dataProvider isBlocking + */ + public function testCouldNotEnterUnenterableSection(bool $isBlocking): void + { + if ($isBlocking) { + self::expectException(Exception\CouldNotEnterCriticalSection::class); + } + self::assertNull(self::skipOnNull($this->createUnenterableCriticalSection($isBlocking))(static fn (): bool => true)); + } + + public function testCouldNotLeaveUnleavableSection(): void + { + self::expectException(Exception\CouldNotLeaveCriticalSection::class); + self::skipOnNull($this->createUnleavableCriticalSection())(static fn (): bool => true); + } + + public static function isBlocking(): array + { + return [ + 'blocking' => [true], + 'non-blocking' => [false] + ]; + } + + abstract protected function createCriticalSection(bool $isBlocking): CriticalSection|null; + + abstract protected function createUnenterableCriticalSection(bool $isBlocking): CriticalSection|null; + + abstract protected function createUnleavableCriticalSection(): CriticalSection|null; + + private static function skipOnNull(CriticalSection|null $criticalSection): CriticalSection + { + if ($criticalSection === null) { + self::markTestSkipped('Irrelevant test'); + } + return $criticalSection; + } +} diff --git a/tests/NonCriticalSectionTest.php b/tests/NonCriticalSectionTest.php deleted file mode 100644 index fbfdac4..0000000 --- a/tests/NonCriticalSectionTest.php +++ /dev/null @@ -1,38 +0,0 @@ - [true], - 'no' => [false], - ]; - } - - public function testDoesNotUseSelfAsWrappingReference(): void - { - self::markTestIncomplete(); - } -} diff --git a/tests/SymfonyLockCriticalSectionTest.php b/tests/Symfony/Lock/CriticalSectionTest.php similarity index 54% rename from tests/SymfonyLockCriticalSectionTest.php rename to tests/Symfony/Lock/CriticalSectionTest.php index 3b2b928..9f46e42 100644 --- a/tests/SymfonyLockCriticalSectionTest.php +++ b/tests/Symfony/Lock/CriticalSectionTest.php @@ -2,19 +2,17 @@ declare(strict_types=1); -namespace PetrKnap\CriticalSection; +namespace PetrKnap\CriticalSection\Symfony\Lock; use Exception; -use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection; -use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection; -use PHPUnit\Framework\TestCase; +use PetrKnap\CriticalSection\CriticalSection; +use PetrKnap\CriticalSection\CriticalSectionTestCase; use stdClass; -use Symfony\Component\Lock\Exception\LockAcquiringException; use Symfony\Component\Lock\Exception\LockConflictedException; use Symfony\Component\Lock\Exception\LockReleasingException; use Symfony\Component\Lock\LockInterface; -final class SymfonyLockCriticalSectionTest extends TestCase +final class CriticalSectionTest extends CriticalSectionTestCase { public function testAcquiresLockBeforeCriticalSectionIsExecuted(): void { @@ -23,12 +21,12 @@ public function testAcquiresLockBeforeCriticalSectionIsExecuted(): void $lock = self::createMock(LockInterface::class); $lock->expects(self::once()) ->method('acquire') - ->willReturnCallback(function () use ($shared) { + ->willReturnCallback(static function () use ($shared) { $shared->isLocked = true; return true; }); - CriticalSection::withLock($lock)(function () use ($shared) { + CriticalSection::withLock($lock)(static function () use ($shared) { self::assertTrue($shared->isLocked); }); } @@ -41,11 +39,11 @@ public function testReleasesLockAfterCriticalSectionWasExecuted(): void $lock->method('acquire')->willReturn(true); $lock->expects(self::once()) ->method('release') - ->willReturnCallback(function () use ($shared) { + ->willReturnCallback(static function () use ($shared) { $shared->isLocked = false; }); - CriticalSection::withLock($lock)(function () use ($shared) { + CriticalSection::withLock($lock)(static function () use ($shared) { self::assertTrue($shared->isLocked); }); @@ -60,13 +58,13 @@ public function testReleasesLockAndThrowsWhenCriticalSectionThrows(): void $lock->method('acquire')->willReturn(true); $lock->expects(self::once()) ->method('release') - ->willReturnCallback(function () use ($shared) { + ->willReturnCallback(static function () use ($shared) { $shared->isLocked = false; }); $expectedException = new Exception(); try { - CriticalSection::withLock($lock)(function () use ($shared, $expectedException) { + CriticalSection::withLock($lock)(static function () use ($shared, $expectedException) { self::assertTrue($shared->isLocked); throw $expectedException; }); @@ -77,42 +75,32 @@ public function testReleasesLockAndThrowsWhenCriticalSectionThrows(): void } } - /** @dataProvider dataThrowsWhenLockThrowsOnAcquire */ - public function testThrowsWhenLockThrowsOnAcquire(Exception $lockException): void + protected function createCriticalSection(bool $isBlocking): CriticalSection|null { $lock = self::createMock(LockInterface::class); - $lock->expects(self::once()) - ->method('acquire') - ->willThrowException($lockException); - - self::expectException(CouldNotEnterCriticalSection::class); + $lock->method('acquire')->willReturn(true); - CriticalSection::withLock($lock)(function () { - }); + return CriticalSection::withLock($lock); } - public static function dataThrowsWhenLockThrowsOnAcquire(): iterable + protected function createUnenterableCriticalSection(bool $isBlocking): CriticalSection|null { - $knownExceptions = [ - new LockConflictedException(), - new LockAcquiringException(), - ]; - foreach ($knownExceptions as $knownException) { - yield get_class($knownException) => [$knownException]; + $lock = self::createMock(LockInterface::class); + if ($isBlocking) { + $lock->method('acquire')->willThrowException(self::createStub(LockConflictedException::class)); + } else { + $lock->method('acquire')->willReturn(false); } + + return CriticalSection::withLock($lock); } - public function testThrowsWhenLockThrowsOnRelease(): void + protected function createUnleavableCriticalSection(): CriticalSection|null { $lock = self::createMock(LockInterface::class); $lock->method('acquire')->willReturn(true); - $lock->expects(self::once()) - ->method('release') - ->willThrowException(new LockReleasingException()); + $lock->method('release')->willThrowException(self::createStub(LockReleasingException::class)); - self::expectException(CouldNotLeaveCriticalSection::class); - - CriticalSection::withLock($lock)(function () { - }); + return CriticalSection::withLock($lock); } } diff --git a/tests/WrappingCriticalSectionTest.php b/tests/WrappingCriticalSectionTest.php index 32e76d2..76c1992 100644 --- a/tests/WrappingCriticalSectionTest.php +++ b/tests/WrappingCriticalSectionTest.php @@ -4,42 +4,9 @@ namespace PetrKnap\CriticalSection; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Lock\LockInterface; - -final class WrappingCriticalSectionTest extends TestCase +final class WrappingCriticalSectionTest extends CriticalSectionTestCase { - private const FOO = 'bar'; - - public function testForwardsArgumentsIntoCriticalSection(): void - { - $expectedArgs = ['string', 1, null]; - $receivedArgs = []; - (new NonCriticalSection(canEnter: true))( - function (string $s, int $i, mixed $n) use (&$receivedArgs): void { - $receivedArgs = func_get_args(); - }, - ...$expectedArgs, - ); - - self::assertEquals( - $expectedArgs, - $receivedArgs, - ); - } - - public function testReturnsValueReturnedByExecutedCriticalSection(): void - { - self::assertSame( - self::FOO, - (new NonCriticalSection(canEnter: true))(fn () => self::FOO), - ); - } - - public function testSkipsCriticalSectionWhenItIsOccupiedInNonBlockingMode(): void - { - self::assertNull((new NonCriticalSection(canEnter: false))(fn () => self::FOO)); - } + use CriticalSectionStaticFactory; public function testComposesCriticalSections(): void { @@ -50,11 +17,11 @@ public function testComposesCriticalSections(): void $prepareSection = function (int $id, ?WrappingCriticalSection $outer) use ($shared) { return new class ($id, $outer, $shared) extends WrappingCriticalSection { public function __construct( - private int $id, + private readonly int $id, ?WrappingCriticalSection $wrappedCriticalSection, - private \stdClass $shared, + private readonly \stdClass $shared, ) { - parent::__construct($wrappedCriticalSection); + parent::__construct($wrappedCriticalSection, false); } public function enter(): bool @@ -74,10 +41,34 @@ public function leave(): void $section2 = $prepareSection(2, $section1); $section3 = $prepareSection(3, $section2); - $section3(function () use ($shared) { + $section3(static function () use ($shared) { self::assertSame([3, 2, 1], $shared->enters); self::assertSame([], $shared->leaves); }); self::assertSame([1, 2, 3], $shared->leaves); } + + protected function createCriticalSection(bool $isBlocking): CriticalSection|null + { + return $isBlocking ? null : self::nonCritical( + self::nonCritical(canEnter: true), + canEnter: true, + ); + } + + protected function createUnenterableCriticalSection(bool $isBlocking): CriticalSection|null + { + return $isBlocking ? null : self::nonCritical( + self::nonCritical( + self::nonCritical(canEnter: true), + canEnter: false, + ), + canEnter: true, + ); + } + + protected function createUnleavableCriticalSection(): CriticalSection|null + { + return null; + } }