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.
}
Mock-DDK 概览
模拟 ddk 只是以一组 zx_device_t
的形式存在,用于跟踪设备与模拟驱动程序主机的互动,并允许调用设备。没有全局状态 - 如果根“父”设备超出范围,所有 zx_device_t
都将销毁并删除其配套设备。
以下是 mock-ddk 如何与驱动程序交互的交互模型:
虚假 ddk 的主要变化
记账
fake_ddk | 模拟-DDK |
---|---|
所有驱动程序信息都包含在全局 fake_ddk::Bind 变量中。 | 每台设备的信息均存储在该设备的 zx_device_t 中。 |
不支持多个驱动程序 | zx_device_t 保存有关父项和子项的信息,以便发现从同一根父项降序排列的所有设备。 |
没有对已创建设备的引用 | 与驱动程序主机一样,每个 zx_device_t 都会存储设备上下文 |
请参阅“获取设备上下文”部分,了解如何在 mock-ddk 中获取设备上下文。
模拟 Driverhost 行为
fake_ddk | 模拟-DDK |
---|---|
* 针对 Init 和 Remove/Unbind/Release 复制驱动程序主机行为 | * 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.
自动取消绑定并释放
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 函数会将完成器作为参数。您可以创建一个客户端,以通过 Fitbit 通道与设备类进行通信。
伪造的 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
中查看许可名单。
执行任务
- 如果没有,请将该错误分配给自己。
更改了构建规则和 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
从
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()
的使用(请参阅上文的说明。)您应检查特定的设备状态,以确保初始化和关闭按预期运行。例如,不妨从示例生命周期测试着手。请勿明确删除测试设备,只是通过调用
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
- 使用
MockDevice::InitOp()
调用 Init - 使用
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@。