Mock DDK

A driver unit testing framework

Simple example

Here is a basic example of a driver that uses the using the mock-ddk library to mock the driverhost framework to allow for testing.

First, a simple driver that needs testing. This example driver will be used for all of the code in this documentation.

// Very simple driver:
class MyDevice;
using MyDeviceType = ddk::Device<MyDevice, ddk::Unbindable, ddk::Initializable>;

class MyDevice : public MyDeviceType {
  public:
    MyDevice(zx_device_t* parent)
      : MyDeviceType(parent), client_(parent) {}

    static zx_status_t Create(void* ctx, zx_device_t* parent) {
        auto device = std::make_unique<MyDevice>(parent);
        // Usually do init stuff here that might fail.

        auto status = device->DdkAdd("my-device-name");
        if (status == ZX_OK) {
          // Intentionally leak this device because it's owned by the driver framework.
           [[maybe_unused]] auto unused = device.release();
        }
        return status;
    }

    // Methods required by the ddk mixins
    void DdkInit(ddk::InitTxn txn) { txn.Reply(ZX_OK); }
    void DdkUnbind(ddk::UnbindTxn txn) { txn.Reply(); }
    void DdkRelease() { delete this; }
  private:
    ddk::FooProtocolClient function_;
};

static zx_driver_ops_t my_driver_ops = []() -> zx_driver_ops_t {
  zx_driver_ops_t ops{};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = MyDevice::Create;
  return ops;
}();

ZIRCON_DRIVER(my_device, my_driver_ops, "fuchsia", "0.1");

Normally this driver can only be loaded by the driver host, as the libraries it requires are not made available to normal components. Further, drivers tend to get information from their parent devices. With the mock-ddk library, the device can be loaded and make calls to and from a mock driverhost interface:

