blob: db5a309cad3fddf7256329a44a4fe77ca86e7f64 [file] [log] [blame] [view]
# Black box testing with component manager
## Motivation
The black box testing framework enables an integration test to observe or
influence the behavior of component manager without depending on its internal
libraries.
Creating dependencies on component manager's internal libraries is problematic
for a number of reasons:
- A test can set up component manager inconsistently with how component manager
is normally started.
- A test can modify component manager’s behavior in arbitrary ways.
- Changes to component manager may require changing the test.
## Features
To test the behavior of a component or a component manager feature, you must be
able to write a black box test that can:
- Start component manager in a hermetic environment.
- Communicate with component manager using only FIDL and the [hub](hub.md).
- Access the hub of the root component.
- Wait for events to occur in component manager.
- Halt a component manager task on an event.
- Inject or mock out capabilities.
- Interpose between a client and a service.
Note: The black box testing framework covers all of these points.
## Libraries
The testing framework provides two Rust libraries for black box testing:
- `BlackBoxTest`
- `EventSource`
### BlackBoxTest
`BlackBoxTest` is a Rust library provided by the testing framework. You can use
the classes and methods in this library to automate large parts of the setup
needed for a black box test.
#### Minimum requirements
For the `BlackBoxTest` library to function correctly, the integration test
component manifest must specify (at minimum) the following features and
services:
```rust
"sandbox": {
"features": [
"hub"
],
"services": [
"fuchsia.process.Launcher",
"fuchsia.sys.Launcher",
"fuchsia.sys.Environment",
"fuchsia.logger.LogSink"
]
}
```
These services and features ensure that `BlackBoxTest` can set up a hermetic
environment and launch component manager.
#### Usage
In the simplest case, a black box test looks like the following:
```rust
let test = BlackBoxTest::default("fuchsia-pkg://fuchsia.com/foo#meta/root.cm").await?;
```
By the end of this statement:
- A component manager instance has been created in a hermetic environment.
- The root component is specified by the given URL.
- Component manager is waiting to be unblocked by the `EventSource`.
- The root [component manifest](component_manifests.md) (`root.cm`) has been
resolved.
- No component has been started.
- Component manager’s outgoing directory is serving:
- The hub of the root component at `$out/hub`.
- The `BlockingEventSource` FIDL service at
`$out/svc/fuchsia.sys2.BlockingEventSource`.
- The state of the hub reflects the following:
- Static children of the root component should be visible.
- Grandchildren of the root component should not be visible (because they
haven't been resolved yet).
- There should be no `exec` directories for any component.
Use the `BlockingEventSource` FIDL service to subscribe to events and unblock
the component manager. The following example shows you how to use the
`BlockingEventSource` service:
```rust
let event_source = test.connect_to_event_source().await?;
let event_stream = event_source.subscribe(vec![Stopped::TYPE]).await?;
event_source.start_component_tree().await?;
```
By the end of this code block:
- An `event_stream` has been created which receives `Stopped` events.
- Component manager’s execution has begun.
- The root component (and its eager children, if any) will be started soon.
#### Custom tests and convenience methods
In some cases, you may want to customize `BlackBoxTest::default`.
`BlackBoxTest::custom` allows you to specify:
- The component manager manifest to be used for the test.
- Additional directories to be created in component manager's namespace.
- A file descriptor to redirect output from components.
```rust
let test = BlackBoxTest::custom(
"fuchsia-pkg://fuchsia.com/my_custom_cm#meta/component_manager.cmx",
"fuchsia-pkg://fuchsia.com/foo#meta/root.cm",
vec![("my_dir", my_dir_handle)],
output_file_descriptor
).await?;
```
The `BlackBoxTest` library also provides convenience methods for starting up
component manager and expecting a particular output:
```rust
launch_component_and_expect_output(
"fuchsia-pkg://fuchsia.com/echo#meta/echo.cm",
"Hippos rule!",
).await?;
```
### EventSource
The `EventSource` addresses the problem of verifying state in component
manager and is analogous to a debugger's breakpoint system.
Since the `EventSource` is built on top of system events:
- A subscription can only be set on a system event.
- It supports all system events in component manager.
- It can be scoped down to a [realm](realms.md) of the component hierarchy.
- It follows the component manager’s rules of event propagation (i.e - an
event dispatched at a child realm is also dispatched to its parent).
Note: When component manager is in [debug mode](#debug-mode), an `EventSource`
is installed at the root. Hence it receives events from all components.
For reliable state verification, a test must be able to:
- Expect or wait for various events to occur in component manager.
- Halt the component manager task that is processing the event.
The workflow for the `EventSource` library looks something like this:
```rust
// Create a EventSource using ::new() or use the client
// provided by BlackBoxTest
let test = BlackBoxTest::default("fuchsia-pkg://fuchsia.com/foo#meta/root.cm").await?;
// Get an event stream of the `Started` event.
let event_source = test.connect_to_event_source().await?;
let event_stream = event_source.subscribe(vec![Started::TYPE]).await?;
// Unblock component manager
event_source.start_component_tree().await?;
// Wait for an event
let event = event_stream.expect_type::<Started>().await?;
// Verify state
...
// Resume from event
event.resume().await?;
```
### Scoping of events
The `EventSource` can be requested by any component instance within the
component topology served by the component manager if its available to the component.
Events are capailities themselves so they have to be requested as well. Refer
to [event capabilities][event-capabilities] for more details on this.
A component instance can request a scoped `BlockingEventSource` in its manifest
file as follows:
```
{
"program": {
"binary": "bin/client",
},
"use": [
{
"protocol": [
"/svc/fuchsia.sys2.BlockingEventSource",
],
"from": "framework"
},
{
event: [ "started", "stopped" ],
from: "framework",
}
],
}
```
Another component can pass along its scope of system events by passing along the
`BlockingEventSource` capability through the conventional routing operations `offer`,
`expose` and `use`.
If a component requests a `BlockingEventSource` then its children cannot start until it explicitly
calls `start_component_tree`.
### Additional functionality
With complex component hierarchies, event propagation is hard to predict and
may even be non-deterministic due to the asynchronous nature of component
manager. To deal with these cases, `EventSource` offers the following additional
functionality:
- [Multiple event streams](#multiple-event-streams)
- [Discardable event streams](#discardable-event-streams)
- [Capability injection](#capability-injection)
- [Capability interposition](#capability-interposition)
- [Event logs](#event-logs)
#### Multiple event streams {#multiple-event-streams}
It is possible to register multiple event streams, each listening to their own set
of events:
```rust
// Started and CapabilityRouted events can be interleaved,
// so use different event streams.
let start_event_stream = event_source.subscribe(vec![Started::TYPE]).await?;
let route_event_stream =
event_source.subscribe(vec![CapabilityRouted::TYPE]).await?;
// Expect 5 components to start
for _ in 1..=5 {
let event = start_event_stream.expect_type::<Started>().await?;
event.resume().await?;
}
// Expect a CapabilityRouted event from ./foo:0
let event = route_event_stream.expect_exact::<CapabilityRouted>("./foo:0").await?;
event.resume().await?;
```
#### Discardable event streams {#discardable-event-streams}
It is possible to listen for specific events and then discard the event stream,
causing future events to be ignored:
```rust
// Subscribe to Stopped events
let stop_event_stream = event_source.subscribe(vec![Stopped::TYPE]).await?;
{
// Temporarily subscribe to CapabilityRouted events
let use_event_stream = event_source.subscribe(vec![CapabilityRouted::TYPE]).await?;
// Expect a CapabilityRouted event from ./bar:0
let event = route_event_stream.expect_exact::<CapabilityRouted>("./bar:0").await?;
println!("/bar:0 used capability -> {}", event.capability_id);
event.resume().await?;
}
// At this point, the test does not care about CapabilityRouted events, so the
// event stream can be dropped. If the event stream were left instantiated,
// component manager would halt on future CapabilityRouted events.
// Expect a Stopped event
let event = stop_event_stream.expect_type::<Stopped>().await?;
println!("{} was stopped!", event.target_moniker);
event.resume().await?;
```
#### Capability injection {#capability-injection}
Several tests need to communicate with components directly. It is often also
desirable to mock out capabilities that a component connects to in the test. The
simplest way to do this is to implement an `Injector`.
```rust
/// Client <---> EchoCapability
/// EchoCapability implements the Echo protocol and responds to clients.
struct EchoCapability;
#[async_trait]
impl Injector for EchoCapability {
type Marker = fecho::EchoMarker;
async fn serve(self: Arc<Self>, mut request_stream: fecho::EchoRequestStream) {
// Start listening to requests from client
while let Some(Ok(fecho::EchoRequest::EchoString { value: Some(input), responder })) =
request_stream.next().await
{
// Respond to the client with the echo string.
responder.send(Some(&input)).expect("failed to send echo response");
}
}
}
```
It is possible to listen for a `CapabilityRouted` event and install the injector:
```rust
// Create the server end of EchoService
let echo_capability = EchoCapability::new();
// Subscribe to CapabilityRouted events
let event_stream = event_source.subscribe(vec![CapabilityRouted::TYPE]).await?;
// Wait until ./foo:0 attempts to connect to the EchoService component capability
let event = event_stream.wait_until_component_capability(
"./foo:0",
"/svc/fuchsia.echo.EchoService",
).await?;
event.inject(echo_capability).await?;
// Resume from the event
event.resume().await?;
```
Alternatively, the `BlockingEventSource` can automatically install an injector
matching the capability requested by any component in the test.
```rust
let echo_capability = EchoCapability::new();
event_source.install_injector(echo_capability).await?;
// Subscribe to other events.
event_source.start_component_tree().await?;
```
#### Capability interposition {#capability-interposition}
Tests may want to silently observe or mutate messages between a client
and service. It is possible to interpose a capability and manipulate the traffic
over the channel. Consider an interposer for an Echo service that mutates the input from
the client before sending it to the service:
```rust
/// Client <---> EchoInterposer <---> Echo service
/// The EchoInterposer copies all echo responses from the service
/// and sends them over an mpsc::Channel to the test.
struct EchoInterposer;
#[async_trait]
impl Interposer for EchoInterposer {
type Marker = fecho::EchoMarker;
async fn interpose(
self: Arc<Self>,
mut from_client: fecho::EchoRequestStream,
to_service: fecho::EchoProxy,
) {
// Start listening to requests from client
while let Some(Ok(fecho::EchoRequest::EchoString { value: Some(input), responder })) =
from_client.next().await
{
// Copy the response from the service and send it to the test
let modified_input = format!("{} Let there be chaos!", input);
// Forward the request to the service and get a response
let out = to_service
.echo_string(Some(&modified_input))
.await
.expect("echo_string failed")
.expect("echo_string got empty result");
// Respond to the client with the response from the service
responder.send(Some(out.as_str())).expect("failed to send echo response");
}
}
}
```
The test can use `EchoInterposer` on any `CapabilityRouted` event:
```rust
// Wait for echo_looper to attempt to connect to the Echo service
let event = event_stream
.wait_until_component_capability("/echo_looper:0", "/svc/fidl.examples.routing.echo.Echo")
.await?;
// Setup the interposer
let (interposer, mut rx) = EchoInterposer::new();
event.interpose(interposer).await?;
event.resume().await?;
```
Alternatively, the `BlockingEventSource` can automatically install an interposer
matching the capability requested by any component in the test.
```rust
let (interposer, mut rx) = EchoInterposer::new();
event_source.install_interposer(interposer).await?;
// Subscribe to othere events.
event_source.start_component_tree().await?;
```
#### Event logs {#event-logs}
It is possible to record events of certain types asynchronously and flush them at a later
point in time:
```rust
let event_stream = event_source.subscribe(vec![Destroyed::TYPE]).await?;
let event_log = event_source.record_events(vec![Started::TYPE]).await?;
// Wait for the root component to be destroyed
let event = event_stream.expect_exact::<Destroyed>(".").await?;
event.resume().await?;
// Flush events from the log
let events = event_log.flush().await;
// Verify that the 3 components were started in the correct order
assert_eq!(events, vec![
RecordedEvent {
event_type: Started::TYPE,
target_moniker: "./".to_string()
},
RecordedEvent {
event_type: Started::TYPE,
target_moniker: "./foo:0".to_string()
},
RecordedEvent {
event_type: Started::TYPE,
target_moniker: "./foo:0/bar:0".to_string()
}
]);
```
Note that recording of events will continue until the `EventLog` object goes out
of scope.
## Debug Mode {#debug-mode}
Both `BlackBoxTest` and `EventSource` rely on component manager’s
debug mode.
To start component manager in debug mode, pass in `--debug` as an additional
argument to the `component_manager.cmx` component. In fact, this is exactly what
`BlackBoxTest::default` does when setting up a black box test.
When component manager is in debug mode, it does the following:
1. Creates the root realm and built-in services.
1. Creates the hub and the `EventSource`.
1. Serves the following from component manager's outgoing directory:
- The hub of the root component at `$out/hub`.
- The `BlockingEventSource` FIDL service at
`$out/svc/fuchsia.sys2.BlockingEventSource`.
1. Waits to be unblocked by the `BlockingEventSource` FIDL service.
1. Starts up the root component (including any eager children).
[event-capabilities]: capabilities/event.md