blob: 0c80cb7efd307aa335ae7b0f00e0e4a71d0998ac [file] [log] [blame] [view] [edit]
# Driver testing reference
## TestNode
This class serves as the server for two FIDLs that are provided by the driver
framework's driver manager:
- `fuchsia.driver.framework/Node`
- `fuchsia.driver.framework/NodeController`
The Node FIDL is how drivers communicate with the driver framework to add child
nodes into the driver topology. The `NodeController protocol`
is how a driver can manage the children nodes that it has created.
The [TestNode class](https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/sdk/lib/driver/testing/cpp/test_node.cc)
is a part of the unit test's environment that is given
to the driver being tested. This is done through the
`fuchsia.driver.framework/DriverStartArgs` FIDL table.
To simplify the creation of this type and have easy access to the other ends
of channels the driver is provided with through this,
there is a struct defined in this class called
`CreateStartArgsResult` and a corresponding method.
```cpp
struct CreateStartArgsResult {
fuchsia_driver_framework::DriverStartArgs start_args;
fidl::ServerEnd<fuchsia_io::Directory> incoming_directory_server;
fidl::ClientEnd<fuchsia_io::Directory> outgoing_directory_client;
};
zx::result<CreateStartArgsResult> CreateStartArgsAndServe();
```
This method is the starting point for a test to create the `DriverStartArgs`
that will later be provided to the driver.
The values in the struct are as follows:
- **`start_args`**: The actual start args table to be given to the driver.
- **`incoming_directory_server`**: Drivers require an incoming directory to
provide them with the various FIDLs that they need (these are defined in
the driver's cml file with using statements). This is the server end for
that incoming directory, which can be provided to the driver using the
`TestEnvironment` class. This server end must be given to
`TestEnvironment::initialize`.
- **`outgoing_directory_client`**: Drivers themselves provide various FIDLs
back out to the system and other drivers through an outgoing directory
(these are defined in the driver's cml as capabilities/exposed). This
client end is the other side of that outgoing directory and can be used by
the test to use these driver provided FIDLs.
## TestEnvironment
The [TestEnvironment class](https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/sdk/lib/driver/testing/cpp/test_environment.cc)
is used to provide the driver's incoming directory. This includes any FIDL
that the driver needs to function, at least any of them that it needs
to function just for the unit test, not necessarily everything.
Adding into the `TestEnvironment` requires grabbing the
`fdf::OutgoingDirectory` that is wrapped inside of it
through the `incoming_directory()` call.
Note: the confusing outgoing/incoming terms being used together here is because
`OutgoingDirectory` is the name of the class that can back FIDL servers,
which are generally done by components as part of providing an
outgoing namespace. But this is outgoing from the test, into the driver, and
so it is the driver's incoming namespace that it is providing.
The interface to `fdf::OutgoingDirectory` is very similar to that of
`component::OutgoingDirectory`, except that it also allows providing driver
transport based services along with plain zircon channel based services. Drivers
are discouraged from using protocols, but instead should use services; therefore,
this only provides `AddService`/`RemoveService` calls. If a plain protocol must
be added, the internal `component::OutgoingDirectory` can be accessed using the
`component() `function.
## DriverUnderTest
This class is a RAII wrapper for the driver being tested. It provides the
various lifecycle hooks for the driver, and arrow and star operators for
accessing the underlying driver pointer (the type is provided as a template
argument, void type is used when no type is provided).
On destruction, it will call |Stop| for the driver if it hasn't already been
called, but |PrepareStop| must have been manually called and awaited by the
test.
This class works by using the C lifecycle definition that is exported. Most
drivers have already done this through the `FUCHSIA_DRIVER_EXPORT` macro, although
a custom one can be provided if desired.
The lifecycle interface for this is as follows:
```cpp
// Start the driver. This is an asynchronous operation.
// Use |DriverRuntime::RunToCompletion| to await the completion of the async task.
// The resulting zx::result is the result of the start operation.
DriverRuntime::AsyncTask<zx::result<>> Start(fdf::DriverStartArgs start_args);
// PrepareStop the driver. This is an asynchronous operation.
// Use |DriverRuntime::RunToCompletion| to await the completion of the async task.
// The resulting zx::result is the result of the prepare stop operation.
DriverRuntime::AsyncTask<zx::result<>> PrepareStop();
// Stop the driver. The PrepareStop operation must have been completed before Stop
// is called.
// Returns the result of the stop operation.
zx::result<> Stop();
```
## DriverRuntime
The [DriverRuntime class](https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/sdk/lib/driver/testing/cpp/driver_runtime.cc)
is used by the test as a client into the features that are provided
by the driver runtime. The driver runtime includes things like the
background managed thread pool for background driver dispatchers,
support for driver transport protocols, creating dispatchers, and
running the foreground attached dispatcher.
Test authors should be mindful of the various pieces they have in their tests,
how they interact, and what dispatcher they are using to back each piece.
Otherwise they can introduce memory safety issues, deadlocks, race conditions,
and flaky tests.
### Foreground dispatcher
One area that can cause confusion for driver unit test authors is the threading
model. On a full system, the dispatchers provided by the driver runtime are all
run on a shared thread pool that is managed by the driver runtime.
This model makes interacting safely with the driver
through the test more difficult, as the test code is running
on the foreground (initial/main) thread. To solve this problem,
the driver runtime has been modified to provide a test-specific dispatcher
that is meant to be run on the foreground thread, and
not scheduled to run on its background managed thread pool.
The foreground dispatcher is a special type of dispatcher that was added to the
driver runtime specifically for unit tests. It is attached to the test's main
thread as the current dispatcher (`fdf::Dispatcher::GetCurrent() `can get it).
Tasks that are posted to this dispatcher must be manually run using the
various Run methods in the `DriverRuntime`. It allows for thread safe direct
access for objects that are set to live on it.
Generally the driver being tested should be put on the foreground dispatcher so
that methods on the driver can be called directly by the test. The only
situation where this shouldn't be the case is if the test wants to make sync
FIDL calls into the driver's outgoing directory from the test thread.
There can only be 1 foreground dispatcher created. This dispatcher is
automatically created when the `DriverRuntime` class is constructed.
The user does not have to manually create the foreground dispatcher.
The foreground dispatcher is torn down during the destruction
of the `DriverRuntime` class.
### Background dispatchers
Background dispatchers are plain driver dispatchers that are used by drivers on
the full system. These dispatchers share a common thread pool that the driver
runtime manages the number of threads for. The same thread in the thread pool
can be used to run any dispatcher, and each dispatcher can end up running on any
thread in the pool.
With a synchronized dispatcher, there is a guarantee that no two threads will
be executing the dispatcher at the same time.
The threading model of driver dispatchers are discussed in-depth in the
[driver runtime RFC](/docs/contribute/governance/rfcs/0126_driver_runtime.md) and the
[dispatcher documentation](/docs/concepts/drivers/driver-dispatcher-and-threads.md).
Tests can create any number of background dispatchers using this `DriverRuntime`
function:
```cpp
fdf::UnownedSynchronizedDispatcher StartBackgroundDispatcher();
```
The dispatcher created here will be owned by the `DriverRuntime` instance and
will be torn down during the destructor of the `DriverRuntime` class.
Generally the pieces of the test environment (`TestNode` and `TestEnvironment`
instances) should live on a background dispatcher. This is to make sync calls
from the driver into the environment be allowed. Otherwise the driver must
ensure to only make async calls into the environment.
Class instances that live on a background dispatcher (and are marked as
thread-unsafe) are not safe for direct access through the test thread. These
should be wrapped inside of an `async_patterns::TestDispatcherBound` type, which
ensures thread safety.
Note: If a driver is creating its own dispatcher as part of normal operation,
then that dispatcher will be a background dispatcher, even if the driver itself
starts out on the foreground dispatcher.
### Parallels to async::Loop
The design of the `DriverRuntime` takes some inspiration from the `async::Loop`
that other Fuchsia components use. The `DriverRuntime` object itself is a
similar idea to the `async::Loop` object.
It is used to get dispatchers and it is the executor of the dispatchers
that are created by it using various Run methods.
The foreground dispatcher is the same as creating an `async::Loop` with the
`kAsyncLoopConfigAttachToCurrentThread` configuration. The loop dispatcher must
be manually run using the various Run methods available on the `async::Loop`
object. The `DriverRuntime` has all the same Run methods as `async::Loop`. The
dispatcher can be accessed with `fdf::Dispatcher::GetCurrent()` similar to how
the attached loop dispatcher can be grabbed with the default dispatcher getter
`async_get_default_dispatcher()`.
Starting background dispatchers is similar to creating an `async::Loop` with
the `kAsyncLoopConfigNoAttachToCurrentThread` configuration and calling
`StartThread` on the loop to create a background thread that runs the loop
dispatcher. The background dispatcher can be accessed while it is running with
the `fdf::Dispatcher::GetCurrent()`, similar to how the `async::Loop` dispatcher
could be accessed using the default dispatcher getter
`async_get_default_dispatcher()`. While `async::Loop` would allow running these
manually as well (using the various Run methods) for a multi-threaded
dispatcher, the `DriverRuntime` does not allow this.
The main difference here is that the `DriverRuntime` can create both types of
these dispatchers and create and own multiple background dispatchers. With
`async::Loop`, there is a 1:1 relationship with the loop and dispatcher so for
each dispatcher, a new `async::Loop` has to be created with the configuration
for what type of dispatcher is needed.