模拟 DDK 迁移

fake_ddk 驱动程序测试库将被新库 mock_ddk 取代。驱动程序框架团队需要帮助来迁移 Fuchsia 中的 100 多项驱动程序测试。

目标和动机

Fake DDK 在测试内容方面存在许多不确定性,并且对驱动程序结构做出了许多越来越无效的假设。mock_ddk 提供了一个更直接的单元测试框架,明确了可测试的内容。

技术背景

简单示例

fake-ddk 和 mock-ddk 之间存在许多根本性差异,但下面展示了如何迁移一个非常简单的测试:

伪造的 DDK

TEST(FooDevice, BasicTest) {
  fake_ddk::Bind ddk;
  device = std::make_unique<FooDevice>(fake_ddk::FakeParent(), other_args);
  ASSERT_EQ(device->Init(), ZX_OK);
  // Do some testing here
  device->DdkAsyncRemove();
  EXPECT_TRUE(ddk.Ok());
}

模拟 DDK

TEST(FooDevice, BasicTest) {
  std::shared_ptr<MockDevice> fake_parent = MockDevice::FakeRootParent();
  device = std::make_unique<FooDevice>(fake_parent.get(), other_args);
  ASSERT_EQ(device->Init(), ZX_OK);
  device.release(); // let go of the reference to the device
  // Do some testing here

  // The mock-ddk will automatically call DdkRelease() on any remaining children of
  // fake_parent upon destruction.
}

模拟 DDK 概览

模拟 DDK 仅作为一组 zx_device_t 存在,用于跟踪设备与模拟驱动程序主机之间的互动,并允许调用设备。没有全局状态 - 如果根“父”设备超出范围,所有 zx_device_t 都会销毁并删除其随附的设备。

以下是模拟 DDK 与驱动程序互动的交互模型:

图:互动模型

与伪造 DDK 相比的主要变化

记账

fake_ddk mock-ddk
所有驱动程序信息都包含在全局 fake_ddk::Bind 变量中。 每个设备的信息都存储在该设备的 zx_device_t 中。
不支持多个驱动程序 zx_device_t 会维护有关父设备和子设备的信息,因此可以发现源自同一根父设备的所有设备。
未持有对所创建设备的引用 与驱动程序宿主一样,每个 zx_device_t 都会存储设备上下文

如需了解如何在模拟 DDK 中获取设备上下文,请参阅获取设备上下文部分。

模仿 Driverhost 行为

fake_ddk mock-ddk
* 复制了针对 Init 和 Remove/Unbind/Release 的 driverhost 行为 * DriverHost 行为保持在最低限度。
* 有一种内置行为:每个 zx_device_t 在销毁时都会对其设备上下文调用 release()。

不再提供 fake_ddk::Bind::Ok() 功能

Ok() 函数实际上并未测试 driverhost 协议的正确使用情况。 虽然没有可替代 Ok() 函数的替代函数,但测试编写者可以像驱动程序宿主一样启动和关闭驱动程序,以确保设备状态已正确初始化和关闭。下文的生命周期测试示例部分提供了此类测试的示例。

使用模拟 DDK

与 Driverhost 的互动

mock_ddk 会模拟并提供与 driverhost 的来回调用。

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

伪造的 DDK

fake_ddk::Bind bind;
TestDevice* device  = TestDevice::Create(fake_ddk::kFakeParent);
device->DdkAsyncRemove();
EXPECT_TRUE(ddk_.Ok());
device->DdkRelease();

模拟 DDK

