blob: e1c630e2e6a18dc4cfd5d498dd5ef4d153f79a5e [file] [log] [blame]
// Copyright 2024 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.
#ifndef LIB_DRIVER_TESTING_CPP_DRIVER_TEST_H_
#define LIB_DRIVER_TESTING_CPP_DRIVER_TEST_H_
#include <lib/async_patterns/testing/cpp/dispatcher_bound.h>
#include <lib/driver/testing/cpp/internal/internals.h>
// This library provides RAII-style classes for writing driver unit tests. It contains two classes,
// |ForegroundDriverTest| and |BackgroundDriverTest| that contain all the logic for a driver test.
// These classes can also be used directly in a test as a test fixture field, or as a local in a
// non-fixture based test method. This should be the first field or local that is being created.
//
// The choice between foreground and background driver tests lies in how the test plans to
// communicate with the driver-under-test. If the test will be calling public methods on the driver
// a lot, the foreground driver test should be chosen. If the test will be calling through the
// driver's exposed FIDL more often, then the background driver test should be chosen.
//
// Both test kinds can be configured through a struct/class provided through a template parameter.
// This configuration must define two types through using statements:
//
// DriverType: The type of the driver under test.
// If using a test-specific driver, ensure the DriverType contains a static function
// in the format below. This registration is what the test uses to manage the driver.
// `static DriverRegistration GetDriverRegistration()`
// If the driver type is not known to the test, or is not needed by the test, then use the
// |EmptyDriverType| class as a placeholder.
//
// EnvironmentType: A class that contains environment dependencies of the driver-under-test.
// This must inherit from the |Environment| base class and provide the |Serve| method.
// It must also contain a default constructor.
// The environment will live on a background dispatcher during the lifetime of the test.
namespace fdf_testing {
// The configuration must be given a non-void DriverType, but not all tests need access to a
// driver type (eg. they don't need to access or call anything on the driver class). This can be
// used as a placeholder to avoid build errors. The driver lifecycle is managed separately, through
// the `__fuchsia_driver_registration__` symbol, or through its custom registration provided with
// GetDriverRegistration.
class EmptyDriverType {};
// The EnvironmentType must implement this class.
class Environment {
public:
virtual ~Environment() = default;
// This function is called on the dispatcher context of the environment. The class should serve
// its elements (eg. compat::DeviceServer, FIDL servers, etc...) to the |to_driver_vfs|.
// This object is serving the incoming directory of the driver under test.
virtual zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) = 0;
};
namespace internal {
// Common logic for both background and foreground driver tests.
//
// Both foreground and background test classes inherit from this common class and provide
// workflows for their specific threading-model.
//
// This class is not meant for direct use so its in the internal namespace.
template <typename Configuration>
class DriverTestCommon {
public:
using DriverType = typename ConfigurationExtractor<Configuration>::DriverType;
using EnvironmentType = typename ConfigurationExtractor<Configuration>::EnvironmentType;
DriverTestCommon()
: env_dispatcher_(runtime_.StartBackgroundDispatcher()),
env_wrapper_(env_dispatcher_->async_dispatcher(), std::in_place) {}
virtual ~DriverTestCommon() = default;
// Access the driver runtime object. This can be used to create new background dispatchers
// or to run the foreground dispatcher.
fdf_testing::DriverRuntime& runtime() { return runtime_; }
// Connects to a service member that the driver under test provides.
template <typename ServiceMember,
typename = std::enable_if_t<fidl::IsServiceMemberV<ServiceMember>>>
zx::result<fidl::internal::ClientEndType<typename ServiceMember::ProtocolType>> Connect(
std::string_view instance = component::kDefaultInstance) {
if constexpr (std::is_same_v<typename ServiceMember::ProtocolType::Transport,
fidl::internal::ChannelTransport>) {
return component::ConnectAtMember<ServiceMember>(ConnectToDriverSvcDir(), instance);
} else if constexpr (std::is_same_v<typename ServiceMember::ProtocolType::Transport,
fidl::internal::DriverTransport>) {
return fdf::internal::DriverTransportConnect<ServiceMember>(ConnectToDriverSvcDir(),
instance);
} else {
static_assert(std::false_type{});
}
}
// Runs a task on the dispatcher context of the EnvironmentType. This will be a different thread
// than the main test thread, so be careful when capturing and returning pointers to objects that
// live on different dispatchers like test fixture properties, or the driver.
//
// Returns the result of the given task once it has completed.
template <typename T>
T RunInEnvironmentTypeContext(fit::callback<T(EnvironmentType&)> task) {
return env_wrapper_.SyncCall(
[env_task = std::move(task)](EnvWrapper<EnvironmentType>* env_ptr) mutable {
return env_task(env_ptr->user_env());
});
}
// Runs a task on the dispatcher context of the EnvironmentType. This will be a different thread
// than the main test thread, so be careful when capturing and returning pointers to objects that
// live on different dispatchers like test fixture properties, or the driver.
//
// Returns when the given task has completed.
void RunInEnvironmentTypeContext(fit::callback<void(EnvironmentType&)> task) {
env_wrapper_.SyncCall(
[env_task = std::move(task)](EnvWrapper<EnvironmentType>* env_ptr) mutable {
env_task(env_ptr->user_env());
});
}
// Runs a task on the dispatcher context of the TestNode. This will be a different thread than
// the main test thread, so be careful when capturing and returning pointers to objects that live
// on different dispatchers like test fixture properties, or the driver.
//
// Returns the result of the given task once it has completed.
template <typename T>
T RunInNodeContext(fit::callback<T(fdf_testing::TestNode&)> task) {
return env_wrapper_.SyncCall(
[node_task = std::move(task)](EnvWrapper<EnvironmentType>* env_ptr) mutable {
return node_task(env_ptr->node_server());
});
}
// Runs a task on the dispatcher context of the TestNode. This will be a different thread than
// the main test thread, so be careful when capturing and returning pointers to objects that live
// on different dispatchers like test fixture properties, or the driver.
//
// Returns when the given task has completed.
void RunInNodeContext(fit::callback<void(fdf_testing::TestNode&)> task) {
env_wrapper_.SyncCall(
[node_task = std::move(task)](EnvWrapper<EnvironmentType>* env_ptr) mutable {
node_task(env_ptr->node_server());
});
}
// Connect to a zircon transport based protocol through a devfs node that the driver under test
// exports. The |devfs_node_name| is the name of the created node with the 'devfs_args'. This
// node must have been created through the driver's immediate node. If the devfs node is nested,
// use the variant that takes a vector of strings.
template <typename ProtocolType, typename = std::enable_if_t<fidl::IsProtocolV<ProtocolType>>>
zx::result<fidl::ClientEnd<ProtocolType>> ConnectThroughDevfs(std::string_view devfs_node_name) {
return ConnectThroughDevfs<ProtocolType>(std::vector{std::string(devfs_node_name)});
}
// Connect to a zircon transport based protocol through a devfs node that the driver under test
// exports. The |devfs_node_name_path| is a list of node names that should be traversed to reach
// the devfs node. The last element in the vector is the name of the created node with the
// 'devfs_args'.
template <typename ProtocolType, typename = std::enable_if_t<fidl::IsProtocolV<ProtocolType>>>
zx::result<fidl::ClientEnd<ProtocolType>> ConnectThroughDevfs(
std::vector<std::string> devfs_node_name_path) {
zx::result<zx::channel> raw_channel_result =
env_wrapper_.SyncCall([devfs_node_name_path = std::move(devfs_node_name_path)](
EnvWrapper<EnvironmentType>* env_ptr) mutable {
fdf_testing::TestNode* current = &env_ptr->node_server();
for (auto& node : devfs_node_name_path) {
current = &current->children().at(node);
}
return current->ConnectToDevice();
});
if (raw_channel_result.is_error()) {
return raw_channel_result.take_error();
}
return zx::ok(fidl::ClientEnd<ProtocolType>(std::move(raw_channel_result.value())));
}
// Start the driver.
zx::result<> StartDriver() {
ZX_ASSERT_MSG(
!start_result_.has_value(),
"Cannot call |StartDriver| multiple times in a row. If multiple starts are needed, "
"ensure to go through |StopDriver| and |ShutdownAndDestroyDriver| first.");
fdf::DriverStartArgs start_args = env_wrapper_.SyncCall(&EnvWrapper<EnvironmentType>::Init);
outgoing_directory_client_ =
env_wrapper_.SyncCall(&EnvWrapper<EnvironmentType>::TakeOutgoingClient);
AddServiceValidation(start_args);
start_result_ = StartDriverInner(std::move(start_args));
return *start_result_;
}
// Start the driver with modified DriverStartArgs. This is done through the |args_modifier|
// which is called with a reference to the start args that will be used to start the driver.
// Modifications can happen in-place with this reference.
zx::result<> StartDriverWithCustomStartArgs(
fit::callback<void(fdf::DriverStartArgs&)> args_modifier) {
ZX_ASSERT_MSG(
!start_result_.has_value(),
"Cannot call |StartDriver| multiple times in a row. If multiple starts are needed, "
"ensure to go through |StopDriver| and |ShutdownAndDestroyDriver| first.");
fdf::DriverStartArgs start_args = env_wrapper_.SyncCall(&EnvWrapper<EnvironmentType>::Init);
outgoing_directory_client_ =
env_wrapper_.SyncCall(&EnvWrapper<EnvironmentType>::TakeOutgoingClient);
args_modifier(start_args);
AddServiceValidation(start_args);
start_result_ = StartDriverInner(std::move(start_args));
return *start_result_;
}
// Stops the driver by calling the driver's PrepareStop hook and waiting for it to complete.
zx::result<> StopDriver() {
ZX_ASSERT_MSG(start_result_.has_value(), "Cannot stop without having started.");
ZX_ASSERT_MSG(!prepare_stop_result_.has_value(),
"Ensure |StopDriver| is only called once after a |StartDriver| call.");
if (StartedSuccessfully()) {
prepare_stop_result_ = StopDriverInner();
} else {
// Drivers that failed to stop don't receive a PrepareStop call. Set the result as ok so
// that the teardown continues successfully.
prepare_stop_result_ = zx::ok();
}
return *prepare_stop_result_;
}
// Shuts down the driver dispatchers fully, and then calls the driver's destroy hook.
// Can be called multiple times, but it will only do the required work the first time.
//
// If the driver running was started successfully, this must be called after |StopDriver|.
void ShutdownAndDestroyDriver() {
// Allow for calling this method multiple times, but only do this work the first time.
if (DriverExists()) {
if (StartedSuccessfully()) {
ZX_ASSERT_MSG(prepare_stop_result_.has_value(),
"Ensure |ShutdownAndDestroyDriver| is only called once after "
"a |StopDriver| call when start was successful.");
}
// This will shut down the driver dispatcher (and sub-dispatchers) and call the driver's
// destroy hook.
ShutdownAndDestroyDriverInner();
// Reset the start_result_ and prepare_stop_result_ to allow another iteration of the driver.
start_result_.reset();
prepare_stop_result_.reset();
}
}
fidl::ClientEnd<fuchsia_io::Directory> ConnectToDriverSvcDir() {
auto [client_end, server_end] = fidl::Endpoints<fuchsia_io::Directory>::Create();
zx_status_t status = fdio_open3_at(
outgoing_directory_client_.handle()->get(), "/svc",
uint64_t{fuchsia_io::wire::kPermReadable | fuchsia_io::wire::Flags::kProtocolDirectory},
server_end.TakeChannel().release());
ZX_ASSERT_MSG(ZX_OK == status, "Failed to fdio_open3_at '/svc' on the driver's outgoing: %s.",
zx_status_get_string(status));
return std::move(client_end);
}
// This will enable the service instance validation on the driver. Equivalent to setting
// `service_connect_validation: "true"` in the cml of the driver.
// This must be called before |StartDriver|/|StartDriverWithCustomStartArgs| as it will only
// take affect on the next start of the driver and cannot modify an already started driver.
void SetServiceValidator(bool enable) { enable_service_connect_validation_ = enable; }
private:
virtual zx::result<> StartDriverInner(fdf::DriverStartArgs start_args) = 0;
virtual zx::result<> StopDriverInner() = 0;
virtual bool DriverExists() = 0;
virtual void ShutdownAndDestroyDriverInner() = 0;
bool StartedSuccessfully() const { return start_result_.has_value() && start_result_->is_ok(); }
void AddServiceValidation(fdf::DriverStartArgs& start_args) {
if (enable_service_connect_validation_) {
if (start_args.program() == std::nullopt) {
start_args.program(fuchsia_data::Dictionary{});
}
if (start_args.program()->entries() == std::nullopt) {
start_args.program().value().entries(std::vector<fuchsia_data::DictionaryEntry>{});
}
start_args.program()->entries()->push_back(fuchsia_data::DictionaryEntry(
"service_connect_validation", std::make_unique<fuchsia_data::DictionaryValue>(
fuchsia_data::DictionaryValue::WithStr("true"))));
}
}
fdf_testing::DriverRuntime runtime_;
fdf::UnownedSynchronizedDispatcher env_dispatcher_;
async_patterns::TestDispatcherBound<EnvWrapper<EnvironmentType>> env_wrapper_;
fidl::ClientEnd<fuchsia_io::Directory> outgoing_directory_client_;
std::optional<zx::result<>> start_result_;
std::optional<zx::result<>> prepare_stop_result_;
bool enable_service_connect_validation_ = false;
};
} // namespace internal
// Background driver tests have the driver-under-test executing on a background driver dispatcher.
// This allows for tests to use sync FIDL clients directly from their main test thread.
// This is good for unit tests that more heavily exercise the driver-under-test through its exposed
// FIDL services, rather than its public methods.
//
// The test can run tasks on the driver context using the |RunInDriverContext()| methods, but sync
// client tasks can be run directly on the main test thread.
template <typename Configuration>
class BackgroundDriverTest final : public internal::DriverTestCommon<Configuration> {
using DriverType = typename Configuration::DriverType;
public:
~BackgroundDriverTest() { this->ShutdownAndDestroyDriver(); }
// Runs a task on the dispatcher context of the driver under test. This will be a different
// thread than the main test thread, so be careful when capturing and returning pointers to
// objects that live on different dispatchers like test fixture properties, or the environment.
//
// Returns the result of the given task once it has completed.
template <typename T>
T RunInDriverContext(fit::callback<T(DriverType&)> task) {
ZX_ASSERT_MSG(dut_.has_value(),
"Cannot call RunInDriverContext after |ShutdownAndDestroyDriver|.");
return dut_->SyncCall([driver_task = std::move(task)](
fdf_testing::internal::DriverUnderTest<DriverType>* dut_ptr) mutable {
return driver_task(***dut_ptr);
});
}
// Runs a task on the dispatcher context of the driver under test. This will be a different
// thread than the main test thread, so be careful when capturing and returning pointers to
// objects that live on different dispatchers like test fixture properties, or the environment.
//
// Returns when the given task has completed.
void RunInDriverContext(fit::callback<void(DriverType&)> task) {
ZX_ASSERT_MSG(dut_.has_value(),
"Cannot call RunInDriverContext after |ShutdownAndDestroyDriver|.");
dut_->SyncCall([driver_task = std::move(task)](
fdf_testing::internal::DriverUnderTest<DriverType>* dut_ptr) mutable {
driver_task(***dut_ptr);
});
}
private:
zx::result<> StartDriverInner(fdf::DriverStartArgs start_args) override {
DriverRegistration symbol;
if constexpr (internal::HasGetDriverRegistrationV<DriverType>) {
symbol = DriverType::GetDriverRegistration();
} else {
symbol = __fuchsia_driver_registration__;
}
dut_dispatcher_ = this->runtime().StartBackgroundDispatcher();
dut_.emplace(dut_dispatcher_->async_dispatcher(), std::in_place, symbol);
return this->runtime().RunToCompletion(dut_->SyncCall(
&fdf_testing::internal::DriverUnderTest<DriverType>::Start, std::move(start_args)));
}
zx::result<> StopDriverInner() override {
return this->runtime().RunToCompletion(
dut_->SyncCall(&fdf_testing::internal::DriverUnderTest<DriverType>::PrepareStop));
}
bool DriverExists() override { return dut_.has_value(); }
void ShutdownAndDestroyDriverInner() override {
this->runtime().ShutdownBackgroundDispatcher(dut_dispatcher_->get(),
[this]() { dut_.reset(); });
}
fdf::UnownedSynchronizedDispatcher dut_dispatcher_;
std::optional<
async_patterns::TestDispatcherBound<fdf_testing::internal::DriverUnderTest<DriverType>>>
dut_;
};
// Foreground driver tests have the driver-under-test executing on the foreground (main) test
// thread. This allows for the test to directly reach into the driver to call methods on it.
// This is good for unit tests that more heavily test a driver through its public methods,
// rather than its exposed FIDL services.
//
// The test can access the driver under test using the |driver()| method and directly make calls
// into it, but sync client tasks must go through |RunOnBackgroundDispatcherSync()|.
template <typename Configuration>
class ForegroundDriverTest final : public internal::DriverTestCommon<Configuration> {
using DriverType = typename Configuration::DriverType;
public:
~ForegroundDriverTest() { this->ShutdownAndDestroyDriver(); }
// Runs a task in a background context while running the foreground driver. This must be used
// if calling synchronously into the driver (eg. a fidl call through a SyncClient).
//
// Returns the result of the given task once it has completed.
template <typename T>
zx::result<T> RunOnBackgroundDispatcherSync(fit::callback<T()> task) {
if (!bg_task_dispatcher_.has_value()) {
bg_task_dispatcher_.emplace(this->runtime().StartBackgroundDispatcher());
}
libsync::Completion completion;
std::optional<T> result_container;
zx_status_t status =
async::PostTask(bg_task_dispatcher_.value()->async_dispatcher(), [&]() mutable {
result_container.emplace(task());
completion.Signal();
});
if (status != ZX_OK) {
return zx::error(status);
}
while (!completion.signaled()) {
this->runtime().RunUntilIdle();
}
return zx::ok(std::move(result_container.value()));
}
// Runs a task in a background context while running the foreground driver. This must be used
// if calling synchronously into the driver (eg. a fidl call through a SyncClient).
//
// Returns when the given task has completed.
zx::result<> RunOnBackgroundDispatcherSync(fit::callback<void()> task) {
if (!bg_task_dispatcher_.has_value()) {
bg_task_dispatcher_.emplace(this->runtime().StartBackgroundDispatcher());
}
libsync::Completion completion;
zx_status_t status =
async::PostTask(bg_task_dispatcher_.value()->async_dispatcher(), [&]() mutable {
task();
completion.Signal();
});
if (status != ZX_OK) {
return zx::error(status);
}
while (!completion.signaled()) {
this->runtime().RunUntilIdle();
}
return zx::ok();
}
// Access the driver under test. Can only be called while the driver is active, after
// |StartDriver| has been called, but before |ShutdownAndDestroyDriver|.
DriverType* driver() {
ZX_ASSERT_MSG(dut_.has_value(), "Cannot call |driver| after |ShutdownAndDestroyDriver|.");
return *dut_.value();
}
private:
zx::result<> StartDriverInner(fdf::DriverStartArgs start_args) override {
DriverRegistration symbol;
if constexpr (internal::HasGetDriverRegistrationV<DriverType>) {
symbol = DriverType::GetDriverRegistration();
} else {
symbol = __fuchsia_driver_registration__;
}
dut_.emplace(symbol);
return this->runtime().RunToCompletion(dut_->Start(std::move(start_args)));
}
zx::result<> StopDriverInner() override {
return this->runtime().RunToCompletion(dut_->PrepareStop());
}
bool DriverExists() override { return dut_.has_value(); }
void ShutdownAndDestroyDriverInner() override {
this->runtime().ResetForegroundDispatcher([this]() { dut_.reset(); });
}
std::optional<fdf::UnownedSynchronizedDispatcher> bg_task_dispatcher_;
std::optional<fdf_testing::internal::DriverUnderTest<DriverType>> dut_;
};
} // namespace fdf_testing
#endif // LIB_DRIVER_TESTING_CPP_DRIVER_TEST_H_