Skip to content
...

Revolt

Revolt is the standard event loop for asynchronous PHP: a loop that waits for timers, sockets, and streams to become ready, then resumes the coroutines waiting on them, one at a time. It powers the amphp libraries. This plugin adds the #[RunInRevolt]#[RunInRevolt()]Runs the test on the Revolt event loop. attribute, which runs a test on the process-wide Revolt loop, where it can await asynchronous work (timers, streams, Future::await()) without blocking the process.

The plugin does not require setup.

Installation

bash
composer require --dev testo/bridge-revolt

#[RunInRevolt]

Runs the test on the Revolt event loop.

#[RunInRevolt()]

The test body runs on the event loop, but from the outside the call is still an ordinary blocking one: the pipeline waits for the test to finish.

Can be applied to a method or a class (Test Case). On a class, the attribute sends every test in the case through the loop, but one at a time: the next test enters the loop only after the previous one has fully finished.

Examples:

Await real asynchronous work (a Revolt timer) inside a test:

php
use Revolt\EventLoop;
use Testo\Assert;
use Testo\Bridge\Revolt\RunInRevolt;
use Testo\Test;

#[Test]
#[RunInRevolt]
public function resolvesAfterDelay(): void
{
    $suspension = EventLoop::getSuspension();
    EventLoop::delay(0.1, static fn() => $suspension->resume('ready'));

    Assert::same('ready', $suspension->suspend()); // the loop keeps spinning while we wait
}

Suspend only through Suspension\Revolt\EventLoop\Suspension

The test's fiber belongs to the Revolt event loop, so suspend through a Suspension\Revolt\EventLoop\Suspension tied to a loop event (a timer, I/O): nothing on the loop will ever resume a bare \Fiber::suspend(). And if what you want is bare fibers managed by Testo itself, that's a different job — reach for #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler. from the Fiber plugin.

The reverse is also true: Future::await() and Suspension\Revolt\EventLoop\Suspension only work on the Revolt event loop, that is, under #[RunInRevolt]#[RunInRevolt()]Runs the test on the Revolt event loop..

One test at a time

#[RunInRevolt]#[RunInRevolt()]Runs the test on the Revolt event loop. doesn't run tests in parallel: only the test body goes onto the event loop, and tests never share a loop run. Data providers, retries, and Testo's own plumbing run outside the loop, and the next test in the case enters it only after the previous one has fully finished.

This is a deliberate trade-off: test isolation matters more than interleaving. If interleaving tests to hunt for races is exactly what you need, use #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler. from the Fiber plugin: it runs on plain fibers that Testo manages itself.

Why can't all tests run at once, like with RunInFiber?

It comes down to who controls the fibers. Isolation under #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler. from the Fiber plugin relies on guards: the test body runs inside the guard's nested fiber, so every \Fiber::suspend() passes through the guard, which detaches its test's state on suspend and restores it on resume. Under #[RunInRevolt]#[RunInRevolt()]Runs the test on the Revolt event loop., the fibers belong to Revolt: it suspends and resumes them itself, bypassing the guards, so the state never gets switched. If several tests landed on the loop at once, their contexts would bleed into each other: assertions, messages, and coverage would be credited to the wrong tests.

RunInRevolt or RunInFiber?

#[RunInRevolt]#[RunInRevolt()]Runs the test on the Revolt event loop. is about asynchrony: give the test an event loop so it can await real I/O (amphp, Revolt timers and streams), isolated from other tests. #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler. is about concurrency: interleave the tests in a case on plain fibers to flush out races in cooperative code. They solve different problems: #[RunInRevolt] doesn't run tests in parallel, and #[RunInFiber] doesn't spin an event loop.