模拟 DDK

驱动程序单元测试框架

简单示例

以下是一个基本的驱动程序示例,该驱动程序使用 mock-ddk 库模拟 Driverhost 以便进行测试

首先,一个需要测试的简单驱动程序。此示例驱动程序将 本文档中的所有代码。

// 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");

通常,此驱动程序只能由驱动程序主机加载,因为它与 不适用于普通组件。此外,驾驶员往往 从家长设备获取信息。使用 mock-ddk 库后,设备 可以加载,并调用模拟 Driverhost 接口并从中调用:

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.
}

Mock-DDK 概览

模拟 ddk 只是作为一组 zx_device_t 来跟踪 设备与模拟的驾驶员主机之间的交互,并允许呼叫 插入设备没有全局状态 - 如果根“父级”设备 一旦超出范围,所有 zx_device_t 都将被销毁和删除 其配套设备。

以下是有关 mock-ddk 如何与司机互动的交互模型:

图:互动模型

与 Driverhost 的交互

mock_ddk 进行模拟,并提供与 Driverhost 之间的调用。

调用设备
(设备操作)
调用 drivehost
(Libdriver API)
通过 MockDevice 调用设备操作。函数命名为操作名称 + Op
示例
使用 InitOp() 调用 init 函数
libdriver API 中的所有调用都会记录在相应设备上,但不会执行任何操作。
示例
如需测试 device_init_reply() 是否已调用,请调用 InitReplyCalled()
或等待调用 WaitUntilInitReplyCalled()

生命周期测试示例

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.

自动解除绑定并释放

在释放驱动程序之前,Driverhost 始终会调用 unbind 必须在 mock-ddk 中手动完成。 如果您有多个受测驱动程序,则采用自动化操作可能更容易 解除绑定和释放行为。模拟 DDK 有一个辅助函数 目的:

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());

获取设备上下文

mock-ddk 仅处理与设备关联的 zx_device_t。 但是,如果您已分配了设备上下文,例如使用 ddktl 库之后,您可能需要访问相应的 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>();

与其他司机的互动

有些信息可以添加到设备(通常是家长设备上),以便其 被测设备可以检索预期值。

模拟父级协议

先将父级协议添加到父级,再将子级设备添加到 可拨打 device_get_protocol() 访问这些服务

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

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

fragment 协议

复合设备从多个父级“fragment”获取协议。表现 使用名称进行键控的协议中Mock-ddk 允许将名称绑定到协议 以表明它来自某个 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.

模拟 FIDL 连接

如果设备提供 FIDL 协议,则测试可能需要调用 fidl 函数。这可能会有点困难,因为 fidl 函数需要 completer 作为参数。您可以创建一个客户端 设备类别。

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.

模拟元数据

元数据可添加到被测设备的任何祖先实体中。 元数据会传播,以供所有后代使用。

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

加载固件

加载固件是已弃用的函数,但包含了加载固件 仍然需要它的驱动程序:

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());

常见问题

  • 不调用 init/Unbind <ph type="x-smartling-placeholder">
      </ph>
    • 使用 MockDevice::InitOp() 调用 Init
    • 使用 MockDevice::UnbindOp() 调用 Unbind,或者调用 device_async_remove() 并调用 mock_ddk::ReleaseFlaggedDevices
  • 直接删除设备 <ph type="x-smartling-placeholder">
      </ph>
    • 解决方案:调用 DdkAdd() 后,从当前作用域中释放设备