Driver unit testing tutorial

This guide provides a detailed understanding of the Driver Framework v2 testing libraries and how to use them including the DriverRuntime, TestNode, TestEnvironment, and DriverUnderTest classes.

The following image shows the entirety of the driver testing framework:

Driver unit testing framework

At a glance, the DriverRuntime automatically creates the foreground dispatcher and attaches it to the main testing thread. The foreground dispatcher synchronously runs tests and uses what it needs in the test environment, including FIDL services, through the background dispatcher(s). DriverUnderTest wraps the driver being tested and provides the ability to call its lifecycle hooks.

Throughout this tutorial, you will dive into each specific part of the driver testing framework and learn how to write code to make your tests work. It may help to return to this image as you dive into each of its parts. Once you are familiar with the testing framework, refer to the quick start and reference as needed.

Set up testing framework

Set up dispatchers and create driver runtime instance

The first step in setting up the testing framework is to create the driver runtime instance and start the background dispatchers.

Driver runtime instance

Create driver runtime instance

When you start your test, you create the instance of your driver runtime. This automatically creates the foreground dispatcher:

fdf_testing::DriverRuntime runtime_;

Examples:

Start background dispatcher

Use the StartBackgroundDispatcher method on the DriverRuntime to start the background dispatcher(s)manually (if needed):

fdf::UnownedSynchronizedDispatcher env_dispatcher_ = runtime_.StartBackgroundDispatcher();

This method gives a pointer for the background dispatcher where you can put your environment dependencies.

Examples:

Create TestNode object

The second step in setting up the testing framework is to create the test node object. TestNode is a mock server for the driver framework node protocol.

Test node object

Use the wrapper class, TestDispatcherBound, to instantiate the class by creating the TestNode object. This is done through a wrapper class for memory and thread safety:

async_patterns::TestDispatcherBound<fdf_testing::TestNode> node_server_{
    env_dispatcher(), std::in_place, std::string("root")};

At this point, you aren't serving anything yet, just representing the root node of your tests. The driver being tested binds to this node. It is completely detached from the tree so that you can hook your driver to it.

The point of the node protocol is to add children into the node topology. Similar to how components live inside a tree structure, so do drivers. The node protocol is how you add children to the node topology in the driver framework. The driver binds to the node and can add children to node for other drivers to bind to.

Examples:

Create TestEnvironment object

The third step in setting up the testing framework is to create the test environment object. The TestEnvironment provides FIDL services to the driver, simulating the FIDL server that's accessible to the driver.

Test environment object

As with TestNode, use the wrapper class, TestDispatcherBound, to instantiate the class by creating the TestEnvironment object. This is done through a wrapper class for memory and thread safety:

async_patterns::TestDispatcherBound<fdf_testing::TestEnvironment> test_environment_{
    env_dispatcher(), std::in_place};

Examples:

Set up custom FIDL server class

The fourth step in setting up the testing framework is to setup the custom FIDL server class. This is an optional step, required to support any FIDLs that the driver needs for this particular unit test. There are three parts to this set-up: create the custom FIDL server class, get the server handler, and move the server handler into the TestEnvironment class.

Create custom FIDL server class

Custom FIDL server class

This example code in Driver FIDL test, lines 121-124 creates server classes for Zircon and Driver to support both Zircon and Driver services.

async_patterns::TestDispatcherBound<ZirconProtocolServer> zircon_proto_server_{env_dispatcher(),
                                                                               std::in_place};
async_patterns::TestDispatcherBound<DriverProtocolServer> driver_proto_server_{env_dispatcher(),
                                                                               std::in_place};

Get custom FIDL server handler

Custom FIDL server handler

This example code in Driver FIDL test, lines 71-74 gets handlers for both the Zircon and Driver services:

fuchsia_driver_component_test::ZirconService::InstanceHandler zircon_proto_handler =
    zircon_proto_server_.SyncCall(&ZirconProtocolServer::GetInstanceHandler);
fuchsia_driver_component_test::DriverService::InstanceHandler driver_proto_handler =
    driver_proto_server_.SyncCall(&DriverProtocolServer::GetInstanceHandler);

Move custom FIDL server handler into test environment

Move FIDL server handler

This example code in Driver FIDL test, lines 76-87 moves both the Zircon and Driver handlers into the test environment:

