blob: 82da83fb4c2776f8af21adb7d33532248f12a9f7 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/fpromise/bridge.h>
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
// TODO(https://fxbug.dev/42116817): delete once log output is properly interleaved.e
#include <lib/syslog/cpp/macros.h>
#include <chrono>
#include <thread>
#include "src/ui/scenic/lib/shutdown/shutdown_manager.h"
namespace {
class ShutdownManagerTest : public gtest::TestLoopFixture {
public:
// Provides synchronized access to test state. This is required because the timeout callback will
// be invoked on a different thread.
class State {
public:
bool get_quit_callback_invoked() {
std::lock_guard<std::mutex> lock(mut);
return quit_callback_invoked;
}
bool get_timeout_callback_invoked() {
std::lock_guard<std::mutex> lock(mut);
return timeout_callback_invoked;
}
bool get_timeout_callback_invocation_value() {
std::lock_guard<std::mutex> lock(mut);
return timeout_callback_invocation_value;
}
void set_quit_callback_invoked() {
std::lock_guard<std::mutex> lock(mut);
quit_callback_invoked = true;
}
void set_timeout_callback_invoked(bool value) {
std::lock_guard<std::mutex> lock(mut);
timeout_callback_invoked = true;
timeout_callback_invocation_value = value;
}
private:
std::mutex mut;
bool quit_callback_invoked = false;
bool timeout_callback_invoked = false;
bool timeout_callback_invocation_value = false;
};
protected:
void SetUp() {
gtest::TestLoopFixture::SetUp();
clock_time_ = std::make_shared<std::atomic<zx::time>>();
UpdateTimeoutClock();
state_ = std::make_shared<State>();
manager_ = scenic_impl::ShutdownManager::New(
dispatcher(),
// |quit_callback|
[state = state_] { state->set_quit_callback_invoked(); },
// |timeout_callback|
[state = state_](bool timed_out) { state->set_timeout_callback_invoked(timed_out); });
manager_->set_clock_callback([clock = clock_time_] { return clock->load(); });
}
void TearDown() {
manager_.reset();
state_.reset();
clock_time_.reset();
gtest::TestLoopFixture::TearDown();
}
// Same as superclass, plus updates the timeout-clock time.
bool RunLoopUntil(zx::time deadline) {
auto result = gtest::TestLoopFixture::RunLoopUntil(deadline);
UpdateTimeoutClock();
return result;
}
// Same as superclass, plus updates the timeout-clock time.
bool RunLoopFor(zx::duration duration) {
auto result = gtest::TestLoopFixture::RunLoopFor(duration);
UpdateTimeoutClock();
return result;
}
// Same as superclass, plus updates the timeout-clock time.
bool RunLoopUntilIdle() {
auto result = gtest::TestLoopFixture::RunLoopUntilIdle();
UpdateTimeoutClock();
return result;
}
const std::shared_ptr<scenic_impl::ShutdownManager>& manager() { return manager_; }
void ResetManager() { manager_.reset(); }
State* state() { return state_.get(); }
void WaitForTimeoutInvocation() {
while (!state()->get_timeout_callback_invoked()) {
std::this_thread::yield();
}
}
private:
void UpdateTimeoutClock() {
FX_CHECK(clock_time_);
clock_time_->store(Now());
}
std::shared_ptr<State> state_;
std::shared_ptr<scenic_impl::ShutdownManager> manager_;
std::shared_ptr<std::atomic<zx::time>> clock_time_;
};
constexpr zx::duration kTimeout(20000000); // 20ms
} // anonymous namespace
// Verify that Shutdown() succeeds when no clients are registered.
TEST_F(ShutdownManagerTest, NoClients) {
manager()->Shutdown(kTimeout);
EXPECT_TRUE(state()->get_quit_callback_invoked());
EXPECT_TRUE(state()->get_timeout_callback_invoked());
EXPECT_FALSE(state()->get_timeout_callback_invocation_value());
}
// Verify that Shutdown() succeeds when all registered clients shutdown before the timeout occurs.
TEST_F(ShutdownManagerTest, ClientsComplete) {
// Register 2 clients. Their callback notifications won't be invoked until Shutdown() is called.
fpromise::bridge<> client1, client2;
bool client1_notified = false;
bool client2_notified = false;
manager()->RegisterClient([&] {
client1_notified = true;
return client1.consumer.promise();
});
manager()->RegisterClient([&] {
client2_notified = true;
return client2.consumer.promise();
});
EXPECT_FALSE(client1_notified);
EXPECT_FALSE(client2_notified);
EXPECT_FALSE(state()->get_quit_callback_invoked());
// Initiate shutdown. The clients should be notified immediately.
manager()->Shutdown(kTimeout);
EXPECT_TRUE(client1_notified);
EXPECT_TRUE(client2_notified);
EXPECT_FALSE(state()->get_quit_callback_invoked());
// Complete the clients' promises. The shutdown isn't complete because the dispatcher
// needs to "tick" in order to respond to the completion of the promises.
client1.completer.complete_ok();
client2.completer.complete_ok();
EXPECT_FALSE(state()->get_quit_callback_invoked());
bool tasks_were_run = RunLoopUntilIdle();
EXPECT_TRUE(tasks_were_run);
EXPECT_TRUE(state()->get_quit_callback_invoked());
RunLoopFor(kTimeout);
WaitForTimeoutInvocation();
EXPECT_FALSE(state()->get_timeout_callback_invocation_value());
}
// Verify that Shutdown() succeeds when some of the registered clients fail to shut down before the
// deadline.
TEST_F(ShutdownManagerTest, ClientTimesOut) {
// Register 2 clients. Their callback notifications won't be invoked until Shutdown() is called.
fpromise::bridge<> client1, client2;
bool client1_notified = false;
bool client2_notified = false;
manager()->RegisterClient([&] {
client1_notified = true;
return client1.consumer.promise();
});
manager()->RegisterClient([&] {
client2_notified = true;
return client2.consumer.promise();
});
EXPECT_FALSE(client1_notified);
EXPECT_FALSE(client2_notified);
EXPECT_FALSE(state()->get_quit_callback_invoked());
EXPECT_FALSE(state()->get_timeout_callback_invoked());
// Initiate shutdown. The clients should be notified immediately.
manager()->Shutdown(kTimeout);
EXPECT_TRUE(client1_notified);
EXPECT_TRUE(client2_notified);
EXPECT_FALSE(state()->get_quit_callback_invoked());
EXPECT_FALSE(state()->get_timeout_callback_invoked());
// Complete only one client promise. Because the second isn't completed, shutdown won't
// complete until the timeout occurs.
client1.completer.complete_ok();
bool tasks_were_run = RunLoopUntilIdle();
EXPECT_TRUE(tasks_were_run);
EXPECT_FALSE(state()->get_quit_callback_invoked());
EXPECT_FALSE(state()->get_timeout_callback_invoked());
RunLoopFor(kTimeout);
WaitForTimeoutInvocation();
EXPECT_FALSE(state()->get_quit_callback_invoked());
EXPECT_TRUE(state()->get_timeout_callback_invocation_value());
}
// Verify that nothing bad happens when some clients finish shutting down after the ShutdownManager
// has been destroyed.
TEST_F(ShutdownManagerTest, ManagerDeleted) {
// This client outlives the ShutdownManager.
fpromise::bridge<> client;
bool client_notified = false;
{
manager()->RegisterClient([&] {
client_notified = true;
return client.consumer.promise();
});
EXPECT_FALSE(client_notified);
// Initiate shutdown. The client should be notified immediately.
manager()->Shutdown(kTimeout);
EXPECT_TRUE(client_notified);
// Verify that timeout thread doesn't hang onto the manager.
std::weak_ptr<scenic_impl::ShutdownManager> weak(manager());
ResetManager();
EXPECT_FALSE(weak.lock());
}
client.completer.complete_ok();
RunLoopUntilIdle();
WaitForTimeoutInvocation();
EXPECT_FALSE(state()->get_quit_callback_invoked());
EXPECT_FALSE(state()->get_timeout_callback_invocation_value());
}