模擬 DDK 遷移

fake_ddk 驅動程式庫測試程式庫將由新的 mock_ddk 程式庫取代。驅動程式庫架構團隊需要協助,將 Fuchsia 中的 100 多項驅動程式庫測試遷移。

目標與動機

Fake DDK 在測試內容方面有許多模糊不清之處,且對驅動程式庫結構做出許多假設,但這些假設越來越不適用。mock_ddk 提供更簡單的單元測試架構,並清楚劃分可測試的範圍。

技術背景

簡易範例

fake-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.
}

模擬 DDK 總覽

模擬 DDK 僅做為一組 zx_device_t,可追蹤裝置與模擬驅動程式代管程序的互動,並允許呼叫裝置。沒有全域狀態 - 如果根「父項」裝置超出範圍,所有 zx_device_t 都會毀損並刪除隨附的裝置。

以下是模擬 DDK 與驅動程式庫的互動模型:

圖:互動模式

fake-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 和 Remove/Unbind/Release 的 driverhost 行為 * 盡量減少 DriverHost 行為。
* 內建行為:每個 zx_device_t 都會在銷毀時,對裝置環境呼叫 release()。

不再提供 fake_ddk::Bind::Ok() 函式

Ok() 函式實際上並未測試正確使用 driverhost 協定。 雖然沒有 Ok() 函式的替代項目,但測試撰寫者可以像驅動程式代管程序一樣啟動及停止驅動程式庫,確保裝置狀態已正確初始化及關閉。下方的「生命週期測試範例」一節提供這類測試的範例。

使用模擬 DDK

與 Driverhost 互動

mock_ddk 會模擬並提供與 driverhost 之間的通話。

撥打電話給裝置
(裝置作業)
呼叫 driverhost
(Libdriver API)
透過 MockDevice 呼叫裝置作業。函式會命名為 op 名稱 + 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.
自動取消繫結及發布

在釋出驅動程式庫前,driverhost 一律會呼叫 unbind,但這個步驟必須在模擬 DDK 中手動完成。如果您有多個受測驅動程式,自動解除繫結和釋放行為可能會比較容易。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());

取得裝置脈絡

模擬 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 函式會將完成器做為引數。您可以建立用戶端,透過 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());

如何提供協助

選取工作

我們已針對所有剩餘的 Fake DDK 用途提報錯誤 (例如 https://fxbug.dev/42066345)。標籤為 df-mock-ddk-migration

你也可以在src/devices/testing/fake_ddk/BUILD.gn中查看允許清單。

執行工作

  1. 如果尚未指派錯誤,請將錯誤指派給自己。
  2. 變更建構規則和包含項目,以模擬 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
    
  3. src/devices/testing/fake_ddk/BUILD.gn 的 fake_ddk 許可清單中移除驅動程式庫資料夾

  4. 將「fake_ddk::Bind」的使用方式改為「auto fake_parent = MockDevice::FakeRootParent();

    1. 請注意,由於 fake_parent 不涉及任何全域變數,因此您可能需要更加注意其範圍。
    2. 如果測試會建立從 fake_ddk::Bind 繼承的類別,您應該會發現 mock-ddk 支援需要建立子類別的功能。如果沒有,請聯絡 garratt@。
  5. fake_ddk::kFakeParentfake_ddk::FakeParent() 的用法變更為 fake_parent.get()

  6. 移除 fake_ddk::Bind::Ok() 的用法 (請參閱上方的說明)。請改為檢查特定裝置狀態,確保初始化和關機作業正常運作。例如: 生命週期測試範例是個不錯的起點。

  7. 請勿明確刪除測試裝置,除非呼叫 ReleaseOp。這麼做會違反模擬 DDK (和 driverhost) 的運作方式,並導致雙重釋放錯誤。(請參閱上方的「模仿 Driverhost 行為」一節)

  8. Bind::SetProtocolBind:SetFragments 移植到 MockDevice::AddProtocol. 請注意,MockDevice::AddProtocol 會分別取得作業和內容。

  9. 雖然中繼資料是在裝置的父項而非 fake_ddk::Bind 上呼叫,但模擬中繼資料應維持不變。

  10. fake_ddk::FidlMessenger 的執行個體移植到 mock-ddk 對等項目。

  11. 這可能是有人一段時間以來首次查看這個驅動程式的單元測試。如果測試似乎不足 (例如只包含一個「生命週期」測試),請提出錯誤並新增「improve_driver_unit_tests」標籤。在錯誤中,指出幾個可編寫的潛在測試。

  12. 將測試目標新增至建構作業並進行測試。測試不應需要特定硬體。

常見問題:

  • 未呼叫 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 Used fake_ddk::FidlMessenger
fxr/560246 使用 SetMetadata 和 SetProtocol
fxr/552027 呼叫 fake_ddk::Bind::Ok()

贊助者

如有任何問題或需要協助,請隨時傳送電子郵件至 garratt@ 或 tq-df-eng@。