From c835869cebb5ca387d5a4886b6d13ac2a467184f Mon Sep 17 00:00:00 2001
From: James Robinson <jamesr@google.com>
Date: Sat, 27 Apr 2019 02:55:05 +0000
Subject: [PATCH] [sys] Start component_test_runner, a component runner for
 tests

This starts a component_test_runner which is an implementation of the
fuchsia.sys.Runner interface that can instantiate a component within an
environment configured for tests.  The specification of the test
environment and of the component under test is the component manifest
itself. This way a test author can configure different environments to
run a test in without having to modify the code for the component under
test.

This patch responds to incoming Runner requests and starts parsing the
test specification data, but doesn't yet actually attempt to create the
test environment or actually instantiate the component.

Change-Id: Ibc70a60202c026174268a109a376c072bd370a10
---
 src/BUILD.gn                                  |   1 +
 src/sys/BUILD.gn                              |  10 +
 src/sys/component_test_runner/BUILD.gn        |  66 +++++
 .../meta/component_test_runner.cmx            |  11 +
 .../meta/component_test_runner_tests.cmx      |   6 +
 src/sys/component_test_runner/src/main.rs     | 270 ++++++++++++++++++
 6 files changed, 364 insertions(+)
 create mode 100644 src/sys/BUILD.gn
 create mode 100644 src/sys/component_test_runner/BUILD.gn
 create mode 100644 src/sys/component_test_runner/meta/component_test_runner.cmx
 create mode 100644 src/sys/component_test_runner/meta/component_test_runner_tests.cmx
 create mode 100644 src/sys/component_test_runner/src/main.rs

diff --git a/src/BUILD.gn b/src/BUILD.gn
index 07ea36f447b..f134d7f332c 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -16,6 +16,7 @@ group("src") {
     "modular",
     "recovery",
     "speech",
+    "sys",
     "testing",
   ]
 }