test_environment_.SyncCall([zircon_proto_handler = std::move(zircon_proto_handler),
                            driver_proto_handler = std::move(driver_proto_handler)](
                               fdf_testing::TestEnvironment* env) mutable {
  zx::result result =
      env->incoming_directory().AddService<fuchsia_driver_component_test::ZirconService>(
          std::move(zircon_proto_handler));
  ASSERT_EQ(ZX_OK, result.status_value());

  result = env->incoming_directory().AddService<fuchsia_driver_component_test::DriverService>(
      std::move(driver_proto_handler));
  ASSERT_EQ(ZX_OK, result.status_value());
});

Call CreateStartArgsAndServe

The fifth step in setting up the testing framework is to call the CreateStartArgsAndServe method on the the TestNode class. Calling this method returns three objects: driver start_args table, outgoing_directory_client, and incoming_directory_server:

CreateStartArgsAndServe

Calling this method also starts serving the node protocol (fdf::Node). This is a channel from the TestNode class to the client of TestNode in the DriverStartArgs table.

zx::result start_args = node_server_.SyncCall(&fdf_testing::TestNode::CreateStartArgsAndServe);
ASSERT_EQ(ZX_OK, start_args.status_value());

Examples:

Initialize test environment

The sixth step in setting up the testing framework is to initialize the test environment. Calling the initialize() function on the TestEnvironment class moves the incoming directory server (from CreateStartArgsResult) into the test environment:

Initialize test environment

The purpose of this call is to tell the test environment to start serving on the channel from the server end. The channel now goes from the DriverStartArgs incoming namespace client to the test environment class.

zx::result init_result =
    test_environment_.SyncCall(&fdf_testing::TestEnvironment::Initialize,
                               std::move(start_args->incoming_directory_server));
ASSERT_EQ(ZX_OK, init_result.status_value());

Both the TestNode and TestEnvironment are things that the driver needs to run and so are passed to the driver through its start arguments. Note, however, they go in as different channels.

TestEnvironment contains the VFS (virtual file system) that provides FIDL services to the driver. When your driver connects to a protocol through it's incoming directory, the server providing the protocol is on the test environment, simulating that the FIDL server is accessible by the driver.

Examples:

Run actual tests

Start driver

DriverUnderTest is a wrapper class that provides life cycle hooks for the driver being tested. To start the driver, call the start method on the DriverUnderTest class:

Start driver

The driver lives on the foreground dispatcher. The start method is an asynchronous operation and won't wait for the foreground dispatcher to run. The start method is passed to the RunToCompletion method and the RunToCompletion method runs the foreground dispatcher until the start class is complete.

zx::result result = runtime().RunToCompletion(driver_.SyncCall(
  &fdf_testing::DriverUnderTest<TestDriver>::Start, std::move(start_args())));

Once start happens, the driver and everything in the testing environment runs on the foreground dispatcher.

Examples:

Run tests

The next step is to run the actual unit tests themselves. At this point, you can call into any of the driver methods to test the driver.

Run tests

Each test is unique in what it's testing on the driver. For example, starting the PcielwlwifiDriver triggers AddNode for the wlanphy virtual device. The iwlwifi driver lifecycle test, line 176, gets the number of children nodes:

size_t GetNodeNumber() { return node_server_.SyncCall(&TestNodeLocal::GetchildrenCount); }

And checks that the number of nodes is 1 (line 204:

EXPECT_EQ(GetNodeNumber(), (size_t)1);

Set up channel for outgoing directory, if needed

There's one last object left in the CreateStartArgs table: the outgoing directory. Use this object to validate something the driver interacts with, for example, if the driver is exporting services.

Set up channel for outgoing directory

The following code sets up a channel that will allow you to connect any FIDLs your driver is exporting as part of its outgoing directory (Driver FIDL test, lines 100-102):

zx_status_t status = fdio_open_at(driver_outgoing_.handle()->get(), "/svc",
                                  static_cast<uint32_t>(fuchsia_io::OpenFlags::kDirectory),
                                  svc_endpoints->server.TakeChannel().release());

Call PrepareStop

The final step is to call PrepareStop and pass to RunToCompletion:

PrepareStop

Like the start method, PrepareStop method is asynchronous, and hence it is passed to the RunToCompletion method which runs the foreground dispatcher until PrepareStop is complete (Driver FIDL test, line 159):

zx::result result = runtime().RunToCompletion(driver_.PrepareStop());

RunToCompletion goes until the driver is prepared to fully stop. At this point, all of the classes go out of scope and are turned down automatically. All objects go out of scope until everything is gone.