Google is committed to advancing racial equity for Black communities. See how.

Realm builder

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. This document uses Rust in its example code, but the concepts shown exist in all versions of the library. For a comprehensive list of features and which languages they are supported in, see the feature matrix at the end of this document.

Set up realm builder

Add the realm builder CML shard as an include in the test's manifest:

{
    include: [
        "//src/lib/fuchsia-component-test/meta/fuchsia_component_test.shard.cml",
    ],
    ...
}

This shard declares a component collection called fuchsia_component_test_collection where the constructed realms run, and offers a set of default capabilities to those realms:

{
    protocol: [
        "fuchsia.diagnostics.ArchiveAccessor",
        "fuchsia.logger.LogSink",
        "fuchsia.sys2.EventSource",
    ],
    from: "parent",
    to: [ "#fuchsia_component_test_collection" ],
},

Construct the component topology

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.

Use the RealmBuilder instance to add child components to the realm with the add_component() function. Each child component requires the following:

  1. Component name: Unique identifier for the component inside the realm. For static components, this maps to the name attribute of an instance listed in the children section of the component manifest.
  2. Component source: Defines how the component is created when the realm is built. For static components, this should be a ComponentSource::url with a valid component URL. This maps to the url attribute of an instance listed in the children section of a component manifest.

The example below adds two static child components to the created realm:

  • Component a loads from fuchsia-pkg://fuchsia.com/foo#meta/foo.cm
  • Component b loads from fuchsia-pkg://fuchsia.com/bar#meta/bar.cm
// Create a new RealmBuilder instance, which we will use to define a new realm
let mut builder = RealmBuilder::new().await?;
builder
    // Add component `a` to the realm, which will be fetched with a URL
    .add_component("a", ComponentSource::url("fuchsia-pkg://fuchsia.com/foo#meta/foo.cm"))
    .await?
    // Add component `b` to the realm, which will be fetched with a URL
    .add_component("b", ComponentSource::url("fuchsia-pkg://fuchsia.com/bar#meta/bar.cm"))
    .await?;

This creates the following component topology:

   <root>
  /      \
 a        b

Adding a mock component

Mock components allow tests to supply a local function that behaves as a dedicated component. Realm builder implements the protocols that enables the component framework to treat the local function as a component and handle incoming FIDL connections. The local function 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 serves the fidl.examples.routing.echo.Echo protocol from a mock component that handles protocol requests from a local echo_server_mock() function:

// Create a new oneshot for passing a message from the echo server function
let (send_echo_server_called, receive_echo_server_called) = oneshot::channel();

// Wrap the sender in an Arc, Mutex, and Option so that it can safely be sent
// across threads, only interacted with by one thread at a time, and removed
// from the mutex to be consumed.
let send_echo_server_called = Arc::new(Mutex::new(Some(send_echo_server_called)));

// Build a new realm
let mut builder = RealmBuilder::new().await?;
builder
    // Add the echo server, which is implemented by the echo_server_mock
    // function (defined below). Give this function access to the oneshot
    // created above, along with the mock component's handles
    .add_component(
        "a",
        ComponentSource::mock(move |mock_handles: mock::MockHandles| {
            Box::pin(echo_server_mock(
                DEFAULT_ECHO_STR,
                send_echo_server_called.clone(),
                mock_handles,
            ))
        }),
    )
    .await?
    // Add the echo client with a URL source
    .add_eager_component(
        "b",
        ComponentSource::url(
            "fuchsia-pkg://fuchsia.com/fuchsia-component-test-tests#meta/echo_client.cm",
        ),
    )
    .await?
    // Route the fidl.examples.routing.echo.Echo protocol from a to b
    .add_route(CapabilityRoute {
        capability: Capability::protocol("fidl.examples.routing.echo.Echo"),
        source: RouteEndpoint::component("a"),
        targets: vec![RouteEndpoint::component("b")],
    })?
    // Route the logsink to `b`, so it can inform us of any issues
    .add_route(CapabilityRoute {
        capability: Capability::protocol("fuchsia.logger.LogSink"),
        source: RouteEndpoint::above_root(),
        targets: vec![RouteEndpoint::component("b")],
    })?;

// Create the realm
let _child_instance = builder.build().create().await?;

// Wait for the oneshot we created above to receive a message
receive_echo_server_called.await?;

The echo_server_mock() creates a new ServiceFs to handle incoming FIDL connections. When the echo_string() protocol function is called, the mock sends a message to the test controller.

// A mock echo server implementation, that will crash if it doesn't receive anything other than the
// contents of `expected_echo_str`. It takes and sends a message over `send_echo_server_called`
// once it receives one echo request.
async fn echo_server_mock(
    expected_echo_string: &'static str,
    send_echo_server_called: Arc<Mutex<Option<oneshot::Sender<()>>>>,
    mock_handles: mock::MockHandles,
) -> Result<(), Error> {
    // Create a new ServiceFs to host FIDL protocols from
    let mut fs = fserver::ServiceFs::new();
    let mut tasks = vec![];

    // Add the echo protocol to the ServiceFs
    fs.dir("svc").add_fidl_service(move |mut stream: fecho::EchoRequestStream| {
        let send_echo_server_called = send_echo_server_called.clone();
        tasks.push(fasync::Task::local(async move {
            while let Some(fecho::EchoRequest::EchoString { value, responder }) =
                stream.try_next().await.expect("failed to serve echo service")
            {
                assert_eq!(Some(expected_echo_string.to_string()), value);
                // Send the received string back to the client
                responder.send(value.as_ref().map(|s| &**s)).expect("failed to send echo response");

                // Take the sender from send_echo_server_called and pass a
                // message through it
                send_echo_server_called
                    .lock()
                    .await
                    .take()
                    .unwrap()
                    .send(())
                    .expect("failed to send results");
            }
        }));
    });

    // Run the ServiceFs on the outgoing directory handle from the mock handles
    fs.serve_connection(mock_handles.outgoing_dir.into_channel())?;
    fs.collect::<()>().await;
    Ok(())
}

