| // Copyright 2020 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 { |
| super::SuiteServer, |
| crate::errors::ArgumentError, |
| anyhow::{anyhow, Context}, |
| async_trait::async_trait, |
| fidl::endpoints::create_proxy, |
| fidl::endpoints::{ClientEnd, ProtocolMarker, Proxy, ServerEnd}, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_runner as fcrunner, |
| fidl_fuchsia_io as fio, |
| fidl_fuchsia_ldsvc::LoaderMarker, |
| fidl_fuchsia_test_runner::{ |
| LibraryLoaderCacheBuilderMarker, LibraryLoaderCacheMarker, LibraryLoaderCacheProxy, |
| }, |
| fuchsia_async::{self as fasync, TimeoutExt}, |
| fuchsia_component::client::connect_to_protocol, |
| fuchsia_component::server::ServiceFs, |
| fuchsia_runtime::job_default, |
| fuchsia_zircon::{self as zx, AsHandleRef}, |
| futures::future::abortable, |
| futures::{future::BoxFuture, prelude::*}, |
| namespace::Namespace, |
| std::{ |
| mem, |
| ops::Deref, |
| path::Path, |
| sync::{Arc, Mutex}, |
| }, |
| thiserror::Error, |
| tracing::{error, info, warn}, |
| vfs::{ |
| directory::entry_container::Directory, execution_scope::ExecutionScope, |
| file::vmo::read_only, tree_builder::TreeBuilder, |
| }, |
| zx::{HandleBased, Task}, |
| }; |
| |
| static PKG_PATH: &'static str = "/pkg"; |
| |
| // Maximum time that the runner will wait for break_on_start eventpair to signal. |
| // This is set to prevent debuggers from blocking us for too long, either intentionally |
| // or unintentionally. |
| const MAX_WAIT_BREAK_ON_START: zx::Duration = zx::Duration::from_millis(300); |
| |
| /// Error encountered running test component |
| #[derive(Debug, Error)] |
| pub enum ComponentError { |
| #[error("start info is missing resolved url")] |
| MissingResolvedUrl, |
| |
| #[error("error for test {}: {:?}", _0, _1)] |
| InvalidArgs(String, anyhow::Error), |
| |
| #[error("Cannot run test {}, no namespace was supplied.", _0)] |
| MissingNamespace(String), |
| |
| #[error("Cannot run test {}, as no outgoing directory was supplied.", _0)] |
| MissingOutDir(String), |
| |
| #[error("Cannot run test {}, as no runtime directory was supplied.", _0)] |
| MissingRuntimeDir(String), |
| |
| #[error("Cannot run test {}, as no /pkg directory was supplied.", _0)] |
| MissingPkg(String), |
| |
| #[error("Cannot load library for {}: {}.", _0, _1)] |
| LibraryLoadError(String, anyhow::Error), |
| |
| #[error("Cannot load executable binary '{}': {}", _0, _1)] |
| LoadingExecutable(String, anyhow::Error), |
| |
| #[error("Cannot create vmo child for test {}: {}", _0, _1)] |
| VmoChild(String, anyhow::Error), |
| |
| #[error("Cannot run suite server: {:?}", _0)] |
| ServeSuite(anyhow::Error), |
| |
| #[error("Cannot serve runtime directory: {:?}", _0)] |
| ServeRuntimeDir(anyhow::Error), |
| |
| #[error("{}: {:?}", _0, _1)] |
| Fidl(String, fidl::Error), |
| |
| #[error("cannot create job: {:?}", _0)] |
| CreateJob(zx::Status), |
| |
| #[error("Cannot set config vmo: {:?}", _0)] |
| ConfigVmo(anyhow::Error), |
| |
| #[error("cannot create channel: {:?}", _0)] |
| CreateChannel(zx::Status), |
| |
| #[error("cannot duplicate job: {:?}", _0)] |
| DuplicateJob(zx::Status), |
| |
| #[error("invalid url")] |
| InvalidUrl, |
| } |
| |
| impl ComponentError { |
| /// Convert this error into its approximate `fuchsia.component.Error` equivalent. |
| pub fn as_zx_status(&self) -> zx::Status { |
| let status = match self { |
| Self::MissingResolvedUrl => fcomponent::Error::InvalidArguments, |
| Self::InvalidArgs(_, _) => fcomponent::Error::InvalidArguments, |
| Self::MissingNamespace(_) => fcomponent::Error::InvalidArguments, |
| Self::MissingOutDir(_) => fcomponent::Error::InvalidArguments, |
| Self::MissingRuntimeDir(_) => fcomponent::Error::InvalidArguments, |
| Self::MissingPkg(_) => fcomponent::Error::InvalidArguments, |
| Self::LibraryLoadError(_, _) => fcomponent::Error::Internal, |
| Self::LoadingExecutable(_, _) => fcomponent::Error::InstanceCannotStart, |
| Self::VmoChild(_, _) => fcomponent::Error::Internal, |
| Self::ServeSuite(_) => fcomponent::Error::Internal, |
| Self::ServeRuntimeDir(_) => fcomponent::Error::Internal, |
| Self::Fidl(_, _) => fcomponent::Error::Internal, |
| Self::CreateJob(_) => fcomponent::Error::ResourceUnavailable, |
| Self::CreateChannel(_) => fcomponent::Error::ResourceUnavailable, |
| Self::DuplicateJob(_) => fcomponent::Error::Internal, |
| Self::InvalidUrl => fcomponent::Error::InvalidArguments, |
| Self::ConfigVmo(_) => fcomponent::Error::Internal, |
| }; |
| zx::Status::from_raw(status.into_primitive().try_into().unwrap()) |
| } |
| } |
| |
| /// All information about this test ELF component. |
| #[derive(Debug)] |
| pub struct Component { |
| /// Component URL |
| pub url: String, |
| |
| /// Component name |
| pub name: String, |
| |
| /// Binary path for this component relative to /pkg in 'ns' |
| pub binary: String, |
| |
| /// Arguments for this test. |
| pub args: Vec<String>, |
| |
| /// Environment variables for this test. |
| pub environ: Option<Vec<String>>, |
| |
| /// Namespace to pass to test process. |
| pub ns: Namespace, |
| |
| /// Parent job in which all test processes should be executed. |
| pub job: zx::Job, |
| |
| /// Options to create process with. |
| pub options: zx::ProcessOptions, |
| |
| /// Handle to library loader cache. |
| lib_loader_cache: LibraryLoaderCacheProxy, |
| |
| /// cached executable vmo. |
| executable_vmo: zx::Vmo, |
| |
| /// The structured config vmo. |
| pub config_vmo: Option<zx::Vmo>, |
| } |
| |
| pub struct BuilderArgs { |
| /// Component URL |
| pub url: String, |
| |
| /// Component name |
| pub name: String, |
| |
| /// Binary path for this component relative to /pkg in 'ns' |
| pub binary: String, |
| |
| /// Arguments for this test. |
| pub args: Vec<String>, |
| |
| /// Environment variables for this test. |
| pub environ: Option<Vec<String>>, |
| |
| /// Namespace to pass to test process. |
| pub ns: Namespace, |
| |
| /// Parent job in which all test processes should be executed. |
| pub job: zx::Job, |
| |
| /// The options to create the process with. |
| pub options: zx::ProcessOptions, |
| |
| /// The structured config vmo. |
| pub config: Option<zx::Vmo>, |
| } |
| |
| impl Component { |
| /// Create new object using `ComponentStartInfo`. |
| /// On success returns self and outgoing_dir from `ComponentStartInfo`. |
| pub async fn new<F>( |
| start_info: fcrunner::ComponentStartInfo, |
| validate_args: F, |
| ) -> Result< |
| (Self, ServerEnd<fio::DirectoryMarker>, ServerEnd<fio::DirectoryMarker>), |
| ComponentError, |
| > |
| where |
| F: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>, |
| { |
| let url = |
| runner::get_resolved_url(&start_info).ok_or(ComponentError::MissingResolvedUrl)?; |
| let name = Path::new(&url) |
| .file_name() |
| .ok_or_else(|| ComponentError::InvalidUrl)? |
| .to_str() |
| .ok_or_else(|| ComponentError::InvalidUrl)? |
| .to_string(); |
| |
| let args = runner::get_program_args(&start_info) |
| .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?; |
| validate_args(&args).map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?; |
| |
| let binary = runner::get_program_binary(&start_info) |
| .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?; |
| |
| // It's safe to unwrap `start_info.program` below because if the field |
| // were empty, this func would have a returned an error by now. |
| let program = start_info.program.as_ref().unwrap(); |
| let environ = runner::get_environ(program) |
| .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?; |
| let is_shared_process = runner::get_bool(program, "is_shared_process").unwrap_or(false); |
| |
| let ns = start_info.ns.ok_or_else(|| ComponentError::MissingNamespace(url.clone()))?; |
| let ns = Namespace::try_from(ns) |
| .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?; |
| |
| let outgoing_dir = |
| start_info.outgoing_dir.ok_or_else(|| ComponentError::MissingOutDir(url.clone()))?; |
| |
| let runtime_dir = |
| start_info.runtime_dir.ok_or_else(|| ComponentError::MissingRuntimeDir(url.clone()))?; |
| |
| let (pkg_dir, lib_proxy) = get_pkg_and_lib_proxy(&ns, &url)?; |
| |
| let executable_vmo = library_loader::load_vmo(pkg_dir, &binary) |
| .await |
| .map_err(|e| ComponentError::LoadingExecutable(binary.clone(), e))?; |
| let lib_loader_cache_builder = connect_to_protocol::<LibraryLoaderCacheBuilderMarker>() |
| .map_err(|e| ComponentError::LibraryLoadError(url.clone(), e))?; |
| |
| let (lib_loader_cache, server_end) = create_proxy::<LibraryLoaderCacheMarker>() |
| .map_err(|e| ComponentError::Fidl("Cannot create proxy".into(), e))?; |
| lib_loader_cache_builder |
| .create(lib_proxy.into_channel().unwrap().into_zx_channel().into(), server_end) |
| .map_err(|e| { |
| ComponentError::Fidl("cannot communicate with lib loader cache".into(), e) |
| })?; |
| |
| let config_vmo = match start_info.encoded_config { |
| None => None, |
| Some(config) => Some(runner::get_config_vmo(config).map_err(|e| { |
| ComponentError::ConfigVmo(anyhow!("Failed to get config vmo: {}", e)) |
| })?), |
| }; |
| |
| Ok(( |
| Self { |
| url: url, |
| name: name, |
| binary: binary, |
| args: args, |
| environ, |
| ns: ns, |
| job: job_default().create_child_job().map_err(ComponentError::CreateJob)?, |
| executable_vmo, |
| lib_loader_cache, |
| options: if is_shared_process { |
| zx::ProcessOptions::SHARED |
| } else { |
| zx::ProcessOptions::empty() |
| }, |
| config_vmo, |
| }, |
| outgoing_dir, |
| runtime_dir, |
| )) |
| } |
| |
| pub fn config_vmo(&self) -> Result<Option<zx::Vmo>, ComponentError> { |
| match &self.config_vmo { |
| None => Ok(None), |
| Some(vmo) => Ok(Some( |
| vmo.as_handle_ref() |
| .duplicate(fuchsia_zircon::Rights::SAME_RIGHTS) |
| .map_err(|_| { |
| ComponentError::VmoChild( |
| self.url.clone(), |
| anyhow!("Failed to clone config_vmo"), |
| ) |
| })? |
| .into(), |
| )), |
| } |
| } |
| |
| pub fn executable_vmo(&self) -> Result<zx::Vmo, ComponentError> { |
| vmo_create_child(&self.executable_vmo) |
| .map_err(|e| ComponentError::VmoChild(self.url.clone(), e)) |
| } |
| |
| pub fn loader_service(&self, loader: ServerEnd<LoaderMarker>) { |
| if let Err(e) = self.lib_loader_cache.serve(loader) { |
| error!("Cannot serve lib loader: {:?}", e); |
| } |
| } |
| |
| pub async fn create_for_tests(args: BuilderArgs) -> Result<Self, ComponentError> { |
| let (pkg_dir, lib_proxy) = get_pkg_and_lib_proxy(&args.ns, &args.url)?; |
| let executable_vmo = library_loader::load_vmo(pkg_dir, &args.binary) |
| .await |
| .map_err(|e| ComponentError::LoadingExecutable(args.url.clone(), e))?; |
| let lib_loader_cache_builder = connect_to_protocol::<LibraryLoaderCacheBuilderMarker>() |
| .map_err(|e| ComponentError::LibraryLoadError(args.url.clone(), e))?; |
| |
| let (lib_loader_cache, server_end) = create_proxy::<LibraryLoaderCacheMarker>() |
| .map_err(|e| ComponentError::Fidl("Cannot create proxy".into(), e))?; |
| lib_loader_cache_builder |
| .create(lib_proxy.into_channel().unwrap().into_zx_channel().into(), server_end) |
| .map_err(|e| { |
| ComponentError::Fidl("cannot communicate with lib loader cache".into(), e) |
| })?; |
| |
| Ok(Self { |
| url: args.url, |
| name: args.name, |
| binary: args.binary, |
| args: args.args, |
| environ: args.environ, |
| ns: args.ns, |
| job: args.job, |
| lib_loader_cache, |
| executable_vmo, |
| options: args.options, |
| config_vmo: None, |
| }) |
| } |
| } |
| |
| fn vmo_create_child(vmo: &zx::Vmo) -> Result<zx::Vmo, anyhow::Error> { |
| let size = vmo.get_size().context("Cannot get vmo size.")?; |
| vmo.create_child( |
| zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE, |
| 0, |
| size, |
| ) |
| .context("cannot create child vmo") |
| } |
| |
| // returns (pkg_dir, lib_proxy) |
| fn get_pkg_and_lib_proxy<'a>( |
| ns: &'a Namespace, |
| url: &String, |
| ) -> Result<(&'a ClientEnd<fio::DirectoryMarker>, fio::DirectoryProxy), ComponentError> { |
| // Locate the '/pkg' directory proxy previously added to the new component's namespace. |
| let pkg_dir = ns |
| .get(&PKG_PATH.parse().unwrap()) |
| .ok_or_else(|| ComponentError::MissingPkg(url.clone()))?; |
| |
| let lib_proxy = fuchsia_component::directory::open_directory_no_describe( |
| pkg_dir, |
| "lib", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .map_err(Into::into) |
| .map_err(|e| ComponentError::LibraryLoadError(url.clone(), e))?; |
| Ok((pkg_dir, lib_proxy)) |
| } |
| |
| #[async_trait] |
| impl runner::component::Controllable for ComponentRuntime { |
| async fn kill(&mut self) { |
| if let Some(component) = &self.component { |
| info!("kill request component: {}", component.url); |
| } |
| self.kill_self(); |
| } |
| |
| fn stop<'a>(&mut self) -> BoxFuture<'a, ()> { |
| if let Some(component) = &self.component { |
| info!("stop request component: {}", component.url); |
| } |
| self.kill_self(); |
| async move {}.boxed() |
| } |
| } |
| |
| impl Drop for ComponentRuntime { |
| fn drop(&mut self) { |
| if let Some(component) = &self.component { |
| info!("drop component: {}", component.url); |
| } |
| self.kill_self(); |
| } |
| } |
| |
| /// Information about all the test instances running for this component. |
| struct ComponentRuntime { |
| /// handle to abort component's outgoing services. |
| outgoing_abortable_handle: Option<futures::future::AbortHandle>, |
| |
| /// handle to abort running test suite servers. |
| suite_service_abortable_handles: Option<Arc<Mutex<Vec<futures::future::AbortHandle>>>>, |
| |
| /// job containing all processes in this component. |
| job: Option<zx::Job>, |
| |
| /// component object which is stored here for safe keeping. It would be dropped when test is |
| /// stopped/killed. |
| component: Option<Arc<Component>>, |
| } |
| |
| impl ComponentRuntime { |
| fn new( |
| outgoing_abortable_handle: futures::future::AbortHandle, |
| suite_service_abortable_handles: Arc<Mutex<Vec<futures::future::AbortHandle>>>, |
| job: zx::Job, |
| component: Arc<Component>, |
| ) -> Self { |
| Self { |
| outgoing_abortable_handle: Some(outgoing_abortable_handle), |
| suite_service_abortable_handles: Some(suite_service_abortable_handles), |
| job: Some(job), |
| component: Some(component), |
| } |
| } |
| |
| fn kill_self(&mut self) { |
| // drop component. |
| if let Some(component) = self.component.take() { |
| info!("killing component: {}", component.url); |
| } |
| |
| // kill outgoing server. |
| if let Some(h) = self.outgoing_abortable_handle.take() { |
| h.abort(); |
| } |
| |
| // kill all suite servers. |
| if let Some(handles) = self.suite_service_abortable_handles.take() { |
| let handles = handles.lock().unwrap(); |
| for h in handles.deref() { |
| h.abort(); |
| } |
| } |
| |
| // kill all test processes if running. |
| if let Some(job) = self.job.take() { |
| let _ = job.kill(); |
| } |
| } |
| } |
| |
| /// Setup and run test component in background. |
| /// |
| /// * `F`: Function which returns new instance of `SuiteServer`. |
| pub async fn start_component<F, U, S>( |
| start_info: fcrunner::ComponentStartInfo, |
| mut server_end: ServerEnd<fcrunner::ComponentControllerMarker>, |
| get_test_server: F, |
| validate_args: U, |
| ) -> Result<(), ComponentError> |
| where |
| F: 'static + Fn() -> S + Send, |
| U: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>, |
| S: SuiteServer, |
| { |
| let resolved_url = runner::get_resolved_url(&start_info).unwrap_or(String::new()); |
| if let Err(e) = |
| start_component_inner(start_info, &mut server_end, get_test_server, validate_args).await |
| { |
| // Take ownership of `server_end`. |
| let server_end = take_server_end(&mut server_end); |
| runner::component::report_start_error( |
| e.as_zx_status(), |
| format!("{}", e), |
| &resolved_url, |
| server_end, |
| ); |
| return Err(e); |
| } |
| Ok(()) |
| } |
| |
| async fn start_component_inner<F, U, S>( |
| mut start_info: fcrunner::ComponentStartInfo, |
| server_end: &mut ServerEnd<fcrunner::ComponentControllerMarker>, |
| get_test_server: F, |
| validate_args: U, |
| ) -> Result<(), ComponentError> |
| where |
| F: 'static + Fn() -> S + Send, |
| U: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>, |
| S: SuiteServer, |
| { |
| let break_on_start = start_info.break_on_start.take(); |
| let (component, outgoing_dir, runtime_dir) = Component::new(start_info, validate_args).await?; |
| let component = Arc::new(component); |
| |
| // Debugger support: |
| // 1. Serve the runtime directory providing the "elf/job_id" entry. |
| let mut runtime_dir_builder = TreeBuilder::empty_dir(); |
| let job_id = component |
| .job |
| .get_koid() |
| .map_err(|s| ComponentError::ServeRuntimeDir(anyhow!("cannot get job koid: {}", s)))? |
| .raw_koid(); |
| runtime_dir_builder |
| .add_entry(&["elf", "job_id"], read_only(job_id.to_string())) |
| .map_err(|e| ComponentError::ServeRuntimeDir(anyhow!("cannot add elf/job_id: {}", e)))?; |
| runtime_dir_builder.build().open( |
| ExecutionScope::new(), |
| fio::OpenFlags::RIGHT_READABLE, |
| vfs::path::Path::dot(), |
| ServerEnd::<fio::NodeMarker>::new(runtime_dir.into_channel()), |
| ); |
| // 2. Wait on `break_on_start` before spawning any processes. |
| if let Some(break_on_start) = break_on_start { |
| fasync::OnSignals::new(&break_on_start, zx::Signals::OBJECT_PEER_CLOSED) |
| .on_timeout(MAX_WAIT_BREAK_ON_START, || Err(zx::Status::TIMED_OUT)) |
| .await |
| .err() |
| .map(|e| warn!("Failed to wait break_on_start on {}: {}", component.name, e)); |
| } |
| |
| let job_runtime_dup = component |
| .job |
| .duplicate_handle(zx::Rights::SAME_RIGHTS) |
| .map_err(ComponentError::DuplicateJob)?; |
| |
| let job_watch_dup = component |
| .job |
| .duplicate_handle(zx::Rights::SAME_RIGHTS) |
| .map_err(ComponentError::DuplicateJob)?; |
| let mut fs = ServiceFs::new(); |
| |
| let suite_server_abortable_handles = Arc::new(Mutex::new(vec![])); |
| let weak_test_suite_abortable_handles = Arc::downgrade(&suite_server_abortable_handles); |
| let weak_component = Arc::downgrade(&component); |
| |
| let url = component.url.clone(); |
| fs.dir("svc").add_fidl_service(move |stream| { |
| let abortable_handles = weak_test_suite_abortable_handles.upgrade(); |
| if abortable_handles.is_none() { |
| return; |
| } |
| let abortable_handles = abortable_handles.unwrap(); |
| let mut abortable_handles = abortable_handles.lock().unwrap(); |
| let abortable_handle = get_test_server().run(weak_component.clone(), &url, stream); |
| abortable_handles.push(abortable_handle); |
| }); |
| |
| fs.serve_connection(outgoing_dir).map_err(ComponentError::ServeSuite)?; |
| let (fut, abortable_handle) = abortable(fs.collect::<()>()); |
| |
| let component_runtime = ComponentRuntime::new( |
| abortable_handle, |
| suite_server_abortable_handles, |
| job_runtime_dup, |
| component, |
| ); |
| |
| fasync::Task::spawn(async move { |
| // as error on abortable will always return Aborted, |
| // no need to check that, as it is a valid usecase. |
| fut.await.ok(); |
| }) |
| .detach(); |
| |
| let server_end = take_server_end(server_end); |
| let controller_stream = server_end.into_stream().map_err(|e| { |
| ComponentError::Fidl("failed to convert server end to controller".to_owned(), e) |
| })?; |
| let controller = runner::component::Controller::new(component_runtime, controller_stream); |
| |
| let epitaph_fut = Box::pin(async move { |
| // Just return 'OK' here. Any actual errors will be handled through |
| // the test protocol. |
| let _ = |
| fasync::OnSignals::new(&job_watch_dup.as_handle_ref(), zx::Signals::TASK_TERMINATED) |
| .await; |
| zx::Status::OK.try_into().unwrap() |
| }); |
| |
| fasync::Task::spawn(controller.serve(epitaph_fut)).detach(); |
| |
| Ok(()) |
| } |
| |
| fn take_server_end<P: ProtocolMarker>(end: &mut ServerEnd<P>) -> ServerEnd<P> { |
| let invalid_end: ServerEnd<P> = zx::Handle::invalid().into(); |
| mem::replace(end, invalid_end) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{ |
| elf::EnumeratedTestCases, |
| errors::{EnumerationError, RunTestError}, |
| }, |
| anyhow::Error, |
| assert_matches::assert_matches, |
| fidl::endpoints::{self}, |
| fidl_fuchsia_test::{Invocation, RunListenerProxy}, |
| futures::future::{AbortHandle, Aborted}, |
| namespace::NamespaceError, |
| std::sync::Weak, |
| }; |
| |
| fn create_ns_from_current_ns( |
| dir_paths: Vec<(&str, fio::OpenFlags)>, |
| ) -> Result<Namespace, NamespaceError> { |
| let mut ns = vec![]; |
| for (path, permission) in dir_paths { |
| let chan = fuchsia_fs::directory::open_in_namespace(path, permission) |
| .unwrap() |
| .into_channel() |
| .unwrap() |
| .into_zx_channel(); |
| let handle = ClientEnd::new(chan); |
| |
| ns.push(fcrunner::ComponentNamespaceEntry { |
| path: Some(path.to_string()), |
| directory: Some(handle), |
| ..Default::default() |
| }); |
| } |
| Namespace::try_from(ns) |
| } |
| |
| macro_rules! child_job { |
| () => { |
| job_default().create_child_job().unwrap() |
| }; |
| } |
| |
| async fn sample_test_component() -> Result<Arc<Component>, Error> { |
| let ns = create_ns_from_current_ns(vec![( |
| "/pkg", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| )])?; |
| |
| Ok(Arc::new( |
| Component::create_for_tests(BuilderArgs { |
| url: "fuchsia-pkg://fuchsia.com/sample_test#test.cm".to_owned(), |
| name: "test.cm".to_owned(), |
| binary: "bin/test_runners_lib_lib_test".to_owned(), //reference self binary |
| args: vec![], |
| environ: None, |
| ns: ns, |
| job: child_job!(), |
| options: zx::ProcessOptions::empty(), |
| config: None, |
| }) |
| .await?, |
| )) |
| } |
| |
| async fn dummy_func() -> u32 { |
| 2 |
| } |
| |
| struct DummyServer {} |
| |
| #[async_trait] |
| impl SuiteServer for DummyServer { |
| fn run( |
| self, |
| _component: Weak<Component>, |
| _test_url: &str, |
| _stream: fidl_fuchsia_test::SuiteRequestStream, |
| ) -> AbortHandle { |
| let (_, handle) = abortable(async {}); |
| handle |
| } |
| |
| async fn enumerate_tests( |
| &self, |
| _test_component: Arc<Component>, |
| ) -> Result<EnumeratedTestCases, EnumerationError> { |
| Ok(Arc::new(vec![])) |
| } |
| |
| async fn run_tests( |
| &self, |
| _invocations: Vec<Invocation>, |
| _run_options: fidl_fuchsia_test::RunOptions, |
| _component: Arc<Component>, |
| _run_listener: &RunListenerProxy, |
| ) -> Result<(), RunTestError> { |
| Ok(()) |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn start_component_error() { |
| let start_info = fcrunner::ComponentStartInfo { |
| resolved_url: None, |
| program: None, |
| ns: None, |
| outgoing_dir: None, |
| runtime_dir: None, |
| ..Default::default() |
| }; |
| let (client_controller, server_controller) = endpoints::create_proxy().unwrap(); |
| let get_test_server = || DummyServer {}; |
| let err = start_component(start_info, server_controller, get_test_server, |_| Ok(())).await; |
| assert_matches!(err, Err(ComponentError::MissingResolvedUrl)); |
| let expected_status = zx::Status::from_raw( |
| fcomponent::Error::InvalidArguments.into_primitive().try_into().unwrap(), |
| ); |
| let s = assert_matches!( |
| client_controller.take_event_stream().next().await, |
| Some(Err(fidl::Error::ClientChannelClosed { status: s, .. })) => s |
| ); |
| assert_eq!(s, expected_status); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn start_component_works() { |
| let _ = sample_test_component().await.unwrap(); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn component_runtime_kill_job_works() { |
| let component = sample_test_component().await.unwrap(); |
| |
| let mut futs = vec![]; |
| let mut handles = vec![]; |
| for _i in 0..10 { |
| let (fut, handle) = abortable(dummy_func()); |
| futs.push(fut); |
| handles.push(handle); |
| } |
| |
| let (out_fut, out_handle) = abortable(dummy_func()); |
| let mut runtime = ComponentRuntime::new( |
| out_handle, |
| Arc::new(Mutex::new(handles)), |
| child_job!(), |
| component.clone(), |
| ); |
| |
| assert_eq!(Arc::strong_count(&component), 2); |
| runtime.kill_self(); |
| |
| for fut in futs { |
| assert_eq!(fut.await, Err(Aborted)); |
| } |
| |
| assert_eq!(out_fut.await, Err(Aborted)); |
| |
| assert_eq!(Arc::strong_count(&component), 1); |
| } |
| } |