模擬 DDK 遷移

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 的許可清單。

執行工作

  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() 的使用情形 (請參閱上方的說明)。請改為檢查特定裝置狀態,確保初始化和關閉作業能正常運作。例如:生命週期測試範例可能是個好的開始。

  7. 請勿明確刪除測試裝置,除非呼叫 ReleaseOp。此做法違反模擬元件 (和驅動程式主機) 的運作方式,會導致重複執行重複錯誤。(請參閱上方「示範驅動程式主機行為」一節)。

  8. Bind::SetProtocolBind:SetFragments 通訊埠設為 MockDevice::AddProtocol.。請注意,MockDevice::AddProtocol 會分別擷取作業和結構定義。

  9. 模擬中繼資料應保持不變,不過是在裝置的父項 (而非 fake_ddk::Bind) 上呼叫。

  10. fake_ddk::FidlMessenger 的執行個體移植到對等的模擬模組。

  11. 這可能是有人第一次查看這個驅動程式的單元測試。如果測試看似缺少 (例如,其中僅包含一項「lifecycle」測試),請回報錯誤並加上「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 使用偽假資訊:FidlMessenger
fxr/560246 使用 SetMetadata 和 SetProtocol
fxr/552027 名為 fake_ddk::Bind::Ok()

贊助商

如需任何協助或有任何問題,歡迎隨時與 garratt@ 或 tq-df-eng@ 聯絡。