瞭解 Bt-host 單元測試

本指南詳細說明 SMP_Phase1Test 測試韌體和 FeatureExchangeBothSupportSCFeaturesHaveSC 測試案例,說明如何為 Fuchsia 的 bt-host 驅動程式編寫測試。

bt-host 驅動程式庫會實作大多數的藍牙核心規格 v5.2、磁碟區 3 (主機子系統)。

Fuchsia 專案特別重視自動化測試,而藍牙團隊致力成為測試文化的領導者

如要合併 Fuchsia 來源樹狀結構中的 bt-host 驅動程式庫變更,您必須為這項變更編寫自動化測試。

說明

bt-host 是相對大型的程式碼集,具有許多抽象層。

以下是 bt-host 驅動程式庫主要元件的圖表:

bt-host 主要邏輯元件的圖表 {#frameworkure-diagram}

圖表中的每個抽象層大致對應至整個通訊協定。

使用 bt-host 測試時,請著重於瞭解目前處理的圖層 (例如 SM) 與其正下方的層 (例如 L2CAP) 之間的關係。

雖然每層可能都有額外的內部抽象層,但這些跨通訊協定關係最常在測試中模擬及/或進行。

資源

bt-host 是以 C++ 編寫,Fuchsia 則使用 gUnit Googletest 程式庫進行 C++ 測試。

如要使用 bt-host 單元測試,您必須確實瞭解下列資源:

在編寫 bt-host 測試時,有時會出現下列主題,您可以視需要加以參照:

bt-host」單元測試總覽

大多數的 bt-host 測試都是以下列模式編寫:

  1. 建立測試固件來儲存測試替身和資料層測試 (LUT)。

  2. 在該測試固件內建構 LUT,並使用測試替身取代 LUT 的依附元件。

  3. 在 LUT 中運動。例如,本指南會檢查 FeatureExchangeBothSupportSCFeaturesHaveSC 測試案例。

  4. 驗證較高層級的指令是否會產生預期行為。

測試固件

GTest 測試固件是可重複使用的環境,通常儲存測試依附元件,並提供撰寫單元測試的便利方法。本範例使用的測試韌體為 SMP_Phase1Test 類別

測試替身

測試雙精度浮點數是用來取代測試程式碼中實際物件的物件。測試替身類型有很多種。SMP_Phase1Test 中使用的兩個測試替身範例為 FakeChannelFakeListener

SMP_Phase1Test測試固件

SMP_Phase1Test 是用來測試 sm::Phase1 類別的測試韌體。

Phase1 類別負責藍牙低功耗 (BLE) 配對的第 1 階段,裝置會在該配對程序中協商配對的安全性功能。

本節註解 SMP_Phase1Test 的測試固件設定程式碼,做為「建立測試固件」和「建構 LUT」的代表範例。閱讀本節時,建議開啟 phase_1_unittest.cc

SetUp()TearDown()NewPhase1() 方法

SMP_Phase1Test 的建構函式不會執行任何動作。bt-host 測試韌體通常會使用 GTest SetUp() 方法初始化測試韌體。

void SetUp() override { NewPhase1(); }

NewPhase1 是包含可預設值參數的 protected 瀏覽權限方法。bt-host 測試韌體通常會委派給 New<test-fixture-name> 方法,其中包含 SetUp() 中的預設參數。New* 方法負責設定資源/測試替身並建立 LUT。這可讓測試案例1以不同參數呼叫 New* 來重新初始化測試韌體。

void NewPhase1(Role role = Role::kInitiator,
               Phase1Args phase_args = Phase1Args(),
               hci::Connection::LinkType ll_type =
                   hci::Connection::LinkType::kLE) {

如果是 NewPhase1,可設定面向如下:

  • 裝置角色
  • 傳輸類型
  • 保留其餘第 1 階段引數的結構,較容易只變更其中一個「default」引數:
Phase1Args 結構體
struct Phase1Args {
 PairingRequestParams preq = PairingRequestParams();
 IOCapability io_capability = IOCapability::kNoInputNoOutput;
 BondableMode bondable_mode = BondableMode::Bondable;
 SecurityLevel level = SecurityLevel::kEncrypted;
 bool sc_supported = false;
};

L2CAP 模擬依附元件

L2CAP 管道提供與對等通訊協定/服務的邏輯連線,並依附於較高層級的通訊協定,例如 ATTGATTSMPSDP

FakeChannel 會做為模擬依附元件,以測試實際物件如何透過 L2CAP 管道收發訊息。

NewPhase1 中建立的第一個測試替身是 FakeChannel 模擬物件:

uint16_t mtu =
  phase_args.sc_supported ? l2cap::kMaxMTU : kNoSecureConnectionsMtu;
ChannelOptions options(cid, mtu);
options.link_type = ll_type;
...
fake_chan_ = CreateFakeChannel(options);
sm_chan_ = std::make_unique<PairingChannel>(fake_chan_);

可用的 CreateFakeChannel 方法,因為 SMP_Phase1Test 繼承自 FakeChannelTest2

FakeListener

在實際的程式碼中,PairingPhase 會使用 PairingPhase::Listener 與較高層級的 SecurityManager 類別通訊。FakeListener 提供此依附元件的模擬功能以供測試。

listener_ = std::make_unique<FakeListener>();

雖然不是通訊協定層級的依附元件,但 FakeListener 遵循其他常見的 bt-host 測試模式。類別通常會使用介面指標與上方的圖層進行通訊。實作這些介面的測試會重複傳遞至 LUT,以確認 LUT 是否能與上方的層正確通訊。

完成回呼

Phase1 會儲存回呼參數。Phase1 完成後,就會透過這個回呼傳回 Phase1 的結果。將 Phase1 執行個體化時,complete_cb 會用做這個回呼。

complete_cb 會將 Phase1 的結果 (本例中為 featurespreqpres 引數) 儲存至測試固件變數 (features_last_pairing_req_last_pairing_res_),以便測試案例檢查這些變數是否正確產生。

auto complete_cb = [this](PairingFeatures features,
                         PairingRequestParams preq,
                         PairingResponseParams pres) {
 feature_exchange_count_++;
 features_ = features;
 last_pairing_req_ = util::NewPdu(sizeof(PairingRequestParams));
 last_pairing_res_ = util::NewPdu(sizeof(PairingResponseParams));
 PacketWriter preq_writer(kPairingRequest, last_pairing_req_.get());
 PacketWriter pres_writer(kPairingResponse, last_pairing_res_.get());
 *preq_writer.mutable_payload<PairingRequestParams>() = preq;
 *pres_writer.mutable_payload<PairingResponseParams>() = pres;
};

LUT 例項化

下一步是根據 NewPhase1 參數建立 Phase1 LUT。LUT 會儲存在測試固件的 phase_1_ 變數中。

if (role == Role::kInitiator) {
 phase_1_ = Phase1::CreatePhase1Initiator(
     sm_chan_->GetWeakPtr(), listener_->as_weak_ptr(),
     phase_args.io_capability, phase_args.bondable_mode,
     phase_args.level, std::move(complete_cb));
} else {
 phase_1_ = Phase1::CreatePhase1Responder(
     sm_chan_->GetWeakPtr(), listener_->as_weak_ptr(),
     phase_args.preq, phase_args.io_capability,
     phase_args.bondable_mode, phase_args.level,
     std::move(complete_cb));
}

其餘方法

其餘的 Phase1 方法,都是簡單的取得方法執行下列操作:

FeatureExchangeBothSupportSCFeaturesHaveSC 個測試案例

此測試案例會確認所使用的兩個裝置是否都支援某項功能 (在此案例中,安全連線 (SC) 功能),Phase1 完整回呼傳回的 PairingFeatures 會正確回報此情況。

預設的 NewPhase1 參數「不支援安全連線」,因此程式碼只會設定 Phase1Args 的 SC 欄位,其餘設定會預設為 NewPhase1

Phase1Args args;
args.sc_supported = true;
NewPhase1(Role::kInitiator, args);

這個測試案例中使用的 L2CAP 訊息會寫出,功能位元處於測試 (kSC) 設定:

const auto kRequest = StaticByteBuffer(
   // [...omitted]
   AuthReq::kSC | AuthReq::kBondingFlag,
   // [...omitted]
);
const auto kResponse = StaticByteBuffer(
   // [...omitted]
   AuthReq::kSC | AuthReq::kBondingFlag,
   // [...omitted]
);

bt-host 的部分會在非同步工作調度工具上執行。在此情況下,FakeChannelTest 會在調度工具上執行其 FakeChannelPhase1::Start 負責執行 Phase1 的工作,也需要在這個調派程式上執行。

PostTask 會將 Start 方法放入調度工具。接著,FakeChannelTest::Expect 會執行調度工具,並檢查 Phase1 傳送至 L2CAP 的下一個訊息是否為 kRequest

// Initiate the request in a loop task for Expect to detect it.
async::PostTask(dispatcher(), [this] { phase_1()->Start(); });
ASSERT_TRUE(Expect(kRequest));

fake_chan 會用來模擬接收對等點的回應,以完成第 1 階段功能交換。在此情況下,程式碼會呼叫 RunLoopUntilIdle() 來明確執行工作調度工具迴圈,而 FakeChannelTest::Expect 則是在內部執行此動作:

fake_chan()->Receive(kResponse);
RunLoopUntilIdle();

最後,程式碼會驗證下列事項:

  • Phase1」不會通知 FakeListener 錯誤
  • 所有預期的參數都會在 Phase1 的完整回呼中傳遞。
EXPECT_EQ(0, listener()->pairing_error_count());
EXPECT_EQ(1, feature_exchange_count());
EXPECT_TRUE(features().initiator);
EXPECT_TRUE(features().secure_connections);
ASSERT_TRUE(last_preq());
ASSERT_TRUE(last_pres());
EXPECT_TRUE(ContainersEqual(kRequest, *last_preq()));
EXPECT_TRUE(ContainersEqual(kResponse, *last_pres()));

附註


  1. 測試案例實際上是測試固件的子類別

  2. PairingChannel 是 SM 專用的包裝函式,與這些測試的功能無關。