From 581bc7043f4cd67f4563be4bba2a98a82b378085 Mon Sep 17 00:00:00 2001 From: Mario Juarez Date: Wed, 29 Jul 2026 08:36:34 +0200 Subject: [PATCH] Add custom sleep handler for testability --- src/Config.php | 29 +++++++++++++++++++++++++ src/Http/Middleware/DelayMiddleware.php | 5 ++++- src/Traits/Connector/SendsRequests.php | 3 ++- tests/Feature/DelayRequestTest.php | 26 ++++++++++++++++++++++ tests/Feature/RetryRequestTest.php | 28 ++++++++++++++++++++++++ tests/Unit/ConfigTest.php | 13 +++++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/Config.php b/src/Config.php index f0d7f4a7..ec86cbc0 100644 --- a/src/Config.php +++ b/src/Config.php @@ -52,6 +52,13 @@ final class Config */ private static bool $preventStrayRequests = false; + /** + * Custom sleep handler used instead of usleep() + * + * @var callable|null + */ + private static mixed $sleepHandler = null; + /** * Write a custom sender resolver */ @@ -107,4 +114,26 @@ public static function allowStrayRequests(): void { self::$preventStrayRequests = false; } + + /** + * Write a custom sleep handler + * + * The handler receives the number of microseconds to sleep for. Useful + * for skipping or observing sleeps in tests. Pass null to restore the + * default usleep() behaviour. + */ + public static function sleepUsing(?callable $sleepHandler): void + { + self::$sleepHandler = $sleepHandler; + } + + /** + * Sleep for the given number of microseconds + */ + public static function sleep(int $microseconds): void + { + $sleepHandler = self::$sleepHandler; + + is_callable($sleepHandler) ? $sleepHandler($microseconds) : usleep($microseconds); + } } diff --git a/src/Http/Middleware/DelayMiddleware.php b/src/Http/Middleware/DelayMiddleware.php index fcfe89fa..4662a63b 100644 --- a/src/Http/Middleware/DelayMiddleware.php +++ b/src/Http/Middleware/DelayMiddleware.php @@ -4,6 +4,7 @@ namespace Saloon\Http\Middleware; +use Saloon\Config; use Saloon\Http\PendingRequest; use Saloon\Contracts\RequestMiddleware; @@ -16,6 +17,8 @@ public function __invoke(PendingRequest $pendingRequest): void { $delay = $pendingRequest->delay()->get() ?? 0; - usleep($delay * 1000); + if ($delay > 0) { + Config::sleep($delay * 1000); + } } } diff --git a/src/Traits/Connector/SendsRequests.php b/src/Traits/Connector/SendsRequests.php index a4482adf..6f1d3081 100644 --- a/src/Traits/Connector/SendsRequests.php +++ b/src/Traits/Connector/SendsRequests.php @@ -4,6 +4,7 @@ namespace Saloon\Traits\Connector; +use Saloon\Config; use LogicException; use Saloon\Http\Pool; use Saloon\Http\Request; @@ -58,7 +59,7 @@ public function send(Request $request, ?MockClient $mockClient = null, ?callable ? $retryInterval * (2 ** ($attempts - 2)) * 1000 : $retryInterval * 1000; - usleep($sleepTime); + Config::sleep($sleepTime); } try { diff --git a/tests/Feature/DelayRequestTest.php b/tests/Feature/DelayRequestTest.php index 52704b11..8def13eb 100644 --- a/tests/Feature/DelayRequestTest.php +++ b/tests/Feature/DelayRequestTest.php @@ -2,10 +2,15 @@ declare(strict_types=1); +use Saloon\Config; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; use Saloon\Tests\Fixtures\Requests\UserRequest; +afterEach(function () { + Config::sleepUsing(null); +}); + test('async request delay works', function () { $request = new UserRequest; $request->delay()->set(1000); @@ -96,3 +101,24 @@ $connector->send($request); expect(round(microtime(true) - $start))->toBeGreaterThanOrEqual(1); }); + +test('the delay is sent through the custom sleep handler when one is defined', function () { + $microseconds = []; + + Config::sleepUsing(function (int $duration) use (&$microseconds) { + $microseconds[] = $duration; + }); + + $mockClient = new MockClient([ + MockResponse::make(['name' => 'Sam']), + ]); + + $request = new UserRequest; + $request->delay()->set(1000); + + $start = microtime(true); + connector()->send($request, $mockClient); + + expect(microtime(true) - $start)->toBeLessThan(1); + expect($microseconds)->toEqual([1_000_000]); +}); diff --git a/tests/Feature/RetryRequestTest.php b/tests/Feature/RetryRequestTest.php index 18c428f8..3d957b8a 100644 --- a/tests/Feature/RetryRequestTest.php +++ b/tests/Feature/RetryRequestTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Saloon\Config; use Saloon\Http\Request; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; @@ -13,6 +14,10 @@ use Saloon\Tests\Fixtures\Requests\HeaderErrorRetryRequest; use Saloon\Exceptions\Request\Statuses\InternalServerErrorException; +afterEach(function () { + Config::sleepUsing(null); +}); + test('a failed request can be retried', function () { $mockClient = new MockClient([ MockResponse::make(['name' => 'Sam'], 500), @@ -108,6 +113,29 @@ expect(round(microtime(true) - $start))->toBeGreaterThanOrEqual(2); }); +test('the interval between attempts is sent through the custom sleep handler when one is defined', function () { + $microseconds = []; + + Config::sleepUsing(function (int $duration) use (&$microseconds) { + $microseconds[] = $duration; + }); + + $mockClient = new MockClient([ + MockResponse::make(['name' => 'Sam'], 500), + MockResponse::make(['name' => 'Gareth'], 500), + MockResponse::make(['name' => 'Teodor'], 200), + ]); + + $connector = new TestConnector; + $connector->withMockClient($mockClient); + + $connector->send(new RetryUserRequest(3, 200)); + + expect($microseconds)->toEqual([200_000, 200_000]); + + $mockClient->assertSentCount(3); +}); + test('an exception other than a request exception will not be retried', function () { $mockClient = new MockClient([ MockResponse::make(['name' => 'Sam'], 500), diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index ee0f22db..e0cc0683 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -16,6 +16,7 @@ afterEach(function () { Config::clearGlobalMiddleware(); Config::$defaultSender = GuzzleSender::class; + Config::sleepUsing(null); }); test('the config can specify global middleware', function () { @@ -100,3 +101,15 @@ Config::clearGlobalMiddleware(); }); + +test('the config can specify a custom sleep handler', function () { + $microseconds = []; + + Config::sleepUsing(function (int $duration) use (&$microseconds) { + $microseconds[] = $duration; + }); + + Config::sleep(50_000); + + expect($microseconds)->toEqual([50_000]); +});