TEST(FooDevice, BasicTest) {
  std::shared_ptr<MockDevice> fake_parent = MockDevice::FakeRootParent();
  ASSERT_OK(MyDevice::Create(nullptr, fake_parent.get());
  auto child_dev = fake_parent->GetLatestChild();
  child_dev->InitOp(); // Call the device's Init op
  // Do some testing here

  child_dev->UnbindOp(); // Call the Unbind op if needed
  // The mock-ddk will automatically call DdkRelease() on any remaining children of
  // fake_parent upon destruction.
}

Overview of the Mock-DDK

The mock ddk exists simply as a set of zx_device_t’s that track the interactions a device has with the mocked driver host, and allow calls into the device. There is no global state - if the root “parent” device ever goes out of scope, all the zx_device_t’s will destruct and delete their accompanying device.

Here is an interaction model of how the mock-ddk interacts with a driver:

Figure: Interaction Model

Interactions with the Driverhost

The mock_ddk mocks out and makes available calls to and from the driverhost.

Calling into the device
(device ops)
Calling out to the driverhost
(Libdriver API)
Call device ops through the MockDevice. Functions are named as op name + Op
Example:
Call the init function using InitOp()
All calls in the libdriver API are recorded on the appropriate device, but no action is taken.
Example:
To test if device_init_reply() has been called, call InitReplyCalled()
or to wait on the call, WaitUntilInitReplyCalled().

An example lifecycle test

auto parent = MockDevice::FakeRootParent();
MyDevice::Create(nullptr, parent.get());
// make sure the child device is there
ASSERT_EQ(1, parent->child_count());
auto* child = parent->GetLatestChild();
// If your device has an init function:
child->InitOp();
// Use this if init replies asynchronously:
EXPECT_EQ(ZX_OK,  child->WaitUntilInitReplyCalled());
// Otherwise, can just verify init replied:
EXPECT_TRUE(child->InitReplyCalled());
// If your device has an unbind function:
child->UnbindOp();
// Use this if unbind replies asynchronously:
EXPECT_EQ(ZX_OK, child->WaitUntilUnbindReplyCalled());
// Otherwise, can just verify init replied:
EXPECT_TRUE(child->UnbindReplyCalled());
// Mock-ddk will release all the devices on destruction, or you can do it manually.

Automatically Unbind and Release

The driverhost will always call unbind before releasing a driver, but that step must be done manually in the mock-ddk. If you have multiple drivers under test, it may be easier to automate the unbinding and releasing behavior. The Mock DDK has a helper function for this purpose:

auto parent = MockDevice::FakeRootParent();
MyDevice::Create(nullptr, parent.get());
zx_device_t* child_dev = parent->GetLatestChild();

MyDevice::Create(nullptr, child_dev);
// The state of the tree is now:
//         parent   <--  FakeRootParent
//           |
//        child_dev
//           |
//       grandchild

// You want to remove both test devices, by calling unbind and release in the right order?
device_async_remove(child_dev);

// ReleaseFlaggedDevices performs the unbind and release of any device
// below the input device that has had device_async_remove called on it.
mock_ddk::ReleaseFlaggedDevices(parent.get());

Getting Device Context

The mock-ddk only deals with the zx_device_t's that are associated with a device. However, if you have assigned a device context, by for example using the ddktl library, you may want to access corresponding the ddk::Device:

  auto fake_parent = MockDevice::FakeRootParent();
  // May not get the device* back from bind:
  ASSERT_OK(MyDevice::Create(nullptr, fake_parent.get());

  // Never fear! Recover device from parent:
  MockDevice* child_dev = fake_parent->GetLatestChild();
  MyDevice* test_dev = child_dev->GetDeviceContext<MyDevice>();

Interactions with other drivers

Some information can be added to a device (usually a parent) so that the device under test can retrieve expected values.

Mocking Parent Protocols

Parent protocols are added to the parent before a child device is expected to access them with a call to device_get_protocol()

auto parent = MockDevice::FakeRootParent();
const void* ctx = reinterpret_cast<void*>(0x10),
const void* ops = nullptr,

parent->AddProtocol(8, ops, ctx);

Fragment protocols

Composite devices get protocols from multiple parent “fragments”. This is manifested in protocols being keyed by a name. Mock-ddk allows binding a name to a protocol, to indicate it comes from a fragment.

auto parent = MockDevice::FakeRootParent();
// Mock-ddk uses the same call as adding a
// normal parent protocol:
parent->AddProtocol(ZX_PROTOCOL_GPIO, gpio.GetProto()->ops, gpio.GetProto()->ctx, "fragment-1");
parent->AddProtocol(ZX_PROTOCOL_I2C, i2c.GetProto()->ops, i2c.GetProto()->ctx, "fragment-2");
parent->AddProtocol(ZX_PROTOCOL_CODEC, codec.GetProto()->ops, codec.GetProto()->ctx, "fragment-3");
// gpio, i2c, and codec are device objects with mocked/faked HW interfaces.

Mocking FIDL connections

If the device serves a FIDL protocol, the test may want to call the fidl functions provided. This can be difficult as the fidl functions take a completer as an argument. You can create a client to communicate with the device class over a fidl channel.

auto fake_parent = MockDevice::FakeRootParent();
ASSERT_OK(MyDevice::Create(nullptr, fake_parent.get());
MockDevice* child_dev = fake_parent->GetLatestChild();
MyDevice* test_dev = child_dev->GetDeviceContext<MyDevice>();

async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
auto endpoints = fidl::CreateEndpoints<fidl_proto>();
std::optional<fidl::ServerBindingRef<fidl_proto>> fidl_server;
fidl_server = fidl::BindServer(
    loop.dispatcher(), std::move(endpoints->server), test_dev);
loop.StartThread("thread-name");
fidl::WireSyncClient fidl_client{std::move(endpoints->client)};
// fidl_client can be used synchronously.

Mocking Metadata

Metadata can be added to any ancestor of the device under test. Metadata is propagated to be available to all descendants.

auto parent = MockDevice::FakeRootParent();
const char kSource[] = "test";
parent->SetMetadata(kFakeMetadataType, kSource, sizeof(kSource));

Load Firmware

Load firmware is a deprecated function, but is included for the drivers that still need it:

auto fake_parent = MockDevice::FakeRootParent();
ASSERT_OK(MyDevice::Create(nullptr, fake_parent.get());
MockDevice* child_dev = fake_parent->GetLatestChild();
MyDevice* test_dev = child_dev->GetDeviceContext<MyDevice>();
constexpr std::string_view kFirmwarePath = "test path";
std::vector<uint8_t> kFirmware(200, 42);
child_dev->SetFirmware(kFirmware, kFirmwarePath);
EXPECT_TRUE(test_dev->LoadFirmware(kFirmwarePath).is_ok());

Common Issues

  • Not calling Init/Unbind
    • Call Init using the MockDevice::InitOp()
    • Call Unbind using the MockDevice::UnbindOp(), or call device_async_remove() and call mock_ddk::ReleaseFlaggedDevices
  • Deleting the device directly
    • Solution: release the Device from the current scope after calling DdkAdd()