diff --git a/src/BUILD.gn b/src/BUILD.gn index 07ea36f447b4be0eac11e642d424e07a6b0a25ab..f134d7f332c318ab3049be1e60ab6843ae134379 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 0000000000000000000000000000000000000000..a20d0efddd2380c5864b5aee79f6d75d4c74d0d3 --- /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 0000000000000000000000000000000000000000..911fde9918cd002abf54efa5ade90ab02f9cd74b --- /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 0000000000000000000000000000000000000000..b2f1f24a0b0ca89062d499197d8fe2fcdc265fe9 --- /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 0000000000000000000000000000000000000000..42558c524581281f8a92a1911e686756bdf98ce9 --- /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 0000000000000000000000000000000000000000..65aadab42de5e8e3609ff11fed2647d1e69d1915 --- /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(()) + } +}