Add capability routes

By default there are no capability routes in the created realm. To route capabilities to components using RealmBuilder, call the add_route() function with the appropriate CapabilityRoute.

The following example adds a CapabilityRoute to offer component b the fidl.examples.routing.echo.Echo protocol from component a.

// Add a new route for the protocol capability `fidl.examples.routing.echo.Echo`
// from `a` to `b`
builder.add_route(CapabilityRoute {
    capability: Capability::protocol("fidl.examples.routing.echo.Echo"),
    source: RouteEndpoint::component("a"),
    targets: vec![RouteEndpoint::component("b")],
})?;

Exposing realm capabilities

To route capabilities provided from inside the created realm to the test controller, set the target of the CapabilityRoute using RouteEndpoint::above_root(). The created realm will automatically expose the capability to its parent. This allows the RealmBuilder instance to access the exposed capability.

The following example exposes a fidl.examples.routing.echo.EchoClientStats protocol to the parent test component:

// Adds a route for the protocol capability
// `fidl.examples.routing.echo.EchoClientStats` from `b` to the realm's parent
builder.add_route(CapabilityRoute {
    capability: Capability::protocol("fidl.examples.routing.echo.EchoClientStats"),
    source: RouteEndpoint::component("b"),
    targets: vec![RouteEndpoint::above_root()],
})?;

let realm = builder.build();
// Creates the realm, and add it to the collection to start its execution
let realm_instance = realm.create().await?;

// Connects to `fidl.examples.routing.echo.EchoClientStats`, which is provided
// by `b` in the created realm
let echo_client_stats_proxy =
    realm_instance.root.connect_to_protocol_at_exposed_dir::<EchoClientStatsMarker>()?;

Offering external capabilities

To route capabilities from the test controller to components inside the created realm, set the source of the CapabilityRoute using RouteEndpoint::above_root(). Consider the following example to make the fuchsia.logger.LogSink protocol from the parent's realm available to components a and b:

// Routes `fuchsia.logger.LogSink` from above root to `a` and `b`
builder.add_route(CapabilityRoute {
    capability: Capability::protocol("fuchsia.logger.LogSink"),
    source: RouteEndpoint::above_root(),
    targets: vec![RouteEndpoint::component("a"), RouteEndpoint::component("b")],
})?;

The fuchsia.logger.LogSink protocol is offered by default to the created realm through the realm builder shard. To route a capability that isn't in the realm builder shard, offer it directly:

{
    include: [
        "//src/lib/fuchsia-component-test/meta/fuchsia_component_test.shard.cml",
    ],
    children: [
        {
            name: "some-child",
            url: "...",
        },
    ],
    offer: [
        {
            protocol: "fuchsia.example.Foo",
            from: "#some-child",
            to: [ "#fuchsia_component_test_collection" ],
        },
    ],
    ...
}

Creating the realm

After you have added all the components and routes needed for the test case, use realm.create() to create the realm and make its components ready to execute.

// Creates the realm, and add it to the collection to start its execution
let realm_instance = realm.create().await?;

Use the realm_instance returned by create() to perform additional tasks. Any eager components in the realm will immediately execute, and any capabilities routed using above_root are now accessible by the test.

// Connects to `fidl.examples.routing.echo.EchoClientStats`, which is provided
// by `b` in the created realm
let echo_client_stats_proxy =
    realm_instance.root.connect_to_protocol_at_exposed_dir::<EchoClientStatsMarker>()?;

Destroying the realm

When the test no longer needs the realm, it can be destroyed by destroying the realm instance returned by create():

// As per rust semantics, this also happens when `realm_instance` goes out of
// scope.
drop(realm_instance);

This action instructs Component Manager to destroy the realm and all its children.

Advanced configuration

Modifying generated manifests

For cases where the capability routing features supported by add_route() 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 controller.

After constructing the realm:

  1. Use the get_decl() function of the constructed realm to obtain a specific child's manifest.
  2. Modify the appropriate manifest attributes.
  3. Substitute the updated manifest for the component by calling the set_decl() function.

See the following example:

let mut realm = builder.build();
let mut root_manifest = realm.get_decl(&Moniker::root()).await?;
// root_manifest is mutated in whatever way is needed
realm.set_component(&Moniker::root(), root_manifest).await?;

let mut a_manifest = realm.get_decl(&"a".into()).await?;
// a_manifest is mutated in whatever way is needed
realm.set_component(&"a".into(), a_manifest).await?;

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.

Troubleshooting

Invalid capability routes

The add_route() function cannot validate if a capability is properly offered to the created realm from the test controller.

If you attempt to route capabilities with a source of above_root 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: Failed to route protocol `fidl.examples.routing.echo.Echo` with target component `/core:0/test_manager:0/tests:auto-10238282593681900609:4/test_wrapper:0/test_root:0/fuchsia_component_test_
[86842.197][klog][I] collection:auto-4046836611955440668:16/echo-client:0`: An `offer from parent` declaration was found at `/core:0/test_manager:0/tests:auto-10238282593681900609:4/test_wrapper:0/test_root:0/fuchsia_component_test_colle
[86842.197][klog][I] ction:auto-4046836611955440668:16` 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 external capabilities.

Language feature matrix

Rust
Legacy components N
Mock components Y
Strong capability routes Y
Weak capability routes N
Custom environments N
Setting subdirectories N