Realm 构建器库旨在通过允许运行时构建Realm 和特定于各个测试用例的模拟组件,来简化组件的集成测试。
如果测试希望启动子组件,Realm 构建器可能非常适合协助测试。
如果测试无法从为每个测试用例量身定制的 Realm 或包含每个测试用例专属模拟组件的 Realm 中受益,那么通过使用静态组件清单,可以简化测试的实现、理解和维护。如果测试需要执行这两项操作(或其中任一项),Realm 构建器非常适合协助测试。
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 构建器库添加到 BUILD.gn
文件中
deps = [
"//sdk/lib/sys/component/cpp/testing:cpp",
# ...
]
初始化 Realm 构建器
添加必要的依赖项后,在测试组件中初始化 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
现在,您已经为目标构建了 Realm Builder 对象,可以开始组装 realm 了。
使用 Realm 构建器实例,通过语言的 add 组件函数将子组件添加到 realm。每个子组件都需要满足以下要求:
- 组件名称:令牌网格中组件的唯一标识符。对于静态组件,此属性会映射到组件清单的
children
部分中列出的实例的name
属性。 - 组件来源:定义在构建王国时如何创建组件。对于静态组件,此值应为包含有效组件网址的
URL
。这会映射到组件清单的children
部分中列出的实例的url
属性。
以下示例向创建的 realm 添加了两个静态子组件:
- 组件
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 构建器会实现一些协议,以便组件框架将本地实现视为组件并处理传入的 FIDL 连接。本地实现可以包含特定于其所用测试用例的状态,从而允许每个构建的 Realm 针对其特定用例拥有一个模拟对象。
以下示例演示了一个实现 fidl.examples.routing.echo.Echo
协议的模拟组件。
首先,您必须实现模拟组件。
Rust
在 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_;
};
模拟实现完成后,您可以将其添加到您的 Realm:
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()); });
路由功能
默认情况下,创建的领域中没有capability 路由。如需使用 Realm Builder 将 capability 路由到组件,请使用适当的 capability 路由调用 add route 函数。
在子组件之间路由
以下示例向组件 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"}}});
公开 Realm 功能
如需将从创建的领域内提供的 capability 路由到测试组件,请将 capability 路线的目标设置为 parent
。创建的 realm 会自动将该 capability exposes
给其父级。这样,Realm 构建器实例便可访问公开的 capability。
以下示例向父级测试组件公开了 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()}});
提供测试功能
如需将 capability 从测试组件路由到创建的 realm 内的组件,请将 capability 路由的来源设置为 parent
。这包括 Realm 构建器分片为测试提供的功能:
{
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
协议从测试组件路由到 Realm 的子组件:
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"}}});
创建领域
添加测试用例所需的所有组件和路由后,请使用 build 方法创建 realm 并使其组件准备就绪以进行执行。
Rust
let realm = builder.build().await?;
C++
auto realm = builder.Build(dispatcher());
使用 build 方法返回的 realm 执行其他任务。该领域中的所有提前组件都会立即执行,并且测试现在可以访问使用 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()
,以确保在下一个测试用例之前进行完整的拆解。
不推荐:等待 realm 对象超出作用域,以向组件管理器发出信号来销毁 realm 及其子项。
C++
auto echo = realm.component().ConnectSync<fidl::examples::routing::echo::Echo>();
fidl::StringPtr response;
echo->EchoString("hello", &response);
ASSERT_EQ(response, "hello");
当 Realm 对象超出作用域时,Component Manager 会销毁该 Realm 及其子项。
高级配置
修改生成的清单
如果 addRoute 方法支持的 capability 路由功能不够用,您可以手动调整清单声明。Realm Builder 支持以下组件类型:
- Realm 构建器创建的模拟组件。
- 与测试组件位于同一软件包中的网址组件。
构建 Realm 后:
- 使用构建的 realm 的 get decl 方法获取特定子项的清单。
- 修改适当的清单属性。
- 通过调用 replace 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 中,而不是使用构建器实例。这样可确保在创建王国时,系统会针对修改后的组件对路由进行正确验证。
测试组件标识名
Realm 构建器子组件的标识符如下所示:
fuchsia_component_test_collection:child-name/component-name
该标识符由以下元素组成:
child-name
:Realm 的集合的自动生成名称,由 Realm 构建器库创建。通过调用构建的领域的child_name()
函数获取。component-name
:构建 Realm 时向Add Component
组件提供的“组件名称”参数。
如需获取子名称,请对构建的 Realm 调用以下方法:
Rust
println!("Child Name: {}", realm.root.child_name());
C++
std::cout << "Child Name: " << realm.component().GetChildName() << std::endl;
问题排查
无效的 capability 路由
add route 函数无法验证是否已从测试组件正确向创建的 realm 提供 capability。
如果您尝试将来源为 parent
且没有相应优惠的 capability 路由到其他 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++ | |
---|---|---|
旧版组件 | ||
模拟组件 | ||
替换配置值 | ||
操纵组件声明 |