我們即將以新的程式庫 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@。