Follow this quick start to write a driver unit test based on the simple unit test code example:
Include library dependencies
Include this library dependency, as well as the gtest dependency:
#include <lib/driver/testing/cpp/driver_test.h>
#include <gtest/gtest.h>
The library provides two classes that can be used in tests.
In the example we use ForegroundDriverTest
, but there is also a
BackgroundDriverTest
available.
See Foreground vs. Background below for details.
Create configuration class
Tests define a configuration class to pass into the library through a template parameter. The class takes care of managing the individual parts of the unit test, making sure they are run in the correct dispatcher context.
This configuration class must define two types, one for the driver, the other for the environment dependencies of the driver which we show in the next section.
Here is an example of a configuration class from the example:
class TestConfig final {
public:
using DriverType = simple::SimpleDriver;
using EnvironmentType = SimpleDriverTestEnvironment;
};
Define environment type class
The EnvironmentType
must be an isolated class
that provides your driver’s custom dependencies.
It does not need to provide framework dependencies (except for compat::DeviceServer
),
as the library does that already.
If no extra dependencies are needed, use fdf_testing::MinimalCompatEnvironment
which provides a default compat::DeviceServer
(note this is only available
in-tree as the compat protocol is not in the SDK).
Here is our environment from the example:
class SimpleDriverTestEnvironment : public fdf_testing::Environment {
public:
zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) override {
// Perform any additional initialization here, such as setting up compat device servers
// and FIDL servers.
return zx::ok();
}
};
Define the test
Now we can put it all together and make our test. Here is what that looks like in our example:
class SimpleDriverTest : public ::testing::Test {
public:
void SetUp() override {
zx::result<> result = driver_test().StartDriver();
ASSERT_EQ(ZX_OK, result.status_value());
}
void TearDown() override {
zx::result<> result = driver_test().StopDriver();
ASSERT_EQ(ZX_OK, result.status_value());
}
fdf_testing::ForegroundDriverTest<TestConfig>& driver_test() {
return driver_test_;
}
private:
fdf_testing::ForegroundDriverTest<TestConfig> driver_test_;
};
TEST_F(SimpleDriverTest, VerifyChildNode) {
driver_test().RunInNodeContext([](fdf_testing::TestNode& node) {
EXPECT_EQ(1u, node.children().size());
EXPECT_TRUE(node.children().count("simple_child"));
});
}
Run unit tests
Driver unit tests are executed from within the test folder of the driver itself. For example, execute the following command to run the driver tests for the iwlwifi driver:
tools/bazel test third_party/iwlwifi/test:iwlwifi_test_pkg
Configuration arguments
DriverType
The type of the driver under test that will be provided back through the
driver()
and RunInDriverContext()
functions.
By default this is NOT used for driver lifecycle management
(ie. starting/stopping the driver).
That happens through the driver registration symbol
created by the FUCHSIA_DRIVER_EXPORT
macro call from the driver.
When using a custom test-specific driver in DriverType
(for example to
provide test-specific functions), add a static GetDriverRegistration
function as shown below. This will override the global registration symbol.
static DriverRegistration GetDriverRegistration()
EnvironmentType
A class that contains custom dependencies for the driver under test. The environment will always live on a background dispatcher.
It must be default constructible, derive from the fdf_testing::Environment class
,
and override the following function:
zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) override;
The function is called automatically on the background environment dispatcher
when starting the driver-under-test.
It must add its parts to the provided fdf::OutgoingDirectory object
,
generally done through the AddService
method.
The OutgoingDirectory
backs the driver's incoming namespace, hence its name,
to_driver_vfs
.
Here is what a custom environment that provides the compat protocol and a custom test-defined FIDL server looks like:
class MyFidlServer : public fidl::WireServer<fuchsia_examples_gizmo::Proto> {...};
class CustomEnvironment : public fdf_testing::Environment {
public:
zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) {
device_server_.Initialize(component::kDefaultInstance);
EXPECT_EQ(ZX_OK, device_server_.Serve(
fdf::Dispatcher::GetCurrent()->async_dispatcher(), &to_driver_vfs));
EXPECT_EQ(ZX_OK, to_driver_vfs.AddService<fuchsia_examples_gizmo::Service::Proto>(
custom_server_.CreateInstanceHandler()).status_value());
return zx::ok();
}
private:
compat::DeviceServer device_server_;
MyFidlServer custom_server_;
};
Foreground vs. Background
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.
When using the foreground version, the test can access the driver under test
using a driver()
method and directly make calls into it,
but sync client tasks send to driver-provided FIDL must go through a
RunOnBackgroundDispatcherSync()
.
When using the background version the test can make sync FIDL calls into
driver-provided FIDL, but must go through a RunInDriverContext()
when
accessing the driver instance.
The driver_test()
As seen in the example test above, there is a driver_test()
getter that the
test created to return a reference to the library class. This object provides
all of the controls that a test can use to do various operations for their test,
like starting the driver, connecting to it, and running tasks.
There are some methods available under both foreground and background tests,
and some that are specific to the threading mode. See below for the available
methods.
Methods available on foreground tests
driver
This can be used to access the driver directly from the test. Since the driver is on the foreground it is safe to access this on the main test thread.
RunOnBackgroundDispatcherSync
Runs a task on a background dispatcher, separate from the driver. This is done to avoid deadlocking with the driver when making sync client calls into a driver that is on the foreground.
Methods available on background tests
RunInDriverContext
This can be used to run a callback on the driver under test. The callback input will have a reference to the driver. All accesses to the driver must go through this as it is unsafe to touch the driver on the main test thread when it is on the background.
Methods available on both
runtime
Access the driver runtime object. This can be used to create new background dispatchers or to run the foreground dispatcher. The user does not need to explicitly create dispatchers for the environment or the driver as the library takes care of that.
StartDriver
This can be used to start the driver under test. Waits for the start to complete before returning the result.
StartDriverWithCustomStartArgs
Same as StartDriver but can modify the driver start arguments before sending it to the driver.
StopDriver
Stops the driver under test. This calls PrepareStop on the DriverBase implementation and waits for the completion. This must be called if StartDriver succeeded. It can also be called if StartDriver failed, but to match the behavior of the driver host, it will be a no-op.
ShutdownAndDestroyDriver
Shuts down the driver dispatchers belonging to the driver-under-test, and then call the driver's destroy hook. This happens automatically on the destruction of the test, but can also be called manually by a test if it needs to happen earlier for some validation or to start the driver again
Connect
Connects to an instance of a service member that the driver under test provides. This can be either a driver transport or a zircon channel transport based service.
ConnectThroughDevfs
Connects to a protocol that the driver has exported through devfs
.
This can be given the node_name of the devfs
node,
or a list of node names to traverse before reaching the devfs
node.
RunInEnvironmentTypeContext
Runs a task on the EnvironmentType
instance that the test is using.
RunInNodeContext
Runs a task on the fdf_testing::TestNode
instance that the test is using.
This can be used to validate the driver’s interactions with the driver framework node
(like checking how many children have been added).
Starting the driver multiple times in a single test
To start/stop the driver multiple times in a test without changing the environment, ensure to go through all 3 steps: - StartDriver/StartDriverWithCustomStartArgs - StopDriver - ShutdownAndDestroyDriver
Run* functions warning
Be careful when using the Run* functions
(RunInDriverContext
, RunOnBackgroundDispatcherSync
, RunInEnvironmentTypeContext
, RunInNodeContext
).
These tasks run on specific dispatchers, so it might be unsafe to:
- Pass raw pointers into them from another context (main thread or a different Run* kind) to use in the function
- Return a raw pointer (through a captured ref or return type) out of them to use on the main thread or to capture/use in another Run* function (except for a Run* function of the same kind).