| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| anyhow::{Context, Error}, |
| component_events::{events::*, matcher::*, sequence::*}, |
| diagnostics_data::{Data, Logs}, |
| diagnostics_reader::ArchiveReader, |
| fuchsia_async as fasync, |
| fuchsia_component_test::{Capability, ChildOptions, ChildRef, RealmBuilder, Ref, Route}, |
| fuchsia_zircon::DurationNum, |
| regex::Regex, |
| std::future::Future, |
| }; |
| |
| /// Represents a component under test. The `name` is the test-local name assigned to the component, |
| /// whereas the path is the relative path to its component manifest (ex: "#meta/client.cm"). |
| pub trait Component { |
| fn get_name(&self) -> String; |
| fn get_path(&self) -> String; |
| fn get_regex_matcher(&self) -> String; |
| fn matches_log(&self, raw_log: &Data<Logs>) -> bool; |
| } |
| |
| /// Represents the client component under test. |
| #[derive(Clone)] |
| pub struct Client<'a> { |
| name: String, |
| path: &'a str, |
| regex: Regex, |
| matcher: String, |
| } |
| |
| impl<'a> Client<'a> { |
| /// Create a new instance of a client component, passing in two string references: the name of |
| /// the test that created this component, and the path to the `*.cm` file describing this |
| /// component in the current package: |
| /// |
| /// Client::new("my_test_case", "#meta/some_client.cm"); |
| /// |
| pub fn new(test_prefix: &str, path: &'a str) -> Self { |
| let name = format!("{}_client", test_prefix); |
| let matcher = format!("{}$", name); |
| Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher } |
| } |
| } |
| |
| impl<'a> Component for Client<'a> { |
| fn get_name(&self) -> String { |
| self.name.clone() |
| } |
| fn get_path(&self) -> String { |
| self.path.to_owned() |
| } |
| fn get_regex_matcher(&self) -> String { |
| self.matcher.clone() |
| } |
| fn matches_log(&self, raw_log: &Data<Logs>) -> bool { |
| self.regex.is_match(raw_log.moniker.as_str()) |
| } |
| } |
| |
| /// Represents a proxy component under test. |
| #[derive(Clone)] |
| pub struct Proxy<'a> { |
| name: String, |
| path: &'a str, |
| regex: Regex, |
| matcher: String, |
| } |
| |
| impl<'a> Proxy<'a> { |
| /// Create a new instance of a proxy component, passing in two string references: the name of |
| /// the test that created this component, and the path to the `*.cm` file describing this |
| /// component in the current package: |
| /// |
| /// Proxy::new("my_test_case", "#meta/some_proxy.cm"); |
| /// |
| pub fn new(test_prefix: &str, path: &'a str) -> Self { |
| let name = format!("{}_proxy", test_prefix); |
| let matcher = format!("{}$", name); |
| Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher } |
| } |
| } |
| |
| impl<'a> Component for Proxy<'a> { |
| fn get_name(&self) -> String { |
| self.name.clone() |
| } |
| fn get_path(&self) -> String { |
| self.path.to_string() |
| } |
| fn get_regex_matcher(&self) -> String { |
| self.matcher.clone() |
| } |
| fn matches_log(&self, raw_log: &Data<Logs>) -> bool { |
| self.regex.is_match(raw_log.moniker.as_str()) |
| } |
| } |
| |
| /// Represents a server component under test. |
| #[derive(Clone)] |
| pub struct Server<'a> { |
| name: String, |
| path: &'a str, |
| regex: Regex, |
| matcher: String, |
| } |
| |
| impl<'a> Server<'a> { |
| /// Create a new instance of a server component, passing in two string references: the name of |
| /// the test that created this component, and the path to the `*.cm` file describing this |
| /// component in the current package: |
| /// |
| /// Server::new("my_test_case", "#meta/some_server.cm"); |
| /// |
| pub fn new(test_prefix: &str, path: &'a str) -> Self { |
| let name = format!("{}_server", test_prefix); |
| let matcher = format!("{}$", name); |
| Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher } |
| } |
| } |
| |
| impl<'a> Component for Server<'a> { |
| fn get_name(&self) -> String { |
| self.name.clone() |
| } |
| fn get_path(&self) -> String { |
| self.path.to_string() |
| } |
| fn get_regex_matcher(&self) -> String { |
| self.matcher.clone() |
| } |
| fn matches_log(&self, raw_log: &Data<Logs>) -> bool { |
| self.regex.is_match(raw_log.moniker.as_str()) |
| } |
| } |
| |
| /// This framework supports three kinds of tests: |
| /// - 3 components: client <-> proxy <-> server |
| /// - 2 components: client <-> server |
| /// - 1 component: standalone client |
| pub enum TestKind<'a> { |
| StandaloneComponent { client: &'a Client<'a> }, |
| ClientAndServer { client: &'a Client<'a>, server: &'a Server<'a> }, |
| ClientProxyAndServer { client: &'a Client<'a>, proxy: &'a Proxy<'a>, server: &'a Server<'a> }, |
| } |
| |
| /// Runs a test of the specified protocol, using one of the `TestKind`s enumerated above. The |
| /// `input_setter` closure may be used to pass structured config values to the client, which is how |
| /// the test is meant to receive its inputs. The `logs_reader` closure provides the raw logs |
| /// collected from all child processes under test, allowing test authors to assert against the |
| /// logged values. Note that these are raw logs - most users will want to process the logs into |
| /// string form, which can be accomplished by passing the raw log vector to the `logs_to_str` helper |
| /// function. |
| pub async fn run_test<'a, Fut, FutLogsReader>( |
| protocol_name: &str, |
| test_kind: TestKind<'a>, |
| input_setter: impl FnOnce(RealmBuilder, ChildRef) -> Fut, |
| logs_reader: impl Fn(ArchiveReader) -> FutLogsReader, |
| ) -> Result<(), Error> |
| where |
| Fut: Future<Output = Result<(RealmBuilder, ChildRef), Error>> + 'a, |
| FutLogsReader: Future<Output = ()> + 'a, |
| { |
| // Subscribe to started events for child components. |
| let event_stream = EventStream::open().await.unwrap(); |
| |
| // Create a new empty test realm. |
| let builder = RealmBuilder::new().await?; |
| |
| // Add the client to the realm, and make the client eager so that it starts automatically. |
| let (client_name, client_path, client_regex_matcher) = match test_kind { |
| TestKind::StandaloneComponent { client, .. } |
| | TestKind::ClientAndServer { client, .. } |
| | TestKind::ClientProxyAndServer { client, .. } => { |
| (client.get_name(), client.get_path(), client.get_regex_matcher()) |
| } |
| }; |
| let client = |
| builder.add_child(client_name.clone(), client_path, ChildOptions::new().eager()).await?; |
| |
| // Apply the supplied configs to the client to allow the test runner to pass "arguments" in. |
| let (builder, client) = input_setter(builder, client).await?; |
| |
| // Route the LogSink to all children so that all realm members are able to send us logs. |
| let mut log_sink_route = Route::new() |
| .capability(Capability::protocol_by_name("fuchsia.logger.LogSink")) |
| .from(Ref::parent()) |
| .to(&client); |
| |
| // Take note of child names - we'll use these to setup logging filters further down the line. |
| let mut child_names = vec![client_name]; |
| |
| // Create event listeners waiting on client component startup. |
| let mut start_event_matchers = |
| vec![EventMatcher::ok().moniker_regex(client_regex_matcher.clone())]; |
| |
| // Setup the test in each of the three supported configurations. |
| if !std::matches!(test_kind, TestKind::StandaloneComponent { .. }) { |
| // We have a server - add it to the realm. |
| let (server_name, server_path, server_regex_matcher) = match test_kind { |
| TestKind::ClientAndServer { server, .. } |
| | TestKind::ClientProxyAndServer { server, .. } => { |
| (server.get_name(), server.get_path(), server.get_regex_matcher()) |
| } |
| _ => panic!("unreachable!"), |
| }; |
| let server = |
| builder.add_child(server_name.clone(), server_path, ChildOptions::new()).await?; |
| child_names.push(server_name); |
| |
| // Setup logging. |
| log_sink_route = log_sink_route.to(&server); |
| |
| // Add event matchers waiting on server component startup/shutdown. |
| start_event_matchers.push(EventMatcher::ok().moniker_regex(server_regex_matcher)); |
| |
| if std::matches!(test_kind, TestKind::ClientAndServer { .. }) { |
| // If there is no proxy, connect the client to the server directly. |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::protocol_by_name(protocol_name)) |
| .from(&server) |
| .to(&client), |
| ) |
| .await?; |
| } else { |
| // We have a proxy - add it to the realm. |
| let (proxy_name, proxy_path, proxy_regex_matcher) = match test_kind { |
| TestKind::ClientProxyAndServer { proxy, .. } => { |
| (proxy.get_name(), proxy.get_path(), proxy.get_regex_matcher()) |
| } |
| _ => panic!("unreachable!"), |
| }; |
| let proxy = |
| builder.add_child(proxy_name.clone(), proxy_path, ChildOptions::new()).await?; |
| child_names.insert(1, proxy_name); |
| |
| // Setup logging. |
| log_sink_route = log_sink_route.to(&proxy); |
| |
| // Add event matchers waiting on server component startup/shutdown. The proxy watcher needs to be |
| // inserted prior to the server watcher, as the startup sequence for 3 process tests is |
| // client then proxy then server. |
| start_event_matchers.insert(1, EventMatcher::ok().moniker_regex(proxy_regex_matcher)); |
| |
| // Route the capabilities from the server to the proxy. |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::protocol_by_name(protocol_name)) |
| .from(&server) |
| .to(&proxy), |
| ) |
| .await?; |
| |
| // Route the capabilities from the proxy to the client. |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::protocol_by_name(protocol_name)) |
| .from(&proxy) |
| .to(&client), |
| ) |
| .await?; |
| } |
| } |
| |
| // Route the LogSink to all children so that all realm members are able to send us logs. |
| builder.add_route(log_sink_route.to_owned()).await?; |
| |
| // Create the realm instance. |
| let realm_instance = builder.build().await?; |
| |
| // Verify that we get expected start and stop (clean) events. |
| EventSequence::new() |
| .has_subset(start_event_matchers, Ordering::Unordered) |
| .has_subset( |
| vec![EventMatcher::ok() |
| .stop(Some(ExitStatusMatcher::Clean)) |
| .moniker_regex(client_regex_matcher)], |
| Ordering::Unordered, |
| ) |
| .expect(event_stream) |
| .await |
| .unwrap(); |
| |
| // Setup the archivist link, but don't read the logs yet! |
| let mut archivist_reader = ArchiveReader::new(); |
| child_names.iter().for_each(|child_name| { |
| let moniker = format!("realm_builder:{}/{}", realm_instance.root.child_name(), child_name); |
| archivist_reader.select_all_for_moniker(moniker.as_str()); |
| }); |
| |
| // Clean up the realm instance, and close all open processes. |
| realm_instance.destroy().await?; |
| |
| // Read all of the logs out to the test, and exit. |
| logs_reader(archivist_reader); |
| Ok(()) |
| } |
| |
| /// Takes a vector of raw logs, and returns an iterator over the string representations of said |
| /// logs. The second argument allows for optional filtering by component. For example, if one only |
| /// wants to see server logs, the invocation may look like: |
| /// |
| /// logs_to_str(&raw_logs, Some(vec![&server_component_definition])); |
| /// |
| pub fn logs_to_str<'a>( |
| raw_logs: &'a Vec<Data<Logs>>, |
| maybe_filter_by_process: Option<Vec<&'a dyn Component>>, |
| ) -> impl Iterator<Item = &'a str> + 'a { |
| logs_to_str_filtered(raw_logs, maybe_filter_by_process, |_raw_log| true) |
| } |
| |
| /// Same as |logs_to_str|, except an additional filtering function may be used to trim arbitrary |
| /// logs. This is particularly useful if one or more languages produces logs that we don't want to |
| /// include in the final, common output to be compared across language implementations. |
| pub fn logs_to_str_filtered<'a>( |
| raw_logs: &'a Vec<Data<Logs>>, |
| maybe_filter_by_process: Option<Vec<&'a dyn Component>>, |
| filter_by_log: impl FnMut(&&Data<Logs>) -> bool + 'a, |
| ) -> impl Iterator<Item = &'a str> + 'a { |
| raw_logs |
| .iter() |
| .filter(move |raw_log| match maybe_filter_by_process { |
| Some(ref process_list) => { |
| for process in process_list.iter() { |
| if process.matches_log(*raw_log) { |
| return true; |
| } |
| } |
| return false; |
| } |
| None => true, |
| }) |
| .filter(filter_by_log) |
| .map(|raw_log| { |
| raw_log.payload_message().expect("payload not found").properties[0] |
| .string() |
| .expect("message is not string") |
| }) |
| } |
| |
| /// Takes the logs for a single component and compares them to the appropriate golden file. The path |
| /// of the file is expected to match the template `/pkg/data/goldens/{COMPONENT_NAME}.log.golden`. |
| /// The {COMPONENT_NAME} is itself generally a template of the form `{TEST_NAME}_{COMPONENT_ROLE}`. |
| /// Thus, for the three-component `test_foo_bar`, we expect the following golden logs to exist: |
| /// |
| /// /pkg/data/goldens/test_foo_bar_client.log.golden |
| /// /pkg/data/goldens/test_foo_bar_proxy.log.golden |
| /// /pkg/data/goldens/test_foo_bar_server.log.golden |
| /// |
| pub async fn assert_logs_eq_to_golden<'a>(log_reader: &'a ArchiveReader, comp: &'a dyn Component) { |
| assert_filtered_logs_eq_to_golden(&log_reader, comp, |_raw_log| true).await; |
| } |
| |
| /// Same as |assert_logs_eq_to_golden|, except an additional filtering function may be used to trim |
| /// arbitrary logs. This is particularly useful if one or more languages produces logs that we don't |
| /// want to include in the final, common output to be compared across language implementations. |
| pub async fn assert_filtered_logs_eq_to_golden<'a>( |
| log_reader: &'a ArchiveReader, |
| comp: &'a dyn Component, |
| filter_by_log: impl FnMut(&&Data<Logs>) -> bool + 'a + Copy, |
| ) { |
| // Extract the golden log data. |
| let golden_path = format!("/pkg/data/goldens/{}.log.golden", comp.get_name()); |
| let golden_file = std::fs::read_to_string(golden_path.clone()) |
| .with_context(|| format!("Failed to load {golden_path}")) |
| .unwrap(); |
| let golden_logs = golden_file.as_str().trim(); |
| |
| const MAX_ATTEMPTS: usize = 10; |
| let mut attempts = 0; |
| while attempts < MAX_ATTEMPTS { |
| attempts += 1; |
| let raw_logs = log_reader.snapshot::<Logs>().await.expect("can read from the accessor"); |
| |
| // Compare it to the actual components actual logs, asserting if there is a mismatch. |
| let logs = logs_to_str_filtered(&raw_logs, Some(vec![comp]), filter_by_log) |
| .collect::<Vec<&str>>() |
| .join("\n"); |
| if logs == golden_logs.trim() { |
| break; |
| } else if attempts == MAX_ATTEMPTS { |
| print!( |
| " |
| |
| Logs golden mismatch in '{}' ({}) |
| Please copy the output between the '===' bounds into the golden file at {} in the fuchsia.git tree |
| ==================================================================================================== |
| {} |
| ==================================================================================================== |
| |
| |
| ", |
| comp.get_name(), |
| comp.get_path(), |
| golden_path, |
| logs |
| ); |
| assert_eq!(logs, golden_logs) |
| } |
| fasync::Timer::new(fasync::Time::after(500.millis())).await; |
| } |
| } |