Driver unit testing quick start

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_.Init(component::kDefaultInstance, "root");
    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).

Examples