驱动程序单元测试框架
简单示例
下面是一个基本的驱动程序示例,该示例使用 mock-ddk 库来模拟 drivehost 框架,以便进行测试。
首先,一个需要测试的简单驱动程序。本文档中的所有代码都将使用此示例驱动程序。
// 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 库,可以加载设备,并调用模拟 drivehost 接口或从中发出调用:
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.
}
模拟 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 函数会将完成器作为参数。您可以创建一个客户端,通过 fidl 通道与设备类进行通信。
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
- 使用
MockDevice::InitOp()
调用 Init - 使用
MockDevice::UnbindOp()
调用 Unbind,或者调用device_async_remove()
并调用mock_ddk::ReleaseFlaggedDevices
- 使用
- 直接删除设备
- 解决方案:在调用
DdkAdd()
后从当前范围内释放设备
- 解决方案:在调用