use anyhow::{Error, Result, anyhow};
use colored::*;
use serde::Deserialize;
use serde_yaml::{Value, from_value};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use crate::config::{ConfigData, ConfigSpec, TestStepGroupReference};
use crate::test_step::{
RunnableTestStep, TestStep, TestStepFailureReason, TestStepResult, TestStepSpec,
};
#[derive(Clone)]
pub struct Test {
pub name: String,
path: PathBuf,
pub config: Option<Arc<RwLock<ConfigData>>>,
pub groups: Option<Vec<String>>,
setup: Option<String>,
teardown: Option<String>,
steps: Vec<Arc<RwLock<dyn RunnableTestStep + Send + Sync>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct TestSpec {
setup: Option<String>,
teardown: Option<String>,
steps: Vec<Value>,
config: Option<ConfigSpec>,
groups: Option<Vec<String>>,
}
pub fn print_test_results(test_results: &Vec<TestResult>, duration_secs: f32) {
let mut passes: Vec<&TestResult> = vec![];
let mut fails: Vec<&TestResult> = vec![];
for test_result in test_results.iter() {
if test_result.passed() {
passes.push(test_result);
} else {
fails.push(test_result);
}
}
let total = test_results.len();
let num_passes = passes.len();
let num_failures = fails.len();
let divider = "─".repeat(40);
println!("{}", divider.dimmed());
if num_failures == 0 {
println!(
"{} ({} total, {:.2}s)",
format!("Results: {} passed", num_passes).green(),
total,
duration_secs
);
} else {
println!(
"Results: {} ({} total, {:.2}s)",
format!("{} passed, {} failed", num_passes, num_failures).red(),
total,
duration_secs
);
}
println!("{}", divider.dimmed());
if !fails.is_empty() {
println!();
println!("{}", "FAILURES".bold());
for failure in fails.iter() {
println!();
println!(" {} {}", "✗".red(), failure.test_name.bold());
println!(" File: {}", failure.test_path.display());
if let Some(msg) = failure.get_failure_message() {
println!(" Error: {}", msg);
}
}
println!();
}
}
pub struct TestResult {
test_name: String,
test_path: PathBuf,
failing_step: Option<TestStepResult>,
success: bool,
}
impl TestResult {
pub fn name(&self) -> &str {
&self.test_name
}
pub fn get_failing_step(&self) -> &Option<TestStepResult> {
&self.failing_step
}
pub fn get_failure_message(&self) -> Option<String> {
if let Some(step) = &self.failing_step {
if let Some(msg) = &step.failure_message {
return Some(msg.clone());
}
}
return None;
}
pub fn passed(&self) -> bool {
self.success
}
pub fn make_failure_from_error(
test_name: &String,
test_path: &PathBuf,
step_id: Option<String>,
failure_reason: TestStepFailureReason,
error_prefix: String,
error: Error,
) -> TestResult {
let step_result = TestStepResult::make_failure(
&step_id,
failure_reason,
format!("{}: {}", error_prefix, error),
);
TestResult {
test_name: test_name.to_string(),
test_path: test_path.to_path_buf(),
failing_step: Some(step_result),
success: false,
}
}
pub fn make_failure(
test_name: &String,
test_path: &PathBuf,
failure: TestStepResult,
) -> TestResult {
TestResult {
test_name: test_name.to_string(),
test_path: test_path.to_path_buf(),
failing_step: Some(failure),
success: false,
}
}
}
fn is_test_name(key: String) -> bool {
let lower_name = key.to_lowercase();
lower_name.starts_with("test") || lower_name.ends_with("test")
}
impl Test {
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn add_config(&mut self, config: Arc<RwLock<ConfigData>>) {
match &self.config {
Some(cfg) => {
let o_new_config_dir = config.read().unwrap().path.clone();
let o_current_config_dir = cfg.read().unwrap().path.clone();
if let (Some(new_dir), Some(current_dir)) =
(o_new_config_dir.parent(), o_current_config_dir.parent())
{
if current_dir.starts_with(new_dir) {
cfg.write().unwrap().set_parent(config);
} else if new_dir.starts_with(current_dir) {
config.write().unwrap().set_parent(Arc::clone(cfg));
} else {
panic!(
"ERROR: Cannot set parentage with unrelated configs {} {}",
new_dir.display(),
current_dir.display()
);
}
}
}
None => {
self.config = Some(Arc::clone(&config));
}
}
}
pub fn from_spec(path: PathBuf, name: String, spec: TestSpec) -> Result<Test> {
let mut config: Option<Arc<RwLock<ConfigData>>> = None;
if let Some(config_spec) = spec.config {
let loaded_config = ConfigData::from_spec(&path, config_spec)?;
config = Some(Arc::new(RwLock::new(loaded_config)));
}
let mut test_steps: Vec<Arc<RwLock<dyn RunnableTestStep + Send + Sync>>> = vec![];
for step in spec.steps.into_iter() {
match from_value::<TestStepSpec>(step.clone()) {
Ok(test_step_spec) => {
let step = TestStep::from_spec(test_step_spec);
test_steps.push(Arc::new(RwLock::new(step)));
}
Err(_) => {
match step.clone().as_str() {
Some(step_name) => {
let step = TestStepGroupReference::from_id(step_name.to_string());
test_steps.push(Arc::new(RwLock::new(step)));
}
None => return Err(anyhow!("Error Decoding Step in test {}", name)),
}
}
}
}
Ok(Test {
name,
path,
setup: spec.setup,
teardown: spec.teardown,
steps: test_steps,
config,
groups: spec.groups,
})
}
pub fn load_from_file(path: &PathBuf) -> Result<(Option<ConfigData>, Vec<Test>), Error> {
let mut config: Option<ConfigData> = None;
let mut tests: Vec<Test> = vec![];
if let Ok(file) = File::open(path) {
let reader = BufReader::new(file);
let test_file_result = serde_yaml::from_reader::<_, Value>(reader);
match test_file_result {
Ok(test_file) => {
if let Some(config_value) = test_file.get("config") {
config = Some(ConfigData::from_val(&config_value, path)?);
}
if let Some(mapping) = test_file.as_mapping() {
for key in mapping.keys().filter_map(|v| v.as_str()) {
if is_test_name(key.to_string()) {
if let Some(test_value) = mapping.get(key) {
match from_value::<TestSpec>(test_value.clone()) {
Ok(test_spec) => {
let test = Test::from_spec(
path.clone(),
key.to_string(),
test_spec,
)?;
tests.push(test);
}
Err(e) => {
panic!(
"Failed to parse test: {} at {}\n{}",
key,
path.display(),
e
);
}
}
}
}
}
}
}
Err(e) => {
return Err(Error::from(e));
}
}
}
Ok((config, tests))
}
pub async fn run(&self) -> TestResult {
let mut prior_steps: HashMap<String, TestStepResult> = HashMap::new();
if let (Some(setup_id), Some(cfg)) = (self.setup.clone(), &self.config) {
match cfg.read().unwrap().get_step_group(setup_id.clone()) {
Ok(setup) => match setup.run(&self.config, &prior_steps).await {
Ok(result) => {
prior_steps.insert("setup".to_string(), result);
}
Err(e) => {
return TestResult::make_failure_from_error(
&self.name,
&self.path,
Some("setup".to_string()),
TestStepFailureReason::Miscellaneous,
"setup failed".to_string(),
e,
);
}
},
Err(e) => {
return TestResult::make_failure_from_error(
&self.name,
&self.path,
Some("setup".to_string()),
TestStepFailureReason::SharedStepNotFoundError,
"setup step-set not found".to_string(),
e,
);
}
}
}
for step in self.steps.iter() {
let real_step = step.read().unwrap();
match real_step.run(&self.config, &prior_steps).await {
Ok(result) => {
if result.status != TestStepFailureReason::NoFailure {
return TestResult::make_failure(&self.name, &self.path, result);
} else {
if let Some(id) = real_step.get_id() {
prior_steps.insert(id.clone(), result);
}
}
}
Err(e) => {
let mut step_id: Option<String> = None;
if let Some(actual_step_id) = real_step.get_id() {
step_id = Some(actual_step_id.clone());
}
return TestResult::make_failure_from_error(
&self.name,
&self.path,
step_id,
TestStepFailureReason::Miscellaneous,
"step failed".to_string(),
e,
);
}
}
}
if let (Some(teardown_id), Some(cfg)) = (self.teardown.clone(), &self.config) {
match cfg.read().unwrap().get_step_group(teardown_id.clone()) {
Ok(teardown) => match teardown.run(&self.config, &prior_steps).await {
Ok(result) => {
prior_steps.insert("teardown".to_string(), result);
}
Err(e) => {
return TestResult::make_failure_from_error(
&self.name,
&self.path,
Some("teardown".to_string()),
TestStepFailureReason::Miscellaneous,
"teardown failed".to_string(),
e,
);
}
},
Err(e) => {
return TestResult::make_failure_from_error(
&self.name,
&self.path,
Some("teardown".to_string()),
TestStepFailureReason::SharedStepNotFoundError,
"teardown step-set not found".to_string(),
e,
);
}
}
}
TestResult {
test_name: self.name.clone(),
test_path: self.path.clone(),
failing_step: None,
success: true,
}
}
}