From 58311f1ccbc88d4e2d9026c91b0847a76892d12e Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 15:48:04 +0100 Subject: [PATCH 1/7] feat: clock testing DX --- docker-compose.yml | 28 ++++---- packages/Dbal/src/EnqueueDbal/DbalContext.php | 5 +- .../DbalBackedMessageChannelTest.php | 43 ++++++++++-- packages/Ecotone/src/Lite/EcotoneLite.php | 1 + .../DelayedMessageReleaseHandler.php | 8 ++- ...nfiguredMessagingSystemWithTestSupport.php | 3 + .../Ecotone/src/Lite/Test/FlowTestSupport.php | 24 ++++++- .../RegisterSingletonMessagingServices.php | 16 ++++- .../Config/MessagingSystemConfiguration.php | 2 +- .../src/Messaging/Scheduling/Clock.php | 35 ++++------ .../src/Messaging/Scheduling/TimeSpan.php | 16 +++++ .../Ecotone/src/Test/ClockSensitiveTrait.php | 48 -------------- .../Ecotone/src/Test/ComponentTestBuilder.php | 2 + packages/Ecotone/src/Test/StaticPsrClock.php | 35 ++++++++++ .../MessagingTestSupportFrameworkTest.php | 6 +- .../Fixture/Scheduling/StaticPsrClock.php | 33 ---------- .../DelayedMessageAgainstGlobalClockTest.php | 48 ++++++++++++-- .../Scheduling/SleepableStaticClockTest.php | 2 +- .../Unit/Scheduling/StaticGlobalClockTest.php | 66 ------------------- 19 files changed, 215 insertions(+), 206 deletions(-) delete mode 100644 packages/Ecotone/src/Test/ClockSensitiveTrait.php create mode 100644 packages/Ecotone/src/Test/StaticPsrClock.php delete mode 100644 packages/Ecotone/tests/Messaging/Fixture/Scheduling/StaticPsrClock.php delete mode 100644 packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php diff --git a/docker-compose.yml b/docker-compose.yml index 2df6cefce..52a5fe760 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - "$PWD:/data/app" working_dir: "/data/app" command: sleep 99999 - container_name: "ecotone_development" + container_name: "${PHP_CONTAINER_NAME:-ecotone_development}" user: "${USER_PID:-1000}:${USER_PID:-1000}" extra_hosts: - "host.docker.internal:host-gateway" @@ -32,7 +32,7 @@ services: - "$PWD:/data/app" working_dir: "/data/app" command: sleep 99999 - container_name: "ecotone_development_8_2" + container_name: "${PHP_8_2_CONTAINER_NAME:-ecotone_development_8_2}" user: "${USER_PID:-1000}:${USER_PID:-1000}" extra_hosts: - "host.docker.internal:host-gateway" @@ -73,16 +73,16 @@ services: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest ports: - - "15672:15672" - - "5672:5672" + - '${RABBITMQ_PORT:-5672}:5672' + - '${RABBITMQ_MGMT_PORT:-15672}:15672' localstack: image: localstack/localstack:3.0.0 environment: LOCALSTACK_HOST: 'localstack' SERVICES: 'sqs,sns' ports: - - "4566:4566" # LocalStack Gateway - - "4510-4559:4510-4559" # external services port range + - "${LOCALSTACK_PORT:-4566}:4566" # LocalStack Gateway +# - "4510-4559:4510-4559" # external services port range redis: image: redis:7-alpine ports: @@ -96,14 +96,14 @@ services: - ./.docker/collector/otel-collector-config.yaml:/etc/otel-collector-config.yml ports: - "9411" # Zipkin receiver - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP/HTTP receiver +# - "4317:4317" # OTLP gRPC receiver +# - "4318:4318" # OTLP/HTTP receiver zipkin: image: openzipkin/zipkin-slim networks: - default ports: - - 9411:9411 + - '${ZIPKIN_PORT:-9411}:9411' jaeger: image: jaegertracing/all-in-one:latest environment: @@ -111,18 +111,18 @@ services: networks: - default ports: - - 16686:16686 + - '${JAEGER_PORT:-16686}:16686' kafka: image: 'apache/kafka:3.9.0' ports: - - '9094:9092' + - '${KAFKA_PORT:-9094}:9092' environment: - KAFKA_NODE_ID=0 - KAFKA_PROCESS_ROLES=broker,controller - KAFKA_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER - - KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 + - KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:${KAFKA_PORT:-9094} + - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:${KAFKA_PORT:-9094} - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 @@ -133,7 +133,7 @@ services: kafdrop: image: 'obsidiandynamics/kafdrop:latest' ports: - - '9999:9000' + - '${KAFDROP_PORT:-9999}:9000' environment: - KAFKA_BROKERCONNECT=kafka:9092 networks: diff --git a/packages/Dbal/src/EnqueueDbal/DbalContext.php b/packages/Dbal/src/EnqueueDbal/DbalContext.php index 9c8b90c4c..710cc4451 100644 --- a/packages/Dbal/src/EnqueueDbal/DbalContext.php +++ b/packages/Dbal/src/EnqueueDbal/DbalContext.php @@ -41,7 +41,6 @@ class DbalContext implements Context * @var array */ private $config; - private EcotoneClockInterface $clock; /** * Callable must return instance of Doctrine\DBAL\Connection once called. @@ -63,8 +62,6 @@ public function __construct($connection, array $config = []) } else { throw new InvalidArgumentException(sprintf('The connection argument must be either %s or callable that returns %s.', Connection::class, Connection::class)); } - - $this->clock = Clock::get(); } /** @@ -257,6 +254,6 @@ public function createDataBaseTable(): void public function getClock(): EcotoneClockInterface { - return $this->clock; + return Clock::get(); } } diff --git a/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php b/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php index 9ead4df97..8e15a465e 100644 --- a/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php +++ b/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php @@ -16,6 +16,7 @@ use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; use Ecotone\Messaging\Scheduling\StubUTCClock; +use Ecotone\Messaging\Scheduling\TimeSpan; use Ecotone\Messaging\Support\MessageBuilder; use Ecotone\Test\ClockSensitiveTrait; use Ecotone\Test\StubLogger; @@ -35,8 +36,6 @@ */ class DbalBackedMessageChannelTest extends DbalMessagingTestCase { - use ClockSensitiveTrait; - public function test_sending_and_receiving_via_channel() { $channelName = Uuid::uuid4()->toString(); @@ -196,7 +195,7 @@ public function test_reconnecting_on_disconnected_channel_with_manager_registry( $this->assertNotNull($receivedMessage, 'Not received message'); } - public function test_delaying_the_message() + public function test_delaying_the_message_with_custom_clock() { $channelName = Uuid::uuid4()->toString(); $clock = new StubUTCClock(); @@ -217,8 +216,6 @@ public function test_delaying_the_message() /** @var PollableChannel $messageChannel */ $messageChannel = $ecotoneLite->getMessageChannel($channelName); - Clock::set($clock); - $messageChannel->send( MessageBuilder::withPayload('some') ->setHeader(MessageHeaders::DELIVERY_DELAY, 2000) @@ -232,6 +229,42 @@ public function test_delaying_the_message() $this->assertNotNull($messageChannel->receive()); } + public function test_delaying_the_message_with_native_clock() + { + $channelName = Uuid::uuid4()->toString(); + $clock = new StubUTCClock(); + + $ecotoneLite = EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + DbalConnectionFactory::class => $this->getConnectionFactory(true), + ClockInterface::class => $clock, + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DBAL_PACKAGE])) + ->withExtensionObjects([ + DbalBackedMessageChannelBuilder::create($channelName) + ->withReceiveTimeout(1), + ]) + ); + + /** @var PollableChannel $messageChannel */ + $messageChannel = $ecotoneLite->getMessageChannel($channelName); + + $messageChannel->send( + MessageBuilder::withPayload('some') + ->setHeader(MessageHeaders::DELIVERY_DELAY, 2000) + ->build() + ); + + $ecotoneLite->waitTill(TimeSpan::withSeconds(1)); + + $this->assertNull($messageChannel->receive()); + + $ecotoneLite->waitTill(TimeSpan::withSeconds(3)); + + $this->assertNotNull($messageChannel->receive()); + } + public function test_sending_message() { $queueName = Uuid::uuid4()->toString(); diff --git a/packages/Ecotone/src/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index 7bdd90211..6bcf1f4a4 100644 --- a/packages/Ecotone/src/Lite/EcotoneLite.php +++ b/packages/Ecotone/src/Lite/EcotoneLite.php @@ -22,6 +22,7 @@ use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\InMemoryConfigurationVariableService; +use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Support\Assert; use Ecotone\Modelling\BaseEventSourcingConfiguration; diff --git a/packages/Ecotone/src/Lite/Test/Configuration/DelayedMessageReleaseHandler.php b/packages/Ecotone/src/Lite/Test/Configuration/DelayedMessageReleaseHandler.php index b9b3d65f9..3eab91574 100644 --- a/packages/Ecotone/src/Lite/Test/Configuration/DelayedMessageReleaseHandler.php +++ b/packages/Ecotone/src/Lite/Test/Configuration/DelayedMessageReleaseHandler.php @@ -18,13 +18,19 @@ final class DelayedMessageReleaseHandler { public function releaseMessagesAwaitingFor(string $channelName, int|TimeSpan|DateTimeInterface $timeInMillisecondsOrDateTime, ChannelResolver $channelResolver): void { + if (!$channelResolver->hasChannelWithName($channelName)) { + return; + } + /** @var DelayableQueueChannel|MessageChannelInterceptorAdapter $channel */ $channel = $channelResolver->resolve($channelName); if ($channel instanceof MessageChannelInterceptorAdapter) { $channel = $channel->getInternalMessageChannel(); } - Assert::isTrue($channel instanceof DelayableQueueChannel, sprintf('Used %s channel to release delayed message, use instead of %s.', $channel::class, DelayableQueueChannel::class)); + if (! $channel instanceof DelayableQueueChannel) { + return; + } $channel->releaseMessagesAwaitingFor($timeInMillisecondsOrDateTime); } diff --git a/packages/Ecotone/src/Lite/Test/ConfiguredMessagingSystemWithTestSupport.php b/packages/Ecotone/src/Lite/Test/ConfiguredMessagingSystemWithTestSupport.php index 7f616c49d..7e9db9a7f 100644 --- a/packages/Ecotone/src/Lite/Test/ConfiguredMessagingSystemWithTestSupport.php +++ b/packages/Ecotone/src/Lite/Test/ConfiguredMessagingSystemWithTestSupport.php @@ -12,6 +12,8 @@ use Ecotone\Messaging\MessageChannel; use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\MessagePublisher; +use Ecotone\Messaging\Scheduling\Clock; +use Ecotone\Messaging\Scheduling\EcotoneClockInterface; use Ecotone\Modelling\AggregateFlow\SaveAggregate\AggregateResolver\AggregateDefinitionRegistry; use Ecotone\Modelling\CommandBus; use Ecotone\Modelling\DistributedBus; @@ -89,6 +91,7 @@ public function getFlowTestSupport(): FlowTestSupport $this->getServiceFromContainer(AggregateDefinitionRegistry::class), $this->getMessagingTestSupport(), $this->getGatewayByName(MessagingEntrypoint::class), + $this->getServiceFromContainer(EcotoneClockInterface::class), $this->configuredMessagingSystem ); } diff --git a/packages/Ecotone/src/Lite/Test/FlowTestSupport.php b/packages/Ecotone/src/Lite/Test/FlowTestSupport.php index ca6ab582e..947f8463c 100644 --- a/packages/Ecotone/src/Lite/Test/FlowTestSupport.php +++ b/packages/Ecotone/src/Lite/Test/FlowTestSupport.php @@ -17,6 +17,9 @@ use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\MessagingException; use Ecotone\Messaging\PollableChannel; +use Ecotone\Messaging\Scheduling\Clock; +use Ecotone\Messaging\Scheduling\Duration; +use Ecotone\Messaging\Scheduling\EcotoneClockInterface; use Ecotone\Messaging\Scheduling\TimeSpan; use Ecotone\Messaging\Support\Assert; use Ecotone\Messaging\Support\MessageBuilder; @@ -46,6 +49,7 @@ public function __construct( private AggregateDefinitionRegistry $aggregateDefinitionRegistry, private MessagingTestSupport $testSupportGateway, private MessagingEntrypoint $messagingEntrypoint, + private EcotoneClockInterface $clock, private ConfiguredMessagingSystem $configuredMessagingSystem ) { } @@ -141,9 +145,7 @@ public function receiveMessageFrom(string $channelName): ?Message */ public function run(string $name, ?ExecutionPollingMetadata $executionPollingMetadata = null, TimeSpan|DateTimeInterface|null $releaseAwaitingFor = null): self { - if ($releaseAwaitingFor) { - $this->testSupportGateway->releaseMessagesAwaitingFor($name, $releaseAwaitingFor); - } + $this->testSupportGateway->releaseMessagesAwaitingFor($name, $releaseAwaitingFor ?? Clock::get()->now()); $this->configuredMessagingSystem->run($name, $executionPollingMetadata); return $this; @@ -190,6 +192,22 @@ public function getEventStreamEvents(string $streamName): array return $this->getGateway(EventStore::class)->load($streamName); } + public function waitTill(TimeSpan|DateTimeInterface $time): self + { + if ($time instanceof DateTimeInterface) { + if ($time < $this->clock->now()) { + throw new MessagingException("Time to wait is in the past. Now: {$this->clock->now()}, time to wait: {$time}"); + } + } + + $this->clock->sleep($time instanceof TimeSpan + ? $time->toDuration() + : Timespan::fromDateInterval($time->diff($this->clock->now()))->toDuration() + ); + + return $this; + } + /** * @param Event[]|object[]|array[] $events */ diff --git a/packages/Ecotone/src/Messaging/Config/Container/Compiler/RegisterSingletonMessagingServices.php b/packages/Ecotone/src/Messaging/Config/Container/Compiler/RegisterSingletonMessagingServices.php index 7d8306304..900589741 100644 --- a/packages/Ecotone/src/Messaging/Config/Container/Compiler/RegisterSingletonMessagingServices.php +++ b/packages/Ecotone/src/Messaging/Config/Container/Compiler/RegisterSingletonMessagingServices.php @@ -10,7 +10,9 @@ use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\Container\ReferenceSearchServiceWithContainer; use Ecotone\Messaging\Config\MessagingSystemContainer; +use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceCacheConfiguration; +use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Handler\Bridge\Bridge; use Ecotone\Messaging\Handler\ChannelResolver; use Ecotone\Messaging\Handler\Enricher\PropertyEditorAccessor; @@ -30,11 +32,23 @@ */ class RegisterSingletonMessagingServices implements CompilerPass { + public function __construct( + private ServiceConfiguration $serviceConfiguration, + ) { + } + public function process(ContainerBuilder $builder): void { $this->registerDefault($builder, Bridge::class, new Definition(Bridge::class)); $this->registerDefault($builder, Reference::toChannel(NullableMessageChannel::CHANNEL_NAME), new Definition(NullableMessageChannel::class)); - $this->registerDefault($builder, EcotoneClockInterface::class, new Definition(Clock::class, [new Reference(ClockInterface::class, ContainerImplementation::NULL_ON_INVALID_REFERENCE)])); + $this->registerDefault($builder, EcotoneClockInterface::class, new Definition( + Clock::class, + [ + new Reference(ClockInterface::class, ContainerImplementation::NULL_ON_INVALID_REFERENCE), + $this->serviceConfiguration->isModulePackageEnabled(ModulePackageList::TEST_PACKAGE), + ], + factory: [Clock::class, 'createBasedOnConfig'] + )); $this->registerDefault($builder, ChannelResolver::class, new Definition(ChannelResolverWithContainer::class, [new Reference(ContainerInterface::class)])); $this->registerDefault($builder, ReferenceSearchService::class, new Definition(ReferenceSearchServiceWithContainer::class, [new Reference(ContainerInterface::class)])); $this->registerDefault($builder, ExpressionEvaluationService::REFERENCE, new Definition(SymfonyExpressionEvaluationAdapter::class, [new Reference(ReferenceSearchService::class)], 'create')); diff --git a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php index dd3e37615..4b106d994 100644 --- a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php +++ b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php @@ -989,7 +989,7 @@ public function process(ContainerBuilder $builder): void } $messagingBuilder->register(ConfiguredMessagingSystem::class, new Definition(MessagingSystemContainer::class, [new Reference(ContainerInterface::class), $messagingBuilder->getPollingEndpoints(), $gatewayListReferences])); - (new RegisterSingletonMessagingServices())->process($builder); + (new RegisterSingletonMessagingServices($this->applicationConfiguration))->process($builder); foreach ($this->compilerPasses as $compilerPass) { $compilerPass->process($builder); } diff --git a/packages/Ecotone/src/Messaging/Scheduling/Clock.php b/packages/Ecotone/src/Messaging/Scheduling/Clock.php index 9d6588fb6..0221694a0 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/Clock.php +++ b/packages/Ecotone/src/Messaging/Scheduling/Clock.php @@ -7,23 +7,25 @@ namespace Ecotone\Messaging\Scheduling; +use Ecotone\Test\StaticPsrClock; use Psr\Clock\ClockInterface as PsrClockInterface; class Clock implements EcotoneClockInterface { private static ?EcotoneClockInterface $globalClock = null; - public function __construct( - private readonly ?PsrClockInterface $clock = null, - ) { - if (!self::$globalClock) { - self::$globalClock = $this->clock ? $this : self::defaultClock(); - } + public function __construct(private PsrClockInterface $clock) + { + self::$globalClock = $this; } - public static function set(PsrClockInterface $clock): void + public static function createBasedOnConfig(?PsrClockInterface $clock, bool $isTestingEnabled): EcotoneClockInterface { - self::$globalClock = $clock instanceof EcotoneClockInterface ? $clock : new self($clock); + if ($clock === null) { + return new self($isTestingEnabled ? new StaticPsrClock('now') : self::defaultClock()); + } + + return new self($clock); } /** @@ -31,17 +33,12 @@ public static function set(PsrClockInterface $clock): void */ public static function get(): EcotoneClockInterface { - return self::$globalClock ??= self::defaultClock(); - } - - public static function resetToNativeClock(): void - { - self::$globalClock = null; + return self::$globalClock ?? new self(self::defaultClock()); } public function now(): DatePoint { - $now = ($this->clock ?? self::get())->now(); + $now = $this->clock->now(); if (! $now instanceof DatePoint) { $now = DatePoint::createFromInterface($now); } @@ -51,13 +48,7 @@ public function now(): DatePoint public function sleep(Duration $duration): void { - $clock = $this->clock ?? self::get(); - - if ($clock instanceof SleepInterface) { - $clock->sleep($duration); - } else { - self::defaultClock()->sleep($duration); - } + $this->clock->sleep($duration); } private static function defaultClock(): EcotoneClockInterface diff --git a/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php b/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php index 86d4ee538..d5f24ff09 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php +++ b/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php @@ -22,6 +22,17 @@ public function __construct( } + public static function fromDateInterval(DateInterval $dateInterval): self + { + return new self( + milliseconds: $dateInterval->f * 1000, + seconds: $dateInterval->s, + minutes: $dateInterval->i, + hours: $dateInterval->h, + days: $dateInterval->d, + ); + } + public static function withMilliseconds(int $milliseconds): self { return new self(milliseconds: $milliseconds); @@ -52,6 +63,11 @@ public function toMilliseconds(): int return $this->milliseconds + $this->seconds * 1000 + $this->minutes * 60 * 1000 + $this->hours * 60 * 60 * 1000 + $this->days * 24 * 60 * 60 * 1000; } + public function toDuration(): Duration + { + return Duration::milliseconds($this->toMilliseconds()); + } + public function getDefinition(): Definition { return new Definition( diff --git a/packages/Ecotone/src/Test/ClockSensitiveTrait.php b/packages/Ecotone/src/Test/ClockSensitiveTrait.php deleted file mode 100644 index b016959eb..000000000 --- a/packages/Ecotone/src/Test/ClockSensitiveTrait.php +++ /dev/null @@ -1,48 +0,0 @@ -getServiceFromContainer(AggregateDefinitionRegistry::class), $configuredMessagingSystem->getGatewayByName(MessagingTestSupport::class), $configuredMessagingSystem->getGatewayByName(MessagingEntrypoint::class), + new Clock(new StaticPsrClock('now')), $configuredMessagingSystem ); } diff --git a/packages/Ecotone/src/Test/StaticPsrClock.php b/packages/Ecotone/src/Test/StaticPsrClock.php new file mode 100644 index 000000000..ae99c5748 --- /dev/null +++ b/packages/Ecotone/src/Test/StaticPsrClock.php @@ -0,0 +1,35 @@ +sleepDuration = Duration::zero(); + } + + public function now(): DateTimeImmutable + { + $now = $this->now === null ? new DateTimeImmutable() : new DateTimeImmutable($this->now); + + return $now->modify("+{$this->sleepDuration->zeroIfNegative()->inMicroseconds()} microseconds"); + } + + public function sleep(Duration $duration): void + { + $this->sleepDuration = $this->sleepDuration->add($duration); + } +} diff --git a/packages/Ecotone/tests/Lite/Test/MessagingTestSupportFrameworkTest.php b/packages/Ecotone/tests/Lite/Test/MessagingTestSupportFrameworkTest.php index 02d75e05c..b46c91350 100644 --- a/packages/Ecotone/tests/Lite/Test/MessagingTestSupportFrameworkTest.php +++ b/packages/Ecotone/tests/Lite/Test/MessagingTestSupportFrameworkTest.php @@ -476,16 +476,16 @@ public function test_releasing_delayed_message_time_time_span_object() $orderId = 'someId'; $ecotoneTestSupport->sendCommandWithRoutingKey('order.register', new PlaceOrder($orderId), metadata: [ - MessageHeaders::DELIVERY_DELAY => new TimeSpan(100), + MessageHeaders::DELIVERY_DELAY => TimeSpan::withHours(1), ]); $ecotoneTestSupport->run('orders'); $this->assertEquals([], $ecotoneTestSupport->sendQueryWithRouting('order.getNotifiedOrders')); - $ecotoneTestSupport->run('orders', releaseAwaitingFor: new TimeSpan(milliseconds: 10)); + $ecotoneTestSupport->run('orders', releaseAwaitingFor: TimeSpan::withMinutes(59)); $this->assertEquals([], $ecotoneTestSupport->sendQueryWithRouting('order.getNotifiedOrders')); - $ecotoneTestSupport->run('orders', releaseAwaitingFor: new TimeSpan(100)); + $ecotoneTestSupport->run('orders', releaseAwaitingFor: TimeSpan::withHours(1)); $this->assertEquals([$orderId], $ecotoneTestSupport->sendQueryWithRouting('order.getNotifiedOrders')); } diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/StaticPsrClock.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/StaticPsrClock.php deleted file mode 100644 index 7a7f86198..000000000 --- a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/StaticPsrClock.php +++ /dev/null @@ -1,33 +0,0 @@ -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 index f7c78c545..a58827d40 100644 --- a/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php +++ b/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php @@ -9,12 +9,13 @@ use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; use Ecotone\Messaging\Scheduling\EcotoneClockInterface; +use Ecotone\Test\StaticPsrClock; use PHPUnit\Framework\TestCase; +use Psr\Clock\ClockInterface; use Test\Ecotone\Messaging\Fixture\Scheduling\CustomNotifier; use Test\Ecotone\Messaging\Fixture\Scheduling\NotificationService; use Test\Ecotone\Messaging\Fixture\Scheduling\OrderService; use Test\Ecotone\Messaging\Fixture\Scheduling\PlaceOrder; -use Test\Ecotone\Messaging\Fixture\Scheduling\StaticPsrClock; /** * Class StaticGlobalClockTest @@ -32,20 +33,18 @@ 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()], + [ClockInterface::class => $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) @@ -64,4 +63,45 @@ public function test_delayed_message_observes_clock_changes() count($notifier->getNotificationsOf('placedOrder')) ); } + + public function test_delayed_message_observes_clock_changes_natively_by_moving_time() + { + $ecotoneTestSupport = EcotoneLite::bootstrapFlowTesting( + [EcotoneClockInterface::class, OrderService::class, NotificationService::class, CustomNotifier::class], + [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 = Clock::get(); + $clock->sleep(Duration::minutes(1)->add(Duration::seconds(1))); + + // 2. Releasing messages awaiting for 60 seconds + $ecotoneTestSupport->run('notifications'); + + $this->assertEquals( + 1, + count($notifier->getNotificationsOf('placedOrder')) + ); + } + + public function test_clock_moves_in_time_when_not_injected(): void + { + EcotoneLite::bootstrapFlowTesting( + [OrderService::class, NotificationService::class, CustomNotifier::class], + [new OrderService(), new NotificationService(), $notifier = new CustomNotifier()], + enableAsynchronousProcessing: [ + SimpleMessageChannelBuilder::createQueueChannel('notifications', true) + ] + ); + + $time = Clock::get()->now(); + $nextMoment = Clock::get()->now(); + + $this->assertGreaterThan($time->getMicrosecond(), $nextMoment->getMicrosecond()); + } } diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php index eabb1878d..ad4196454 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php @@ -6,8 +6,8 @@ use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; +use Ecotone\Test\StaticPsrClock; use PHPUnit\Framework\TestCase; -use Test\Ecotone\Messaging\Fixture\Scheduling\StaticPsrClock; /** * Class SleepableStaticClockTest diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php deleted file mode 100644 index b070bd65d..000000000 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * @internal - */ -/** - * licence Apache-2.0 - * @internal - */ -class StaticGlobalClockTest extends TestCase -{ - protected function setUp(): void - { - parent::tearDown(); - Clock::resetToNativeClock(); - } - - protected function tearDown(): void - { - parent::tearDown(); - Clock::resetToNativeClock(); - } - - 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() - { - $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')); - } -} From e4fec6b2b338cbd058745ee23a6f858bca0a9e79 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 15:55:55 +0100 Subject: [PATCH 2/7] fixes --- .../DbalBackedMessageChannelTest.php | 39 ++++++++++++++++++- .../src/Messaging/Scheduling/TimeSpan.php | 4 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php b/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php index 8e15a465e..2f1a88892 100644 --- a/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php +++ b/packages/Dbal/tests/Integration/DbalBackedMessageChannelTest.php @@ -15,6 +15,7 @@ use Ecotone\Messaging\PollableChannel; use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; +use Ecotone\Messaging\Scheduling\EcotoneClockInterface; use Ecotone\Messaging\Scheduling\StubUTCClock; use Ecotone\Messaging\Scheduling\TimeSpan; use Ecotone\Messaging\Support\MessageBuilder; @@ -232,12 +233,10 @@ public function test_delaying_the_message_with_custom_clock() public function test_delaying_the_message_with_native_clock() { $channelName = Uuid::uuid4()->toString(); - $clock = new StubUTCClock(); $ecotoneLite = EcotoneLite::bootstrapFlowTesting( containerOrAvailableServices: [ DbalConnectionFactory::class => $this->getConnectionFactory(true), - ClockInterface::class => $clock, ], configuration: ServiceConfiguration::createWithDefaults() ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DBAL_PACKAGE])) @@ -265,6 +264,42 @@ public function test_delaying_the_message_with_native_clock() $this->assertNotNull($messageChannel->receive()); } + public function test_delaying_the_message_with_native_clock_using_date_time() + { + $channelName = Uuid::uuid4()->toString(); + + $ecotoneLite = EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + DbalConnectionFactory::class => $this->getConnectionFactory(true), + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DBAL_PACKAGE])) + ->withExtensionObjects([ + DbalBackedMessageChannelBuilder::create($channelName) + ->withReceiveTimeout(1), + ]) + ); + + /** @var PollableChannel $messageChannel */ + $messageChannel = $ecotoneLite->getMessageChannel($channelName); + + $messageChannel->send( + MessageBuilder::withPayload('some') + ->setHeader(MessageHeaders::DELIVERY_DELAY, 2000) + ->build() + ); + + /** @var EcotoneClockInterface $clock */ + $clock = $ecotoneLite->getServiceFromContainer(EcotoneClockInterface::class); + $ecotoneLite->waitTill($clock->now()->add(Duration::seconds(1))); + + $this->assertNull($messageChannel->receive()); + + $ecotoneLite->waitTill($clock->now()->add(Duration::seconds(3))); + + $this->assertNotNull($messageChannel->receive()); + } + public function test_sending_message() { $queueName = Uuid::uuid4()->toString(); diff --git a/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php b/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php index d5f24ff09..0861800b1 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php +++ b/packages/Ecotone/src/Messaging/Scheduling/TimeSpan.php @@ -22,10 +22,10 @@ public function __construct( } - public static function fromDateInterval(DateInterval $dateInterval): self + public static function fromDateInterval(\DateInterval $dateInterval): self { return new self( - milliseconds: $dateInterval->f * 1000, + milliseconds: (int)($dateInterval->f * 1000), seconds: $dateInterval->s, minutes: $dateInterval->i, hours: $dateInterval->h, From 7709fdfb08002398e5e36f9ff88b2cf463d440af Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 16:08:11 +0100 Subject: [PATCH 3/7] fixes --- packages/Ecotone/src/Messaging/Scheduling/Clock.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/Ecotone/src/Messaging/Scheduling/Clock.php b/packages/Ecotone/src/Messaging/Scheduling/Clock.php index 0221694a0..0547435d7 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/Clock.php +++ b/packages/Ecotone/src/Messaging/Scheduling/Clock.php @@ -10,6 +10,8 @@ use Ecotone\Test\StaticPsrClock; use Psr\Clock\ClockInterface as PsrClockInterface; +use function usleep; + class Clock implements EcotoneClockInterface { private static ?EcotoneClockInterface $globalClock = null; @@ -48,7 +50,16 @@ public function now(): DatePoint public function sleep(Duration $duration): void { - $this->clock->sleep($duration); + if ($this->clock instanceof SleepInterface) { + $this->clock->sleep($duration); + return; + } + + if ($duration->isNegativeOrZero()) { + return; + } + + self::defaultClock()->sleep($duration); } private static function defaultClock(): EcotoneClockInterface From 1a05ca872adf1dafc571dd17404571d8a5ecf659 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 16:42:34 +0100 Subject: [PATCH 4/7] fixes --- .../tests/phpunit/SymfonyMessengerFinalFailureStrategyTest.php | 3 +-- phpunit.xml.dist | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/Symfony/tests/phpunit/SymfonyMessengerFinalFailureStrategyTest.php b/packages/Symfony/tests/phpunit/SymfonyMessengerFinalFailureStrategyTest.php index f712b75ab..894d536ae 100644 --- a/packages/Symfony/tests/phpunit/SymfonyMessengerFinalFailureStrategyTest.php +++ b/packages/Symfony/tests/phpunit/SymfonyMessengerFinalFailureStrategyTest.php @@ -47,10 +47,9 @@ public function test_resend_failure_strategy_rejects_message_on_exception() { $channelName = 'messenger_async'; - // Boot in dev environment to have Messenger transport configured $ecotoneTestSupport = EcotoneLite::bootstrapFlowTesting( [MessengerAsyncCommandHandler::class], - $this->bootKernel(['environment' => 'dev'])->getContainer(), + $this->bootKernel()->getContainer(), ServiceConfiguration::createWithAsynchronicityOnly() ->withExtensionObjects([ SymfonyMessengerMessageChannelBuilder::create($channelName) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f9787f476..1cd9dded0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -31,6 +31,7 @@ + From c8a56a1e5abdb9f9385e88395e4c4540bf224bd4 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 17:23:26 +0100 Subject: [PATCH 5/7] fixes --- packages/Symfony/tests/phpunit/MessengerIntegrationTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php index f0772fdf6..77de49eb5 100644 --- a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php +++ b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php @@ -331,7 +331,8 @@ public function test_sending_with_delay_using_datetime() $messaging->sendCommandWithRoutingKey('execute.example_command', $messagePayload, metadata: [ MessageHeaders::DELIVERY_DELAY => (new DateTimeImmutable())->modify('+1 second'), ]); - $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup(maxExecutionTimeInMilliseconds: 2000)); + sleep(1); + $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup()); $this->assertCount(1, $messaging->sendQueryWithRouting('consumer.getMessages')); } } From dc1d6fa1b4e404483a0af818fedd252919588a6e Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 17:53:23 +0100 Subject: [PATCH 6/7] fixes --- .../tests/Queue/LaravelQueueFinalFailureStrategyTest.php | 3 ++- packages/Symfony/tests/phpunit/MessengerIntegrationTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/Laravel/tests/Queue/LaravelQueueFinalFailureStrategyTest.php b/packages/Laravel/tests/Queue/LaravelQueueFinalFailureStrategyTest.php index 0c82b5003..f43861dc9 100644 --- a/packages/Laravel/tests/Queue/LaravelQueueFinalFailureStrategyTest.php +++ b/packages/Laravel/tests/Queue/LaravelQueueFinalFailureStrategyTest.php @@ -105,7 +105,8 @@ public function test_sending_with_delay_using_datetime() $ecotoneTestSupport->sendCommandWithRoutingKey('execute.delayed_command', new DelayedCommand('test_1'), metadata: [ MessageHeaders::DELIVERY_DELAY => (new DateTimeImmutable())->modify('+1 second'), ]); - $ecotoneTestSupport->run('async', ExecutionPollingMetadata::createWithTestingSetup(maxExecutionTimeInMilliseconds: 2000)); + sleep(2); + $ecotoneTestSupport->run('async', ExecutionPollingMetadata::createWithTestingSetup()); $this->assertEquals(['test_1'], $delayedService->getMessages()); } diff --git a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php index 77de49eb5..ad3382d64 100644 --- a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php +++ b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php @@ -331,7 +331,7 @@ public function test_sending_with_delay_using_datetime() $messaging->sendCommandWithRoutingKey('execute.example_command', $messagePayload, metadata: [ MessageHeaders::DELIVERY_DELAY => (new DateTimeImmutable())->modify('+1 second'), ]); - sleep(1); + sleep(2); $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup()); $this->assertCount(1, $messaging->sendQueryWithRouting('consumer.getMessages')); } From ed7d6b13c6958df60c97d8ce4df29e133670ade4 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Sun, 25 Jan 2026 18:45:18 +0100 Subject: [PATCH 7/7] fixes --- .../Application/Execution/LaravelQueueIntegrationTest.php | 3 ++- packages/Symfony/tests/phpunit/MessengerIntegrationTest.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/Laravel/tests/Application/Execution/LaravelQueueIntegrationTest.php b/packages/Laravel/tests/Application/Execution/LaravelQueueIntegrationTest.php index 9638d0e10..63bfde5df 100644 --- a/packages/Laravel/tests/Application/Execution/LaravelQueueIntegrationTest.php +++ b/packages/Laravel/tests/Application/Execution/LaravelQueueIntegrationTest.php @@ -355,7 +355,8 @@ public function test_sending_with_delay() $messaging->sendCommandWithRoutingKey('execute.example_command', $messagePayload, metadata: [ MessageHeaders::DELIVERY_DELAY => 1000, ]); - $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup(maxExecutionTimeInMilliseconds: 2000)); + sleep(2); + $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup()); $this->assertCount(1, $messaging->sendQueryWithRouting('consumer.getMessages')); } diff --git a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php index ad3382d64..683d5a3f4 100644 --- a/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php +++ b/packages/Symfony/tests/phpunit/MessengerIntegrationTest.php @@ -310,7 +310,8 @@ public function test_sending_with_delay() $messaging->sendCommandWithRoutingKey('execute.example_command', $messagePayload, metadata: [ MessageHeaders::DELIVERY_DELAY => 1000, ]); - $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup(maxExecutionTimeInMilliseconds: 2000)); + sleep(2); + $messaging->run($channelName, ExecutionPollingMetadata::createWithTestingSetup()); $this->assertCount(1, $messaging->sendQueryWithRouting('consumer.getMessages')); }