1use super::SuiteServer;
6use crate::errors::ArgumentError;
7use anyhow::{anyhow, Context};
8use async_trait::async_trait;
9use fidl::endpoints::{create_proxy, ClientEnd, ProtocolMarker, Proxy, ServerEnd};
10use fidl_fuchsia_ldsvc::LoaderMarker;
11use fidl_fuchsia_test_runner::{
12 LibraryLoaderCacheBuilderMarker, LibraryLoaderCacheMarker, LibraryLoaderCacheProxy,
13};
14use fuchsia_async::{self as fasync, TimeoutExt};
15use fuchsia_component::client::connect_to_protocol;
16use fuchsia_component::server::ServiceFs;
17use fuchsia_runtime::job_default;
18use futures::future::{abortable, BoxFuture};
19use futures::prelude::*;
20use log::{error, info, warn};
21use namespace::Namespace;
22use runner::component::StopInfo;
23use std::mem;
24use std::ops::Deref;
25use std::path::Path;
26use std::sync::{Arc, Mutex};
27use thiserror::Error;
28use vfs::execution_scope::ExecutionScope;
29use vfs::file::vmo::read_only;
30use vfs::tree_builder::TreeBuilder;
31use zx::{self as zx, AsHandleRef, HandleBased, Task};
32use {
33 fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_runner as fcrunner,
34 fidl_fuchsia_io as fio,
35};
36
37static PKG_PATH: &'static str = "/pkg";
38
39const MAX_WAIT_BREAK_ON_START: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(300);
43
44#[derive(Debug, Error)]
46pub enum ComponentError {
47 #[error("start info is missing resolved url")]
48 MissingResolvedUrl,
49
50 #[error("error for test {}: {:?}", _0, _1)]
51 InvalidArgs(String, anyhow::Error),
52
53 #[error("Cannot run test {}, no namespace was supplied.", _0)]
54 MissingNamespace(String),
55
56 #[error("Cannot run test {}, as no outgoing directory was supplied.", _0)]
57 MissingOutDir(String),
58
59 #[error("Cannot run test {}, as no runtime directory was supplied.", _0)]
60 MissingRuntimeDir(String),
61
62 #[error("Cannot run test {}, as no /pkg directory was supplied.", _0)]
63 MissingPkg(String),
64
65 #[error("Cannot load library for {}: {}.", _0, _1)]
66 LibraryLoadError(String, anyhow::Error),
67
68 #[error("Cannot load executable binary '{}': {}", _0, _1)]
69 LoadingExecutable(String, anyhow::Error),
70
71 #[error("Cannot create vmo child for test {}: {}", _0, _1)]
72 VmoChild(String, anyhow::Error),
73
74 #[error("Cannot run suite server: {:?}", _0)]
75 ServeSuite(anyhow::Error),
76
77 #[error("Cannot serve runtime directory: {:?}", _0)]
78 ServeRuntimeDir(anyhow::Error),
79
80 #[error("{}: {:?}", _0, _1)]
81 Fidl(String, fidl::Error),
82
83 #[error("cannot create job: {:?}", _0)]
84 CreateJob(zx::Status),
85
86 #[error("Cannot set config vmo: {:?}", _0)]
87 ConfigVmo(anyhow::Error),
88
89 #[error("cannot create channel: {:?}", _0)]
90 CreateChannel(zx::Status),
91
92 #[error("cannot duplicate job: {:?}", _0)]
93 DuplicateJob(zx::Status),
94
95 #[error("invalid url")]
96 InvalidUrl,
97}
98
99impl ComponentError {
100 pub fn as_zx_status(&self) -> zx::Status {
102 let status = match self {
103 Self::MissingResolvedUrl => fcomponent::Error::InvalidArguments,
104 Self::InvalidArgs(_, _) => fcomponent::Error::InvalidArguments,
105 Self::MissingNamespace(_) => fcomponent::Error::InvalidArguments,
106 Self::MissingOutDir(_) => fcomponent::Error::InvalidArguments,
107 Self::MissingRuntimeDir(_) => fcomponent::Error::InvalidArguments,
108 Self::MissingPkg(_) => fcomponent::Error::InvalidArguments,
109 Self::LibraryLoadError(_, _) => fcomponent::Error::Internal,
110 Self::LoadingExecutable(_, _) => fcomponent::Error::InstanceCannotStart,
111 Self::VmoChild(_, _) => fcomponent::Error::Internal,
112 Self::ServeSuite(_) => fcomponent::Error::Internal,
113 Self::ServeRuntimeDir(_) => fcomponent::Error::Internal,
114 Self::Fidl(_, _) => fcomponent::Error::Internal,
115 Self::CreateJob(_) => fcomponent::Error::ResourceUnavailable,
116 Self::CreateChannel(_) => fcomponent::Error::ResourceUnavailable,
117 Self::DuplicateJob(_) => fcomponent::Error::Internal,
118 Self::InvalidUrl => fcomponent::Error::InvalidArguments,
119 Self::ConfigVmo(_) => fcomponent::Error::Internal,
120 };
121 zx::Status::from_raw(status.into_primitive().try_into().unwrap())
122 }
123}
124
125#[derive(Debug)]
127pub struct Component {
128 pub url: String,
130
131 pub name: String,
133
134 pub binary: String,
136
137 pub args: Vec<String>,
139
140 pub environ: Option<Vec<String>>,
142
143 pub ns: Namespace,
145
146 pub job: zx::Job,
148
149 pub options: zx::ProcessOptions,
151
152 lib_loader_cache: LibraryLoaderCacheProxy,
154
155 executable_vmo: zx::Vmo,
157
158 pub config_vmo: Option<zx::Vmo>,
160
161 pub component_instance: Option<fidl::Event>,
163}
164
165pub struct BuilderArgs {
166 pub url: String,
168
169 pub name: String,
171
172 pub binary: String,
174
175 pub args: Vec<String>,
177
178 pub environ: Option<Vec<String>>,
180
181 pub ns: Namespace,
183
184 pub job: zx::Job,
186
187 pub options: zx::ProcessOptions,
189
190 pub config: Option<zx::Vmo>,
192}
193
194impl Component {
195 pub async fn new<F>(
198 start_info: fcrunner::ComponentStartInfo,
199 validate_args: F,
200 ) -> Result<
201 (Self, ServerEnd<fio::DirectoryMarker>, ServerEnd<fio::DirectoryMarker>),
202 ComponentError,
203 >
204 where
205 F: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>,
206 {
207 let url =
208 runner::get_resolved_url(&start_info).ok_or(ComponentError::MissingResolvedUrl)?;
209 let name = Path::new(&url)
210 .file_name()
211 .ok_or_else(|| ComponentError::InvalidUrl)?
212 .to_str()
213 .ok_or_else(|| ComponentError::InvalidUrl)?
214 .to_string();
215
216 let args = runner::get_program_args(&start_info)
217 .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
218 validate_args(&args).map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
219
220 let binary = runner::get_program_binary(&start_info)
221 .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
222
223 let program = start_info.program.as_ref().unwrap();
226 let environ = runner::get_environ(program)
227 .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
228 let is_shared_process = runner::get_bool(program, "is_shared_process").unwrap_or(false);
229
230 let ns = start_info.ns.ok_or_else(|| ComponentError::MissingNamespace(url.clone()))?;
231 let ns = Namespace::try_from(ns)
232 .map_err(|e| ComponentError::InvalidArgs(url.clone(), e.into()))?;
233
234 let outgoing_dir =
235 start_info.outgoing_dir.ok_or_else(|| ComponentError::MissingOutDir(url.clone()))?;
236
237 let runtime_dir =
238 start_info.runtime_dir.ok_or_else(|| ComponentError::MissingRuntimeDir(url.clone()))?;
239
240 let (pkg_dir, lib_proxy) = get_pkg_and_lib_proxy(&ns, &url)?;
241
242 let executable_vmo = library_loader::load_vmo(pkg_dir, &binary)
243 .await
244 .map_err(|e| ComponentError::LoadingExecutable(binary.clone(), e))?;
245 let lib_loader_cache_builder = connect_to_protocol::<LibraryLoaderCacheBuilderMarker>()
246 .map_err(|e| ComponentError::LibraryLoadError(url.clone(), e))?;
247
248 let (lib_loader_cache, server_end) = create_proxy::<LibraryLoaderCacheMarker>();
249 lib_loader_cache_builder
250 .create(lib_proxy.into_channel().unwrap().into_zx_channel().into(), server_end)
251 .map_err(|e| {
252 ComponentError::Fidl("cannot communicate with lib loader cache".into(), e)
253 })?;
254
255 let config_vmo = match start_info.encoded_config {
256 None => None,
257 Some(config) => Some(runner::get_config_vmo(config).map_err(|e| {
258 ComponentError::ConfigVmo(anyhow!("Failed to get config vmo: {}", e))
259 })?),
260 };
261
262 Ok((
263 Self {
264 url: url,
265 name: name,
266 binary: binary,
267 args: args,
268 environ,
269 ns: ns,
270 job: job_default().create_child_job().map_err(ComponentError::CreateJob)?,
271 executable_vmo,
272 lib_loader_cache,
273 options: if is_shared_process {
274 zx::ProcessOptions::SHARED
275 } else {
276 zx::ProcessOptions::empty()
277 },
278 config_vmo,
279 component_instance: start_info.component_instance,
280 },
281 outgoing_dir,
282 runtime_dir,
283 ))
284 }
285
286 pub fn config_vmo(&self) -> Result<Option<zx::Vmo>, ComponentError> {
287 match &self.config_vmo {
288 None => Ok(None),
289 Some(vmo) => Ok(Some(
290 vmo.as_handle_ref()
291 .duplicate(zx::Rights::SAME_RIGHTS)
292 .map_err(|_| {
293 ComponentError::VmoChild(
294 self.url.clone(),
295 anyhow!("Failed to clone config_vmo"),
296 )
297 })?
298 .into(),
299 )),
300 }
301 }
302
303 pub fn executable_vmo(&self) -> Result<zx::Vmo, ComponentError> {
304 vmo_create_child(&self.executable_vmo)
305 .map_err(|e| ComponentError::VmoChild(self.url.clone(), e))
306 }
307
308 pub fn loader_service(&self, loader: ServerEnd<LoaderMarker>) {
309 if let Err(e) = self.lib_loader_cache.serve(loader) {
310 error!("Cannot serve lib loader: {:?}", e);
311 }
312 }
313
314 pub async fn create_for_tests(args: BuilderArgs) -> Result<Self, ComponentError> {
315 let (pkg_dir, lib_proxy) = get_pkg_and_lib_proxy(&args.ns, &args.url)?;
316 let executable_vmo = library_loader::load_vmo(pkg_dir, &args.binary)
317 .await
318 .map_err(|e| ComponentError::LoadingExecutable(args.url.clone(), e))?;
319 let lib_loader_cache_builder = connect_to_protocol::<LibraryLoaderCacheBuilderMarker>()
320 .map_err(|e| ComponentError::LibraryLoadError(args.url.clone(), e))?;
321
322 let (lib_loader_cache, server_end) = create_proxy::<LibraryLoaderCacheMarker>();
323 lib_loader_cache_builder
324 .create(lib_proxy.into_channel().unwrap().into_zx_channel().into(), server_end)
325 .map_err(|e| {
326 ComponentError::Fidl("cannot communicate with lib loader cache".into(), e)
327 })?;
328
329 Ok(Self {
330 url: args.url,
331 name: args.name,
332 binary: args.binary,
333 args: args.args,
334 environ: args.environ,
335 ns: args.ns,
336 job: args.job,
337 lib_loader_cache,
338 executable_vmo,
339 options: args.options,
340 config_vmo: None,
341 component_instance: None,
342 })
343 }
344}
345
346fn vmo_create_child(vmo: &zx::Vmo) -> Result<zx::Vmo, anyhow::Error> {
347 let size = vmo.get_size().context("Cannot get vmo size.")?;
348 vmo.create_child(
349 zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE,
350 0,
351 size,
352 )
353 .context("cannot create child vmo")
354}
355
356fn get_pkg_and_lib_proxy<'a>(
358 ns: &'a Namespace,
359 url: &String,
360) -> Result<(&'a ClientEnd<fio::DirectoryMarker>, fio::DirectoryProxy), ComponentError> {
361 let pkg_dir = ns
363 .get(&PKG_PATH.parse().unwrap())
364 .ok_or_else(|| ComponentError::MissingPkg(url.clone()))?;
365
366 let lib_proxy =
367 fuchsia_component::directory::open_directory_async(pkg_dir, "lib", fio::RX_STAR_DIR)
368 .map_err(Into::into)
369 .map_err(|e| ComponentError::LibraryLoadError(url.clone(), e))?;
370 Ok((pkg_dir, lib_proxy))
371}
372
373#[async_trait]
374impl runner::component::Controllable for ComponentRuntime {
375 async fn kill(&mut self) {
376 if let Some(component) = &self.component {
377 info!("kill request component: {}", component.url);
378 }
379 self.kill_self();
380 }
381
382 fn stop<'a>(&mut self) -> BoxFuture<'a, ()> {
383 if let Some(component) = &self.component {
384 info!("stop request component: {}", component.url);
385 }
386 self.kill_self();
387 async move {}.boxed()
388 }
389}
390
391impl Drop for ComponentRuntime {
392 fn drop(&mut self) {
393 if let Some(component) = &self.component {
394 info!("drop component: {}", component.url);
395 }
396 self.kill_self();
397 }
398}
399
400struct ComponentRuntime {
402 outgoing_abortable_handle: Option<futures::future::AbortHandle>,
404
405 suite_service_abortable_handles: Option<Arc<Mutex<Vec<futures::future::AbortHandle>>>>,
407
408 job: Option<zx::Job>,
410
411 component: Option<Arc<Component>>,
414}
415
416impl ComponentRuntime {
417 fn new(
418 outgoing_abortable_handle: futures::future::AbortHandle,
419 suite_service_abortable_handles: Arc<Mutex<Vec<futures::future::AbortHandle>>>,
420 job: zx::Job,
421 component: Arc<Component>,
422 ) -> Self {
423 Self {
424 outgoing_abortable_handle: Some(outgoing_abortable_handle),
425 suite_service_abortable_handles: Some(suite_service_abortable_handles),
426 job: Some(job),
427 component: Some(component),
428 }
429 }
430
431 fn kill_self(&mut self) {
432 if let Some(component) = self.component.take() {
434 info!("killing component: {}", component.url);
435 }
436
437 if let Some(h) = self.outgoing_abortable_handle.take() {
439 h.abort();
440 }
441
442 if let Some(handles) = self.suite_service_abortable_handles.take() {
444 let handles = handles.lock().unwrap();
445 for h in handles.deref() {
446 h.abort();
447 }
448 }
449
450 if let Some(job) = self.job.take() {
452 let _ = job.kill();
453 }
454 }
455}
456
457pub async fn start_component<F, U, S>(
461 start_info: fcrunner::ComponentStartInfo,
462 mut server_end: ServerEnd<fcrunner::ComponentControllerMarker>,
463 get_test_server: F,
464 validate_args: U,
465) -> Result<(), ComponentError>
466where
467 F: 'static + Fn() -> S + Send,
468 U: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>,
469 S: SuiteServer,
470{
471 let resolved_url = runner::get_resolved_url(&start_info).unwrap_or(String::new());
472 if let Err(e) =
473 start_component_inner(start_info, &mut server_end, get_test_server, validate_args).await
474 {
475 let server_end = take_server_end(&mut server_end);
477 runner::component::report_start_error(
478 e.as_zx_status(),
479 format!("{}", e),
480 &resolved_url,
481 server_end,
482 );
483 return Err(e);
484 }
485 Ok(())
486}
487
488async fn start_component_inner<F, U, S>(
489 mut start_info: fcrunner::ComponentStartInfo,
490 server_end: &mut ServerEnd<fcrunner::ComponentControllerMarker>,
491 get_test_server: F,
492 validate_args: U,
493) -> Result<(), ComponentError>
494where
495 F: 'static + Fn() -> S + Send,
496 U: 'static + Fn(&Vec<String>) -> Result<(), ArgumentError>,
497 S: SuiteServer,
498{
499 let break_on_start = start_info.break_on_start.take();
500 let (component, outgoing_dir, runtime_dir) = Component::new(start_info, validate_args).await?;
501 let component = Arc::new(component);
502
503 let mut runtime_dir_builder = TreeBuilder::empty_dir();
506 let job_id = component
507 .job
508 .get_koid()
509 .map_err(|s| ComponentError::ServeRuntimeDir(anyhow!("cannot get job koid: {}", s)))?
510 .raw_koid();
511 runtime_dir_builder
512 .add_entry(&["elf", "job_id"], read_only(job_id.to_string()))
513 .map_err(|e| ComponentError::ServeRuntimeDir(anyhow!("cannot add elf/job_id: {}", e)))?;
514
515 vfs::directory::serve_on(
516 runtime_dir_builder.build(),
517 fio::PERM_READABLE,
518 ExecutionScope::new(),
519 runtime_dir,
520 );
521
522 if let Some(break_on_start) = break_on_start {
524 fasync::OnSignals::new(&break_on_start, zx::Signals::OBJECT_PEER_CLOSED)
525 .on_timeout(MAX_WAIT_BREAK_ON_START, || Err(zx::Status::TIMED_OUT))
526 .await
527 .err()
528 .map(|e| warn!("Failed to wait break_on_start on {}: {}", component.name, e));
529 }
530
531 let job_runtime_dup = component
532 .job
533 .duplicate_handle(zx::Rights::SAME_RIGHTS)
534 .map_err(ComponentError::DuplicateJob)?;
535
536 let job_watch_dup = component
537 .job
538 .duplicate_handle(zx::Rights::SAME_RIGHTS)
539 .map_err(ComponentError::DuplicateJob)?;
540 let mut fs = ServiceFs::new();
541
542 let suite_server_abortable_handles = Arc::new(Mutex::new(vec![]));
543 let weak_test_suite_abortable_handles = Arc::downgrade(&suite_server_abortable_handles);
544 let weak_component = Arc::downgrade(&component);
545
546 let url = component.url.clone();
547 fs.dir("svc").add_fidl_service(move |stream| {
548 let abortable_handles = weak_test_suite_abortable_handles.upgrade();
549 if abortable_handles.is_none() {
550 return;
551 }
552 let abortable_handles = abortable_handles.unwrap();
553 let mut abortable_handles = abortable_handles.lock().unwrap();
554 let abortable_handle = get_test_server().run(weak_component.clone(), &url, stream);
555 abortable_handles.push(abortable_handle);
556 });
557
558 fs.serve_connection(outgoing_dir).map_err(ComponentError::ServeSuite)?;
559 let (fut, abortable_handle) = abortable(fs.collect::<()>());
560
561 let component_runtime = ComponentRuntime::new(
562 abortable_handle,
563 suite_server_abortable_handles,
564 job_runtime_dup,
565 component,
566 );
567
568 fasync::Task::spawn(async move {
569 fut.await.ok();
572 })
573 .detach();
574
575 let server_end = take_server_end(server_end);
576 let (controller_stream, control) = server_end.into_stream_and_control_handle();
577 let controller =
578 runner::component::Controller::new(component_runtime, controller_stream, control);
579
580 let termination_fut = Box::pin(async move {
581 let _ =
584 fasync::OnSignals::new(&job_watch_dup.as_handle_ref(), zx::Signals::TASK_TERMINATED)
585 .await;
586 StopInfo::from_ok(None)
587 });
588
589 fasync::Task::spawn(controller.serve(termination_fut)).detach();
590
591 Ok(())
592}
593
594fn take_server_end<P: ProtocolMarker>(end: &mut ServerEnd<P>) -> ServerEnd<P> {
595 let invalid_end: ServerEnd<P> = zx::Handle::invalid().into();
596 mem::replace(end, invalid_end)
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::elf::EnumeratedTestCases;
603 use crate::errors::{EnumerationError, RunTestError};
604 use anyhow::Error;
605 use assert_matches::assert_matches;
606 use fidl::endpoints::{self};
607 use fidl_fuchsia_test::{Invocation, RunListenerProxy};
608 use futures::future::{AbortHandle, Aborted};
609 use namespace::NamespaceError;
610 use std::sync::Weak;
611
612 fn create_ns_from_current_ns(
613 dir_paths: Vec<(&str, fio::Flags)>,
614 ) -> Result<Namespace, NamespaceError> {
615 let mut ns = vec![];
616 for (path, permission) in dir_paths {
617 let chan = fuchsia_fs::directory::open_in_namespace(path, permission)
618 .unwrap()
619 .into_channel()
620 .unwrap()
621 .into_zx_channel();
622 let handle = ClientEnd::new(chan);
623
624 ns.push(fcrunner::ComponentNamespaceEntry {
625 path: Some(path.to_string()),
626 directory: Some(handle),
627 ..Default::default()
628 });
629 }
630 Namespace::try_from(ns)
631 }
632
633 macro_rules! child_job {
634 () => {
635 job_default().create_child_job().unwrap()
636 };
637 }
638
639 async fn sample_test_component() -> Result<Arc<Component>, Error> {
640 let ns =
641 create_ns_from_current_ns(vec![("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)])?;
642
643 Ok(Arc::new(
644 Component::create_for_tests(BuilderArgs {
645 url: "fuchsia-pkg://fuchsia.com/sample_test#test.cm".to_owned(),
646 name: "test.cm".to_owned(),
647 binary: "bin/test_runners_lib_lib_test".to_owned(), args: vec![],
649 environ: None,
650 ns: ns,
651 job: child_job!(),
652 options: zx::ProcessOptions::empty(),
653 config: None,
654 })
655 .await?,
656 ))
657 }
658
659 async fn dummy_func() -> u32 {
660 2
661 }
662
663 struct DummyServer {}
664
665 #[async_trait]
666 impl SuiteServer for DummyServer {
667 fn run(
668 self,
669 _component: Weak<Component>,
670 _test_url: &str,
671 _stream: fidl_fuchsia_test::SuiteRequestStream,
672 ) -> AbortHandle {
673 let (_, handle) = abortable(async {});
674 handle
675 }
676
677 async fn enumerate_tests(
678 &self,
679 _test_component: Arc<Component>,
680 ) -> Result<EnumeratedTestCases, EnumerationError> {
681 Ok(Arc::new(vec![]))
682 }
683
684 async fn run_tests(
685 &self,
686 _invocations: Vec<Invocation>,
687 _run_options: fidl_fuchsia_test::RunOptions,
688 _component: Arc<Component>,
689 _run_listener: &RunListenerProxy,
690 ) -> Result<(), RunTestError> {
691 Ok(())
692 }
693 }
694
695 #[fuchsia_async::run_singlethreaded(test)]
696 async fn start_component_error() {
697 let start_info = fcrunner::ComponentStartInfo {
698 resolved_url: None,
699 program: None,
700 ns: None,
701 outgoing_dir: None,
702 runtime_dir: None,
703 ..Default::default()
704 };
705 let (client_controller, server_controller) = endpoints::create_proxy();
706 let get_test_server = || DummyServer {};
707 let err = start_component(start_info, server_controller, get_test_server, |_| Ok(())).await;
708 assert_matches!(err, Err(ComponentError::MissingResolvedUrl));
709 let expected_status = zx::Status::from_raw(
710 fcomponent::Error::InvalidArguments.into_primitive().try_into().unwrap(),
711 );
712 let s = assert_matches!(
713 client_controller.take_event_stream().next().await,
714 Some(Err(fidl::Error::ClientChannelClosed { status: s, .. })) => s
715 );
716 assert_eq!(s, expected_status);
717 }
718
719 #[fuchsia_async::run_singlethreaded(test)]
720 async fn start_component_works() {
721 let _ = sample_test_component().await.unwrap();
722 }
723
724 #[fuchsia_async::run_singlethreaded(test)]
725 async fn component_runtime_kill_job_works() {
726 let component = sample_test_component().await.unwrap();
727
728 let mut futs = vec![];
729 let mut handles = vec![];
730 for _i in 0..10 {
731 let (fut, handle) = abortable(dummy_func());
732 futs.push(fut);
733 handles.push(handle);
734 }
735
736 let (out_fut, out_handle) = abortable(dummy_func());
737 let mut runtime = ComponentRuntime::new(
738 out_handle,
739 Arc::new(Mutex::new(handles)),
740 child_job!(),
741 component.clone(),
742 );
743
744 assert_eq!(Arc::strong_count(&component), 2);
745 runtime.kill_self();
746
747 for fut in futs {
748 assert_eq!(fut.await, Err(Aborted));
749 }
750
751 assert_eq!(out_fut.await, Err(Aborted));
752
753 assert_eq!(Arc::strong_count(&component), 1);
754 }
755}