From e83d776abc49d3dd5a7e8708ec98b2b4670c580c Mon Sep 17 00:00:00 2001 From: JB Cagumbay Date: Sat, 17 Jan 2026 19:36:42 +0100 Subject: [PATCH 1/3] Refactor `Clock` to introduce `resetGlobalClock` and improve default clock handling. Add `StaticGlobalClockTest` to ensure correct clock behavior. --- .../src/Messaging/Scheduling/Clock.php | 15 ++- .../Unit/Scheduling/StaticGlobalClockTest.php | 93 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php diff --git a/packages/Ecotone/src/Messaging/Scheduling/Clock.php b/packages/Ecotone/src/Messaging/Scheduling/Clock.php index 0137c0b6e..c75108b42 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/Clock.php +++ b/packages/Ecotone/src/Messaging/Scheduling/Clock.php @@ -16,6 +16,7 @@ class Clock implements EcotoneClockInterface public function __construct( private readonly ?PsrClockInterface $clock = null, ) { + self::$globalClock = $this->clock ? $this : self::defaultClock(); } public static function set(PsrClockInterface $clock): void @@ -28,7 +29,12 @@ public static function set(PsrClockInterface $clock): void */ public static function get(): EcotoneClockInterface { - return self::$globalClock ??= new NativeClock(); + return self::$globalClock ??= self::defaultClock(); + } + + public static function resetGlobalClock(): void + { + self::$globalClock = null; } public function now(): DatePoint @@ -48,7 +54,12 @@ public function sleep(Duration $duration): void if ($clock instanceof SleepInterface) { $clock->sleep($duration); } else { - (new NativeClock())->sleep($duration); + self::defaultClock()->sleep($duration); } } + + private static function defaultClock(): EcotoneClockInterface + { + return new NativeClock(); + } } diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php new file mode 100644 index 000000000..ac4162684 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php @@ -0,0 +1,93 @@ + + * + * @internal + */ +/** + * licence Apache-2.0 + * @internal + */ +class StaticGlobalClockTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Clock::resetGlobalClock(); + } + + public function test_when_clock_is_not_instantiated_returns_default_native_clock() + { + $now = new DatePoint('now'); + $globalClock = Clock::get(); + + $this->assertInstanceOf(NativeClock::class, $globalClock); + $this->assertEquals($now->format('Y-m-d H:i:s'), $globalClock->now()->format('Y-m-d H:i:s')); + } + + public function test_when_clock_is_not_instantiated_with_null_internal_clock_returns_default_native_clock() + { + $now = new DatePoint('now'); + $clock = new Clock(); + $globalClock = Clock::get(); + + $this->assertInstanceOf(NativeClock::class, $globalClock); + $this->assertEquals($now->format('Y-m-d H:i:s'), $globalClock->now()->format('Y-m-d H:i:s')); + } + + public function test_when_clock_is_not_instantiated_with_not_null_internal_clock_returns_ecotone_clock() + { + $staticClock = $this->createStaticClock('2025-08-11 16:00:00'); + $clock = new Clock($staticClock); + + $globalClock = Clock::get(); + + $this->assertInstanceOf(Clock::class, $globalClock); + $this->assertEquals('2025-08-11 16:00:00', $globalClock->now()->format('Y-m-d H:i:s')); + } + + /** + * @description Create static clock with given date time to mimic external PsrClockInterface implementation + * as Ecotone's Clock dependency override. + * @param string $currentDateTime + * @return ClockInterface + */ + private function createStaticClock(string $currentDateTime): ClockInterface + { + return new class ($currentDateTime) implements ClockInterface, SleepInterface { + private static DateTimeImmutable $now; + + public function __construct(string $now) + { + self::$now = new DateTimeImmutable($now); + } + + public function now(): DateTimeImmutable + { + return self::$now; + } + + public function sleep(Duration $duration): void + { + self::$now = self::$now->modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); + } + }; + } +} From 76cd333b9d4093252e7c28beb49e980a9c505c79 Mon Sep 17 00:00:00 2001 From: JB Cagumbay Date: Sat, 17 Jan 2026 20:26:29 +0100 Subject: [PATCH 2/3] Fix `StaticGlobalClockTest` setup and teardown to properly reset global clock. --- .../Messaging/Unit/Scheduling/StaticGlobalClockTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php index ac4162684..b441d9217 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php @@ -28,8 +28,13 @@ class StaticGlobalClockTest extends TestCase { protected function setUp(): void { - parent::setUp(); + parent::tearDown(); + Clock::resetGlobalClock(); + } + protected function tearDown(): void + { + parent::tearDown(); Clock::resetGlobalClock(); } From f2be8028448607f0e4f6acda4db969544a5ecc04 Mon Sep 17 00:00:00 2001 From: JB Cagumbay Date: Mon, 19 Jan 2026 14:50:33 +0100 Subject: [PATCH 3/3] Introduce test fixtures for scheduling and enhance `Clock` handling. - Add `PlaceOrder`, `OrderWasPlaced`, `OrderService`, and `NotificationService` test fixtures. - Improve `Clock` initialization to ensure `globalClock` is only set when unset. - Rename `resetGlobalClock` to `resetToNativeClock` for better clarity. - Add `StaticPsrClock` implementation for static clock testing. - Create `DelayedMessageAgainstGlobalClockTest` to validate delay handling against clock changes. - Update and refactor existing clock-related tests for consistency. --- .../src/Messaging/Scheduling/Clock.php | 6 +- .../Fixture/Scheduling/CustomNotifier.php | 27 ++++++++ .../Scheduling/NotificationService.php | 23 +++++++ .../Fixture/Scheduling/OrderService.php | 20 ++++++ .../Fixture/Scheduling/OrderWasPlaced.php | 15 +++++ .../Fixture/Scheduling/PlaceOrder.php | 15 +++++ .../Fixture/Scheduling/StaticPsrClock.php | 33 +++++++++ .../DelayedMessageAgainstGlobalClockTest.php | 67 +++++++++++++++++++ .../Scheduling/SleepableStaticClockTest.php | 35 +--------- .../Unit/Scheduling/StaticGlobalClockTest.php | 40 ++--------- 10 files changed, 210 insertions(+), 71 deletions(-) create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/PlaceOrder.php create mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/StaticPsrClock.php create mode 100644 packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php diff --git a/packages/Ecotone/src/Messaging/Scheduling/Clock.php b/packages/Ecotone/src/Messaging/Scheduling/Clock.php index c75108b42..9d6588fb6 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/Clock.php +++ b/packages/Ecotone/src/Messaging/Scheduling/Clock.php @@ -16,7 +16,9 @@ class Clock implements EcotoneClockInterface public function __construct( private readonly ?PsrClockInterface $clock = null, ) { - self::$globalClock = $this->clock ? $this : self::defaultClock(); + if (!self::$globalClock) { + self::$globalClock = $this->clock ? $this : self::defaultClock(); + } } public static function set(PsrClockInterface $clock): void @@ -32,7 +34,7 @@ public static function get(): EcotoneClockInterface return self::$globalClock ??= self::defaultClock(); } - public static function resetGlobalClock(): void + public static function resetToNativeClock(): void { self::$globalClock = null; } diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php new file mode 100644 index 000000000..40f145636 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php @@ -0,0 +1,27 @@ +notifications[$eventName])) { + $this->notifications[$eventName] = []; + } + + $this->notifications[$eventName][] = $value; + } + + public function getNotificationsOf(string $eventName): array + { + return $this->notifications[$eventName] ?? []; + } +} diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php new file mode 100644 index 000000000..d172d5a43 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php @@ -0,0 +1,23 @@ +notify('placedOrder', $event->orderId); + } +} \ No newline at end of file diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php new file mode 100644 index 000000000..d04b10657 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php @@ -0,0 +1,20 @@ +publish(new OrderWasPlaced($command->orderId)); + } +} diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php new file mode 100644 index 000000000..b3677d432 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php @@ -0,0 +1,15 @@ +modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); + } +} diff --git a/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php new file mode 100644 index 000000000..f7c78c545 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php @@ -0,0 +1,67 @@ + + * + * @internal + */ +/** + * licence Apache-2.0 + * @internal + */ +class DelayedMessageAgainstGlobalClockTest extends TestCase +{ + protected function setUp(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + public function test_delayed_message_observes_clock_changes() + { + $ecotoneTestSupport = EcotoneLite::bootstrapFlowTesting( + [EcotoneClockInterface::class, OrderService::class, NotificationService::class, CustomNotifier::class], + [$clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')), new OrderService(), new NotificationService(), $notifier = new CustomNotifier()], + enableAsynchronousProcessing: [ + // 1. Turn on Delayable In Memory Pollable Channel + SimpleMessageChannelBuilder::createQueueChannel('notifications', true) + ] + ); + + $ecotoneTestSupport->sendCommandWithRoutingKey('order.register', new PlaceOrder('123')); + + $clock->sleep(Duration::minutes(1)); + + // 2. Releasing messages awaiting for 60 seconds + $ecotoneTestSupport->run('notifications', releaseAwaitingFor: Clock::get()->now()); + + $this->assertEquals( + 1, + count($notifier->getNotificationsOf('placedOrder')) + ); + } +} diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php index c9ab8c3ac..eabb1878d 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php @@ -4,12 +4,10 @@ namespace Test\Ecotone\Messaging\Unit\Scheduling; -use DateTimeImmutable; use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; -use Ecotone\Messaging\Scheduling\SleepInterface; use PHPUnit\Framework\TestCase; -use Psr\Clock\ClockInterface; +use Test\Ecotone\Messaging\Fixture\Scheduling\StaticPsrClock; /** * Class SleepableStaticClockTest @@ -26,38 +24,9 @@ class SleepableStaticClockTest extends TestCase { public function test_when_given_psr_clock_instance_with_sleep_support_it_increase_the_clock() { - $staticClock = $this->createStaticClock('2025-08-11 16:00:00'); - $clock = new Clock($staticClock); + $clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')); $clock->sleep(Duration::seconds(30)); $this->assertEquals('2025-08-11 16:00:30', $clock->now()->format('Y-m-d H:i:s')); } - - /** - * @description Create static clock with given date time to mimic external PsrClockInterface implementation - * as Ecotone's Clock dependency override. - * @param string $currentDateTime - * @return ClockInterface - */ - private function createStaticClock(string $currentDateTime): ClockInterface - { - return new class ($currentDateTime) implements ClockInterface, SleepInterface { - private static DateTimeImmutable $now; - - public function __construct(string $now) - { - self::$now = new DateTimeImmutable($now); - } - - public function now(): DateTimeImmutable - { - return self::$now; - } - - public function sleep(Duration $duration): void - { - self::$now = self::$now->modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); - } - }; - } } diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php index b441d9217..b070bd65d 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php @@ -4,14 +4,11 @@ namespace Test\Ecotone\Messaging\Unit\Scheduling; -use DateTimeImmutable; use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\DatePoint; -use Ecotone\Messaging\Scheduling\Duration; use Ecotone\Messaging\Scheduling\NativeClock; -use Ecotone\Messaging\Scheduling\SleepInterface; use PHPUnit\Framework\TestCase; -use Psr\Clock\ClockInterface; +use Test\Ecotone\Messaging\Fixture\Scheduling\StaticPsrClock; /** * Class StaticGlobalClockTest @@ -29,13 +26,13 @@ class StaticGlobalClockTest extends TestCase protected function setUp(): void { parent::tearDown(); - Clock::resetGlobalClock(); + Clock::resetToNativeClock(); } protected function tearDown(): void { parent::tearDown(); - Clock::resetGlobalClock(); + Clock::resetToNativeClock(); } public function test_when_clock_is_not_instantiated_returns_default_native_clock() @@ -59,40 +56,11 @@ public function test_when_clock_is_not_instantiated_with_null_internal_clock_ret public function test_when_clock_is_not_instantiated_with_not_null_internal_clock_returns_ecotone_clock() { - $staticClock = $this->createStaticClock('2025-08-11 16:00:00'); - $clock = new Clock($staticClock); + $clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')); $globalClock = Clock::get(); $this->assertInstanceOf(Clock::class, $globalClock); $this->assertEquals('2025-08-11 16:00:00', $globalClock->now()->format('Y-m-d H:i:s')); } - - /** - * @description Create static clock with given date time to mimic external PsrClockInterface implementation - * as Ecotone's Clock dependency override. - * @param string $currentDateTime - * @return ClockInterface - */ - private function createStaticClock(string $currentDateTime): ClockInterface - { - return new class ($currentDateTime) implements ClockInterface, SleepInterface { - private static DateTimeImmutable $now; - - public function __construct(string $now) - { - self::$now = new DateTimeImmutable($now); - } - - public function now(): DateTimeImmutable - { - return self::$now; - } - - public function sleep(Duration $duration): void - { - self::$now = self::$now->modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); - } - }; - } }