blob: 6f7236bc9822d0879fa09072db309cc3b8a507ae [file] [log] [blame]
// Copyright 2023 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::{anyhow, Context as _, Error},
fidl::endpoints::{create_proxy, Proxy},
fidl::HandleBased,
fidl_fuchsia_component_runner as frunner, fidl_fuchsia_data as fdata, fidl_fuchsia_io as fio,
fidl_fuchsia_process as fprocess,
fidl_fuchsia_test::{self as ftest},
frunner::ComponentNamespaceEntry,
fuchsia_runtime as fruntime, fuchsia_zircon as zx,
fuchsiaperf::FuchsiaPerfBenchmarkResult,
futures::StreamExt,
gtest_runner_lib::parser::read_file,
namespace::Namespace,
tracing::debug,
};
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum TestType {
BinderLatency,
Gbenchmark,
Gtest,
GtestXmlOutput,
Gunit,
Ltp,
SingleTest,
}
pub fn get_opt_str_value_from_dict(
dict: &fdata::Dictionary,
name: &str,
) -> Result<Option<String>, Error> {
match runner::get_value(dict, name) {
Some(fdata::DictionaryValue::Str(value)) => Ok(Some(value.clone())),
Some(_) => Err(anyhow!("{} must a string", name)),
_ => Ok(None),
}
}
pub fn get_str_value_from_dict(dict: &fdata::Dictionary, name: &str) -> Result<String, Error> {
match get_opt_str_value_from_dict(dict, name)? {
Some(s) => Ok(s),
None => Err(anyhow!("{} is not specified", name)),
}
}
pub async fn run_starnix_benchmark(
test: ftest::Invocation,
mut start_info: frunner::ComponentStartInfo,
run_listener_proxy: &ftest::RunListenerProxy,
component_runner: &frunner::ComponentRunnerProxy,
test_data_path: &str,
converter: impl FnOnce(&str, &str) -> Result<Vec<FuchsiaPerfBenchmarkResult>, Error>,
) -> Result<(), Error> {
let (case_listener_proxy, case_listener) = create_proxy::<ftest::CaseListenerMarker>()?;
let (numbered_handles, std_handles) = create_numbered_handles();
start_info.numbered_handles = numbered_handles;
debug!("notifying client test case started");
run_listener_proxy.on_test_case_started(&test, std_handles, case_listener)?;
debug!("getting test suite label");
let program = start_info.program.as_ref().context("No program")?;
let test_suite = get_str_value_from_dict(program, "test_suite_label")?;
// Save the custom_artifacts DirectoryProxy for result reporting.
let custom_artifacts =
get_custom_artifacts_directory(start_info.ns.as_mut().expect("No namespace."))?;
// Environment variables BENCHMARK_FORMAT and BENCHMARK_OUT should be set
// so the test writes json results to this directory.
let output_dir = add_output_dir_to_namespace(&mut start_info)?;
// Start the test component.
let component_controller = start_test_component(start_info, component_runner)?;
let result = read_result(component_controller.take_event_stream()).await;
// Read the output it produced.
let test_data = read_file(&output_dir, test_data_path)
.await
.with_context(|| format!("reading {test_data_path} from test data"))?
.trim()
.to_owned();
let perfs =
converter(&test_data, &test_suite).context("converting test output to fuchsiaperf")?;
// Write JSON to custom artifacts directory where perf test infra expects it.
let file_proxy = fuchsia_fs::directory::open_file(
&custom_artifacts,
"results.fuchsiaperf.json",
fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE,
)
.await?;
fuchsia_fs::file::write(&file_proxy, serde_json::to_string(&perfs)?).await?;
case_listener_proxy.finished(&result)?;
Ok(())
}
/// Replace the arguments in `program` with `test_arguments`, which were provided to the test
/// framework directly.
pub fn replace_program_args(test_arguments: Vec<String>, program: &mut fdata::Dictionary) {
update_program_args(test_arguments, program, false);
}
/// Insert `new_args` into the arguments in `program`.
pub fn append_program_args(new_args: Vec<String>, program: &mut fdata::Dictionary) {
update_program_args(new_args, program, true);
}
/// Clones relevant fields of `start_info`. `&mut` is required to clone the `ns` field, but the
/// `start_info` is not modified.
///
/// The `outgoing_dir` of the result will be present, but unusable.
pub fn clone_start_info(
start_info: &mut frunner::ComponentStartInfo,
) -> Result<frunner::ComponentStartInfo, Error> {
let ns = Namespace::try_from(start_info.ns.take().unwrap())?;
// Reset the namespace of the start_info, since it was moved out above.
start_info.ns = Some(ns.clone().try_into()?);
let (outgoing_dir, _outgoing_dir_server) = zx::Channel::create();
Ok(frunner::ComponentStartInfo {
resolved_url: start_info.resolved_url.clone(),
program: start_info.program.clone(),
ns: Some(ns.try_into()?),
outgoing_dir: Some(outgoing_dir.into()),
runtime_dir: None,
numbered_handles: Some(vec![]),
encoded_config: None,
break_on_start: None,
..Default::default()
})
}
/// Creates numbered handles for a test component with a respective `StdHandles` that should be
/// passed to the `RunListener`.
pub fn create_numbered_handles() -> (Option<Vec<fprocess::HandleInfo>>, ftest::StdHandles) {
let (test_stdin, _) = zx::Socket::create_stream();
let (test_stdout, stdout_client) = zx::Socket::create_stream();
let (test_stderr, stderr_client) = zx::Socket::create_stream();
let stdin_handle_info = fprocess::HandleInfo {
handle: test_stdin.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 0).as_raw(),
};
let stdout_handle_info = fprocess::HandleInfo {
handle: test_stdout.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 1).as_raw(),
};
let stderr_handle_info = fprocess::HandleInfo {
handle: test_stderr.into_handle(),
id: fruntime::HandleInfo::new(fruntime::HandleType::FileDescriptor, 2).as_raw(),
};
let numbered_handles = Some(vec![stdin_handle_info, stdout_handle_info, stderr_handle_info]);
let std_handles = ftest::StdHandles {
out: Some(stdout_client),
err: Some(stderr_client),
..Default::default()
};
(numbered_handles, std_handles)
}
/// Starts the test component and returns its proxy.
pub fn start_test_component(
start_info: frunner::ComponentStartInfo,
component_runner: &frunner::ComponentRunnerProxy,
) -> Result<frunner::ComponentControllerProxy, Error> {
let (component_controller, component_controller_server_end) =
create_proxy::<frunner::ComponentControllerMarker>()?;
debug!(?start_info, "asking kernel to start component");
component_runner.start(start_info, component_controller_server_end)?;
Ok(component_controller)
}
/// Reads the epitaph from the provided `event_stream`.
pub async fn read_component_epitaph(
mut event_stream: frunner::ComponentControllerEventStream,
) -> zx::Status {
match event_stream.next().await {
Some(Err(fidl::Error::ClientChannelClosed { status, .. })) => status,
result => {
tracing::error!(
"Didn't get epitaph from the component controller, instead got: {:?}",
result
);
// Fail the test case here, since the component controller's epitaph couldn't be
// read.
zx::Status::INTERNAL
}
}
}
/// Reads the result of the test run from `event_stream`.
///
/// The result is determined by reading the epitaph from the provided `event_stream`.
pub async fn read_result(event_stream: frunner::ComponentControllerEventStream) -> ftest::Result_ {
match read_component_epitaph(event_stream).await {
zx::Status::OK => {
ftest::Result_ { status: Some(ftest::Status::Passed), ..Default::default() }
}
_ => ftest::Result_ { status: Some(ftest::Status::Failed), ..Default::default() },
}
}
pub fn add_output_dir_to_namespace(
start_info: &mut frunner::ComponentStartInfo,
) -> Result<fio::DirectoryProxy, Error> {
const TEST_DATA_DIR: &str = "/tmp/test_data";
let test_data_path = format!("{}/{}", TEST_DATA_DIR, uuid::Uuid::new_v4());
std::fs::create_dir_all(&test_data_path).expect("cannot create test output directory.");
let data_dir_proxy = fuchsia_fs::directory::open_in_namespace(
&test_data_path,
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE,
)
.expect("Cannot open test data directory.");
let data_dir = data_dir_proxy.into_client_end().expect("Cannot get client end from proxy.");
start_info.ns.as_mut().ok_or(anyhow!("Missing namespace."))?.push(ComponentNamespaceEntry {
path: Some("/test_data".to_string()),
directory: Some(data_dir),
..Default::default()
});
let test_data_dir =
fuchsia_fs::directory::open_in_namespace(&test_data_path, fio::OpenFlags::RIGHT_READABLE)
.expect("Cannot open test data directory.");
Ok(test_data_dir)
}
/// Replace or append the arguments in `program` with `new_args`.
fn update_program_args(mut new_args: Vec<String>, program: &mut fdata::Dictionary, append: bool) {
/// The program argument key name.
const ARGS_KEY: &str = "args";
if new_args.is_empty() {
return;
}
let mut new_entry = fdata::DictionaryEntry {
key: ARGS_KEY.to_string(),
value: Some(Box::new(fdata::DictionaryValue::StrVec(new_args.clone()))),
};
if let Some(entries) = &mut program.entries {
if let Some(index) = entries.iter().position(|entry| entry.key == ARGS_KEY) {
let entry = entries.remove(index);
if append {
if let Some(mut box_value) = entry.value {
if let fdata::DictionaryValue::StrVec(ref mut args) = &mut *box_value {
args.append(&mut new_args);
new_entry.value =
Some(Box::new(fdata::DictionaryValue::StrVec(args.to_vec())));
};
}
}
}
entries.push(new_entry);
} else {
let entries = vec![new_entry];
program.entries = Some(entries);
};
}
fn get_custom_artifacts_directory(
namespace: &mut Vec<frunner::ComponentNamespaceEntry>,
) -> Result<fio::DirectoryProxy, Error> {
for entry in namespace {
if entry.path.as_ref().unwrap() == "/custom_artifacts" {
return entry
.directory
.take()
.unwrap()
.into_proxy()
.map_err(|_| anyhow!("Couldn't grab proxy."));
}
}
Err(anyhow!("Couldn't find /custom artifacts."))
}