fake_ddk 驱动程序测试库将被新的库 mock_ddk 取代。 驱动程序框架团队需要帮助来迁移 Fuchsia 的 100 多项驱动程序测试。
目标和动力
假 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.
}
Mock-DDK 概览
模拟 ddk 只是作为一组 zx_device_t 来跟踪
设备与模拟的驾驶员主机之间的交互,并允许呼叫
插入设备没有全局状态 - 如果根“父级”设备
一旦超出范围,所有 zx_device_t 都将被销毁和删除
其配套设备。
以下是有关 mock-ddk 如何与司机互动的交互模型:

与 fake-ddk 相关的重大变更
记账
| fake_ddk | mock-ddk | 
|---|---|
| 所有驱动程序信息都包含在全局 fake_ddk::Bind 变量中。 | 每台设备的信息存储在该设备的 zx_device_t 中。 | 
| 不支持多个驱动程序 | zx_device_t 保留有关父级和子级的信息,因此可以发现来自同一根父级的所有设备。 | 
| 不存储对所创建设备的引用 | 就像驱动程序主机一样,每个 zx_device_t 都会存储设备上下文 | 
请参阅获取设备上下文部分,了解如何在 mock-ddk 中获取设备上下文。
模仿 Driverhost 行为
| fake_ddk | mock-ddk | 
|---|---|
| * 复制 Init 和 Remove/Unbind/Release 的驱动程序主机行为 | * DriverHost 的行为被限制在最低限度。 | 
* 有一种内置行为:每个 zx_device_t 在销毁时都会对其设备上下文调用 release()。 | 
fake_ddk::Bind::Ok() 函数已不存在
Ok() 函数实际上并未测试驱动程序主机协议的使用是否正确。
虽然 Ok() 函数的替换效果并没有下降,但测试编写器
可以像驱动程序主机一样启动和停止驱动程序,
状态已初始化并正确关闭。我们提供了此测试的一个示例
下文的生命周期测试示例部分。
使用模拟 DDK
与 Driverhost 的交互
mock_ddk 进行模拟,并提供与 Driverhost 之间的调用。
| 调用设备  (设备操作)  | 
调用 drivehost  (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.
自动解除绑定并释放
在释放驱动程序之前,Driverhost 始终会调用 unbind 必须在 mock-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());
获取设备上下文
mock-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 函数需要 completer 作为参数。您可以创建一个客户端 设备类别。
假 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());
如何提供帮助
选择任务
已提交有关虚假 DDK 的所有剩余使用 bug(例如 https://fxbug.dev/42066345)。标签为 df-mock-ddk-migration。
您还可以在 src/devices/testing/fake_ddk/BUILD.gn 中查看许可名单。
执行任务
- 如果尚未指定,请将相应错误分配给自己。
 更改构建规则和包含内容,以定位 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从
src/devices/testing/fake_ddk/BUILD.gn的 fake_ddk 许可名单中移除驱动程序文件夹将
fake_ddk::Bind的使用更改为auto fake_parent = MockDevice::FakeRootParent();- 请注意,您可能需要谨慎处理 
fake_parent的作用域。 因为它不涉及任何全局变量。 - 如果您的测试创建一个继承自 fake_ddk::Bind 的类, 您会发现 mock-ddk 支持 创建子类。如果没有,请联系 garratt@。
 
- 请注意,您可能需要谨慎处理 
 将
fake_ddk::kFakeParent和fake_ddk::FakeParent()的用法更改为fake_parent.get()移除了
fake_ddk::Bind::Ok()的使用(请参阅说明) above.)您应改为检查特定的设备状态以确保初始化 以及关闭按预期运行示例: 生命周期测试示例可能是个不错的起点。确保不要明确删除测试设备,除非是通过调用
ReleaseOp。否则会违反 mock-ddk(和 Driverhost) 运行,会导致双重释放错误。(请参阅 模仿 Driverhost 行为)将
Bind::SetProtocol和Bind:SetFragments携号转入MockDevice::AddProtocol.请注意,MockDevice::AddProtocol会将操作和上下文分开处理。模拟元数据应该保持不变,尽管它是在设备的 父级,而不是
fake_ddk::Bind。将
fake_ddk::FidlMessenger的实例移植到 mock-ddk 等效项。这可能是首次有人查看该驱动程序的单元测试 。如果测试似乎缺失(例如,测试仅包含 一项“生命周期”测试),请提交 bug 并添加标签 “improve_driver_unit_tests”.在 bug 中,指出一些可能的测试 可以书写的内容。
将测试目标添加到您的 build 中并对其进行测试。测试不应要求使用特定硬件。
常见问题:
- 不调用 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()后,从当前作用域中释放设备 
 - 解决方案:调用 
 
替换 fake_ddk::Bind::DeviceAdd
一些更复杂的测试子类 fake_ddk::Bind,以便替换 DeviceAdd 方法。通常,这样做是为了拦截来自 device_add_args 的一些信息,例如检查 VMO。
在模拟 DDK 中,您可以改为使用 GetLatestChild 访问设备。
完成任务
- 将更改内容连同 bug 编号一起上传到 
Fixed:标记中。 
示例:
| 更改列表 | 测试... | 
|---|---|
| fxr/560643 | 创建了 fake_ddk::Bind 的子类 | 
| fxr/557553 | 使用了 fake_ddk::FidlMessenger | 
| fxr/560246 | 使用 SetMetadata 和 SetProtocol | 
| fxr/552027 | 调用了 fake_ddk::Bind::Ok() | 
赞助商
如果您需要帮助或有任何疑问,请随时联系 garratt@ 或 tq-df-eng@。