驅動程式庫單元測試架構
簡易範例
以下是使用 mock-ddk 程式庫模擬驅動程式主機架構的驅動程式庫基本範例。
首先,需要測試的簡易驅動程式庫。這個範例驅動程式庫將用於本說明文件中的所有程式碼。
// 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 程式庫,裝置即可載入,並從模擬的驅動程式主機介面進行呼叫:
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 總覽
模擬密碼是以一組 zx_device_t
的形式存在,用於追蹤裝置與模擬驅動程式代管程序的互動,並允許向裝置發出呼叫。沒有全域狀態 - 如果根層級裝置的「父項」裝置超出範圍,所有 zx_device_t
都會刪除並刪除其隨附裝置。
以下是模擬圖與驅動程式庫如何互動的互動模型:
與驅動程式主機互動
mock_ddk 可以模擬,並與駕駛員聯絡,
正在撥打電話給裝置 (裝置作業) |
向驅動程式主機呼叫 (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.
自動解除繫結並釋出
驅動程式主機一律會在釋放驅動程式庫之前呼叫解除繫結,但該步驟必須在模擬背景中手動完成。如果您有多個待測試的驅動程式,那麼自動解除繫結和發布行為可能會比較容易。為達成這個目的,Mock 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());
正在取得裝置背景資訊
這個模擬檔案僅適用於與裝置相關聯的 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);
片段通訊協定
複合式裝置會從多個父項「片段」取得通訊協定。這會在以名稱索引鍵的通訊協定中資訊清單顯示。Mock-ddk 允許將名稱繫結至通訊協定,以表示其來自片段。
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()
呼叫解除繫結,或呼叫device_async_remove()
並呼叫mock_ddk::ReleaseFlaggedDevices
- 使用
- 直接刪除裝置
- 解決方案:呼叫
DdkAdd()
後,將裝置從目前範圍釋出
- 解決方案:呼叫