Realm Builder

可以使用 Realm Builder 程式庫,以便進行整合測試 允許在執行階段建構領域和 個別測試案例專用的模擬元件。

如果測試想要啟動子項元件,那麼 Realm Builder 可能會是 適合輔助測試

如果測試沒有好處,那就是針對每項測試量身打造測試領域 案例或領域,包含每個測試案例專屬的模擬元件 實作、瞭解和維護 靜態元件資訊清單。如果測試呼叫 Realm Builder 很適合用來協助測試

Realm Builder 程式庫支援多種語言, 每種語言提供的語義和功能可能有所不同。如需 功能清單和支援的語言,請參閱 語言特徵矩陣

新增依附元件

Realm Builder 用戶端程式庫需要特殊功能才能運作。因此 使用此程式庫的測試必須在其容器中加入必要的資料分割 測試元件的資訊清單:

include: [
    "sys/component/realm_builder.shard.cml",
    // ...
],

之後,您應針對測試的語言新增 GN 依附元件:

Rust

將 Rust Realm Builder 程式庫新增至 BUILD.gn 檔案

deps = [
  "//src/lib/fuchsia-component-test",

  # ...
]

C++

BUILD.gn 檔案中加入 C++ Realm Builder 程式庫

deps = [
  "//sdk/lib/sys/component/cpp/testing:cpp",

  # ...
]

初始化 Realm Builder

新增必要的依附元件後,請在 測試元件

建議:初始化獨立的建構工具執行個體 。

不建議使用:在以下兩個位置使用共用建構工具執行個體: 所有測試案例。

Rust

本節假設您要撰寫非同步測試,且 元件的某些部分看起來與以下內容類似:

#[fuchsia::test]
async fn test() -> Result<(), Error> {
    // ...
}

匯入 Realm Builder 程式庫

use {
    // ...
    fuchsia_component_test::{
        Capability, ChildOptions, LocalComponentHandles, RealmBuilder, Ref, Route,
    },
    futures::{StreamExt, TryStreamExt},
};

初始化 RealmBuilder 結構

為測試中的每個測試案例建立新的 RealmBuilder 執行個體。 這會建立獨特的隔離子項領域,確保副作用 不會影響其他測試案例

let builder = RealmBuilder::new().await?;

C++

本節假設您要撰寫非同步測試,且 測試正在訊息迴圈中執行。這類案件通常適合 輸入:

#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>

TEST(SampleTest, CallEcho) {
    async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
    // Test code below
}

匯入 Realm Builder 程式庫

#include <lib/sys/component/cpp/testing/realm_builder.h>

使用程式庫命名空間 您可以跳過這個步驟,該程式碼會匯入整個程式庫的命名空間 方便您編寫及讀取測試

// NOLINTNEXTLINE
using namespace component_testing;

初始化 Realm::Builder 類別

為測試中的每個測試案例建立新的 Realm::Builder 執行個體。 這會建立獨特的隔離子項領域,確保副作用 不會影響其他測試案例

auto builder = RealmBuilder::Create();

建造領域

利用建構為目標的 Realm Builder 物件, 組合領域。

使用 Realm Builder 執行個體,利用 新增元件函式每個子元件都需要下列項目:

  1. 元件名稱:領域內部元件的專屬 ID。 如果是靜態元件,這個屬性會對應至執行個體的 name 屬性 列在元件資訊清單的 children 部分。
  2. 元件來源:定義在運作範圍時建立元件的方式 。如果是靜態元件,這個 URL 應為含有 有效元件網址。這會對應至url 元件的 children 部分中列出的執行個體 資訊清單。

以下範例會在已建立的領域中新增兩個靜態子項元件:

  • 從絕對元件網址載入元件 echo_server
  • 元件 echo_client 從相對元件網址載入

Rust

// Add component to the realm, which is fetched using a URL.
let echo_server = builder
    .add_child(
        "echo_server",
        "fuchsia-pkg://fuchsia.com/realm-builder-examples#meta/echo_server.cm",
        ChildOptions::new(),
    )
    .await?;
// Add component to the realm, which is fetched using a fragment-only URL.
// The child is not exposing a service, so the `eager` option ensures the
// child starts when the realm is built.
let echo_client = builder
    .add_child("echo_client", "#meta/echo_client.cm", ChildOptions::new().eager())
    .await?;

C++

