Realm Builder

Realm Builder 库允许在运行时构建 realm 以及特定于各个测试用例的模拟组件,从而促进组件的集成测试。

如果测试希望启动子组件,那么 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++

将 C++ Realm Builder 库添加到 BUILD.gn 文件中

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

  # ...
]

初始化 Realm Builder

添加必要的依赖项后,在测试组件内初始化 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 实例通过相应语言的 add 组件函数向大区添加子组件。每个子组件都需要满足以下要求:

  1. 组件名称:相应组件在领域内的唯一标识符。 对于静态组件,此属性会映射到组件清单的 children 部分中列出的实例的 name 属性。
  2. 组件来源:定义在构建领域时如何创建组件。对于静态组件,应是具有有效组件网址URL。此属性映射到组件清单的 children 部分中列出的实例的 url 属性。

以下示例向已创建领域添加了两个静态子组件:

  • 组件 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 中,模拟组件是通过具有以下签名的函数实现的:

/// The implementation for a mock component. The contained function is called when the framework
/// asks the mock to run, and the function is given the component's handles for its namespace and
/// outgoing directory. The mock component may then use this handles to run a ServiceFs, access
/// capabilities as the mock, or perform other such actions.
#[derive(Clone)]
pub struct Mock(
    Arc<dyn Fn(MockHandles) -> BoxFuture<'static, Result<(), Error>> + Sync + Send + 'static>,
);

MockHandles 是一个结构体,包含组件的传入和传出功能句柄:

/// The handles from the framework over which the mock should interact with other components.
pub struct MockHandles {
    namespace: HashMap<String, fio::DirectoryProxy>,

    /// The outgoing directory handle for a mock component. This can be used to run a ServiceFs for
    /// the mock.
    pub outgoing_dir: ServerEnd<fio::DirectoryMarker>,
}

模拟组件的实现如下所示:

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 指定组件 echo_server 中的 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(&echo_client),
    )
    .await?;

C++

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

公开领域功能

如需将从创建领域内提供的功能路由到测试组件,请将功能路由的目标设置为 parent。已创建的领域会自动将 capability 的 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",
        "directory_ready",
        "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());

使用 build 方法返回的大区执行其他任务。Realm 中的任何 Eager 组件都会立即执行,并且测试现在可以访问使用 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()));

建议:在每次测试结束时对 Realm 实例调用 destroy(),以确保在下一个测试用例之前进行彻底拆解。

不推荐:等待大区对象超出范围,以指示组件管理器销毁大区及其子级。

C++

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

当领域对象超出范围时,组件管理器会销毁该领域及其子级。

高级配置

修改生成的清单

如果 add route 方法支持的功能路由功能不够用,您可以手动调整清单声明。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 子组件的名称如下所示:

fuchsia_component_test_collection:child-name/component-name

名称由以下元素组成:

  • child-name:由 Realm Builder 库为 Realm 的集合自动生成的名称。通过调用构造大范围的 child_name() 函数来获取。
  • component-name:在构建大区时向 Add Component 组件提供的“组件名称”参数。

要获取子名称,请在构造的 Realm 上调用以下方法:

Rust

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

C++

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

问题排查

功能路由无效

添加路由函数无法验证,某个功能是否已从测试组件正确提供给创建的领域。

如果您尝试路由来源为 parent 但没有相应 offer 的 capability,则打开 capability 的请求将无法解析,并且您将看到类似于以下内容的错误消息:

[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++
旧版组件
模拟组件
替换配置值
处理组件声明