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
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.
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 theTestEnvironment
class. This server end must be given toTestEnvironment::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
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.
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:
// 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 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 and the dispatcher documentation.
Tests can create any number of background dispatchers using this DriverRuntime
function:
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.
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.