// Add component server to the realm, which is fetched using a URL.
builder.AddChild("echo_server",
                 "fuchsia-pkg://fuchsia.com/realm-builder-examples#meta/echo_server.cm");
// Add component to the realm, which is fetched using a fragment-only URL. The
// child is not exposing a service, so the `EAGER` option ensures the child
// starts when the realm is built.
builder.AddChild("echo_client", "#meta/echo_client.cm",
                 ChildOptions{.startup_mode = StartupMode::EAGER});

新增模擬元件

模擬元件可讓測試提供本機實作,以做為 專屬元件Realm Builder 實作的通訊協定會啟用 元件架構,將本機實作視為元件和處理 傳入 FIDL 連線本機實作可保留專屬於 測試案例,讓每個建構領域都有一個模型 適合特定用途

下列範例將示範實作 fidl.examples.routing.echo.Echo 通訊協定。

首先,您必須實作模擬元件。

Rust

在 Rust 中,系統會使用下列函式實作模擬元件 簽章:

Missing code example

MockHandles 是包含元件傳入程序的控制代碼 和對外功能:

Missing code example

模擬元件的實作方式如下:

async fn echo_server_mock(handles: LocalComponentHandles) -> Result<(), Error> {
    // Create a new ServiceFs to host FIDL protocols from
    let mut fs = fserver::ServiceFs::new();

    // Add the echo protocol to the ServiceFs
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Run the ServiceFs on the outgoing directory handle from the mock handles
    fs.serve_connection(handles.outgoing_dir)?;

    fs.for_each_concurrent(0, move |IncomingService::Echo(stream)| async move {
        stream
            .map(|result| result.context("Request came with error"))
            .try_for_each(|request| async move {
                match request {
                    fecho::EchoRequest::EchoString { value, responder } => {
                        responder
                            .send(value.as_ref().map(|s| &**s))
                            .expect("failed to send echo response");
                    }
                }
                Ok(())
            })
            .await
            .context("Failed to serve request stream")
            .unwrap_or_else(|e| eprintln!("Error encountered: {:?}", e))
    })
    .await;

    Ok(())
}

C++

在 C++ 中,如要實作模擬元件,您可以建立沿用自 LocalComponent 介面,並覆寫 Start 方法。

// The interface for backing implementations of components with a Source of Mock.
class LocalComponentImpl {
 public:
  virtual ~LocalComponentImpl();

  // Invoked when the Component Manager issues a Start request to the component.
  // |mock_handles| contains the outgoing directory and namespace of
  // the component.
  virtual void OnStart() = 0;

  // The LocalComponentImpl derived class may override this method to be informed if
  // ComponentController::Stop() was called on the controller associated with
  // the component instance. The ComponentController binding will be dropped
  // automatically, immediately after LocalComponentImpl::OnStop() returns.
  virtual void OnStop() {}

  // The component can call this method to terminate its instance. This will
  // release the handles, and drop the |ComponentController|, informing
  // component manager that the component has stopped. Calling |Exit()| will
  // also cause the Realm to drop the |LocalComponentImpl|, which should
  // destruct the component, and the handles and bindings held by the component.
  // Therefore the |LocalComponentImpl| should not do anything else after
  // calling |Exit()|.
  //
  // This method is not valid until |OnStart()| is invoked.
  void Exit(zx_status_t return_code = ZX_OK);

  // Returns the namespace provided to the mock component.
  //
  // This method is not valid until |OnStart()| is invoked.
  fdio_ns_t* ns();

  // Returns a wrapper around the component's outgoing directory. The mock
  // component may publish capabilities using the returned object.
  //
  // This method is not valid until |OnStart()| is invoked.
  sys::OutgoingDirectory* outgoing();

  // Convenience method to construct a ServiceDirectory by opening a handle to
  // "/svc" in the namespace object returned by `ns()`.
  //
  // This method is not valid until |OnStart()| is invoked.
  sys::ServiceDirectory svc();

 private:
  friend internal::LocalComponentRunner;
  // The |LocalComponentHandles| are set by the |LocalComponentRunner| after
  // construction by the factory, and before calling |OnStart()|
  std::unique_ptr<LocalComponentHandles> handles_;
};

LocalComponentHandles 類別包含對元件傳入的控制代碼 和對外功能:

// Handles provided to mock component.
class LocalComponentHandles final {
 public:
  // ...

  // Returns the namespace provided to the mock component. The returned pointer
  // will be invalid once *this is destroyed.
  fdio_ns_t* ns();

