blob: b93f916b0f161c54ee9ba78fc8cd3764dc5402ae [file] [log] [blame]
// Copyright 2022 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 {
anyhow::Error,
fidl_fuchsia_component_resolution as fresolution, fidl_fuchsia_sys as fv1sys,
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_component_test::LocalComponentHandles,
fuchsia_url::AbsoluteComponentUrl,
futures::{StreamExt, TryStreamExt},
std::sync::Arc,
tracing::{error, warn},
};
fn validate_hermetic_package(
component_url_str: &str,
hermetic_test_package_name: &String,
) -> Result<(), fresolution::ResolverError> {
let component_url = AbsoluteComponentUrl::parse(component_url_str)
.map_err(|_| fresolution::ResolverError::InvalidArgs)?;
let package_name = component_url.name();
if hermetic_test_package_name != package_name.as_ref() {
error!(
"failed to resolve component {}: package {} is not in the test package: '{}'
\nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
for more information.",
&component_url_str, package_name, hermetic_test_package_name
);
return Err(fresolution::ResolverError::PackageNotFound);
}
Ok(())
}
pub async fn serve_hermetic_resolver(
handles: LocalComponentHandles,
hermetic_test_package_name: Arc<String>,
universe_resolver: Arc<fresolution::ResolverProxy>,
) -> Result<(), Error> {
let mut fs = ServiceFs::new();
let mut tasks = vec![];
fs.dir("svc").add_fidl_service(move |mut stream: fresolution::ResolverRequestStream| {
let universe_resolver = universe_resolver.clone();
let hermetic_test_package_name = hermetic_test_package_name.clone();
tasks.push(fasync::Task::local(async move {
while let Some(request) =
stream.try_next().await.expect("failed to serve component resolver")
{
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
let mut result = if let Err(err) =
validate_hermetic_package(&component_url, &hermetic_test_package_name)
{
Err(err)
} else {
universe_resolver.resolve(&component_url).await.unwrap_or_else(|err| {
error!("failed to resolve component {}: {:?}", component_url, err);
Err(fresolution::ResolverError::Internal)
})
};
if let Err(e) = responder.send(&mut result) {
warn!("Failed sending load response for {}: {}", component_url, e);
}
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
let mut result = if let Err(err) =
validate_hermetic_package(&component_url, &hermetic_test_package_name)
{
Err(err)
} else {
universe_resolver
.resolve_with_context(&component_url, &context)
.await
.unwrap_or_else(|err| {
error!(
"failed to resolve component {} with context {:?}: {:?}",
component_url, context, err
);
Err(fresolution::ResolverError::Internal)
})
};
if let Err(e) = responder.send(&mut result) {
warn!("Failed sending load response for {}: {}", component_url, e);
}
}
}
}
}));
});
fs.serve_connection(handles.outgoing_dir.into_channel())?;
fs.collect::<()>().await;
Ok(())
}
async fn hermetic_loader(
component_url_str: &str,
hermetic_test_package_name: &String,
loader_service: fv1sys::LoaderProxy,
) -> Option<Box<fv1sys::Package>> {
let component_url = match AbsoluteComponentUrl::parse(component_url_str) {
Ok(u) => u,
Err(e) => {
warn!("Invalid component url {}: {}", component_url_str, e);
return None;
}
};
let package_name = component_url.package_url().name();
if hermetic_test_package_name != package_name.as_ref() {
error!(
"failed to resolve component {}: package {} is not in the test package: '{}'
\nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
for more information.",
&component_url_str, package_name, hermetic_test_package_name
);
return None;
}
match loader_service.load_url(component_url_str).await {
Ok(r) => r,
Err(e) => {
warn!("can't communicate with global loader: {}", e);
None
}
}
}
pub async fn serve_hermetic_loader(
mut stream: fv1sys::LoaderRequestStream,
hermetic_test_package_name: Arc<String>,
loader_service: fv1sys::LoaderProxy,
) {
while let Some(fv1sys::LoaderRequest::LoadUrl { url, responder }) =
stream.try_next().await.expect("failed to serve loader")
{
let mut result = hermetic_loader(&url, &hermetic_test_package_name, loader_service.clone())
.await
.map(|p| *p);
if let Err(e) = responder.send(result.as_mut()) {
warn!("Failed sending load response for {}: {}", url, e);
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
fidl::endpoints::create_proxy_and_stream,
fuchsia_component_test::error::Error as RealmBuilderError,
fuchsia_component_test::{
Capability, ChildOptions, RealmBuilder, RealmInstance, Ref, Route,
},
};
async fn respond_to_resolve_requests(stream: &mut fresolution::ResolverRequestStream) {
let request = stream
.next()
.await
.expect("did not get next request")
.expect("error getting next request");
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
match component_url.as_str() {
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm" => {
responder.send(&mut Ok(fresolution::Component::EMPTY))
}
"fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm" => {
responder.send(&mut Err(fresolution::ResolverError::ResourceUnavailable))
}
_ => responder.send(&mut Err(fresolution::ResolverError::Internal)),
}
.expect("failed sending response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
// This test only responds to Resolve
error!("this test resolver does not support ResolveWithContext, and could not resolve component URL {:?} with context {:?}", component_url, context);
responder
.send(&mut Err(fresolution::ResolverError::InvalidArgs))
.expect("failed sending response");
}
}
}
async fn respond_to_loader_requests(stream: &mut fv1sys::LoaderRequestStream) {
let request = stream
.next()
.await
.expect("did not get next request")
.expect("error getting next request");
match request {
fv1sys::LoaderRequest::LoadUrl { url, responder } => {
match url.as_str() {
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm" => {
responder.send(Some(&mut fv1sys::Package {
data: None,
directory: None,
resolved_url: url,
}))
}
"fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm" => responder.send(None),
_ => responder.send(None),
}
.expect("failed sending response");
}
}
}
// Constructs a test realm that contains a local system resolver that we
// route to our hermetic resolver.
async fn construct_test_realm(
hermetic_test_package_name: Arc<String>,
mock_universe_resolver: Arc<fresolution::ResolverProxy>,
) -> Result<RealmInstance, RealmBuilderError> {
// Set up a realm to test the hermetic resolver.
let builder = RealmBuilder::new().await?;
let hermetic_resolver = builder
.add_local_child(
"hermetic_resolver",
move |handles| {
Box::pin(serve_hermetic_resolver(
handles,
hermetic_test_package_name.clone(),
mock_universe_resolver.clone(),
))
},
ChildOptions::new(),
)
.await?;
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fresolution::ResolverMarker>())
.from(&hermetic_resolver)
.to(Ref::parent()),
)
.await?;
builder.build().await
}
#[fuchsia::test]
async fn test_successful_resolve() {
let pkg_name = "package-one".to_string();
let (resolver_proxy, mut resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock universe resolver proxy");
let universe_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(&mut resolver_request_stream).await;
drop(resolver_request_stream);
});
let realm = construct_test_realm(pkg_name.into(), Arc::new(resolver_proxy))
.await
.expect("failed to construct test realm");
let hermetic_resolver_proxy =
realm.root.connect_to_protocol_at_exposed_dir::<fresolution::ResolverMarker>().unwrap();
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
.await
.unwrap(),
Ok(fresolution::Component::EMPTY)
);
universe_resolver_task.await;
}
#[fuchsia::test]
async fn drop_connection_on_resolve() {
let pkg_name = "package-one".to_string();
let (resolver_proxy, mut resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock universe resolver proxy");
let universe_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(&mut resolver_request_stream).await;
drop(resolver_request_stream);
});
let realm = construct_test_realm(pkg_name.into(), Arc::new(resolver_proxy))
.await
.expect("failed to construct test realm");
let hermetic_resolver_proxy =
realm.root.connect_to_protocol_at_exposed_dir::<fresolution::ResolverMarker>().unwrap();
let _ =
hermetic_resolver_proxy.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm");
drop(hermetic_resolver_proxy); // code should not crash
universe_resolver_task.await;
}
// Logging disabled as this outputs ERROR log, which will fail the test.
#[fuchsia::test(logging = false)]
async fn test_package_not_allowed() {
let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock universe resolver proxy");
let realm =
construct_test_realm("package-two".to_string().into(), Arc::new(resolver_proxy))
.await
.expect("failed to construct test realm");
let hermetic_resolver_proxy =
realm.root.connect_to_protocol_at_exposed_dir::<fresolution::ResolverMarker>().unwrap();
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
.await
.unwrap(),
Err(fresolution::ResolverError::PackageNotFound)
);
}
#[fuchsia::test]
async fn test_failed_resolve() {
let (resolver_proxy, mut resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock universe resolver proxy");
let universe_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(&mut resolver_request_stream).await;
drop(resolver_request_stream);
});
let pkg_name = "package-two".to_string();
let realm = construct_test_realm(pkg_name.into(), Arc::new(resolver_proxy))
.await
.expect("failed to construct test realm");
let hermetic_resolver_proxy =
realm.root.connect_to_protocol_at_exposed_dir::<fresolution::ResolverMarker>().unwrap();
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
.await
.unwrap(),
Err(fresolution::ResolverError::ResourceUnavailable)
);
universe_resolver_task.await;
}
#[fuchsia::test]
async fn test_invalid_url() {
let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock universe resolver proxy");
let pkg_name = "package-two".to_string();
let realm = construct_test_realm(pkg_name.into(), Arc::new(resolver_proxy))
.await
.expect("failed to construct test realm");
let hermetic_resolver_proxy =
realm.root.connect_to_protocol_at_exposed_dir::<fresolution::ResolverMarker>().unwrap();
assert_eq!(
hermetic_resolver_proxy.resolve("invalid_url").await.unwrap(),
Err(fresolution::ResolverError::InvalidArgs)
);
}
mod loader {
use super::*;
#[fuchsia::test]
async fn test_successful_loader() {
let pkg_name = "package-one".to_string();
let (loader_proxy, mut loader_request_stream) =
create_proxy_and_stream::<fv1sys::LoaderMarker>()
.expect("failed to create mock loader proxy");
let (proxy, stream) = create_proxy_and_stream::<fv1sys::LoaderMarker>().unwrap();
let _loader_task = fasync::Task::spawn(async move {
respond_to_loader_requests(&mut loader_request_stream).await;
drop(loader_request_stream);
});
let _serve_task =
fasync::Task::spawn(serve_hermetic_loader(stream, pkg_name.into(), loader_proxy));
assert_matches!(
proxy.load_url("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm").await.unwrap(),
Some(_)
);
}
#[fuchsia::test]
async fn drop_connection_on_load() {
let pkg_name = "package-one".to_string();
let (loader_proxy, mut loader_request_stream) =
create_proxy_and_stream::<fv1sys::LoaderMarker>()
.expect("failed to create mock loader proxy");
let (proxy, stream) = create_proxy_and_stream::<fv1sys::LoaderMarker>().unwrap();
let loader_task = fasync::Task::spawn(async move {
respond_to_loader_requests(&mut loader_request_stream).await;
drop(loader_request_stream);
});
let _serve_task =
fasync::Task::spawn(serve_hermetic_loader(stream, pkg_name.into(), loader_proxy));
let _ = proxy.load_url("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm");
drop(proxy);
loader_task.await;
}
// Logging disabled as this outputs ERROR log, which will fail the test.
#[fuchsia::test(logging = false)]
async fn test_package_not_allowed() {
let (loader_proxy, _) = create_proxy_and_stream::<fv1sys::LoaderMarker>()
.expect("failed to create mock loader proxy");
let (proxy, stream) = create_proxy_and_stream::<fv1sys::LoaderMarker>().unwrap();
let _serve_task = fasync::Task::spawn(serve_hermetic_loader(
stream,
"package-two".to_string().into(),
loader_proxy,
));
assert_eq!(
proxy.load_url("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm").await.unwrap(),
None
);
}
#[fuchsia::test]
async fn test_failed_loader() {
let (loader_proxy, mut loader_request_stream) =
create_proxy_and_stream::<fv1sys::LoaderMarker>()
.expect("failed to create mock loader proxy");
let _loader_task = fasync::Task::spawn(async move {
respond_to_loader_requests(&mut loader_request_stream).await;
drop(loader_request_stream);
});
let pkg_name = "package-two".to_string();
let (proxy, stream) = create_proxy_and_stream::<fv1sys::LoaderMarker>().unwrap();
let _serve_task =
fasync::Task::spawn(serve_hermetic_loader(stream, pkg_name.into(), loader_proxy));
assert_eq!(
proxy.load_url("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm").await.unwrap(),
None
);
}
#[fuchsia::test]
async fn test_invalid_url_loader() {
let (loader_proxy, _) = create_proxy_and_stream::<fv1sys::LoaderMarker>()
.expect("failed to create mock loader proxy");
let pkg_name = "package-two".to_string();
let (proxy, stream) = create_proxy_and_stream::<fv1sys::LoaderMarker>().unwrap();
let _serve_task =
fasync::Task::spawn(serve_hermetic_loader(stream, pkg_name.into(), loader_proxy));
assert_eq!(proxy.load_url("invalid_url").await.unwrap(), None);
}
}
}