blob: 8073f128385e75af556ca628809e9b1123a0bb31 [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 crate::test_config;
use anyhow::{ensure, Error};
use std::env::VarError;
use std::fs;
use std::num::ParseIntError;
use std::path::PathBuf;
use structopt::StructOpt;
use thiserror::Error as ThisError;
pub const ENV_TARGETS: &str = "FUCHSIA_TARGETS";
pub const ENV_TIMEOUT_SECONDS: &str = "FUCHSIA_TIMEOUT_SECONDS";
pub const ENV_SDK_TOOL_PATH: &str = "FUCHSIA_SDK_TOOL_PATH";
pub const ENV_RESOURCE_PATH: &str = "FUCHSIA_RESOURCE_PATH";
pub const ENV_CUSTOM_TEST_ARGS: &str = "FUCHSIA_CUSTOM_TEST_ARGS";
pub const ENV_TEST_FILTER: &str = "FUCHSIA_TEST_FILTER";
pub const ENV_OUT_DIR: &str = "FUCHSIA_TEST_OUTDIR";
pub const ENV_PATH: &str = "PATH";
const ENV_STRICT_MODE: &str = "FUCHSIA_TEST_PILOT_STRICT_MODE";
const ALL_ENV_VARS: [&str; 8] = [
ENV_TARGETS,
ENV_TIMEOUT_SECONDS,
ENV_SDK_TOOL_PATH,
ENV_RESOURCE_PATH,
ENV_OUT_DIR,
ENV_CUSTOM_TEST_ARGS,
ENV_TEST_FILTER,
ENV_STRICT_MODE,
];
#[derive(Debug, StructOpt, Clone)]
#[structopt(name = "Test Pilot", about = "Assist Test Execution.")]
struct CommandLineArgs {
/// Path to the host test binary to execute.
#[structopt(
long,
value_name = "FILEPATH",
required = true,
parse(try_from_str = "file_parse_path")
)]
fuchsia_test_bin_path: PathBuf,
/// Path to test configuration.
#[structopt(
long,
value_name = "FILEPATH",
required = true,
parse(try_from_str = "file_parse_path")
)]
fuchsia_test_configuration: PathBuf,
}
/// Error encountered running test manager
#[derive(Debug, PartialEq, Eq)]
pub struct TestPilotArgs {
/// Path to test configuration.
pub test_config: PathBuf,
/// Path to the host test binary to execute.
pub test_bin_path: PathBuf,
/// PATH environment variable.
pub path: String,
/// Fuchsia targets. User can pass in multiple comma separated targets.
pub targets: Vec<String>,
/// Timeout for the test in seconds. This would be passed to host binary if supported.
pub timeout_seconds: Option<u32>,
/// Path to the SDK tools directory.
pub sdk_tools_path: Option<PathBuf>,
/// Path to the resources directory.
pub resource_path: Option<PathBuf>,
/// Comma-separated glob pattern for test cases to run.
pub test_filter: Option<String>,
/// Output directory.
pub out_dir: Option<PathBuf>,
/// Extra arguments to pass to the host test binary as is.
pub custom_test_args: Option<String>,
/// Pass along extra_env_vars if strict mode is disabled.
pub strict_mode: bool,
/// Extra environment variables passed as it is to the test binary.
pub extra_env_vars: Vec<(String, String)>,
}
// allows us to mock std::env for unit tests.
trait Environment {
fn var(&self, key: &str) -> Result<String, VarError>;
fn vars(&self) -> impl Iterator<Item = (String, String)>;
fn remove_var(&mut self, key: &str);
#[cfg(test)]
fn set_var(&mut self, key: &str, value: &str);
}
struct StandardEnvironment;
impl Environment for StandardEnvironment {
fn var(&self, key: &str) -> Result<String, VarError> {
std::env::var(key)
}
fn vars(&self) -> impl Iterator<Item = (String, String)> {
std::env::vars()
}
fn remove_var(&mut self, key: &str) {
std::env::remove_var(key);
}
#[cfg(test)]
fn set_var(&mut self, key: &str, value: &str) {
std::env::set_var(key, value);
}
}
/// Error encountered parsing Test Pilot arguments
#[derive(Debug, ThisError)]
pub enum TestPilotArgsError {
#[error("Error with environment variable '{1}': {0:?}")]
Var(VarError, &'static str),
#[error("Error validating environment variable: '{1}': {0:?}")]
Validation(anyhow::Error, &'static str),
}
/// Error encountered validating config
#[derive(Debug, ThisError, Eq, PartialEq)]
pub enum ConfigError {
#[error("{0} is required for this test.")]
Required(&'static str),
}
macro_rules! parse_optional_var {
($env:expr, $key:expr, $parser:expr) => {
$env.var($key)
.ok()
.map(|v| $parser(&v).map_err(|e| TestPilotArgsError::Validation(e.into(), $key)))
.transpose()
};
}
macro_rules! required_config {
(option: $option:expr, $config_var:ident) => {
if $option.is_none() {
return Err(ConfigError::Required($config_var));
}
};
(vec: $vec:expr, $config_var:ident) => {
if $vec.is_empty() {
return Err(ConfigError::Required($config_var));
}
};
}
impl TestPilotArgs {
pub fn validate_config(
&self,
test_config: &test_config::TestConfiguration,
) -> Result<(), ConfigError> {
match test_config {
test_config::TestConfiguration::V1 { config } => {
for var in &config.requested_vars.known_vars {
match var.as_str() {
ENV_SDK_TOOL_PATH => {
required_config!(option: self.sdk_tools_path, ENV_SDK_TOOL_PATH)
}
ENV_TARGETS => required_config!(vec: self.targets, ENV_TARGETS),
ENV_RESOURCE_PATH => {
required_config!(option: self.resource_path, ENV_RESOURCE_PATH)
}
_ => {}
}
}
}
};
Ok(())
}
pub fn from_env_and_cmd() -> Result<Self, TestPilotArgsError> {
let mut env = StandardEnvironment;
let cmd = CommandLineArgs::from_args();
Self::from_internal(&mut env, cmd)
}
fn from_internal<E: Environment>(
env: &mut E,
cmd: CommandLineArgs,
) -> Result<Self, TestPilotArgsError> {
let mut s = Self {
test_config: cmd.fuchsia_test_configuration,
test_bin_path: cmd.fuchsia_test_bin_path,
path: env.var(ENV_PATH).unwrap_or("".into()),
timeout_seconds: parse_optional_var!(env, ENV_TIMEOUT_SECONDS, parse_u32)?,
sdk_tools_path: parse_optional_var!(env, ENV_SDK_TOOL_PATH, dir_parse_path)?,
resource_path: parse_optional_var!(env, ENV_RESOURCE_PATH, dir_parse_path)?,
out_dir: parse_optional_var!(env, ENV_OUT_DIR, dir_parse_path)?,
targets: env
.var(ENV_TARGETS)
.ok()
.and_then(|v| Some(v.split(',').map(|s| s.trim().to_string()).collect()))
.unwrap_or_default(),
test_filter: env.var(ENV_TEST_FILTER).ok(),
custom_test_args: env.var(ENV_CUSTOM_TEST_ARGS).ok(),
strict_mode: env.var(ENV_STRICT_MODE).ok().map_or(true, |m| !m.eq("0")),
extra_env_vars: vec![],
};
// Remove known env variables so that we can store extra variables to pass them along.
for var in ALL_ENV_VARS {
env.remove_var(var);
}
for v in env.vars() {
s.extra_env_vars.push(v)
}
Ok(s)
}
}
fn parse_u32(s: &str) -> Result<u32, ParseIntError> {
Ok(s.parse::<u32>()?)
}
fn file_parse_path(path: &str) -> Result<PathBuf, Error> {
let path = PathBuf::from(path);
ensure!(path.exists(), "{:?} does not exist", path);
let metadata = fs::metadata(&path)?;
ensure!(metadata.is_file(), "{:?} should be a file", path);
Ok(path)
}
fn dir_parse_path(path: &str) -> Result<PathBuf, Error> {
let path = PathBuf::from(path);
ensure!(path.exists(), "{:?} does not exist", path);
let metadata = fs::metadata(&path)?;
ensure!(metadata.is_dir(), "{:?} should be a directory", path);
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
use structopt::clap::ErrorKind::MissingRequiredArgument;
use tempfile::tempdir;
use tempfile::NamedTempFile;
use test_config::*;
struct MockEnvironment {
variables: std::collections::HashMap<String, String>,
}
impl MockEnvironment {
fn new() -> Self {
Self { variables: std::collections::HashMap::new() }
}
}
impl Environment for MockEnvironment {
fn var(&self, key: &str) -> Result<String, VarError> {
self.variables.get(key).cloned().ok_or(VarError::NotPresent)
}
fn vars(&self) -> impl Iterator<Item = (String, String)> {
self.variables.clone().into_iter()
}
fn set_var(&mut self, key: &str, value: &str) {
self.variables.insert(key.to_string(), value.to_string());
}
fn remove_var(&mut self, key: &str) {
self.variables.remove(key);
}
}
#[test]
fn test_parse_u32() {
assert_eq!(parse_u32("42"), Ok(42));
assert_eq!(parse_u32("0"), Ok(0));
assert_eq!(parse_u32("12345"), Ok(12345));
assert_eq!(parse_u32("100000"), Ok(100000));
// Invalid inputs
assert!(parse_u32("abc").is_err());
assert!(parse_u32("-10").is_err());
assert!(parse_u32("4294967296").is_err()); // Exceeds u32::MAX
}
#[test]
fn test_dir_parse_path() {
// Create a temporary directory
let temp_dir = tempdir().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
// Valid directory path
assert!(dir_parse_path(temp_dir_path.to_str().unwrap()).is_ok());
assert!(dir_parse_path(".").is_ok());
// Clean up temporary directory after the test
temp_dir.close().expect("Failed to close temporary directory");
// Invalid directory path
assert!(dir_parse_path("/non_existent_path").is_err());
// Create a temporary file
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path().to_str().unwrap().to_string();
assert!(dir_parse_path(temp_file_path.as_str()).is_err()); // File path
}
#[test]
fn test_file_parse_path() {
// Create a temporary file
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
// Get the path of the temporary file
let temp_file_path = temp_file.path().to_str().unwrap().to_string();
// Valid file path
assert!(file_parse_path(temp_file_path.as_str()).is_ok());
// Clean up temporary file after the test
temp_file.close().expect("Failed to close temporary file");
// Invalid file path
assert!(file_parse_path("/non_existent_file").is_err());
assert!(file_parse_path("/tmp").is_err()); // Directory path
}
// Validate that known environment variables are cleared
macro_rules! validate_env_cleared {
($var:expr) => {
for v in ALL_ENV_VARS {
$var.var(v).expect_err(&format!("{} should not be present", v));
}
};
}
#[test]
fn test_args() {
let temp_dir = tempdir().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let temp_config_file =
NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_config_file_path = temp_config_file.path();
let mut env = MockEnvironment::new();
let cmd = CommandLineArgs {
fuchsia_test_bin_path: temp_file_path.to_path_buf(),
fuchsia_test_configuration: temp_config_file_path.to_path_buf(),
};
env.set_var(ENV_TARGETS, "target1, target2");
env.set_var(ENV_TIMEOUT_SECONDS, "10");
env.set_var(ENV_SDK_TOOL_PATH, temp_dir_path.to_str().unwrap());
env.set_var(ENV_RESOURCE_PATH, temp_dir_path.to_str().unwrap());
env.set_var(ENV_OUT_DIR, temp_dir_path.to_str().unwrap());
env.set_var(ENV_TEST_FILTER, "test_filter");
env.set_var(ENV_CUSTOM_TEST_ARGS, "custom_args");
let parsed_args =
TestPilotArgs::from_internal(&mut env, cmd).expect("env args should not fail");
// Check the individual arguments
assert_eq!(parsed_args.targets, vec!["target1".to_string(), "target2".to_string()]);
assert_eq!(parsed_args.timeout_seconds, Some(10));
assert_eq!(parsed_args.sdk_tools_path, Some(temp_dir_path.to_path_buf()));
assert_eq!(parsed_args.resource_path, Some(temp_dir_path.to_path_buf()));
assert_eq!(parsed_args.test_bin_path, temp_file_path.to_path_buf());
assert_eq!(parsed_args.test_config, temp_config_file_path.to_path_buf());
assert_eq!(parsed_args.test_filter, Some("test_filter".to_string()));
assert_eq!(parsed_args.out_dir, Some(temp_dir_path.to_path_buf()));
assert!(parsed_args.strict_mode);
assert_eq!(parsed_args.custom_test_args, Some("custom_args".to_string()));
assert_eq!(parsed_args.extra_env_vars, vec![]);
validate_env_cleared!(env);
// Clean up temporary resources after the test
temp_dir.close().expect("Failed to close temporary directory");
temp_file.close().expect("Failed to close temporary file");
}
#[test]
fn test_cmd_args() {
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let temp_config_file =
NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_config_file_path = temp_config_file.path();
let args = vec![
"test_pilot",
"--fuchsia_test_configuration",
temp_config_file_path.to_str().unwrap(),
"--fuchsia_test_bin_path",
temp_file_path.to_str().unwrap(),
];
// Convert the arguments to OsString and pass them to the arg parser.
let args_os: Vec<std::ffi::OsString> =
args.iter().map(|arg| std::ffi::OsString::from(*arg)).collect();
let args_os_slice: &[std::ffi::OsString] = &args_os;
// Parse command-line arguments
let parsed_args = CommandLineArgs::from_iter_safe(args_os_slice)
.expect("command line args should not fail");
// Check the individual arguments
assert_eq!(parsed_args.fuchsia_test_bin_path, temp_file_path.to_path_buf());
assert_eq!(parsed_args.fuchsia_test_configuration, temp_config_file_path.to_path_buf());
// Clean up temporary resources after the test
temp_file.close().expect("Failed to close temporary file");
temp_config_file.close().expect("Failed to close temporary file");
}
#[test]
fn test_cmd_args_missing_required_param() {
// Simulate command-line arguments without '--test_bin_path'
let args = vec!["test_pilot"];
// Convert the arguments to OsString and pass them to the arg parser.
let args_os: Vec<std::ffi::OsString> =
args.iter().map(|arg| std::ffi::OsString::from(*arg)).collect();
let args_os_slice: &[std::ffi::OsString] = &args_os;
// Parse command-line arguments
let parsed_args = CommandLineArgs::from_iter_safe(args_os_slice);
let err = parsed_args.unwrap_err();
assert_eq!(err.kind, MissingRequiredArgument);
assert!(
err.message.contains(
"The following required arguments were not provided:\n --fuchsia_test_bin_path <FILEPATH>\n --fuchsia_test_configuration <FILEPATH>"
),
"{:?}",
err
);
}
#[test]
fn test_args_only_required_params() {
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let mut env = MockEnvironment::new();
let cmd = CommandLineArgs {
fuchsia_test_bin_path: temp_file_path.to_path_buf(),
fuchsia_test_configuration: temp_file_path.to_path_buf(),
};
let parsed_args = TestPilotArgs::from_internal(&mut env, cmd).unwrap();
// Check the individual arguments
assert_eq!(parsed_args.targets, Vec::<String>::new());
assert_eq!(parsed_args.timeout_seconds, None);
assert_eq!(parsed_args.sdk_tools_path, None);
assert_eq!(parsed_args.resource_path, None);
assert_eq!(parsed_args.test_filter, None);
assert_eq!(parsed_args.out_dir, None);
assert_eq!(parsed_args.custom_test_args, None);
assert_eq!(parsed_args.test_bin_path, temp_file_path.to_path_buf());
assert_eq!(parsed_args.test_config, temp_file_path.to_path_buf());
assert_eq!(parsed_args.extra_env_vars, vec![]);
validate_env_cleared!(env);
}
// Test that extra env variables are not cleared.
#[test]
fn test_extra_env_vars() {
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let mut env = MockEnvironment::new();
let cmd = CommandLineArgs {
fuchsia_test_bin_path: temp_file_path.to_path_buf(),
fuchsia_test_configuration: temp_file_path.to_path_buf(),
};
env.set_var("EXTRA_VAR1", "some_str1");
env.set_var("EXTRA_VAR2", "some_str2");
let mut parsed_args = TestPilotArgs::from_internal(&mut env, cmd).unwrap();
validate_env_cleared!(env);
parsed_args.extra_env_vars.sort();
let mut expected = vec![
("EXTRA_VAR1".to_string(), "some_str1".to_string()),
("EXTRA_VAR2".to_string(), "some_str2".to_string()),
];
expected.sort();
assert_eq!(parsed_args.extra_env_vars, expected);
}
#[test]
fn test_args_invalid_timeout_seconds() {
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let mut env = MockEnvironment::new();
let cmd = CommandLineArgs {
fuchsia_test_bin_path: temp_file_path.to_path_buf(),
fuchsia_test_configuration: temp_file_path.to_path_buf(),
};
env.set_var(ENV_TIMEOUT_SECONDS, "abc");
let parsed_args = TestPilotArgs::from_internal(&mut env, cmd);
match parsed_args.unwrap_err() {
TestPilotArgsError::Validation(_e, var) => {
assert_eq!(var, ENV_TIMEOUT_SECONDS);
}
err => panic!("unexpected error: {}", err),
}
}
#[test]
fn test_args_parse_strict_mode() {
let temp_file = NamedTempFile::new().expect("Failed to create temporary file in the test");
let temp_file_path = temp_file.path();
let mut env = MockEnvironment::new();
let cmd = CommandLineArgs {
fuchsia_test_bin_path: temp_file_path.to_path_buf(),
fuchsia_test_configuration: temp_file_path.to_path_buf(),
};
env.set_var(ENV_STRICT_MODE, "0");
let parsed_args = TestPilotArgs::from_internal(&mut env, cmd.clone()).unwrap();
assert_eq!(parsed_args.strict_mode, false);
env.set_var(ENV_STRICT_MODE, "1");
let parsed_args = TestPilotArgs::from_internal(&mut env, cmd.clone()).unwrap();
assert_eq!(parsed_args.strict_mode, true);
}
// Helper function to create a simple TestConfiguration for testing
fn create_test_config_v1() -> TestConfigV1 {
TestConfigV1 {
tags: Vec::new(),
requested_vars: RequestedVars::default(),
execution: serde_json::json!({}),
}
}
#[test]
fn test_validate_success() {
let args = TestPilotArgs {
test_config: PathBuf::from("test_config.json"),
test_bin_path: PathBuf::from("test_bin"),
path: "/some/path".into(),
targets: vec!["target1".to_string()],
timeout_seconds: Some(30),
sdk_tools_path: Some(PathBuf::from("sdk_tools")),
resource_path: Some(PathBuf::from("resources")),
test_filter: Some("test_filter".to_string()),
out_dir: Some(PathBuf::from("out_dir")),
custom_test_args: Some("extra_args".to_string()),
strict_mode: true,
extra_env_vars: vec![],
};
let mut test_config = create_test_config_v1();
test_config.requested_vars.known_vars = vec![ENV_SDK_TOOL_PATH.into(), ENV_TARGETS.into()];
let test_config = test_config.into();
let result = args.validate_config(&test_config);
assert!(result.is_ok());
let args = TestPilotArgs {
test_config: PathBuf::from("test_config.json"),
test_bin_path: PathBuf::from("test_bin"),
path: "/some/path".into(),
targets: vec![],
timeout_seconds: Some(30),
sdk_tools_path: None,
resource_path: None,
out_dir: None,
test_filter: Some("test_filter".to_string()),
custom_test_args: Some("extra_args".to_string()),
strict_mode: true,
extra_env_vars: vec![],
};
let test_config = create_test_config_v1().into();
let result = args.validate_config(&test_config);
assert!(result.is_ok());
}
#[test]
fn test_validate_missing_sdk_tools_path() {
let args = TestPilotArgs {
test_config: PathBuf::from("test_config.json"),
test_bin_path: PathBuf::from("test_bin"),
path: "/some/path".into(),
targets: vec!["target1".to_string()],
timeout_seconds: Some(30),
sdk_tools_path: None, // Missing sdk_tools_path
resource_path: Some(PathBuf::from("resources")),
out_dir: None,
test_filter: Some("test_filter".to_string()),
custom_test_args: Some("extra_args".to_string()),
strict_mode: true,
extra_env_vars: vec![],
};
let mut test_config = create_test_config_v1();
test_config.requested_vars.known_vars = vec![ENV_SDK_TOOL_PATH.into()];
let test_config = test_config.into();
let result = args.validate_config(&test_config);
assert_eq!(result.unwrap_err(), ConfigError::Required(ENV_SDK_TOOL_PATH));
}
#[test]
fn test_validate_missing_targets() {
let args = TestPilotArgs {
test_config: PathBuf::from("test_config.json"),
test_bin_path: PathBuf::from("test_bin"),
path: "/some/path".into(),
targets: Vec::new(), // Missing targets
timeout_seconds: Some(30),
sdk_tools_path: Some(PathBuf::from("sdk_tools")),
resource_path: Some(PathBuf::from("resources")),
test_filter: Some("test_filter".to_string()),
out_dir: None,
custom_test_args: Some("extra_args".to_string()),
strict_mode: true,
extra_env_vars: vec![],
};
let mut test_config = create_test_config_v1();
test_config.requested_vars.known_vars = vec![ENV_TARGETS.into()];
let test_config = test_config.into();
let result = args.validate_config(&test_config);
assert_eq!(result.unwrap_err(), ConfigError::Required(ENV_TARGETS));
}
#[test]
fn test_validate_missing_resources() {
let args = TestPilotArgs {
test_config: PathBuf::from("test_config.json"),
test_bin_path: PathBuf::from("test_bin"),
path: "/some/path".into(),
targets: Vec::new(), // Missing targets
timeout_seconds: Some(30),
sdk_tools_path: Some(PathBuf::from("sdk_tools")),
resource_path: None,
test_filter: Some("test_filter".to_string()),
out_dir: None,
custom_test_args: Some("extra_args".to_string()),
strict_mode: true,
extra_env_vars: vec![],
};
let mut test_config = create_test_config_v1();
test_config.requested_vars.known_vars = vec![ENV_RESOURCE_PATH.into()];
let test_config = test_config.into();
let result = args.validate_config(&test_config);
assert_eq!(result.unwrap_err(), ConfigError::Required(ENV_RESOURCE_PATH));
}
}