  // Returns a wrapper around the component's outgoing directory. The mock
  // component may publish capabilities using the returned object. The returned
  // pointer will be invalid once *this is destroyed.
  sys::OutgoingDirectory* outgoing();

  // Convenience method to construct a ServiceDirectory by opening a handle to
  // "/svc" in the namespace object returned by `ns()`.
  sys::ServiceDirectory svc();

  // ...
};

模擬元件的實作方式如下:

class LocalEchoServerImpl : public fidl::examples::routing::echo::Echo, public LocalComponentImpl {
 public:
  explicit LocalEchoServerImpl(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}

  // Override `OnStart` from `LocalComponentImpl` class.
  void OnStart() override {
    // When `OnStart()` is called, this implementation can call methods to
    // access handles to the component's incoming capabilities (`ns()` and
    // `svc()`) and outgoing capabilities (`outgoing()`).
    ASSERT_EQ(outgoing()->AddPublicService(bindings_.GetHandler(this, dispatcher_)), ZX_OK);
  }

  // Override `EchoString` from `Echo` protocol.
  void EchoString(::fidl::StringPtr value, EchoStringCallback callback) override {
    callback(std::move(value));
  }

 private:
  async_dispatcher_t* dispatcher_;
  fidl::BindingSet<fidl::examples::routing::echo::Echo> bindings_;
};

模擬實作完成後,您就可以加入領域:

Rust

let echo_server = builder
    .add_local_child(
        "echo_server",
        move |handles: LocalComponentHandles| Box::pin(echo_server_mock(handles)),
        ChildOptions::new(),
    )
    .await?;

C++

// Add component to the realm, providing a mock implementation
builder.AddLocalChild("echo_server",
                      [&]() { return std::make_unique<LocalEchoServerImpl>(dispatcher()); });

路線功能

根據預設,已建立的領域中沒有任何功能路徑。 如要使用 Realm Builder 將功能轉送至元件,請呼叫新增路徑 函式。

子項元件之間的轉送

以下範例將能力路徑新增至 offer 元件 echo_client 從元件中運用 fidl.examples.routing.echo.Echo 通訊協定 echo_server

Rust

builder
    .add_route(
        Route::new()
            .capability(Capability::protocol_by_name("fidl.examples.routing.echo.Echo"))
            .from(&echo_server)
            .to(&echo_client),
    )
    .await?;

C++

builder.AddRoute(Route{.capabilities = {Protocol{"fidl.examples.routing.echo.Echo"}},
                       .source = ChildRef{"echo_server"},
                       .targets = {ChildRef{"echo_client"}}});

公開領域功能

為了將所建立領域內部提供的功能轉送至測試元件, 將能力路徑的目標設為 parent。 已建立的領域會自動 exposes 具備 父項。這可讓 Realm Builder 執行個體存取公開的能力。

以下範例會將 fidl.examples.routing.echo.Echo 通訊協定公開給 父項測試元件

Rust

builder
    .add_route(
        Route::new()
            .capability(Capability::protocol_by_name("fidl.examples.routing.echo.Echo"))
            .from(&echo_server)
            .to(Ref::parent()),
    )
    .await?;

C++

builder.AddRoute(Route{.capabilities = {Protocol{"fidl.examples.routing.echo.Echo"}},
                       .source = ChildRef{"echo_server"},
                       .targets = {ParentRef()}});

提供測試功能

為了將功能從測試元件轉送至已建立的元件 領域,將能力路徑的來源設為 parent。這包括 Realm Builder 資料分割的測試功能:

{
    protocol: [
        "fuchsia.diagnostics.ArchiveAccessor",
        "fuchsia.inspect.InspectSink",
    ],
    from: "parent",
    to: [ "#realm_builder" ],
},
{
    event_stream: [
        "capability_requested",
        "destroyed",
        "running_v2",
        "started",
        "stopped",
    ],
    from: "parent",
    to: "#realm_builder",
},

請參考以下範例轉送 fuchsia.logger.LogSink 通訊協定 從測試元件到領域的子項元件:

Rust

builder
    .add_route(
        Route::new()
            .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
            .from(Ref::parent())
            .to(&echo_server)
            .to(&echo_client),
    )
    .await?;

C++

builder.AddRoute(Route{.capabilities = {Protocol{"fuchsia.logger.LogSink"}},
                       .source = ParentRef(),
                       .targets = {ChildRef{"echo_server"}, ChildRef{"echo_client"}}});

