The Realm Builder library exists to facilitate integration testing of components by allowing for the run-time construction of realms and mocked components specific to individual test cases.
If a test wishes to launch a child component, then Realm Builder is likely a good fit for assisting the test.
If a test does not benefit from having either realms tailor made to each test case or realms containing mocked components unique to each test case, then the test can likely be made simpler to implement, understand, and maintain by using static component manifests. If a test does call for either (or both) of these things, then Realm Builder is a good fit for assisting the test.
The Realm Builder library is available in multiple languages, and the exact semantics and abilities available in each language may vary. For a comprehensive list of features and supported languages, see the Language feature matrix.
Add Dependencies
The Realm Builder client libraries rely on special capabilities to work. Therefore, tests using this library must include the necessary shard in their test component's manifest:
include: [
"sys/component/realm_builder.shard.cml",
// ...
],
Afterwards, you should add the GN dependency for your test's language:
Rust
Add the Rust Realm Builder library to your BUILD.gn
file
deps = [
"//src/lib/fuchsia-component-test",
# ...
]
C++
Add the C++ Realm Builder library to your BUILD.gn
file
deps = [
"//sdk/lib/sys/component/cpp/testing:cpp",
# ...
]
Initialize Realm Builder
After adding the necessary dependencies, initialize Realm Builder inside your test component.
Recommended: Initialize a separate builder instance for each test case.
Not recommended: Use a shared builder instance between all test cases.
Rust
This section assumes that you are writing an asynchronous test and that some part of your component looks similar to this:
#[fuchsia::test]
async fn test() -> Result<(), Error> {
// ...
}
Import Realm Builder library
use {
// ...
fuchsia_component_test::{
Capability, ChildOptions, LocalComponentHandles, RealmBuilder, Ref, Route,
},
futures::{StreamExt, TryStreamExt},
};
Initialize RealmBuilder
struct
Create a new RealmBuilder
instance for each test case in your test.
This creates a unique, isolated, child realm that ensures that the side-effects
of one test case do not affect the others.
let builder = RealmBuilder::new().await?;
C++
This section assumes that you are writing an asynchronous test and that your testing is executing inside a message loop. Typically, such cases look like this:
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
TEST(SampleTest, CallEcho) {
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
// Test code below
}
Import Realm Builder library
#include <lib/sys/component/cpp/testing/realm_builder.h>
Use library namespace This step is optional. It imports the entire library's namespace, for convenience when writing and reading tests.
// NOLINTNEXTLINE
using namespace component_testing;
Initialize Realm::Builder
class
Create a new Realm::Builder
instance for each test case in your test.
This creates a unique, isolated, child realm that ensures that the side-effects
of one test case do not affect the others.
auto builder = RealmBuilder::Create();
Construct Realm
With the constructed Realm Builder object for your target, you can now begin assembling the realm.
Use the Realm Builder instance to add child components to the realm with the language's add component function. Each child component requires the following:
- Component name: Unique identifier for the component inside the realm.
For static components, this maps to the
name
attribute of an instance listed in thechildren
section of the component manifest. - Component source: Defines how the component is created when the realm is
built. For static components, this should be a
URL
with a valid component URL. This maps to theurl
attribute of an instance listed in thechildren
section of a component manifest.
The example below adds two static child components to the created realm:
- Component
echo_server
loads from an absolute component URL Component
echo_client
loads from a relative component URL
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});
Adding Mock Components
Mock components allow tests to supply a local implementation that behaves as a dedicated component. Realm Builder implements the protocols that enables the component framework to treat the local implementation as a component and handle incoming FIDL connections. The local implementation can hold state specific to the test case where it is used, allowing each constructed realm to have a mock for its specific use case.
The following example demonstrates a mock component that implements the
fidl.examples.routing.echo.Echo
protocol.
First, you must implement your mock component.
Rust
In Rust, a mock component is implemented via function that has the following signature:
Missing code example
MockHandles
is a struct containing handles to the component's incoming
and outgoing capabilities:
Missing code example
An implementation for a mock component would look like:
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++
In C++, a mock component is implemented by creating a class that inherits from
LocalComponent interface and overrides the Start
method.
// 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
is a class containing handles to the component's incoming
and outgoing capabilities:
// 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();
// ...
};
An implementation for a mock component would look like:
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_;
};
After your mock implementation is complete, you may add it your 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()); });
Route Capabilities
By default there are no capability routes in the created realm. To route capabilities to components using Realm Builder, call the add route function with the appropriate capability route.
Routing between child components
The following example adds a capability route to offer component
echo_client
the fidl.examples.routing.echo.Echo
protocol from component
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"}}});
Exposing realm capabilities
To route capabilities provided from inside the created realm to the test component,
set the target of the capability route to parent
.
The created realm automatically exposes
the capability to its
parent. This allows the Realm Builder instance to access the exposed capability.
The following example exposes a fidl.examples.routing.echo.Echo
protocol to
the parent test component:
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()}});
Offering test capabilities
To route capabilities from the test component to components inside the created
realm, set the source of the capability route to parent
. This includes the
capabilities provided to tests by the Realm Builder shard:
{
protocol: [
"fuchsia.diagnostics.ArchiveAccessor",
"fuchsia.inspect.InspectSink",
],
from: "parent",
to: [ "#realm_builder" ],
},
{
event_stream: [
"capability_requested",
"destroyed",
"started",
"stopped",
],
from: "parent",
to: "#realm_builder",
},
Consider the following example to route the fuchsia.logger.LogSink
protocol
from the test component to the child components of the 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"}}});
Creating the realm
After you have added all the components and routes needed for the test case, use build method to create the realm and make its components ready to execute.
Rust
let realm = builder.build().await?;
C++
auto realm = builder.Build(dispatcher());
Use the realm returned by the build method to perform additional tasks.
Any eager components in the realm execute immediately, and any capabilities
routed using parent
are now accessible by the test.
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()));
Recommended: Call destroy()
on the realm instance
at the end of each test to ensure clean teardown before the next test case.
Not recommended: Wait for the realm object to go out of scope to signal Component Manager to destroy the realm and its children.
C++
auto echo = realm.component().ConnectSync<fidl::examples::routing::echo::Echo>();
fidl::StringPtr response;
echo->EchoString("hello", &response);
ASSERT_EQ(response, "hello");
When the realm object goes out of scope, Component Manager destroys the realm and its children.
Advanced Configuration
Modifying generated manifests
For cases where the capability routing features supported by the add route method are not sufficient, you can manually adjust the manifest declarations. Realm Builder supports this for the following component types:
- Mock components created by Realm Builder.
- URL components contained in the same package as the test component.
After constructing the realm:
- Use the get decl method of the constructed realm to obtain a specific child's manifest.
- Modify the appropriate manifest attributes.
- Substitute the updated manifest for the component by calling the replace decl method.
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));
When adding routes for modified components, add them directly to the constructed realm where you obtained the manifest instead of using the builder instance. This ensures the routes are properly validated against the modified component when the realm is created.
Test component monikers
The moniker for a Realm Builder child component looks like the following:
fuchsia_component_test_collection:child-name/component-name
The moniker consists of the following elements:
child-name
: An auto-generated name for the realm's collection, created by the Realm Builder library. Obtained by calling thechild_name()
function of the constructed realm.component-name
: The "Component name" parameter provided to theAdd Component
component when constructing the realm.
To obtain the child name invoke the following method on the constructed Realm:
Rust
println!("Child Name: {}", realm.root.child_name());
C++
std::cout << "Child Name: " << realm.component().GetChildName() << std::endl;
Troubleshooting
Invalid capability routes
The add route function cannot validate if a capability is properly offered to the created realm from the test component.
If you attempt to route capabilities with a source of parent
without a
corresponding offer, requests to open the capability will not resolve
and you will see error messages similar to the following:
[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
For more information on how to properly offer capabilities from the test controller, see offering test capabilities.
Language feature matrix
Rust | C++ | |
---|---|---|
Legacy components | ||
Mock components | ||
Override configuration values | ||
Manipulate component declaration |