[cm][test_runners] Clone UTC clock handle when spawning tests

Bug: 68080
Multiply: fuchsia-pkg://fuchsia.com/fs-tests#meta/attr-tests.cm
Test: test-runner-unit-tests
Change-Id: I08e10d01f1033d66f2c34331fdf4f691665cd6b6
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/475977
Fuchsia-Auto-Submit: Adam Lesinski <adamlesinski@google.com>
Reviewed-by: Ankur Mittal <anmittal@google.com>
Reviewed-by: Shai Barack <shayba@google.com>
Reviewed-by: Jody Sankey <jsankey@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
diff --git a/src/lib/fuchsia-runtime/src/lib.rs b/src/lib/fuchsia-runtime/src/lib.rs
index 1c201aa..75f9afb 100644
--- a/src/lib/fuchsia-runtime/src/lib.rs
+++ b/src/lib/fuchsia-runtime/src/lib.rs
@@ -14,9 +14,10 @@
 
 use {
     fuchsia_zircon::{
-        sys::{zx_handle_t, ZX_HANDLE_INVALID}, // handle type (primitive, non-owning)
+        sys::{zx_handle_t, zx_status_t, ZX_HANDLE_INVALID}, // handle type (primitive, non-owning)
         Clock,
         Handle,
+        HandleBased,
         Job,
         Process,
         Rights,
@@ -41,6 +42,10 @@
     pub fn zx_vmar_root_self() -> zx_handle_t;
     pub fn zx_job_default() -> zx_handle_t;
     pub fn zx_utc_reference_get() -> zx_handle_t;
+    pub fn zx_utc_reference_swap(
+        new_handle: zx_handle_t,
+        prev_handle: *mut zx_handle_t,
+    ) -> zx_status_t;
 }
 
 /// Handle types as defined by the processargs protocol.
@@ -336,6 +341,19 @@
     handle.duplicate(rights)
 }
 
+/// Swaps the current process-global UTC clock with `new_clock`, returning
+/// the old clock on success.
+/// If `new_clock` is a valid handle but does not have the ZX_RIGHT_READ right,
+/// an error is returned and `new_clock` is dropped.
+pub fn swap_utc_clock_handle(new_clock: Clock) -> Result<Clock, Status> {
+    Ok(unsafe {
+        let mut prev_handle = ZX_HANDLE_INVALID;
+        Status::ok(zx_utc_reference_swap(new_clock.into_raw(), &mut prev_handle))?;
+        Handle::from_raw(prev_handle)
+    }
+    .into())
+}
+
 /// Reads time from the UTC `Clock` registered with the runtime.
 ///
 /// # Panics
diff --git a/src/sys/test_runners/src/launch.rs b/src/sys/test_runners/src/launch.rs
index 4a4972a..a059d8c 100644
--- a/src/sys/test_runners/src/launch.rs
+++ b/src/sys/test_runners/src/launch.rs
@@ -14,7 +14,7 @@
     runner::component::ComponentNamespace,
     runtime::{HandleInfo, HandleType},
     thiserror::Error,
-    zx::{AsHandleRef, HandleBased, Process, Task},
+    zx::{AsHandleRef, HandleBased, Process, Rights, Task},
 };
 
 /// Error encountered while launching a component.
@@ -41,6 +41,9 @@
     #[error("Error launching process, cannot create socket {:?}", _0)]
     CreateSocket(zx::Status),
 
+    #[error("Error cloning UTC clock: {:?}", _0)]
+    UtcClock(zx::Status),
+
     #[error("unexpected error")]
     UnExpectedError,
 }
@@ -61,7 +64,8 @@
     pub name_infos: Option<Vec<fproc::NameInfo>>,
     /// Process environment variables.
     pub environs: Option<Vec<String>>,
-    // Extra handle infos to add. Handles for stdout and stderr are added.
+    /// Extra handle infos to add. Handles for stdout, stderr, and utc_clock are added.
+    /// The UTC clock handle is cloned from the current process.
     pub handle_infos: Option<Vec<fproc::HandleInfo>>,
 }
 
@@ -70,7 +74,13 @@
     args: LaunchProcessArgs<'_>,
 ) -> Result<(Process, ScopedJob, LoggerStream), LaunchError> {
     let launcher = connect_to_service::<fproc::LauncherMarker>().map_err(LaunchError::Launcher)?;
+    launch_process_impl(args, launcher).await
+}
 
