模擬 DDK 遷移

我們即將以新的程式庫 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」中的許可清單。

執行工作

  1. 如果尚未指派錯誤,請將錯誤指派給自己。
  2. 變更建構規則,並納入目標為 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
    
  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() 的用法 (請參閱說明) above.)請改為檢查特定裝置狀態,以確保初始化 及關機的運作情形範例: 您可以考慮從生命週期測試著手。

  7. 請勿明確刪除測試裝置,但必須呼叫 ReleaseOp。此行為違反 mock-ddk (和駕駛員) 的方式 運作,因此會導致重複釋放錯誤。(請參閱這一節) 模擬 Driverhost 行為)

  8. 通訊埠 Bind::SetProtocolBind:SetFragmentsMockDevice::AddProtocol. 請注意,MockDevice::AddProtocol 會分別處理這些作業和結構定義。

  9. 模擬中繼資料應維持不變,但系統會在裝置上的呼叫器上呼叫這個中繼資料 父項,而不是 fake_ddk::Bind

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

  11. 這可能是某人第一次查看這部駕駛的單元測試 或許太過專業了如果測試似乎少了 (例如僅包含 一個「生命週期」測試),請回報錯誤並加上 “improve_driver_unit_tests”.在錯誤中指出幾項可能的測試 可以寫入的資料

  12. 在版本中新增測試目標並進行測試。測試不需使用特定硬體。

常見問題:

  • 未呼叫 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@。