auto parent = MockDevice::FakeRootParent();
TestDevice::Create(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.
自动解除绑定和释放

驱动程序主机在释放驱动程序之前始终会调用 unbind,但在模拟 DDK 中,必须手动完成该步骤。 如果您有多个待测驱动程序,则可以更轻松地自动执行取消绑定和释放行为。模拟 DDK 具有一个用于此目的的辅助函数:

auto parent = MockDevice::FakeRootParent();
TestDevice* test_device_0 = TestDevice::Create(parent.get());
TestDevice* test_device_1 = TestDevice::Create(test_device_0.zxdev());
// The state of the tree is now:
//         parent   <--  FakeRootParent
//           |
//         child    <--  test_device_0
//           |
//       grandchild <--  test_device_1

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

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

获取设备上下文

模拟 DDK 仅处理与设备关联的 zx_device_t。 不过,如果您已分配设备上下文(例如使用 ddktl 库),则可能需要访问相应的 ddk::Device:

  auto parent = MockDevice::FakeRootParent();
  // May not get the device* back from bind:
  TestDevice::Bind(parent.get());
  // Never fear! Recover device from parent:
  MockDevice* child = parent->GetLatestChild();
  TestDevice* test_dev =
         child->GetDeviceContext<TestDevice>();

与其他司机的互动

模拟父功能主要使用与伪 DDK 相同的调用,但设置模拟只会影响相关设备,而不会将模拟加载到全局状态。

模拟父协议

父协议会添加到父设备,然后子设备才能通过调用 device_get_protocol() 来访问这些协议

伪造的 DDK

 fake_ddk::Bind bind;
 const fake_ddk::Protocol kTestProto = {
   .ctx = reinterpret_cast<void*>(0x10),
   .ops = nullptr,
 };

 bind.SetProtocol(8, &kTestProto);

模拟 DDK

 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。

伪造的 DDK

 fake_ddk::Bind bind;
     fbl::Array<fake_ddk::FragmentEntry> fragments(new fake_ddk::FragmentEntry[2], 2);
     fragments[0].name = "fragment-1";
     fragments[0].protocols.emplace_back(
         fake_ddk::ProtocolEntry{0, fake_ddk::Protocol{nullptr, nullptr}});
     fragments[0].protocols.emplace_back(
         fake_ddk::ProtocolEntry{1, fake_ddk::Protocol{nullptr, nullptr}});
     fragments[1].name = "fragment-2";
fragments[1].protocols.emplace_back(
    fake_ddk::ProtocolEntry{2, fake_ddk::Protocol{nullptr, nullptr}});
bind.SetFragments(std::move(fragments));
```

模拟 DDK

 auto parent = MockDevice::FakeRootParent();
 void* ctx = reinterpret_cast<void*>(0x10),
 void* ops = nullptr,
 // Mock-ddk uses the same call as adding a
 // normal parent protocol:
 parent->AddProtocol(0, ops, ctx, "fragment-1");
 parent->AddProtocol(1, ops, ctx, "fragment-1");
 parent->AddProtocol(2, ops, ctx, "fragment-2");
模拟 FIDL 连接

如果设备提供 FIDL 协议,测试可能需要调用提供的 FIDL 函数。这可能会比较困难,因为 FIDL 函数将完成器作为实参。您可以创建一个客户端,通过 FIDL 渠道与设备类进行通信。

伪造的 DDK

fake_ddk::Bind bind;
TestDevice* dev  = TestDevice::Create(fake_ddk::kFakeParent);
FidlMessenger fidl;
fidl.SetMessageOp((void *)dev,
   [](void* ctx,
      fidl_incoming_msg_t* msg,
      device_fidl_txn_t* txn) -> zx_status_t
          { return static_cast<Device*>(ctx)->DdkMessage(msg, txn)});
<fidl_client_function> (
    <fake_ddk>.local().get(), <args>);
```

模拟 DDK

auto parent = MockDevice::FakeRootParent();
TestDevice* dev  =  TestDevice::Create(parent.get());
async::Loop loop_(&kAsyncLoopConfigNoAttachToCurrentThread);
auto endpoints = fidl::CreateEndpoints<fidl_proto>();
std::optional<fidl::ServerBindingRef<fidl_proto>> binding_;
binding_ = fidl::BindServer(loop_.dispatcher(),
                            std::move(endpoints->server),
                            child->GetDeviceContext<RpmbDevice>());
loop_.StartThread("thread-name")
rpmb_fidl_.Bind(std::move(endpoints->client), loop_.dispatcher());
模拟元数据

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

伪造的 DDK

fake_ddk::Bind bind;
const char kSource[] = "test";
bind.SetMetadata(kFakeMetadataType,
                 kSource, sizeof(kSource));

模拟 DDK

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

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

伪造的 DDK

No Functionality.

模拟 DDK

auto parent = MockDevice::FakeRootParent();
auto result = TestDevice::Bind(parent.get());
TestDevice* test_device = result.value();
constexpr std::string_view kFirmwarePath = "test path";
std::vector<uint8_t> kFirmware(200, 42);
test_device->zxdev()->SetFirmware(kFirmware, kFirmwarePath);
EXPECT_TRUE(test_device->LoadFirmware(kFirmwarePath).is_ok());

如何提供帮助

选择任务

我们已针对所有剩余的 Fake DDK 用例提交 bug(例如,https://fxbug.dev/42066345)。标签为 df-mock-ddk-migration

您还可以在 src/devices/testing/fake_ddk/BUILD.gn 中查看许可名单。

执行任务

  1. 如果该 bug 尚未分配给您,请将其分配给您自己。
  2. 更改了 build 规则和 include,以将目标设为 mock-ddk 而不是 fake_ddk

    $ sed -i 's%testing/fake_ddk%testing/mock-ddk%' path/to/BUILD.gn
    $ sed -i 's%<lib/fake_ddk/fake_ddk.h>%"src/devices/testing/mock-ddk/mock-device.h"%' test.cc
    
  3. src/devices/testing/fake_ddk/BUILD.gn 中的 fake_ddk 许可名单中移除驱动程序文件夹

  4. fake_ddk::Bind 的用法更改为 auto fake_parent = MockDevice::FakeRootParent();

    1. 请注意,由于 fake_parent 不涉及任何全局变量,因此您可能需要更加注意其范围。
    2. 如果您的测试创建了一个继承自 fake_ddk::Bind 的类,您应该会发现 mock-ddk 支持创建该子类所需的功能。如果不是,请联系 garratt@。
  5. fake_ddk::kFakeParentfake_ddk::FakeParent() 的用法更改为 fake_parent.get()

  6. 移除了 fake_ddk::Bind::Ok() 的使用(请参阅上文的说明)。而是检查特定设备状态,以确保初始化和关机按预期运行。示例:生命周期测试示例可能是一个不错的起点。

  7. 请确保您不会明确删除测试设备,除非通过调用 ReleaseOp。这样做违反了模拟 DDK(和 driverhost)的运行方式,会导致双重释放错误。(请参阅上文中的模仿 Driverhost 行为部分)

  8. 将端口 Bind::SetProtocolBind:SetFragments 设为 MockDevice::AddProtocol. 请注意,MockDevice::AddProtocol 会分别获取操作和上下文。

  9. 元数据的模拟应保持不变,尽管它是在设备的父级上(而不是在 fake_ddk::Bind 上)调用的。

  10. fake_ddk::FidlMessenger 的实例移植到模拟 DDK 等效项。

  11. 这可能是有人一段时间以来首次查看此驱动程序的单元测试。如果测试似乎不足(例如,仅包含一个“生命周期”测试),请提交 bug 并添加标签“improve_driver_unit_tests”。在 bug 中,指出一些可能编写的潜在测试。

  12. 将测试目标添加到 build 中并进行测试。测试不应需要特定硬件。

常见问题:

  • 未调用 Init/Unbind
    • 使用 MockDevice::InitOp() 进行通话初始化
    • 使用 MockDevice::UnbindOp() 调用 Unbind,或调用 device_async_remove()mock_ddk::ReleaseFlaggedDevices
  • 直接删除设备
    • 解决方案:在调用 DdkAdd() 后从当前范围释放设备

覆盖 fake_ddk::Bind::DeviceAdd

一些更复杂的测试会继承 fake_ddk::Bind 以替换 DeviceAdd 方法。通常,这是为了拦截 device_add_args 中的某些信息,例如检查 VMO。

在模拟 DDK 中,您可以改为使用 GetLatestChild 访问设备

完成任务

  • 上传包含 Fixed: 标记中的 bug 编号的更改。

示例:

变更列表 测试...的示例
fxr/560643 创建了 fake_ddk::Bind 的子类
fxr/557553 使用了 fake_ddk::FidlMessenger
fxr/560246 使用 SetMetadata 和 SetProtocol
fxr/552027 调用了 fake_ddk::Bind::Ok()

赞助商

如果您在任何时候需要帮助或有疑问,请发送电子邮件至 garratt@ 或 tq-df-eng@。