我們即將以新的程式庫 mock_ddk 取代了偽_ddk 驅動程式庫測試程式庫。 驅動程式庫架構團隊需要協助遷移 Fuchsia 中超過 100 項驅動程式庫測試。
目標和動力
虛構的 DDK 對要測試的內容有一些模糊之處, 驅動程式庫結構越來越無效的假設數量。 mock_ddk 提供更簡單直接的單元測試架構, 能夠測試的特性
技術背景
簡易範例
dum-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 總覽
模擬物件只是以 zx_device_t
追蹤、
與模擬驅動程式的主機互動,以及允許通話
安裝到裝置上。沒有全域狀態 - 如果是根為「父項」裝置
在超出範圍的情況下,zx_device_t
的所有元件都會刪除並刪除
使用者的隨附裝置
以下互動模型說明 mock-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 的驅動程式主機行為,並移除/Unbind/Release | * DriverHost 的行為會保持在最低限度。 |
* 有一個內建行為:每個 zx_device_t 會在刪除時在其裝置結構定義上呼叫 release()。 |
沒有其他 fake_ddk::Bind::Ok()
函式
Ok()
函式實際上並未測試驅動程式主機通訊協定的正確使用情形。
雖然 Ok()
函式無法取代,但測試寫入者
可像驅動程式代管程序一樣啟動及停止驅動程式庫,以確保裝置
並且正確關閉狀態這項測試提供了範例
列於「生命週期測試範例」一節中。
使用模擬 DDK
與駕駛員互動
mock_ddk 會模擬並向驅動程式主機發出可用呼叫。
正在呼叫裝置「 」(裝置作業) |
正在呼叫驅動程式主機 (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.
自動解除繫結並釋出
驅動程式主機在釋放驅動程式庫前一律會被解除繫結, 步驟必須在模擬中手動完成 如果接受測試有多個驅動程式,以這種方式將模型自動化 取消繫結和釋放行為Mock 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);
片段通訊協定
複合裝置會從多個父項「片段」取得通訊協定。這代表了 或以名稱做為索引鍵的通訊協定Mock-ddk 可將名稱繫結至通訊協定 表示它來自片段
假 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 函式會 做為引數您可以建立用戶端 深入瞭解裝置類別
假 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 (例如 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 行為)通訊埠
Bind::SetProtocol
和Bind:SetFragments
至MockDevice::AddProtocol.
請注意,MockDevice::AddProtocol
會分別處理這些作業和結構定義。模擬中繼資料應維持不變,但系統會在裝置上的呼叫器上呼叫這個中繼資料 父項,而不是
fake_ddk::Bind
。將
fake_ddk::FidlMessenger
的執行個體移植至 mock-ddk 同等項目。這可能是某人第一次查看這部駕駛的單元測試 或許太過專業了如果測試似乎少了 (例如僅包含 一個「生命週期」測試),請回報錯誤並加上 “improve_driver_unit_tests”.在錯誤中指出幾項可能的測試 可以寫入的資料
在版本中新增測試目標並進行測試。測試不需使用特定硬體。
常見問題:
- 未呼叫 Init/取消繫結
- 使用
MockDevice::InitOp()
呼叫 Init - 使用
MockDevice::UnbindOp()
呼叫解除繫結,或呼叫device_async_remove()
並呼叫mock_ddk::ReleaseFlaggedDevices
- 使用
- 直接刪除裝置
- 解決方案:呼叫
DdkAdd()
後,從目前的範圍釋出裝置
- 解決方案:呼叫
覆寫 fake_ddk::Bind::DeviceAdd
一些較複雜的測試子類別 fake_ddk::Bind
,用於覆寫 DeviceAdd
方法。這麼做通常會攔截 device_add_args
中的部分資訊,例如檢查 VMO。
在模擬 DDK 中,您可以改為使用 GetLatestChild
存取裝置。
完成工作
- 在
Fixed:
標記中上傳含有錯誤編號的變更。
例如:
變更清單 | 測試時機範例... |
---|---|
fxr/560643 | 建立 fake_ddk::Bind 的子類別 |
fxr/557553 | 二手假_ddk::FidlMessenger |
fxr/560246 | 使用 SetMetadata 和 SetProtocol |
fxr/552027 | 呼叫了 fake_ddk::Bind::Ok() |
贊助者
如果需要協助或有任何問題,請洽詢 garratt@ 或 tq-df-eng@。