| # Hermetic testing using OpaqueTest (Components v2) |
| |
| <<../_v2_banner.md>> |
| |
| ## Motivation |
| |
| The OpaqueTest 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 v2 component, OpaqueTest lets you: |
| |
| - 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. |
| |
| ## Minimum requirements |
| |
| For the OpaqueTest framework to function correctly, the test cmx manifest |
| must specify (at minimum) the following features and services: |
| |
| ```rust |
| "include": [ "sdk/lib/diagnostics/syslog/client.shard.cmx" ], |
| "sandbox": { |
| "features": [ |
| "hub" |
| ], |
| "services": [ |
| "fuchsia.process.Launcher", |
| "fuchsia.sys.Environment", |
| "fuchsia.sys.Launcher" |
| ] |
| }, |
| ``` |
| |
| These services and features ensure that OpaqueTest can set up a hermetic |
| environment and launch component manager. |
| |
| ## Usage |
| |
| In the simplest case, a test can be started as follows: |
| |
| ```rust |
| let test = OpaqueTest::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 |
| |
| In some cases, you may want to customize `OpaqueTest::default`. |
| Use an `OpaqueTestBuilder` 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. |
| |
| - The configuration file to be used by component manager. |
| |
| - Additional command-line args to be used by component manager. |
| |
| ```rust |
| let test = OpaqueTestBuilder::new("fuchsia-boot:///#meta/root.cm") |
| .component_manager_url("fuchsia-pkg://fuchsia.com/base_resolver_test#meta/component_manager_without_loader.cmx") |
| .add_dir_handle("/boot", pkg_channel.into()) |
| .build() |
| .await?; |
| ``` |
| |
| ## EventSource |
| |
| An `EventSource` is used to subscribe to system events sent by component manager. |
| |
| 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 an `EventSource` looks something like this: |
| |
| ```rust |
| // Create an EventSource using ::new_sync() or use the source |
| // provided by OpaqueTest |
| let test = OpaqueTest::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 = EventMatcher::ok().expect_match::<Started>(&mut event_stream).await; |
| |
| // Verify state |
| ... |
| |
| // Resume from event |
| event.resume().await?; |
| ``` |
| |
| Note: Subscribing to an event stream after the component tree has been started is racy. |
| `start_component_tree()` consumes the `EventSource` object to prevent future subscriptions. |
| |
| Calling `resume()` on an event unblocks component manager and allows it to proceed with the |
| event dispatch. |
| |
| Note: It is not strictly necessary to invoke `resume()` on an event. When the event object |
| goes out of scope, `resume()` is called implicitly. |
| |
| ### Scoping of events |
| |
| The `BlockingEventSource` FIDL protocol can be requested by any component instance within the |
| component topology and is served by the component manager. |
| |
| Events are capailities themselves so they have to be routed 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: |
| |
| ```json5 |
| { |
| program: { |
| binary: "bin/client", |
| }, |
| use: [ |
| { |
| protocol: [ |
| "fuchsia.sys2.BlockingEventSource", |
| ], |
| from: "framework" |
| }, |
| { |
| event: [ "started", "stopped" ], |
| from: "framework", |
| } |
| ], |
| } |
| ``` |
| |
| Note: To receive asynchronous events, use the `EventSource` FIDL protocol instead. |
| Asynchronous events do not block component manager and the events do not have `resume()` |
| methods on them. |
| |
| 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) |
| - [Event sequences](#event-sequences) |
| - [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?; |
| |
| // Unblock component manager |
| event_source.start_component_tree().await; |
| |
| // Expect 5 components to start |
| for _ in 1..=5 { |
| let event = EventMatcher::ok().expect_match::<Started>(&mut start_event_stream).await; |
| event.resume().await?; |
| } |
| |
| // Expect a CapabilityRouted event from ./foo:0 |
| let event = EventMatcher::ok() |
| .moniker("./foo:0") |
| .expect_match::<CapabilityRouted>(&mut route_event_stream) |
| .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 route_event_stream = event_source.subscribe(vec![CapabilityRouted::TYPE]).await?; |
| |
| // Expect a CapabilityRouted event from ./bar:0 |
| let event = EventMatcher::ok().moniker("./bar:0").expect_match::<CapabilityRouted>(&mut route_event_stream).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 = EventMatcher::ok().expect_match::<Stopped>(&mut stop_event_stream).await?; |
| println!("{} was stopped!", event.target_moniker); |
| event.resume().await?; |
| ``` |
| |
| #### Event sequences {#event-sequences} |
| |
| When writing tests, it is useful to expect events to occur in some order. |
| Event Sequences allow writers to verify ordering of events: |
| |
| ```rust |
| // This test expects the following events to occur: |
| // 1. the two trigger components stop in any order |
| // 2. the parent component stops |
| // 3. the two trigger components are destroyed in any order |
| // 4. the parent component is destroyed |
| let expectation = EventSequence::new() |
| .all_of( |
| vec![ |
| EventMatcher::ok().r#type(Stopped::TYPE).moniker("./coll:parent:1/trigger_a:0"), |
| EventMatcher::ok().r#type(Stopped::TYPE).moniker("./coll:parent:1/trigger_b:0"), |
| ], |
| Ordering::Unordered, |
| ) |
| .then(EventMatcher::ok().r#type(Stopped::TYPE).moniker("./coll:parent:1")) |
| .all_of( |
| vec![ |
| EventMatcher::ok().r#type(Destroyed::TYPE).moniker("./coll:parent:1/trigger_a:0"), |
| EventMatcher::ok().r#type(Destroyed::TYPE).moniker("./coll:parent:1/trigger_b:0"), |
| ], |
| Ordering::Unordered, |
| ) |
| .then(EventMatcher::ok().r#type(Destroyed::TYPE).moniker("./coll:parent:1")) |
| .subscribe_and_expect(&mut event_source) |
| .await |
| .unwrap(); |
| |
| // Start the component tree |
| event_source.start_component_tree().await; |
| |
| // Wait for the event sequence to occur |
| expectation.await.unwrap(); |
| ``` |
| |
| #### Capability injection {#capability-injection} |
| |
| Several tests need to mock out capabilities that a component connects to in the test. |
| Sometimes, tests may wish to communicate with components directly. 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"); |
| } |
| } |
| } |
| ``` |
| |
| Injectors can automatically install themselves on `CapabilityRouted` events. |
| |
| ```rust |
| let echo_capability: Arc<EchoCapability> = EchoCapability::new(); |
| |
| // Inject the Echo capability when /foo:0 successfully connects to the Echo service |
| echo_capability.inject(&event_source, EventMatcher::ok().moniker("/foo:0")).await; |
| |
| 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"); |
| } |
| } |
| } |
| ``` |
| |
| Interposers can automatically install themselves on `CapabilityRouted` events. |
| |
| ```rust |
| let interposer = EchoInterposer::new(); |
| |
| // Interpose the Echo capability when any component successfully connects to the Echo service |
| echo_interposer.interpose(&event_source, EventMatcher::ok()).await; |
| |
| 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 = EventLog::record_events(&mut event_source, vec![Started::NAME]).await?; |
| event_source.start_component_tree().await; |
| |
| // Wait for the component to be destroyed |
| let event = EventMatcher::ok().expect_match::<Destroyed>(&mut event_stream).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: Recording of events will continue until the `EventLog` object goes out |
| of scope. |
| |
| ## Debug Mode {#debug-mode} |
| |
| Both `OpaqueTest` and `EventSource` rely on component manager’s |
| debug mode. |
| |
| Component manager's startup behavior can be configued via the `--config` flag |
| which accepts a path to a JSON configuration file. |
| |
| To start component manager in debug mode, use the `--config` flag to pass in a |
| JSON configuration file that has `debug` set to `true`. This is exactly what |
| `OpaqueTest::default` does. |
| |
| 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 |