| // Copyright 2021 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. |
| |
| mod connector; |
| mod suite_definition; |
| |
| use crate::connector::RunConnector; |
| use crate::suite_definition::TestParamsOptions; |
| use anyhow::{anyhow, format_err, Context, Result}; |
| use async_trait::async_trait; |
| use either::Either; |
| use errors::{ffx_bail, ffx_bail_with_code, ffx_error, ffx_error_with_code, FfxError}; |
| use ffx_test_args::{ |
| EarlyBootProfileCommand, ListCommand, RunCommand, TestCommand, TestSubCommand, |
| }; |
| use fho::{FfxMain, FfxTool, SimpleWriter}; |
| use fidl::endpoints::create_proxy; |
| use futures::FutureExt; |
| use lazy_static::lazy_static; |
| use run_test_suite_lib::output::Reporter; |
| use signal_hook::consts::signal::{SIGINT, SIGTERM}; |
| use signal_hook::iterator::Signals; |
| use std::fmt::Debug; |
| use std::io::{stdout, Write}; |
| use { |
| fidl_fuchsia_developer_remotecontrol as fremotecontrol, |
| fidl_fuchsia_test_manager as ftest_manager, |
| }; |
| |
| lazy_static! { |
| /// Error code returned if connecting to Test Manager fails. |
| pub static ref SETUP_FAILED_CODE: i32 = -fidl::Status::UNAVAILABLE.into_raw(); |
| /// Error code returned if tests time out. |
| pub static ref TIMED_OUT_CODE: i32 = -fidl::Status::TIMED_OUT.into_raw(); |
| } |
| |
| /// Max number of test suites to run using a single RunBuilder connection. |
| /// Since we need to make n SuiteController channels when running tests on a |
| /// single RunBuilder channel, this max limits the number of channels that need |
| /// to be opened before any tests are run, allowing tests to start running faster. |
| /// It also limits the maximum resources that overnet needs to handle at once. |
| /// This isn't set to 1 as (1 RunBuilder connection) = (1 set of debug data). Since |
| /// pulling debug data off device is expensive we also want to limit the number of |
| /// times this occurs. |
| const SUITE_BATCH_SIZE: usize = 100; |
| |
| #[derive(FfxTool)] |
| pub struct TestTool { |
| #[command] |
| cmd: TestCommand, |
| rcs: fho::Deferred<fremotecontrol::RemoteControlProxy>, |
| } |
| |
| fho::embedded_plugin!(TestTool); |
| |
| #[async_trait(?Send)] |
| impl FfxMain for TestTool { |
| type Writer = SimpleWriter; |
| |
| // TODO(https://fxbug.dev/42078544): use Writer when it becomes possible. |
| async fn main(self, _writer: Self::Writer) -> fho::Result<()> { |
| let writer = Box::new(stdout()); |
| let remote_control = |
| self.rcs.await.map_err(|e| ffx_error_with_code!(*SETUP_FAILED_CODE, "{:?}", e))?; |
| match self.cmd.subcommand { |
| TestSubCommand::Run(run) => run_test(remote_control, writer, run).await?, |
| TestSubCommand::List(list) => { |
| get_tests(&remote_control, writer, list).await?; |
| } |
| TestSubCommand::EarlyBootProfile(cmd) => { |
| early_boot_profile(remote_control, writer, cmd).await?; |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| struct Experiment { |
| name: &'static str, |
| enabled: bool, |
| } |
| |
| struct Experiments { |
| json_input: Experiment, |
| parallel_execution: Experiment, |
| } |
| |
| impl Experiments { |
| async fn get_experiment(experiment_name: &'static str) -> Experiment { |
| Experiment { |
| name: experiment_name, |
| enabled: match ffx_config::get(experiment_name).await { |
| Ok(enabled) => enabled, |
| Err(_) => false, |
| }, |
| } |
| } |
| |
| async fn from_env() -> Self { |
| Self { |
| json_input: Self::get_experiment("test.experimental_json_input").await, |
| parallel_execution: Self::get_experiment("test.enable_experimental_parallel_execution") |
| .await, |
| } |
| } |
| } |
| |
| async fn early_boot_profile<W: 'static + Write + Send + Sync>( |
| remote_control: fremotecontrol::RemoteControlProxy, |
| mut writer: W, |
| cmd: EarlyBootProfileCommand, |
| ) -> Result<()> { |
| let early_boot_profile_proxy = testing_lib::connect_to_early_boot_profile(&remote_control) |
| .await |
| .map_err(|e| ffx_error_with_code!(*SETUP_FAILED_CODE, "{:?}", e))?; |
| |
| let (client, iterator) = fidl::endpoints::create_endpoints(); |
| early_boot_profile_proxy |
| .register_watcher(iterator) |
| .map_err(|e| ffx_error_with_code!(*SETUP_FAILED_CODE, "{:?}", e))?; |
| |
| let reporter = run_test_suite_lib::output::DirectoryReporter::new( |
| cmd.output_directory, |
| run_test_suite_lib::output::SchemaVersion::V1, |
| )?; |
| |
| match reporter.new_directory_artifact( |
| &run_test_suite_lib::output::EntityId::TestRun, |
| &run_test_suite_lib::output::DirectoryArtifactType::Debug, |
| None, |
| ) { |
| Ok(o) => run_test_suite_lib::copy_debug_data(client.into_proxy()?, o).await, |
| Err(e) => { |
| writeln!(writer, "Cannot create output directory: {}", e)?; |
| return Err(e.into()); |
| } |
| }; |
| // save summary |
| reporter.entity_finished(&run_test_suite_lib::output::EntityId::TestRun)?; |
| |
| Ok(()) |
| } |
| |
| async fn run_test<W: 'static + Write + Send + Sync>( |
| remote_control: fremotecontrol::RemoteControlProxy, |
| mut writer: W, |
| cmd: RunCommand, |
| ) -> Result<()> { |
| let experiments = Experiments::from_env().await; |
| |
| let min_log_severity = cmd.min_severity_logs.clone(); |
| let hermetic_test = cmd.realm.is_none(); |
| |
| let output_directory = match (cmd.disable_output_directory, &cmd.output_directory) { |
| (true, maybe_dir) => { |
| writeln!( |
| writer, |
| "WARN: --disable-output-directory is now a no-op and will soon be \ |
| removed, please remove it from your invocation." |
| )?; |
| maybe_dir.clone().map(Into::into) |
| } |
| (false, Some(directory)) => Some(directory.clone().into()), // an override directory is specified. |
| (false, None) => None, |
| }; |
| let output_directory_options = output_directory |
| .map(|root_path| run_test_suite_lib::DirectoryReporterOptions { root_path }); |
| let reporter = |
| run_test_suite_lib::create_reporter(cmd.filter_ansi, output_directory_options, writer)?; |
| |
| let run_params = run_test_suite_lib::RunParams { |
| timeout_behavior: match cmd.continue_on_timeout { |
| false => run_test_suite_lib::TimeoutBehavior::TerminateRemaining, |
| true => run_test_suite_lib::TimeoutBehavior::Continue, |
| }, |
| timeout_grace_seconds: ffx_config::get::<u64, _>("test.timeout_grace_seconds").await? |
| as u32, |
| stop_after_failures: match cmd.stop_after_failures.map(std::num::NonZeroU32::new) { |
| None => None, |
| Some(None) => ffx_bail!("--stop-after-failures should be greater than zero."), |
| Some(Some(stop_after)) => Some(stop_after), |
| }, |
| experimental_parallel_execution: match ( |
| cmd.experimental_parallel_execution, |
| experiments.parallel_execution.enabled, |
| ) { |
| (None, _) => None, |
| (Some(max_parallel_suites), true) => Some(max_parallel_suites), |
| (_, false) => ffx_bail!( |
| "Parallel test suite execution is experimental and is subject to breaking changes. \ |
| To enable parallel test suite execution, run: \n \ |
| 'ffx config set {} true'", |
| experiments.parallel_execution.name |
| ), |
| }, |
| accumulate_debug_data: false, // ffx never accumulates. |
| log_protocol: None, |
| min_severity_logs: min_log_severity, |
| show_full_moniker: cmd.show_full_moniker_in_logs, |
| }; |
| |
| let test_definitions = |
| test_params_from_args(&remote_control, cmd, experiments.json_input.enabled).await?; |
| |
| let (cancel_sender, cancel_receiver) = futures::channel::oneshot::channel::<()>(); |
| let mut signals = Signals::new(&[SIGINT, SIGTERM]).unwrap(); |
| // signals.forever() is blocking, so we need to spawn a thread rather than use async. |
| let _signal_handle_thread = std::thread::spawn(move || { |
| if let Some(signal) = signals.forever().next() { |
| match signal { |
| SIGINT | SIGTERM => { |
| let _ = cancel_sender.send(()); |
| } |
| _ => unreachable!(), |
| } |
| } |
| }); |
| |
| let start_time = std::time::Instant::now(); |
| let outcome = run_test_suite_lib::run_tests_and_get_outcome( |
| RunConnector::new(remote_control, SUITE_BATCH_SIZE), |
| test_definitions, |
| run_params, |
| reporter, |
| cancel_receiver.map(|_| ()), |
| ) |
| .await; |
| let show_realm_warning = outcome == run_test_suite_lib::Outcome::Timedout |
| || outcome == run_test_suite_lib::Outcome::Failed |
| || outcome == run_test_suite_lib::Outcome::DidNotFinish; |
| tracing::info!("ffx test duration: {:?}", start_time.elapsed().as_secs_f32()); |
| if hermetic_test && show_realm_warning { |
| println!( |
| "The test was executed in the hermetic realm. If your test depends on system \ |
| capabilities, pass in correct realm. See https://fuchsia.dev/go/components/non-hermetic-tests" |
| ); |
| } |
| match outcome { |
| run_test_suite_lib::Outcome::Passed => Ok(()), |
| run_test_suite_lib::Outcome::Timedout => { |
| ffx_bail_with_code!(*TIMED_OUT_CODE, "Tests timed out.",) |
| } |
| run_test_suite_lib::Outcome::Failed | run_test_suite_lib::Outcome::DidNotFinish => { |
| ffx_bail!("Tests failed.") |
| } |
| run_test_suite_lib::Outcome::Cancelled => ffx_bail!("Tests cancelled."), |
| run_test_suite_lib::Outcome::Inconclusive => ffx_bail!("Inconclusive test result."), |
| run_test_suite_lib::Outcome::Error { origin } => match &*origin { |
| run_test_suite_lib::RunTestSuiteError::Connection(conn_err) => { |
| ffx_bail_with_code!(*SETUP_FAILED_CODE, "{:?}", conn_err) |
| } |
| run_test_suite_lib::RunTestSuiteError::Launch(launch_err) => match launch_err { |
| ftest_manager::LaunchError::ResourceUnavailable => { |
| ffx_bail!("There were insufficient resources to launch the test.") |
| } |
| ftest_manager::LaunchError::InstanceCannotResolve => { |
| ffx_bail!("Cannot resolve test URL.") |
| } |
| ftest_manager::LaunchError::InvalidArgs => { |
| ffx_bail!("One or more invalid arguments passed.") |
| } |
| ftest_manager::LaunchError::FailedToConnectToTestSuite => { |
| ffx_bail!("Failed to connect to test suite.") |
| } |
| ftest_manager::LaunchError::CaseEnumeration => { |
| ffx_bail!("Failed to enumerate tests.") |
| } |
| ftest_manager::LaunchError::InternalError => { |
| ffx_bail!("An internal error occurred. Please check logs and report bug.") |
| } |
| ftest_manager::LaunchError::NoMatchingCases => { |
| ffx_bail!("No test cases match specified test filters.") |
| } |
| ftest_manager::LaunchError::InvalidManifest => { |
| ffx_bail!("Test manifest is invalid.") |
| } |
| other => ffx_bail!("Launch error: {:?}.", other), |
| }, |
| other if other.is_internal_error() => { |
| Err(anyhow!("There was an internal error running tests: {:?}", other)) |
| } |
| other => ffx_bail!("There was an error running tests: {:?}", other), |
| }, |
| } |
| } |
| |
| /// Generate TestParams from |cmd|. |
| /// |stdin_handle_fn| is a function that generates a handle to stdin and is a parameter to enable |
| /// testing. |
| async fn test_params_from_args( |
| remote_control: &fremotecontrol::RemoteControlProxy, |
| cmd: RunCommand, |
| json_input_experiment_enabled: bool, |
| ) -> Result<impl ExactSizeIterator<Item = run_test_suite_lib::TestParams> + Debug, FfxError> { |
| let lifecycle_controller = ffx_component::rcs::connect_to_lifecycle_controller(&remote_control) |
| .await |
| .map_err(|e| ffx_error!("Parsing realm: Cannot connect to lifecycle controller: {}", e))?; |
| let realm_query = ffx_component::rcs::connect_to_realm_query(&remote_control) |
| .await |
| .map_err(|e| ffx_error!("Parsing realm: Cannot connect to realm query: {}", e))?; |
| match &cmd.test_file { |
| Some(_) if !json_input_experiment_enabled => { |
| return Err(ffx_error!( |
| "The --test-file option is experimental, and the input format is \ |
| subject to breaking changes. To enable using --test-file, run \ |
| 'ffx config set test.experimental_json_input true'" |
| )) |
| } |
| Some(filename) => { |
| if !cmd.test_args.is_empty() { |
| return Err(ffx_error!("Tests may not be specified in both args and by file")); |
| } else { |
| let file = std::fs::File::open(filename) |
| .map_err(|e| ffx_error!("Failed to open file {}: {:?}", filename, e))?; |
| suite_definition::test_params_from_reader( |
| file, |
| &lifecycle_controller, |
| &realm_query, |
| TestParamsOptions { ignore_test_without_known_execution: false }, |
| ) |
| .await |
| .map_err(|e| ffx_error!("Failed to read test definitions: {:?}", e)) |
| } |
| } |
| .map(|file_params| Either::Left(file_params.into_iter())), |
| None => { |
| let mut test_args_iter = cmd.test_args.iter(); |
| let (test_url, test_args) = match test_args_iter.next() { |
| None => return Err(ffx_error!("No tests specified!")), |
| Some(test_url) => { |
| (test_url.clone(), test_args_iter.map(String::clone).collect::<Vec<_>>()) |
| } |
| }; |
| |
| let mut provided_realm = None; |
| if let Some(realm_str) = &cmd.realm { |
| provided_realm = Some( |
| run_test_suite_lib::parse_provided_realm( |
| &lifecycle_controller, |
| &realm_query, |
| &realm_str, |
| ) |
| .await |
| .map_err(|e| ffx_error!("Error parsing realm '{}': {}", realm_str, e))?, |
| ); |
| } |
| |
| let test_params = run_test_suite_lib::TestParams { |
| test_url, |
| realm: provided_realm.into(), |
| timeout_seconds: cmd.timeout.and_then(std::num::NonZeroU32::new), |
| test_filters: if cmd.test_filter.len() == 0 { None } else { Some(cmd.test_filter) }, |
| max_severity_logs: cmd.max_severity_logs, |
| min_severity_logs: cmd.min_severity_logs, |
| also_run_disabled_tests: cmd.run_disabled, |
| parallel: cmd.parallel, |
| test_args, |
| tags: vec![], |
| break_on_failure: cmd.break_on_failure, |
| }; |
| |
| let count = cmd.count.unwrap_or(1); |
| let count = std::num::NonZeroU32::new(count) |
| .ok_or_else(|| ffx_error!("--count should be greater than zero."))?; |
| let repeated = (0..count.get()).map(move |_: u32| test_params.clone()); |
| Ok(repeated) |
| } |
| .map(Either::Right), |
| } |
| } |
| |
| async fn get_tests<W: Write>( |
| remote_control: &fremotecontrol::RemoteControlProxy, |
| mut write: W, |
| cmd: ListCommand, |
| ) -> Result<()> { |
| let query_proxy = testing_lib::connect_to_query(&remote_control) |
| .await |
| .map_err(|e| ffx_error_with_code!(*SETUP_FAILED_CODE, "{:?}", e))?; |
| let writer = &mut write; |
| let (iterator_proxy, iterator) = create_proxy().unwrap(); |
| |
| tracing::info!("launching test suite {}", cmd.test_url); |
| |
| let mut provided_realm = None; |
| if let Some(realm_str) = &cmd.realm { |
| let lifecycle_controller = |
| ffx_component::rcs::connect_to_lifecycle_controller(&remote_control).await.map_err( |
| |e| ffx_error!("Parsing realm: Cannot connect to lifecycle controller: {}", e), |
| )?; |
| |
| let realm_query = ffx_component::rcs::connect_to_realm_query(&remote_control) |
| .await |
| .map_err(|e| ffx_error!("Parsing realm: Cannot connect to realm query: {}", e))?; |
| |
| provided_realm = Some( |
| run_test_suite_lib::parse_provided_realm( |
| &lifecycle_controller, |
| &realm_query, |
| &realm_str, |
| ) |
| .await |
| .map_err(|e| ffx_error!("Error parsing realm '{}': {}", realm_str, e))?, |
| ); |
| } |
| |
| let fut_response = match provided_realm { |
| Some(realm) => { |
| let offers = realm.offers(); |
| query_proxy.enumerate_in_realm( |
| &cmd.test_url, |
| realm |
| .get_realm_client() |
| .map_err(|e| ffx_error!("Cannot connect to realm client: {}", e))?, |
| offers.as_slice(), |
| realm.collection(), |
| iterator, |
| ) |
| } |
| None => query_proxy.enumerate(&cmd.test_url, iterator), |
| }; |
| |
| fut_response |
| .await |
| .context("enumeration failed")? |
| .map_err(|e| format_err!("error launching test: {:?}", e))?; |
| |
| loop { |
| let cases = iterator_proxy.get_next().await?; |
| if cases.is_empty() { |
| return Ok(()); |
| } |
| for case in cases { |
| match case.name { |
| Some(n) => writeln!(writer, "{}", n)?, |
| None => writeln!(writer, "<No name>")?, |
| } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use fidl::endpoints::{create_proxy_and_stream, ProtocolMarker, RequestStream, ServerEnd}; |
| use fidl_fuchsia_sys2 as fsys; |
| use ftest_manager::{ |
| DebugData, DebugDataIteratorMarker, EarlyBootProfileMarker, EarlyBootProfileRequestStream, |
| }; |
| use futures::prelude::*; |
| use lazy_static::lazy_static; |
| use std::io::Read; |
| use std::num::NonZeroU32; |
| use std::sync::Arc; |
| use test_list::TestTag; |
| |
| const VALID_INPUT_FILENAME: &str = "valid_defs.json"; |
| const INVALID_INPUT_FILENAME: &str = "invalid_defs.json"; |
| |
| lazy_static! { |
| static ref VALID_INPUT_FORMAT: String = serde_json::to_string(&serde_json::json!({ |
| "schema_id": "experimental", |
| "data": [ |
| { |
| "name": "{}-test-1", |
| "labels": ["{}-label"], |
| "execution": { |
| "type": "fuchsia_component", |
| "component_url": "{}-test-url-1", |
| }, |
| "tags": [], |
| }, |
| { |
| "name": "{}-test-2", |
| "labels": ["{}-label"], |
| "execution": { |
| "type": "fuchsia_component", |
| "component_url": "{}-test-url-2", |
| "timeout_seconds": 60, |
| }, |
| "tags": [], |
| }, |
| { |
| "name": "{}-test-3", |
| "labels": ["{}-label"], |
| "execution": { |
| "type": "fuchsia_component", |
| "component_url": "{}-test-url-3", |
| "test_args": ["--flag"], |
| "test_filters": ["Unit"], |
| "also_run_disabled_tests": true, |
| "parallel": 4, |
| "max_severity_logs": "INFO", |
| }, |
| "tags": [{ |
| "key": "hermetic", |
| "value": "true", |
| }], |
| } |
| ]})) |
| .expect("serialize json"); |
| static ref VALID_FILE_INPUT: Vec<u8> = |
| VALID_INPUT_FORMAT.replace("{}", "file").into_bytes(); |
| static ref INVALID_INPUT: Vec<u8> = vec![1u8; 64]; |
| } |
| |
| struct FakeRemoteControllerProvider { |
| controller: Arc<fremotecontrol::RemoteControlProxy>, |
| _task: fuchsia_async::Task<()>, |
| } |
| |
| impl FakeRemoteControllerProvider { |
| fn new() -> FakeRemoteControllerProvider { |
| let (remote_control, mut stream) = |
| create_proxy_and_stream::<fremotecontrol::RemoteControlMarker>().unwrap(); |
| let _task = fuchsia_async::Task::spawn(async move { |
| while let Some(request) = stream.try_next().await.unwrap() { |
| // store channels so that they do not die. |
| let mut server_channels = vec![]; |
| match request { |
| fremotecontrol::RemoteControlRequest::OpenCapability { |
| moniker, |
| capability_set, |
| capability_name, |
| server_channel, |
| flags: _, |
| responder, |
| } => { |
| assert_eq!(moniker, "core/remote-control"); |
| assert_eq!(capability_set, fsys::OpenDirType::NamespaceDir); |
| assert!( |
| capability_name == "svc/fuchsia.sys2.RealmQuery.root" |
| || capability_name |
| == "svc/fuchsia.sys2.LifecycleController.root" |
| ); |
| server_channels.push(server_channel); |
| responder.send(Ok(())).expect("error sending EchoString response"); |
| } |
| other => { |
| unreachable!("Got unexpected request: {other:?}"); |
| } |
| } |
| } |
| }); |
| FakeRemoteControllerProvider { controller: remote_control.into(), _task } |
| } |
| |
| fn remote_controller(&self) -> &fremotecontrol::RemoteControlProxy { |
| self.controller.as_ref() |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_get_test_params() { |
| let dir = tempfile::tempdir().expect("Create temp dir"); |
| std::fs::write(dir.path().join("test_defs.json"), &*VALID_FILE_INPUT).expect("write file"); |
| |
| let cases = vec![ |
| ( |
| RunCommand { |
| timeout: None, |
| test_args: vec!["my-test-url".to_string()], |
| test_file: None, |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| vec![run_test_suite_lib::TestParams { |
| test_url: "my-test-url".to_string(), |
| realm: None.into(), |
| timeout_seconds: None, |
| test_filters: None, |
| also_run_disabled_tests: false, |
| parallel: None, |
| test_args: vec![], |
| max_severity_logs: None, |
| min_severity_logs: vec![], |
| tags: vec![], |
| break_on_failure: false, |
| }], |
| ), |
| ( |
| RunCommand { |
| timeout: None, |
| test_args: vec!["my-test-url".to_string()], |
| test_file: None, |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: Some(10), |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: Some(diagnostics_data::Severity::Warn), |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| vec![ |
| run_test_suite_lib::TestParams { |
| test_url: "my-test-url".to_string(), |
| realm: None.into(), |
| timeout_seconds: None, |
| test_filters: None, |
| also_run_disabled_tests: false, |
| max_severity_logs: Some(diagnostics_data::Severity::Warn), |
| min_severity_logs: vec![], |
| parallel: None, |
| test_args: vec![], |
| tags: vec![], |
| break_on_failure: false, |
| }; |
| 10 |
| ], |
| ), |
| ( |
| RunCommand { |
| timeout: Some(10), |
| test_args: vec!["my-test-url".to_string(), "--".to_string(), "arg".to_string()], |
| test_file: None, |
| test_filter: vec!["filter".to_string()], |
| realm: None, |
| run_disabled: true, |
| filter_ansi: false, |
| parallel: Some(20), |
| count: None, |
| show_full_moniker_in_logs: false, |
| min_severity_logs: vec![], |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| vec![run_test_suite_lib::TestParams { |
| test_url: "my-test-url".to_string(), |
| realm: None.into(), |
| timeout_seconds: Some(NonZeroU32::new(10).unwrap()), |
| test_filters: Some(vec!["filter".to_string()]), |
| also_run_disabled_tests: true, |
| max_severity_logs: None, |
| min_severity_logs: vec![], |
| parallel: Some(20), |
| test_args: vec!["--".to_string(), "arg".to_string()], |
| tags: vec![], |
| break_on_failure: false, |
| }], |
| ), |
| ( |
| RunCommand { |
| timeout: None, |
| test_args: vec![], |
| test_file: Some( |
| dir.path().join("test_defs.json").to_str().unwrap().to_string(), |
| ), |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| vec![ |
| run_test_suite_lib::TestParams { |
| test_url: "file-test-url-1".to_string(), |
| realm: None.into(), |
| timeout_seconds: None, |
| test_filters: None, |
| also_run_disabled_tests: false, |
| max_severity_logs: None, |
| min_severity_logs: vec![], |
| parallel: None, |
| test_args: vec![], |
| tags: vec![], |
| break_on_failure: false, |
| }, |
| run_test_suite_lib::TestParams { |
| test_url: "file-test-url-2".to_string(), |
| realm: None.into(), |
| timeout_seconds: Some(NonZeroU32::new(60).unwrap()), |
| test_filters: None, |
| also_run_disabled_tests: false, |
| max_severity_logs: None, |
| min_severity_logs: vec![], |
| parallel: None, |
| test_args: vec![], |
| tags: vec![], |
| break_on_failure: false, |
| }, |
| run_test_suite_lib::TestParams { |
| test_url: "file-test-url-3".to_string(), |
| realm: None.into(), |
| timeout_seconds: None, |
| test_filters: Some(vec!["Unit".to_string()]), |
| also_run_disabled_tests: true, |
| max_severity_logs: Some(diagnostics_data::Severity::Info), |
| min_severity_logs: vec![], |
| parallel: Some(4), |
| test_args: vec!["--flag".to_string()], |
| tags: vec![TestTag { |
| key: "hermetic".to_string(), |
| value: "true".to_string(), |
| }], |
| break_on_failure: false, |
| }, |
| ], |
| ), |
| ]; |
| let fake_contoller = FakeRemoteControllerProvider::new(); |
| for (run_command, expected_test_params) in cases.into_iter() { |
| let result = test_params_from_args( |
| fake_contoller.remote_controller(), |
| run_command.clone(), |
| true, |
| ) |
| .await; |
| assert!( |
| result.is_ok(), |
| "Error getting test params from {:?}: {:?}", |
| run_command, |
| result.unwrap_err() |
| ); |
| assert_eq!(result.unwrap().into_iter().collect::<Vec<_>>(), expected_test_params); |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_get_test_params_count() { |
| // Regression test for https://fxbug.dev/42062444: using an extremely |
| // large test count should result in a modest memory allocation. If |
| // that wasn't the case, this test would fail. |
| const COUNT: u32 = u32::MAX; |
| let fake_contoller = FakeRemoteControllerProvider::new(); |
| let params = test_params_from_args( |
| fake_contoller.remote_controller(), |
| RunCommand { |
| test_args: vec!["my-test-url".to_string()], |
| count: Some(COUNT), |
| timeout: None, |
| test_file: None, |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: Some(diagnostics_data::Severity::Warn), |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| true, |
| ) |
| .await |
| .expect("should succeed"); |
| assert_eq!(params.len(), usize::try_from(COUNT).unwrap()); |
| } |
| |
| #[fuchsia::test] |
| async fn test_get_test_params_invalid_args() { |
| let dir = tempfile::tempdir().expect("Create temp dir"); |
| std::fs::write(dir.path().join(VALID_INPUT_FILENAME), &*VALID_FILE_INPUT) |
| .expect("write file"); |
| std::fs::write(dir.path().join(INVALID_INPUT_FILENAME), &*INVALID_INPUT) |
| .expect("write file"); |
| let cases = vec![ |
| ( |
| "no tests specified", |
| RunCommand { |
| timeout: None, |
| test_args: vec![], |
| test_file: None, |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| ), |
| ( |
| "tests specified in both args and file", |
| RunCommand { |
| timeout: None, |
| test_args: vec!["my-test".to_string()], |
| test_file: Some( |
| dir.path().join(VALID_INPUT_FILENAME).to_str().unwrap().to_string(), |
| ), |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| ), |
| ( |
| "read invalid input from file", |
| RunCommand { |
| timeout: None, |
| test_args: vec![], |
| test_file: Some( |
| dir.path().join(INVALID_INPUT_FILENAME).to_str().unwrap().to_string(), |
| ), |
| test_filter: vec![], |
| realm: None, |
| run_disabled: false, |
| filter_ansi: false, |
| parallel: None, |
| count: None, |
| min_severity_logs: vec![], |
| show_full_moniker_in_logs: false, |
| max_severity_logs: None, |
| output_directory: None, |
| disable_output_directory: false, |
| continue_on_timeout: false, |
| stop_after_failures: None, |
| experimental_parallel_execution: None, |
| break_on_failure: false, |
| }, |
| ), |
| ]; |
| let fake_contoller = FakeRemoteControllerProvider::new(); |
| for (case_name, invalid_run_command) in cases.into_iter() { |
| let result = test_params_from_args( |
| fake_contoller.remote_controller(), |
| invalid_run_command, |
| true, |
| ) |
| .await; |
| assert!( |
| result.is_err(), |
| "Getting test params for case '{}' unexpectedly succeeded", |
| case_name |
| ); |
| } |
| } |
| |
| async fn fake_debug_data_iterator(iterator: ServerEnd<DebugDataIteratorMarker>) { |
| let mut stream = iterator.into_stream().unwrap(); |
| |
| // we just need to send once sample file and not test full logic as that is tested inside the library. |
| let (s1, s2) = fidl::Socket::create_stream(); |
| let mut debug_data = vec![DebugData { |
| name: "test_file".to_string().into(), |
| socket: s1.into(), |
| ..Default::default() |
| }]; |
| s2.write(&[1, 2, 3, 4, 5]).unwrap(); |
| while let Some(request) = stream.try_next().await.unwrap() { |
| match request { |
| ftest_manager::DebugDataIteratorRequest::GetNext { responder } => { |
| responder.send(debug_data.drain(..).collect()).unwrap(); |
| } |
| } |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_early_boot_profile() { |
| let (remote_control, mut stream) = |
| create_proxy_and_stream::<fremotecontrol::RemoteControlMarker>().unwrap(); |
| let task = fuchsia_async::Task::spawn(async move { |
| let mut once = false; |
| while let Some(request) = stream.try_next().await.unwrap() { |
| match request { |
| fremotecontrol::RemoteControlRequest::OpenCapability { |
| moniker, |
| capability_set, |
| capability_name, |
| server_channel, |
| flags: _, |
| responder, |
| } => { |
| assert!(!once); |
| once = true; |
| assert_eq!(moniker, "/core/test_manager"); |
| assert_eq!(capability_set, fsys::OpenDirType::ExposedDir); |
| assert!(capability_name == EarlyBootProfileMarker::DEBUG_NAME); |
| responder.send(Ok(())).expect("error sending EchoString response"); |
| |
| let mut stream = EarlyBootProfileRequestStream::from_channel( |
| fidl::AsyncChannel::from_channel(server_channel), |
| ); |
| while let Some(request) = stream.try_next().await.unwrap() { |
| match request { |
| ftest_manager::EarlyBootProfileRequest::RegisterWatcher { |
| iterator, |
| control_handle: _, |
| } => { |
| fake_debug_data_iterator(iterator).await; |
| } |
| other => { |
| unreachable!("Got unexpected request: {other:?}"); |
| } |
| } |
| } |
| } |
| other => { |
| unreachable!("Got unexpected request: {other:?}"); |
| } |
| } |
| } |
| }); |
| // Create a temporary directory |
| let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory"); |
| let temp_dir_path = temp_dir.path(); |
| early_boot_profile( |
| remote_control, |
| std::io::sink(), |
| EarlyBootProfileCommand { output_directory: temp_dir_path.to_path_buf() }, |
| ) |
| .await |
| .unwrap(); |
| task.await; |
| |
| let mut found_test_file = false; |
| for entry in walkdir::WalkDir::new(temp_dir_path).follow_links(false) { |
| let entry = entry.unwrap(); |
| |
| // Optionally, you can print other information about the entry |
| if entry.file_type().is_file() { |
| if entry.file_name() == "test_file" { |
| found_test_file = true; |
| let mut file_content = Vec::new(); |
| let mut file = std::fs::File::open(entry.path()).expect("Failed to open file"); |
| file.read_to_end(&mut file_content).expect("Failed to read file"); |
| assert_eq!(file_content, [1, 2, 3, 4, 5]); |
| break; |
| } |
| } |
| } |
| assert!(found_test_file); |
| } |
| } |