false 驅動程式庫測試程式庫即將由新的程式庫 mock_ddk 取代。驅動程式庫架構團隊需要幫助,將富吉西亞境內的 100 多項驅動程式庫測試全數遷至客戶。
目標與激勵
假的 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());
}
模擬日期
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
的裝置都會刪除並刪除隨附裝置。
以下是模擬程式與驅動程式庫互動的方式:
假牙的重大變化
簿記
fake_ddk | CANNOT TRANSLATE |
---|---|
所有驅動程式庫資訊都包含在「global fake_ddk::Bind」變數中。 | 每部裝置的資訊會儲存在該裝置的 zx_device_t 中。 |
不支援多個驅動程式 | zx_device_t 會維護父項和子項的相關資訊,因此能搜尋到所有依相同根父項遞減的裝置。 |
未保留所建立裝置的參照 | 就像驅動程式代管程序一樣,每個 zx_device_t 都會儲存裝置結構定義 |
請參閱「取得裝置背景資訊」一節,瞭解如何從模擬中取得裝置背景資訊。
模擬 Driverhost 行為
fake_ddk | CANNOT TRANSLATE |
---|---|
* 複製 Init 和移除/解除繫結/釋出的驅動程式主機行為 | * 減少 DriverHost 的行為。 |
* 這有一個內建行為:刪除時,每個 zx_device_t 都會在裝置結構定義上呼叫 release()。 |
沒有其他 fake_ddk::Bind::Ok()
函式
Ok()
函式實際上並未測試驅動程式主機通訊協定的正確用法。雖然 Ok()
函式的替代沒有減少,但測試寫入器可以像驅動程式代管程序一樣啟動及停止驅動程式庫,以確保裝置狀態初始化並正確關閉。我們將在下方的「生命週期測試範例」一節中提供這項測試的範例。
使用模擬 DDK
與 Driverhost 互動
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();
模擬日期
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.
自動解除繫結並釋出
驅動程式主機在發布驅動程式庫前一律會呼叫解除繫結,但該步驟必須在模擬中手動完成。如果您有多個受測試的驅動程式,那麼直接取消繫結和發布行為可能會比較容易。模擬 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());
取得裝置背景資訊
模擬程式只會處理與裝置相關聯的 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);
模擬日期
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));
```
模擬日期
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 函式會將完整者做為引數。您可以建立用戶端,透過 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>);
```
模擬日期
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));
模擬日期
auto parent = MockDevice::FakeRootParent();
const char kSource[] = "test";
parent->SetMetadata(kFakeMetadataType,
kSource, sizeof(kSource));
載入韌體
載入韌體已不適用,但隨附於仍需要此功能的驅動程式:
虛構的 DDK
No Functionality.
模擬日期
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()
的使用情形 (請參閱上方的說明)。請改為檢查特定裝置狀態,確保初始化和關閉作業能正常運作。例如:生命週期測試範例可能是個好的開始。請勿明確刪除測試裝置,除非呼叫
ReleaseOp
。此做法違反模擬元件 (和驅動程式主機) 的運作方式,會導致重複執行重複錯誤。(請參閱上方「示範驅動程式主機行為」一節)。將
Bind::SetProtocol
和Bind:SetFragments
通訊埠設為MockDevice::AddProtocol.
。請注意,MockDevice::AddProtocol
會分別擷取作業和結構定義。模擬中繼資料應保持不變,不過是在裝置的父項 (而非
fake_ddk::Bind
) 上呼叫。將
fake_ddk::FidlMessenger
的執行個體移植到對等的模擬模組。這可能是有人第一次查看這個驅動程式的單元測試。如果測試看似缺少 (例如,其中僅包含一項「lifecycle」測試),請回報錯誤並加上「improve_driver_unit_tests」標籤。在錯誤中,請指出幾個可能會編寫的測試。
在建構作業中新增測試目標並進行測試。測試不應要求搭配特定硬體。
常見問題:
- 不呼叫 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:
標記中上傳含有錯誤編號的變更。
例如:
變更清單 | 測試範例... |
---|---|
fxr/560643 | 已建立 fake_ddk::Bind 的子類別 |
fxr/557553 | 使用偽假資訊:FidlMessenger |
fxr/560246 | 使用 SetMetadata 和 SetProtocol |
fxr/552027 | 名為 fake_ddk::Bind::Ok() |
贊助商
如需任何協助或有任何問題,歡迎隨時與 garratt@ 或 tq-df-eng@ 聯絡。