[10.x] Siesta
This PR proposes a new utility class that wraps up the native sleep and usleep PHP functions into a "fakeable" and testable class.
The motivation behind this PR is to improve testing of sleeping and while also removing the duration of the sleep from the test itself while maintaining confidence in the test.
Often when an application interacts with a 3rd party library it will need to retry interactions after pausing execution.
An example of this may be a HTTP endpoint. The API may be down for a brief moment. Instead of failing, the application may use a "retry":
Http::retry(3, 500)->post(/* ... */);
When attempting to test the failure / retry logic, a test would take a minimum of 1.5 seconds.
More generally, when using the retry helper, this is also a (minor) annoyance.
return retry(5, function () {
// Attempt 5 times while resting 100ms between attempts...
}, 100);
To address this, we now use the Siesta helper in a few places throughout the Framework. The helper may the be "faked" in tests and some basic assertions may also be made.
API
Fluent API
// minutes...
Siesta::for(3)->minutes();
Siesta::for(1)->minute();
// seconds...
Siesta::for(3)->seconds();
Siesta::for(1)->second();
// milliseconds...
Siesta::for(30000)->milliseconds();
Siesta::for(1)->millisecond();
// microseconds...
Siesta::for(30000)->microseconds();
Siesta::for(1)->microsecond();
Composed durations
Siesta::for(3)->minutes()
->and(5)->seconds()
->and(9)->milliseconds();
Siesta until a given time
Siesta::until(now()->addMinute());
1:1 replacement API
// usleep(30000);
Siesta::usleep(30000);
// sleep(2);
Siesta::sleep(2);
// time_sleep_until(1682641623)
Siesta::until(1682641623);
Testing
As mentioned, the motivation here is to improve testing. If an application simply wants tests to not "sleep", then the following could be added to any test that uses Siesta:
protected function setUp(): void
{
parent::setUp();
Siesta::fake();
}
It is also possible to make assertions:
public function testItSendsWebhookSuccessfluly()
{
Siesta::fake();
Http::fake(['https://laravel.com' => Http::response('', 200)]);
Webhook::send('https://laravel.com');
Siesta::assertInsomniac(); // too much?! ๐
}
public function testItRetriesWithBackoffForFailedWebhooks()
{
$attempts = 0;
Siesta::fake();
Http::fake(['https://laravel.com' => function () use (&$attempts) {
$attempts++;
return $attempts++ === 3
? Http::response('', 200)
: Http::response('', 500);
}]);
Webhook::send('https://laravel.com');
$this->assertSame(3, $attempts);
Siesta::assertSleptTimes(3);
Siesta::assertSequence([
Siesta::for(100)->milliseconds(),
Siesta::for(200)->milliseconds(),
Siesta::for(300)->milliseconds(),
]);
}
// Full API
Siesta::assertSlept(fn (CarbonInterval $duration): bool => /* ... */);
Siesta::assertSlept(fn (CarbonInterval $duration): bool => /* ... */, $times = 5);
// asserts the number of times Siesta was used...
Siesta::assertSleptTimes(5);
// asserts that even if Siesta was used that zero time was spent sleeping...
Siesta::assertInsomniac();
Now any call to sleep via Siesta will not actually pause execution throughout the testsuite.
Naming alternatives
If the framework doesn't jive with "Siesta":
SleepPauseRest- Something else?
โฆbut lowkey โSiestaโ is the best option ๐
Use of Siesta within the Framework
I feel that the places I've used Siesta throughout the framework make sense. If we feel any of them don't make sense, I'm happy to remove any.
๐ Notes
The native time_sleep_until function will throw an error if the time has already passed; we just do not sleep. When testing, you would need to assert that it slept for zero seconds.
The native sleep function will cast a float to an int. Our implementation does not do this:
sleep(1.5); // only sleeps for 1 second
Siesta::for(1.5)->seconds(); // sleeps for 1 second and 500 milliseconds
Siesta::sleep(1.5); // sleeps for 1 second and 500 milliseconds
The native usleep function may not support values larger than 1000000. Our implementation fixes this by using a combination of sleep and usleep.
Although PHP does have the ability to sleep for nanoseconds, via the native time_nanosleep function, I didn't feel the need to add an affordance to this.
An implementation would be possible, however I'm not sure it is worth it. If you need something that fine-grained, even our PHP code in-between things is probably going to be problematic.
Warning: Time is hard. We do execute some PHP between sleeps, so this will never be perfect, although computers also aren't great at time, so it is already unreliable to some degree. YOLO.