Realm Builder

Realm Builder 程式庫可讓您在執行階段建構 realm 和模擬個別測試案例的元件,方便進行元件的整合測試。

如果測試想要啟動子項元件,Realm Builder 可能就很適合用於協助測試。

如果測試無法從為每個測試案例量身打造的領域,或包含每個測試案例專屬模擬元件的領域中受益,那麼您可能可以使用靜態元件資訊清單,讓測試更容易實作、理解及維護。如果測試需要使用這兩種 (或兩者皆是) 資料庫,Realm Builder 就非常適合用於協助測試。

Realm Builder 程式庫支援多種語言,但每種語言的確切語義和功能可能有所不同。如需完整的功能和支援語言清單,請參閱語言功能矩陣

新增依附元件

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

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

接著,您應為測試的語言新增 GN 依附元件:

荒漠油廠

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

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

  # ...
]

C++

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

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

  # ...
]

初始化 Realm 建構工具

新增必要的依附元件後,請在測試元件中初始化 Realm Builder。

建議:針對每個測試案例初始化個別的建構工具例項。

不建議:在所有測試案例之間使用共用的建構工具例項。

荒漠油廠

本節假設您正在編寫非同步測試,且元件的部分內容如下所示:

#[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 建構工具例項,透過語言的 add 元件函式,將子項元件新增至領域。每個子元件都需要下列項目:

  1. 元件名稱:命名領域中元件的專屬 ID。對於靜態元件,這會對應至元件資訊清單 children 部分所列執行個體的 name 屬性。
  2. 元件來源:定義在建構領域時,如何建立元件。對於靜態元件,這應為具有有效元件網址URL。這會對應至元件資訊清單 children 區段中列出的例項的 url 屬性。

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

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

荒漠油廠

// 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 中,模擬元件的實作方式如下所示:

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 LocalComponentImplBase {
 public:
  virtual ~LocalComponentImplBase();

  // 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 LocalComponentImplBase 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 LocalComponentImplBase::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 |LocalComponentImplBase|, which should
  // destruct the component, and the handles and bindings held by the component.
  // Therefore the |LocalComponentImplBase| 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();

// TODO(https://fxbug.dev/296292544): Remove when build support for API level 16 is removed.
#if FUCHSIA_API_LEVEL_LESS_THAN(17)
  // 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_;
#else
 protected:
  // Called by internal::LocalComponentInstance
  zx_status_t Initialize(fdio_ns_t* ns, zx::channel outgoing_dir, async_dispatcher_t* dispatcher,
                         fit::function<void(zx_status_t)> on_exit);

  // The different bindings override this function and provide their own
  // Outgoing_directory calls.
  virtual zx_status_t SetOutgoingDirectory(zx::channel outgoing_dir,
                                           async_dispatcher_t* dispatcher) = 0;

  fdio_ns_t* namespace_ = nullptr;
  bool initialized_ = false;

 private:
  friend internal::LocalComponentInstance;
  fit::function<void(zx_status_t)> on_exit_;
#endif
};

// TODO(https://fxbug.dev/296292544): Remove when build support for API level 16 is removed.
#if FUCHSIA_API_LEVEL_LESS_THAN(17)
using LocalComponentImpl = LocalComponentImplBase;
#else
class LocalHlcppComponent : public LocalComponentImplBase {
 public:
  // 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:
  zx_status_t SetOutgoingDirectory(zx::channel outgoing_dir,
                                   async_dispatcher_t* dispatcher) override {
    return outgoing_dir_.Serve(
        fidl::InterfaceRequest<fuchsia::io::Directory>(std::move(outgoing_dir)), dispatcher);
  }
  sys::OutgoingDirectory outgoing_dir_;
};

// TODO(https://fxbug.dev/383349947): Remove alias from LocalComponentImpl to LocalHlcppComponent
// when all instances in the codebase have been changed.
using LocalComponentImpl = LocalHlcppComponent;

class LocalCppComponent : public LocalComponentImplBase {
 public:
  // 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.
  component::OutgoingDirectory* outgoing();

 private:
  zx_status_t SetOutgoingDirectory(zx::channel outgoing_dir,
                                   async_dispatcher_t* dispatcher) override {
    outgoing_dir_ = std::make_unique<component::OutgoingDirectory>(dispatcher);
    return outgoing_dir_->Serve(fidl::ServerEnd<fuchsia_io::Directory>(std::move(outgoing_dir)))
        .status_value();
  }
  std::unique_ptr<component::OutgoingDirectory> outgoing_dir_;
};
#endif

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_;
};

模擬實作完成後,您可以將其新增至您的領域:

荒漠油廠

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,從元件 echo_server 提供 fidl.examples.routing.echo.Echo 通訊協定。

荒漠油廠

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 通訊協定公開至父項測試元件:

荒漠油廠

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" ],
    from: "parent",
    to: [ "#realm_builder" ],
},
{
    dictionary: "diagnostics",
    from: "parent",
    to: [ "#realm_builder" ],
},

// TODO(b/345827642): Needed for OOT tests
// such as bt-host that use RealmBuilder.
{
    protocol: [
        "fuchsia.inspect.InspectSink",
        "fuchsia.logger.LogSink",
    ],
    from: "parent",
    to: "#realm_builder",
},
{
    event_stream: [
        "capability_requested",
        "destroyed",
        "started",
        "stopped",
    ],
    from: "parent",
    to: "#realm_builder",
},

請參考以下範例,瞭解如何將 fuchsia.logger.LogSink 通訊協定從測試元件路由至領域的子元件:

荒漠油廠

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"}}});

建立領域

新增測試案例所需的所有元件和路徑後,請使用建構方法建立領域,並讓元件準備好執行。

荒漠油廠

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

C++

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

使用建構方法傳回的領域來執行其他工作。任何在領域中執行的急切元件都會立即執行,且任何使用 parent 導向的功能現在都能由測試存取。

荒漠油廠

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. 呼叫 replace decl 方法,將更新後的資訊清單替換為元件。

荒漠油廠

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 時,路徑能正確驗證已修改的元件。

測試元件名稱

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

fuchsia_component_test_collection:child-name/component-name

這個路徑名稱由下列元素組成:

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

如要取得子項名稱,請在建構的 Realm 上叫用下列方法:

荒漠油廠

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

C++

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

疑難排解

無效的能力路徑

新增路徑函式無法驗證能力是否已正確提供至從測試元件建立的領域。

如果您嘗試使用 parent 來源來轉送功能,但沒有相應的 offer,則開啟能力的請求將無法解析,您會看到類似以下的錯誤訊息:

[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

如要進一步瞭解如何從測試控制器提供功能,請參閱「提供測試功能」。

語言功能矩陣

荒漠油廠 C++
舊版元件
模擬元件
覆寫設定值
操控元件宣告