建立領域

新增測試案例所需的所有元件和路徑後 使用建構方法建立領域,讓其元件準備就緒 此程序的第一步 是將程式碼簽入執行所有單元

Rust

let realm = builder.build().await?;

C++

auto realm = builder.Build(dispatcher());

使用建構方法傳回的領域執行其他工作。 領域中的任何加速元件都會立即執行,並列出所有功能 現在可以透過測試存取使用 parent 轉送的功能。

Rust

let echo = realm.root.connect_to_protocol_at_exposed_dir::<fecho::EchoMarker>()?;
assert_eq!(echo.echo_string(Some("hello")).await?, Some("hello".to_owned()));

建議做法:在領域執行個體上呼叫 destroy() 每次測試結束時,以確保在下一個測試案例前拆除。

不建議使用:等待領域物件結束 範圍,要求元件管理服務刪除領域及其子項。

C++

auto echo = realm.component().ConnectSync<fidl::examples::routing::echo::Echo>();
fidl::StringPtr response;
echo->EchoString("hello", &response);
ASSERT_EQ(response, "hello");

當領域物件超出範圍時,元件管理服務會將其刪除 領域及其子項。

進階設定

修改產生的資訊清單

適用於新增路徑支援的能力轉送功能 方法不夠充分,您可以手動調整資訊清單宣告。 Realm Builder 支援以下元件類型執行此操作:

  • 由 Realm Builder 建立的模擬元件。
  • 測試元件位於相同套件中的網址元件。

建構領域後:

  1. 使用建構領域的 get decl 方法,取得 子項資訊清單
  2. 修改適當的資訊清單屬性。
  3. 呼叫 取代 decl 方法

Rust

let mut root_decl = builder.get_realm_decl().await?;
// root_decl is mutated in whatever way is needed
builder.replace_realm_decl(root_decl).await?;

let mut a_decl = builder.get_component_decl("a").await?;
// a_decl is mutated in whatever way is needed
builder.replace_component_decl("a", a_decl).await?;

C++

auto root_decl = realm_builder.GetRealmDecl();
// ... root_decl is mutated as needed
realm_builder.ReplaceRealmDecl(std::move(root_decl));

auto a_decl = realm_builder.GetComponentDecl("a");
// ... a_decl is mutated as needed
realm_builder.ReplaceComponentDecl(std::move(a_decl));

為修改的元件新增路徑時,請直接將這些路徑加入 取得資訊清單的建構領域,而不是使用 建構工具執行個體如此可確保路徑能根據 在建立運作範圍時修改元件。

測試元件單元件

Realm Builder 子項元件的 moniker 如下所示:

fuchsia_component_test_collection:child-name/component-name

測試者包含下列元素:

  • child-name:為領域集合自動產生的名稱 來自 Realm Builder 程式庫透過呼叫 child_name() 取得 建構領域函數
  • component-name:「元件名稱」提供的參數 建構領域時,會使用 Add Component 元件。

如要取得子項名稱,請在建構的領域中叫用以下方法:

Rust

println!("Child Name: {}", realm.root.child_name());

C++

std::cout << "Child Name: " << realm.component().GetChildName() << std::endl;

疑難排解

能力路徑無效

新增路線函式無法驗證功能是否提供正確能力 將測試元件連結至已建立的運作領域

如果您嘗試以 parent 來源轉送功能,卻沒有 對應的優惠,開啟能力開啟要求並不會解析 就會看到類似下列內容的錯誤訊息:

[86842.196][klog][E] [component_manager] ERROR: Required protocol `fidl.examples.routing.echo.Echo` was not available for target component `/core/test_manager/tests:auto-10238282593681900609/test_wrapper/test_root/fuchsia_component_test_
[86842.197][klog][I] collection:auto-4046836611955440668/echo-client`: An `offer from parent` declaration was found at `/core/test_manager/tests:auto-10238282593681900609/test_wrapper/test_root/fuchsia_component_test_colle
[86842.197][klog][I] ction:auto-4046836611955440668` for `fidl.examples.routing.echo.Echo`, but no matching `offer` declaration was found in the parent

進一步瞭解如何正確提供測試功能 控制器,請參閱提供測試功能一節。

語言特徵矩陣

Rust C++
舊版元件
模擬元件
覆寫設定值
操控元件宣告