diff --git a/src/sys/BUILD.gn b/src/sys/BUILD.gn
new file mode 100644
index 00000000000..a20d0efddd2
--- /dev/null
+++ b/src/sys/BUILD.gn
@@ -0,0 +1,10 @@
+# Copyright 2019 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.
+
+group("sys") {
+  testonly = true
+  deps = [
+    "component_test_runner",
+  ]
+}
diff --git a/src/sys/component_test_runner/BUILD.gn b/src/sys/component_test_runner/BUILD.gn
new file mode 100644
index 00000000000..911fde9918c
--- /dev/null
+++ b/src/sys/component_test_runner/BUILD.gn
@@ -0,0 +1,66 @@
+# Copyright 2019 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.
+
+import("//build/package.gni")
+import("//build/rust/rustc_binary.gni")
+
+rustc_binary("bin") {
+  name = "component_test_runner"
+
+  edition = "2018"
+
+  with_unit_tests = true
+
+  deps = [
+    "//garnet/lib/rust/fuchsia_uri",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/fuchsia-async",
+    "//garnet/public/rust/fuchsia-component",
+    "//garnet/public/rust/fuchsia-runtime",
+    "//garnet/public/rust/fuchsia-syslog",
+    "//garnet/public/rust/fuchsia-zircon",
+    "//sdk/fidl/fuchsia.sys:fuchsia.sys-rustc",
+    "//third_party/rust_crates:failure",
+    "//third_party/rust_crates:futures-preview",
+    "//third_party/rust_crates:serde_json",
+    "//zircon/public/fidl/fuchsia-io:fuchsia-io-rustc",
+  ]
+}
+
+package("component_test_runner") {
+  deps = [
+    ":bin",
+  ]
+
+  binaries = [
+    {
+      name = "component_test_runner"
+    },
+  ]
+
+  meta = [
+    {
+      path = "meta/component_test_runner.cmx"
+      dest = "component_test_runner.cmx"
+    },
+  ]
+}
+
+package("component_test_runner_tests") {
+  deps = [
+    ":bin",
+  ]
+
+  tests = [
+    {
+      name = "component_test_runner_bin_test"
+    },
+  ]
+  meta = [
+    {
+      path = "meta/component_test_runner_tests.cmx"
+      dest = "component_test_runner_tests.cmx"
+    },
+  ]
+}
diff --git a/src/sys/component_test_runner/meta/component_test_runner.cmx b/src/sys/component_test_runner/meta/component_test_runner.cmx
new file mode 100644
index 00000000000..b2f1f24a0b0
--- /dev/null
+++ b/src/sys/component_test_runner/meta/component_test_runner.cmx
@@ -0,0 +1,11 @@
+{
+    "program": {
+        "binary": "bin/component_test_runner"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Loader"
+        ]
+    }
+}
diff --git a/src/sys/component_test_runner/meta/component_test_runner_tests.cmx b/src/sys/component_test_runner/meta/component_test_runner_tests.cmx
new file mode 100644
index 00000000000..42558c52458
--- /dev/null
+++ b/src/sys/component_test_runner/meta/component_test_runner_tests.cmx
@@ -0,0 +1,6 @@
+{
+    "program": {
+        "binary": "test/component_test_runner_bin_test"
+    },
+    "sandbox": {}
+}
diff --git a/src/sys/component_test_runner/src/main.rs b/src/sys/component_test_runner/src/main.rs
new file mode 100644
index 00000000000..65aadab42de
--- /dev/null
+++ b/src/sys/component_test_runner/src/main.rs
@@ -0,0 +1,270 @@
+// Copyright 2019 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.
+
+#![feature(async_await, await_macro, futures_api)]
+
+use failure::{bail, format_err, Error, ResultExt};
+use fidl::endpoints::{create_proxy, ServerEnd};
+use fidl_fuchsia_io::{DirectoryProxy, FileMarker, NodeMarker};
+use fidl_fuchsia_sys::{FlatNamespace, RunnerRequest, RunnerRequestStream};
+use fuchsia_async as fasync;
+use fuchsia_component::server::ServiceFs;
+use fuchsia_syslog::fx_log_info;
+use fuchsia_uri::pkg_uri::PkgUri;
+use fuchsia_zircon as zx;
+use futures::prelude::*;
+
+use std::mem;
+
+fn manifest_path_from_url(url: &str) -> Result<String, Error> {
+    match PkgUri::parse(url) {
+        Ok(uri) => match uri.resource() {
+            Some(r) => Ok(r.to_string()),
+            None => bail!("no resource"),
+        },
+        Err(e) => Err(e),
+    }
+    .map_err(|e| format_err!("parse error {}", e))
+}
+
+fn extract_directory_with_name(ns: &mut FlatNamespace, name: &str) -> Result<zx::Channel, Error> {
+    let handle_ref = ns
+        .paths
+        .iter()
+        .zip(ns.directories.iter_mut())
+        .find(|(n, _)| n.as_str() == name)
+        .ok_or_else(|| format_err!("could not find entry matching {}", name))
+        .map(|x| x.1)?;
+    Ok(mem::replace(handle_ref, zx::Channel::from(zx::Handle::invalid())))
+}
+
+async fn file_contents_at_path(dir: zx::Channel, path: &str) -> Result<Vec<u8>, Error> {
+    let dir_proxy = DirectoryProxy::new(fasync::Channel::from_channel(dir)?);
+
+    let (file, server) = create_proxy::<FileMarker>()?;
+
+    dir_proxy.open(0, 0, path, ServerEnd::<NodeMarker>::new(server.into_channel()))?;
+
+    let attr = await!(file.get_attr())?.1;
+
+    let (_, vec) = await!(file.read(attr.content_size))?;
+    Ok(vec)
+}
+
+#[derive(Default)]
+#[allow(unused)]
+struct TestFacet {
+    component_under_test: String,
+    injected_services: Vec<String>,
+    system_services: Vec<String>,
+}
+
+// TODO(jamesr): Use serde to validate and deserialize the facet directly.
+// See //garnet/bin/cmc/src/validate.rs for reference.
+fn test_facet(meta: &serde_json::Value) -> Result<TestFacet, Error> {
+    let facets = meta.get("facets").ok_or_else(|| format_err!("no facets"))?;
+    if !facets.is_object() {
+        bail!("facet not an object");
+    }
+    let fuchsia_test_facet = match facets.get("fuchsia.test") {
+        Some(v) => v,
+        None => bail!("no fuchsia.test facet"),
+    };
+    if !fuchsia_test_facet.is_object() {
+        bail!("fuchsia.test facet not an object");
+    }
+    let component_under_test = match fuchsia_test_facet.get("component_under_test") {
+        Some(v) => v,
+        None => bail!("no component_under_test definition in fuchsia.test facet"),
+    };
+    let component_under_test = component_under_test
+        .as_str()
+        .ok_or_else(|| format_err!("component_under_test in fuchsia.test facet not a string"))?
+        .to_string();
+    Ok(TestFacet {
+        component_under_test: component_under_test,
+        injected_services: Vec::new(),
+        system_services: Vec::new(),
+    })
+}
+
+async fn run_runner_server(mut stream: RunnerRequestStream) -> Result<(), Error> {
+    while let Some(RunnerRequest::StartComponent {
+        package,
+        mut startup_info,
+        controller: _,
+        control_handle,
+    }) = await!(stream.try_next()).context("error running server")?
+    {
+        fx_log_info!("Received runner request for component {}", package.resolved_url);
+
+        let manifest_path = manifest_path_from_url(&package.resolved_url)?;
+
+        fx_log_info!("Component manifest path {}", manifest_path);
+
+        let pkg_directory_channel =
+            extract_directory_with_name(&mut startup_info.flat_namespace, "/pkg")?;
+
+        fx_log_info!("Found package directory handle");
+
+        let meta_contents = await!(file_contents_at_path(pkg_directory_channel, &manifest_path))?;
+
+        fx_log_info!("Meta contents: {:#?}", std::str::from_utf8(&meta_contents)?);
+
+        let meta = serde_json::from_slice::<serde_json::Value>(&meta_contents)?;
+
+        fx_log_info!("Found metadata: {:#?}", meta);
+
+        let _f = test_facet(&meta)?;
+
+        //TODO(jamesr): Configure realm based on |f| then instantiate and watch
+        //|f.component_under_test| within that realm.
+
+        control_handle.shutdown();
+    }
+    Ok(())
+}
+
+enum IncomingServices {
+    Runner(RunnerRequestStream),
+    // ... more services here
+}
+
+#[fasync::run_singlethreaded]
+async fn main() -> Result<(), Error> {
+    fuchsia_syslog::init_with_tags(&["component_test_runner"])?;
+
+    let mut fs = ServiceFs::new_local();
+    fs.dir("public").add_fidl_service(IncomingServices::Runner);
+
+    fs.take_and_serve_directory_handle()?;
+
+    const MAX_CONCURRENT: usize = 10_000;
+    let fut = fs.for_each_concurrent(MAX_CONCURRENT, |IncomingServices::Runner(stream)| {
+        run_runner_server(stream).unwrap_or_else(|e| println!("{:?}", e))
+    });
+
+    await!(fut);
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use fuchsia_zircon::HandleBased;
+    use serde_json::json;
+
+    /// Makes a manifest_path_from_url test
+    /// Arguments:
+    ///     name: name of the test case
+    ///     url: url to parse
+    ///     path: expected path component
+    ///     err: true if an error is expected
+    macro_rules! manifest_path_from_url_test {
+        ( $name:ident, $url:literal, $path:literal, $err:literal ) => {
+            #[test]
+            fn $name() {
+                match manifest_path_from_url($url) {
+                    Ok(path) => {
+                        assert!(!$err);
+                        assert_eq!(path, $path)
+                    }
+                    Err(_) => assert!($err),
+                }
+            }
+        };
+    }
+
+    manifest_path_from_url_test!(empty_string, "", "", true);
+    manifest_path_from_url_test!(no_hash, "fuchsia-pkg://foo/abcdef", "", true);
+    manifest_path_from_url_test!(one_hash, "fuchsia-pkg://foo/abc#def", "def", false);
+    manifest_path_from_url_test!(multiple_hash, "fuchsia-pkg://foo/abc#def#ghi", "def#ghi", false);
+    manifest_path_from_url_test!(last_position_hash, "fuchsia-pkg://foo/abc#", "", true);
+
+    #[test]
+    fn directory_with_name_tests() -> Result<(), Error> {
+        let (a_ch_0, _) = zx::Channel::create()?;
+        let (b_ch_0, _) = zx::Channel::create()?;
+        let mut ns = FlatNamespace {
+            paths: vec![String::from("/a"), String::from("/b")],
+            directories: vec![a_ch_0, b_ch_0],
+        };
+
+        assert!(extract_directory_with_name(&mut ns, "/c/").is_err());
+
+        assert!(extract_directory_with_name(&mut ns, "/b").is_ok());
+        assert!(ns.directories[1].is_invalid_handle());
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_missing_facet() -> Result<(), Error> {
+        let meta = json!({});
+        let f = test_facet(&meta);
+        assert!(f.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_missing_fuchsia_test_facet() -> Result<(), Error> {
+        let meta = json!({
+          "facets": []
+        });
+        let f = test_facet(&meta);
+        assert!(f.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_facets_wrong_type() -> Result<(), Error> {
+        let meta = json!({
+          "facets": []
+        });
+        let f = test_facet(&meta);
+        assert!(f.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_fuchsia_test_facet_wrong_type() -> Result<(), Error> {
+        let meta = json!({
+          "facets": {
+            "fuchsia.test": []
+          }
+        });
+        let f = test_facet(&meta);
+        assert!(f.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_component_under_test() -> Result<(), Error> {
+        let meta = json!({
+          "facets": {
+            "fuchsia.test": {
+              "component_under_test": "fuchsia-pkg://fuchsia.com/test#meta/test.cmx"
+            }
+          }
+        });
+        let f = test_facet(&meta);
+        assert!(!f.is_err());
+        assert_eq!(f?.component_under_test, "fuchsia-pkg://fuchsia.com/test#meta/test.cmx");
+        Ok(())
+    }
+
+    #[test]
+    fn test_facet_component_under_test_not_string() -> Result<(), Error> {
+        let meta = json!({
+          "facets": {
+            "fuchsia.test": {
+              "component_under_test": 42
+            }
+          }
+        });
+        let f = test_facet(&meta);
+        assert!(f.is_err());
+        Ok(())
+    }
+}
-- 
GitLab