blob: c2fd280960972688c63988980d2fab6501462bce [file] [log] [blame]
// Copyright 2020 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.
#![warn(missing_docs)]
//! Crate to provide fidl logging and test setup helpers for conformance tests
//! for fuchsia.io.
use {
async_trait::async_trait,
fidl::{
endpoints::{create_proxy, ClientEnd, ProtocolMarker, Proxy},
prelude::*,
},
fidl_fuchsia_io as fio, fidl_fuchsia_io_test as io_test,
fuchsia_async::{DurationExt, TimeoutExt},
fuchsia_zircon as zx,
futures::{StreamExt as _, TryStreamExt as _},
};
/// Test harness helper struct.
pub mod test_harness;
/// Utility functions for getting combinations of flags.
pub mod flags;
/// A common name for a file to create in a conformance test.
pub const TEST_FILE: &str = "testing.txt";
/// A common set of file contents to write into a test file in a conformance test.
pub const TEST_FILE_CONTENTS: &[u8] = "abcdef".as_bytes();
/// A default value for NodeAttributes, with zeros set for all fields.
pub const EMPTY_NODE_ATTRS: fio::NodeAttributes = fio::NodeAttributes {
mode: 0,
id: 0,
content_size: 0,
storage_size: 0,
link_count: 0,
creation_time: 0,
modification_time: 0,
};
/// Wait for [`fio::NodeEvent::OnOpen_`] to be sent via `node_proxy` and returns its [`zx::Status`].
pub async fn get_open_status(node_proxy: &fio::NodeProxy) -> zx::Status {
let mut events = Clone::clone(node_proxy).take_event_stream();
if let Some(result) = events.next().await {
match result.expect("FIDL error") {
fio::NodeEvent::OnOpen_ { s, info: _ } => zx::Status::from_raw(s),
fio::NodeEvent::OnRepresentation { .. } => panic!(
"This function should only be used with fuchsia.io/Directory.Open, *not* Open2!"
),
}
} else {
zx::Status::PEER_CLOSED
}
}
/// Asserts that no [`fio::NodeEvent::OnOpen_`] event is sent on an opened proxy.
pub async fn assert_on_open_not_received(node_proxy: &fio::NodeProxy) {
let mut events = Clone::clone(node_proxy).take_event_stream();
// Wait at most 200ms for an OnOpen event to appear.
let event =
events.next().on_timeout(zx::Duration::from_millis(200).after_now(), || Option::None).await;
assert!(event.is_none(), "Unexpected OnOpen event received");
}
/// Converts a generic [`fio::NodeProxy`] to either [`fio::FileProxy`] or [`fio::DirectoryProxy`].
/// **WARNING**: This function does _not_ verify that the conversion is valid.
pub fn convert_node_proxy<T: Proxy>(proxy: fio::NodeProxy) -> T {
T::from_channel(proxy.into_channel().expect("Cannot convert node proxy to channel"))
}
/// Helper function to open the desired node in the root folder.
/// Asserts that open_node_status succeeds.
pub async fn open_node<T: ProtocolMarker>(
dir: &fio::DirectoryProxy,
flags: fio::OpenFlags,
path: &str,
) -> T::Proxy {
open_node_status::<T>(dir, flags, path)
.await
.unwrap_or_else(|e| panic!("open_node_status failed for {path} (flags={flags:?}): {e:?}"))
}
/// Helper function to open the desired node in the root folder.
pub async fn open_node_status<T: ProtocolMarker>(
dir: &fio::DirectoryProxy,
flags: fio::OpenFlags,
path: &str,
) -> Result<T::Proxy, zx::Status> {
let flags = flags | fio::OpenFlags::DESCRIBE;
let (node_proxy, node_server) = create_proxy::<fio::NodeMarker>().expect("Cannot create proxy");
dir.open(flags, fio::ModeType::empty(), path, node_server).expect("Cannot open node");
let status = get_open_status(&node_proxy).await;
if status != zx::Status::OK {
Err(status)
} else {
Ok(convert_node_proxy(node_proxy))
}
}
/// Helper function to open a file with the given flags. Only use this if testing something other
/// than the open call directly.
pub async fn open_file_with_flags(
parent_dir: &fio::DirectoryProxy,
flags: fio::OpenFlags,
path: &str,
) -> fio::FileProxy {
open_node::<fio::FileMarker>(&parent_dir, flags | fio::OpenFlags::NOT_DIRECTORY, path).await
}
/// Helper function to open a sub-directory with the given flags. Only use this if testing
/// something other than the open call directly.
pub async fn open_dir_with_flags(
parent_dir: &fio::DirectoryProxy,
flags: fio::OpenFlags,
path: &str,
) -> fio::DirectoryProxy {
open_node::<fio::DirectoryMarker>(&parent_dir, flags | fio::OpenFlags::DIRECTORY, path).await
}
/// Helper function to open a sub-directory as readable and writable. Only use this if testing
/// something other than the open call directly.
pub async fn open_rw_dir(parent_dir: &fio::DirectoryProxy, path: &str) -> fio::DirectoryProxy {
open_dir_with_flags(
parent_dir,
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE,
path,
)
.await
}
/// Helper function to call `get_token` on a directory. Only use this if testing something
/// other than the `get_token` call directly.
pub async fn get_token(dir: &fio::DirectoryProxy) -> fidl::Handle {
let (status, token) = dir.get_token().await.expect("get_token failed");
assert_eq!(zx::Status::from_raw(status), zx::Status::OK);
token.expect("handle missing")
}
/// Helper function to read a file and return its contents. Only use this if testing something other
/// than the read call directly.
pub async fn read_file(dir: &fio::DirectoryProxy, path: &str) -> Vec<u8> {
let file = open_file_with_flags(dir, fio::OpenFlags::RIGHT_READABLE, path).await;
file.read(100).await.expect("read failed").map_err(zx::Status::from_raw).expect("read error")
}
/// Attempts to open the given file, and checks the status is `NOT_FOUND`.
pub async fn assert_file_not_found(dir: &fio::DirectoryProxy, path: &str) {
let (file_proxy, file_server) = create_proxy::<fio::NodeMarker>().expect("Cannot create proxy");
dir.open(
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::DESCRIBE,
fio::ModeType::empty(),
path,
file_server,
)
.expect("Cannot open file");
assert_eq!(get_open_status(&file_proxy).await, zx::Status::NOT_FOUND);
}
/// Returns the .name field from a given DirectoryEntry, otherwise panics.
pub fn get_directory_entry_name(dir_entry: &io_test::DirectoryEntry) -> String {
use io_test::DirectoryEntry;
match dir_entry {
DirectoryEntry::Directory(entry) => entry.name.as_ref(),
DirectoryEntry::RemoteDirectory(entry) => entry.name.as_ref(),
DirectoryEntry::File(entry) => entry.name.as_ref(),
DirectoryEntry::ExecutableFile(entry) => entry.name.as_ref(),
}
.expect("DirectoryEntry name is None!")
.clone()
}
/// Asserts that the given `vmo_rights` align with the `expected_vmo_rights` passed to a
/// get_backing_memory call. We check that the returned rights align with and do not exceed those
/// in the given flags, that we have at least basic VMO rights, and that the flags align with the
/// expected sharing mode.
pub fn validate_vmo_rights(vmo: &zx::Vmo, expected_vmo_rights: fio::VmoFlags) {
let vmo_rights: zx::Rights = vmo.basic_info().expect("failed to get VMO info").rights;
// Ensure that we have at least some basic rights.
assert!(vmo_rights.contains(zx::Rights::BASIC));
assert!(vmo_rights.contains(zx::Rights::MAP));
assert!(vmo_rights.contains(zx::Rights::GET_PROPERTY));
// Ensure the returned rights match and do not exceed those we requested in `expected_vmo_rights`.
assert!(
vmo_rights.contains(zx::Rights::READ) == expected_vmo_rights.contains(fio::VmoFlags::READ)
);
assert!(
vmo_rights.contains(zx::Rights::WRITE)
== expected_vmo_rights.contains(fio::VmoFlags::WRITE)
);
assert!(
vmo_rights.contains(zx::Rights::EXECUTE)
== expected_vmo_rights.contains(fio::VmoFlags::EXECUTE)
);
// Make sure we get SET_PROPERTY if we specified a private copy.
if expected_vmo_rights.contains(fio::VmoFlags::PRIVATE_CLONE) {
assert!(vmo_rights.contains(zx::Rights::SET_PROPERTY));
}
}
/// Creates a directory with the given DirectoryEntry, opening the file with the given
/// file flags, and returning a Buffer object initialized with the given vmo_flags.
pub async fn create_file_and_get_backing_memory(
dir_entry: io_test::DirectoryEntry,
test_harness: &test_harness::TestHarness,
file_flags: fio::OpenFlags,
vmo_flags: fio::VmoFlags,
) -> Result<(zx::Vmo, (fio::DirectoryProxy, fio::FileProxy)), zx::Status> {
let file_path = get_directory_entry_name(&dir_entry);
let root = root_directory(vec![dir_entry]);
let dir_proxy = test_harness.get_directory(root, file_flags);
let file_proxy = open_node_status::<fio::FileMarker>(
&dir_proxy,
file_flags | fio::OpenFlags::NOT_DIRECTORY,
&file_path,
)
.await?;
let vmo = file_proxy
.get_backing_memory(vmo_flags)
.await
.expect("get_backing_memory failed")
.map_err(zx::Status::from_raw)?;
Ok((vmo, (dir_proxy, file_proxy)))
}
/// Constructs a directory from a set of directory entries.
pub fn root_directory(entries: Vec<io_test::DirectoryEntry>) -> io_test::Directory {
// Convert the simple vector of entries into the convoluted FIDL field type.
let entries: Vec<Option<Box<io_test::DirectoryEntry>>> =
entries.into_iter().map(|e| Some(Box::new(e))).collect();
io_test::Directory { name: None, entries: Some(entries), ..Default::default() }
}
/// Makes a subdirectory with a name and a set of entries.
pub fn directory(name: &str, entries: Vec<io_test::DirectoryEntry>) -> io_test::DirectoryEntry {
let mut dir = root_directory(entries);
dir.name = Some(name.to_string());
io_test::DirectoryEntry::Directory(dir)
}
/// Makes a remote directory with a name, which forwards the requests to the given directory proxy.
pub fn remote_directory(name: &str, remote_dir: fio::DirectoryProxy) -> io_test::DirectoryEntry {
let remote_client = ClientEnd::<fio::DirectoryMarker>::new(
remote_dir.into_channel().unwrap().into_zx_channel(),
);
io_test::DirectoryEntry::RemoteDirectory(io_test::RemoteDirectory {
name: Some(name.to_string()),
remote_client: Some(remote_client),
..Default::default()
})
}
/// Makes a file to be placed in the test directory.
pub fn file(name: &str, contents: Vec<u8>) -> io_test::DirectoryEntry {
io_test::DirectoryEntry::File(io_test::File {
name: Some(name.to_string()),
contents: Some(contents),
..Default::default()
})
}
/// Makes an executable file to be placed in the test directory.
pub fn executable_file(name: &str) -> io_test::DirectoryEntry {
io_test::DirectoryEntry::ExecutableFile(io_test::ExecutableFile {
name: Some(name.to_string()),
..Default::default()
})
}
/// Extension trait for [`fio::DirectoryProxy`] to make interactions with the fuchsia.io protocol
/// less verbose.
#[async_trait]
pub trait DirectoryProxyExt {
/// Open `path` using `node_options`, returning a proxy to the remote resource.
///
/// Waits for [`fio::NodeEvent::OnRepresentation`] if [`fio::NodeFlags::GET_REPRESENTATION`]
/// is specified, otherwise calls `fuchsia.io/Node.GetConnectionInfo` to verify the result.
async fn open2_node<T: ProtocolMarker>(
&self,
path: &str,
node_options: fio::NodeOptions,
) -> Result<T::Proxy, zx::Status>;
/// Similar to [`DirectoryProxyExt::open2_node`], but waits for and returns the
/// [`fio::NodeEvent::OnRepresentation`] event sent when opening a resource.
///
/// Requires [`fio::NodeFlags::GET_REPRESENTATION`] to be specified in `node_options`.
async fn open2_node_get_representation<T: ProtocolMarker>(
&self,
path: &str,
node_options: fio::NodeOptions,
) -> Result<(T::Proxy, fio::Representation), zx::Status>;
}
#[async_trait]
impl DirectoryProxyExt for fio::DirectoryProxy {
async fn open2_node<T: ProtocolMarker>(
&self,
path: &str,
node_options: fio::NodeOptions,
) -> Result<T::Proxy, zx::Status> {
Ok(open2_node_impl::<T>(self, path, node_options).await?.0)
}
async fn open2_node_get_representation<T: ProtocolMarker>(
&self,
path: &str,
node_options: fio::NodeOptions,
) -> Result<(T::Proxy, fio::Representation), zx::Status> {
let get_representation = node_options
.flags
.is_some_and(|flags| flags.contains(fio::NodeFlags::GET_REPRESENTATION));
assert!(
get_representation,
"node_options must specify the GET_REPRESENTATION flag to use this function!"
);
let (proxy, on_representation) = open2_node_impl::<T>(self, path, node_options).await?;
Ok((proxy, on_representation.unwrap()))
}
}
async fn open2_node_impl<T: ProtocolMarker>(
dir: &fio::DirectoryProxy,
path: &str,
node_options: fio::NodeOptions,
) -> Result<(T::Proxy, Option<fio::Representation>), zx::Status> {
let get_representation =
node_options.flags.is_some_and(|flags| flags.contains(fio::NodeFlags::GET_REPRESENTATION));
let (proxy, server) = create_proxy::<fio::NodeMarker>().expect("Cannot create proxy");
dir.open2(path, &fio::ConnectionProtocols::Node(node_options), server.into_channel())
.expect("Failed to call open2");
if get_representation {
// Wait for the OnRepresentation event to verify if opening the resource succeeded.
let representation = Some(get_on_representation_event(&proxy).await?);
return Ok((convert_node_proxy(proxy), representation));
}
// Protocols didn't specify GET_REPRESENTATION, call GetConnectionInfo to test that opening the
// resource succeeded. If that fails, return the epitaph from the channel closure.
proxy.get_connection_info().await.map_err(|e| {
if let fidl::Error::ClientChannelClosed { status, .. } = e {
status
} else {
panic!("Unhandled FIDL error: {:?}", e);
}
})?;
Ok((convert_node_proxy(proxy), None))
}
/// Wait for and return a [`fio::NodeEvent::OnRepresentation`] event sent via `node_proxy`.
async fn get_on_representation_event(
node_proxy: &fio::NodeProxy,
) -> Result<fio::Representation, zx::Status> {
// Try to extract the expected NodeEvent, but map channel epitaphs to zx::Status.
let event = Clone::clone(node_proxy)
.take_event_stream()
.try_next()
.await
.map_err(|e| {
if let fidl::Error::ClientChannelClosed { status, .. } = e {
status
} else {
panic!("Unhandled FIDL error: {:?}", e);
}
})?
.expect("Missing NodeEvent in stream!");
let representation = match event {
fio::NodeEvent::OnRepresentation { payload } => payload,
_ => panic!("Found unexpected NodeEvent type in stream!"),
};
Ok(representation)
}