The synchronous RunLoop pattern involves creating a new RunLoop, setting up a specified quit condition for it, then calling Run() on it to block the current thread until that quit condition is reached.
You need to block the current thread until an event happens, and you have a way to get notified of that event, via a callback or observer interface or similar. A couple of common scenarios might be:
- Waiting for an asynchronous event (like a network request) to complete
- Waiting for an animation to finish
- Waiting for a page to have loaded
- Waiting for some call that requires a thread hop to complete
The fact that this blocks a thread means it is almost never appropriate outside test code.
- You don't really need the entire thread to wait
- You don't have and can't add a way to get notified when the event happens
- You're waiting for a timer to fire - for that, TaskEnvironment is likely a better fit.
- TaskEnvironment
- Restructuring your code to not require blocking a thread
This pattern relies on two important facts about base::RunLoop:
base::RunLoop::Quit()
is idempotent - once a RunLoop enters the quit state, quitting it again does nothing- Once a RunLoop is in the quit state, calling
base::RunLoop::Run()
on it is a no-op
That means that if your code does this:
base::RunLoop loop;
maybe-asynchronously { loop.Quit(); }
loop.Run();
LOG(INFO) << "Hello!";
then regardless of whether the maybe-asynchronous loop.Quit()
is executed
before or after loop.Run()
, the "Hello!" message will never be printed before
both loop.Run()
and loop.Quit()
have happened. If the Quit
happens
before the Run
, the Run
will be a no-op; if the Quit
happens after the
Run
has started, the Run
will exit after the Quit
.
If the asynchronous thing in question takes a completion callback:
base::RunLoop run_loop;
Reply reply;
DoThingAndReply(
base::BindLambdaForTesting([&](const Reply& r) {
reply = r;
run_loop.Quit();
}));
run_loop.Run();
or perhaps even just:
base::RunLoop run_loop;
DoThing(run_loop.QuitClosure());
run_loop.Run();
If there exists a GizmoObserver interface with an OnThingDone event:
class TestGizmoObserver : public GizmoObserver {
public:
TestGizmoObserver(base::RunLoop* loop, Gizmo* thing)
: GizmoObserver(thing), loop_(loop) {}
// GizmoObserver:
void OnThingStarted(Gizmo* observed_gizmo) override { ... }
void OnThingProgressed(Gizmo* observed_gizmo) override { ... }
void OnThingDone(Gizmo* observed_gizmo) override {
loop_->Quit();
}
};
base::RunLoop run_loop;
TestGizmoObserver observer(&run_loop, gizmo);
gizmo->StartDoingThing();
run_loop.Run();
This is sometimes wrapped up into a helper class that internally constructs the RunLoop like so, if all you need to do is wait for the event but don't care about observing any intermediate states too:
class ThingDoneWaiter : public GizmoObserver {
public:
ThingDoneWaiter(Gizmo* thing) : GizmoObserver(thing) {}
void Wait() {
run_loop_.Run();
}
// GizmoObserver:
void OnThingDone(Gizmo* observed_gizmo) {
run_loop_.Quit();
}
private:
RunLoop run_loop_;
};
ThingDoneWaiter waiter(gizmo);
gizmo->StartDoingThing();
waiter.Wait();
It's important to differentiate between waiting on an event (such as a notification or callback being fired) vs waiting for a state (such as a property on a given object).
When waiting for events, it is crucial that the observer is constructed in time to see the event (see also waiting too late). States, on the other hand, can be queried beforehand in the body of a Wait()-style function.
The following is an example of a Waiter helper class that waits for a state, as opposed to an event:
class GizmoReadyWaiter : public GizmoObserver {
public:
GizmoReadyObserver(Gizmo* gizmo)
: gizmo_(gizmo) {}
~GizmoReadyObserver() override = default;
void WaitForGizmoReady() {
if (!gizmo_->ready()) {
gizmo_observation_.Observe(gizmo_);
run_loop_.Run();
}
}
// GizmoObserver:
void OnGizmoReady(Gizmo* observed_gizmo) {
run_loop_.Quit();
}
private:
RunLoop run_loop_;
Gizmo* gizmo_;
base::ScopedObservation<Gizmo, GizmoObserver> gizmo_observation_{this};
};
A common mis-use of this pattern is like so:
gizmo->StartDoingThing();
base::RunLoop run_loop;
TestGizmoObserver observer(&run_loop, gizmo);
run_loop.Run();
This looks tempting because it seems that you can write a helper function:
void TerribleHorribleNoGoodVeryBadWaitForThing(Gizmo* gizmo) {
base::RunLoop run_loop;
TestGizmoObserver observer(&run_loop, gizmo);
run_loop.Run();
}
and then your test code can simply read:
gizmo->StartDoingThing();
TerribleHorribleNoGoodVeryBadWaitForThing(gizmo);
However, this is a recipe for a flaky test: if gizmo->StartDoingThing()
completes and would deliver the OnThingDone
callback before your
TestGizmoObserver
is ever constructed, the TestGizmoObserver
will never
receive OnThingDone
, and then your run_loop.Run()
will run forever,
frustrating a future tree sheriff (and then probably you, shortly afterward).
This is especially dangerous when gizmo->StartDoingThing()
involves a thread
hop or network request, because these can unpredictably complete before or after
your observer gets constructed. To be safe, always begin observing the event
before running the code that will eventually cause the event!
If you still really want a helper function, perhaps you just want to inline the start:
void NiceFriendlyDoThingAndWait(Gizmo* gizmo) {
base::RunLoop run_loop;
TestGizmoObserver observer(&run_loop, gizmo);
gizmo->StartDoingThing();
run_loop.Run();
}
with the test code being:
NiceFriendlyDoThingAndWait(gizmo);
Note that this is not an issue when waiting on a state, since the observer can query to see if that state is already the current state.
Sometimes, there's no easy way to observe completion of an event. In that case, if the code under test looks like this:
void StartDoingThing() { PostTask(&StepOne); }
void StepOne() { PostTask(&StepTwo); }
void StepTwo() { /* done! */ }
it can be tempting to do:
gizmo->StartDoingThing();
base::RunLoop().RunUntilIdle();
/* now it's done! */
However, doing this is adding dependencies to your test code on the exact async behavior of the production code - for example, the production code may depend on work happening on another TaskRunner, which this won't successfully wait for. This will make your test brittle and flaky.
Instead of doing this, it's vastly better to add a way (even if it's just via a test API) to observe the event you're interested in.
As with most patterns, lifetimes can be an issue with this pattern when using observers. If you are waiting on a given event to happen, and the object that's being observed instead goes out of scope, the test may hang. Similar badness can happen if the Waiter isn't properly removed as an observer, which could lead to Use-After-Frees.
There are two good mitigation practices here.
Consider something like
TEST_F(GizmoTest, WaitForGizmo) {
GizmoWaiter waiter;
Gizmo gizmo;
gizmo.Initialize();
waiter.WaitForGizmoReady();
ASSERT_TRUE(gizmo.ready());
}
This looks safe, but may not be. If GizmoObserver removes itself as an observer from Gizmo in its destructor, this will result in a Use-After-Free during the test tear down. Instead, scope the GizmoWaiter more narrowly:
TEST_F(GizmoTest, WaitForGizmo) {
Gizmo gizmo;
{
GizmoWaiter waiter;
gizmo.Initialize();
waiter.WaitForGizmoReady();
}
ASSERT_TRUE(gizmo.ready());
}
Since the GizmoWaiter is now narrowly-scoped, it will be destroyed when it is no longer needed, and avoid Use-After-Free concerns.
If you need to potentially handle the case where the object being observed is destroyed while a waiter is still active, you can handle the destruction case gracefully.
class GizmoReadyWaiter : public GizmoObserver {
public:
GizmoReadyObserver(Gizmo* gizmo)
: gizmo_(gizmo) {}
~GizmoReadyObserver() override = default;
void WaitForGizmoReady() {
ASSERT_TRUE(gizmo_)
<< "Trying to call Wait() after the Gizmo was destroyed!";
if (!gizmo_->ready()) {
gizmo_observation_.Observe(gizmo_);
run_loop_.Run();
}
}
// GizmoObserver:
void OnGizmoReady(Gizmo* observed_gizmo) {
gizmo_observation_.Reset();
run_loop_.Quit();
}
void OnGizmoDestroying(Gizmo* observed_gizmo) {
DCHECK_EQ(gizmo_, observed_gizmo);
gizmo_ = nullptr;
// Remove the observer now, to avoid a UAF in the destructor.
gizmo_observation_.Reset();
// Bail out so we don't time out in the test waiting for a ready state
// that will never come.
run_loop_.Quit();
// Was this a possible expected outcome? If not, consider:
// ADD_FAILURE() << "The Gizmo was destroyed before it was ready!";
}
private:
RunLoop run_loop_;
Gizmo* gizmo_;
base::ScopedObservation<Gizmo, GizmoObserver> gizmo_observation_{this};
};