+async fn launch_process_impl(
+    args: LaunchProcessArgs<'_>,
+    launcher: fproc::LauncherProxy,
+) -> Result<(Process, ScopedJob, LoggerStream), LaunchError> {
     const STDOUT: u16 = 1;
     const STDERR: u16 = 2;
 
@@ -89,6 +99,15 @@
         id: HandleInfo::new(HandleType::FileDescriptor, STDERR).as_raw(),
     });
 
+    handle_infos.push(fproc::HandleInfo {
+        handle: runtime::duplicate_utc_clock_handle(
+            Rights::DUPLICATE | Rights::READ | Rights::WAIT | Rights::TRANSFER,
+        )
+        .map_err(LaunchError::UtcClock)?
+        .into_handle(),
+        id: HandleInfo::new(HandleType::ClockUtc, 0).as_raw(),
+    });
+
     // Load the component
     let mut launch_info =
         runner::component::configure_launcher(runner::component::LauncherConfigArgs {
@@ -150,7 +169,15 @@
 
 #[cfg(test)]
 mod tests {
-    use {super::*, fuchsia_runtime::job_default, fuchsia_zircon as zx};
+    use {
+        super::*,
+        fidl::endpoints::{create_proxy_and_stream, ClientEnd, Proxy},
+        fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_io as fio, fuchsia_async as fasync,
+        fuchsia_runtime::{job_default, process_self, swap_utc_clock_handle},
+        fuchsia_zircon as zx,
+        futures::prelude::*,
+        std::convert::TryInto,
+    };
 
     #[test]
     fn scoped_job_works() {
@@ -184,4 +211,83 @@
         // make sure we got back same job handle.
         assert_eq!(ret_job.raw_handle(), raw_handle);
     }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn utc_clock_is_cloned() {
+        let clock =
+            zx::Clock::create(zx::ClockOpts::MONOTONIC, None).expect("failed to create clock");
+        let expected_clock_koid =
+            clock.as_handle_ref().get_koid().expect("failed to get clock koid");
+
+        // We are affecting the process-wide clock here, but since Rust test cases are run in their
+        // own process, this won't interact with other running tests.
+        let _ = swap_utc_clock_handle(clock).expect("failed to swap clocks");
+
+        // We can't fake all the arguments, as there is actual IO happening. Pass in the bare
+        // minimum that a process needs, and use this test's process handle for real values.
+        let pkg = io_util::open_directory_in_namespace(
+            "/pkg",
+            fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
+        )
+        .expect("failed to open pkg");
+        let args = LaunchProcessArgs {
+            bin_path: "bin/test_runners_lib_lib_test", // path to this binary
+            environs: None,
+            args: None,
+            job: None,
+            process_name: "foo",
+            name_infos: None,
+            handle_infos: None,
+            ns: vec![fcrunner::ComponentNamespaceEntry {
+                path: Some("/pkg".into()),
+                directory: Some(ClientEnd::new(pkg.into_channel().unwrap().into_zx_channel())),
+                ..fcrunner::ComponentNamespaceEntry::EMPTY
+            }]
+            .try_into()
+            .unwrap(),
+        };
+        let (mock_proxy, mut mock_stream) = create_proxy_and_stream::<fproc::LauncherMarker>()
+            .expect("failed to create mock handles");
+        let mock_fut = async move {
+            let mut all_handles = vec![];
+            while let Some(request) =
+                mock_stream.try_next().await.expect("failed to get next message")
+            {
+                match request {
+                    fproc::LauncherRequest::AddHandles { handles, .. } => {
+                        all_handles.extend(handles);
+                    }
+                    fproc::LauncherRequest::Launch { responder, .. } => {
+                        responder
+                            .send(
+                                zx::Status::OK.into_raw(),
+                                Some(
+                                    process_self()
+                                        .duplicate(Rights::SAME_RIGHTS)
+                                        .expect("failed to duplicate process handle"),
+                                ),
+                            )
+                            .expect("failed to send reply");
+                    }
+                    _ => {}
+                }
+            }
+            return all_handles;
+        };
+        let client_fut = async move {
+            let _ = launch_process_impl(args, mock_proxy).await.expect("failed to launch process");
+        };
+
+        let (all_handles, ()) = futures::future::join(mock_fut, client_fut).await;
+        let clock_id = HandleInfo::new(HandleType::ClockUtc, 0).as_raw();
+
+        let utc_clock_handle = all_handles
+            .into_iter()
+            .find_map(
+                |hi: fproc::HandleInfo| if hi.id == clock_id { Some(hi.handle) } else { None },
+            )
+            .expect("UTC clock handle");
+        let clock_koid = utc_clock_handle.get_koid().expect("failed to get koid");
+        assert_eq!(expected_clock_koid, clock_koid);
+    }
 }