mod macros;
pub mod testing;
mod v1;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::hash_map::Iter;
use std::collections::HashMap;
use std::fs::{DirBuilder, File};
use std::io::Error;
use std::path::{Path, PathBuf};
use test_list::TestTag;
pub const RUN_SUMMARY_NAME: &str = "run_summary.json";
pub const RUN_NAME: &str = "run";
enumerable_enum! {
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
SchemaVersion {
V1,
}
}
enumerable_enum! {
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
Outcome {
NotStarted,
Passed,
Failed,
Inconclusive,
Timedout,
Error,
Skipped,
}
}
enumerable_enum! {
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
ArtifactType {
Syslog,
RestrictedLog,
Stdout,
Stderr,
Custom,
Report,
Debug,
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct ArtifactSubDirectory {
version: SchemaVersion,
root: PathBuf,
artifacts: HashMap<PathBuf, ArtifactMetadata>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct CommonResult {
pub name: String,
pub artifact_dir: ArtifactSubDirectory,
pub outcome: MaybeUnknown<Outcome>,
pub start_time: Option<u64>,
pub duration_milliseconds: Option<u64>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct TestRunResult<'a> {
pub common: Cow<'a, CommonResult>,
pub suites: Vec<SuiteResult<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuiteResult<'a> {
pub common: Cow<'a, CommonResult>,
pub cases: Vec<TestCaseResult<'a>>,
pub tags: Cow<'a, Vec<TestTag>>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct TestCaseResult<'a> {
pub common: Cow<'a, CommonResult>,
}
impl TestRunResult<'static> {
pub fn from_dir(root: &Path) -> Result<Self, Error> {
v1::parse_from_directory(root)
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Hash)]
pub struct ArtifactMetadata {
pub artifact_type: MaybeUnknown<ArtifactType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component_moniker: Option<String>,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Hash, Clone)]
#[serde(untagged)]
pub enum MaybeUnknown<T> {
Known(T),
Unknown(String),
}
impl<T> From<T> for MaybeUnknown<T> {
fn from(other: T) -> Self {
Self::Known(other)
}
}
impl From<ArtifactType> for ArtifactMetadata {
fn from(other: ArtifactType) -> Self {
Self { artifact_type: MaybeUnknown::Known(other), component_moniker: None }
}
}
pub struct OutputDirectoryBuilder {
version: SchemaVersion,
root: PathBuf,
creation_instant: std::time::Instant,
}
impl OutputDirectoryBuilder {
pub fn new(dir: impl Into<PathBuf>, version: SchemaVersion) -> Result<Self, Error> {
let root = dir.into();
ensure_directory_exists(&root)?;
Ok(Self { version, root, creation_instant: std::time::Instant::now() })
}
pub fn new_artifact_dir(&self) -> Result<ArtifactSubDirectory, Error> {
match self.version {
SchemaVersion::V1 => {
let subdir_root =
self.root.join(format!("{:?}", self.creation_instant.elapsed().as_nanos()));
Ok(ArtifactSubDirectory {
version: self.version,
root: subdir_root,
artifacts: HashMap::new(),
})
}
}
}
pub fn save_summary<'a, 'b>(&'a self, result: &'a TestRunResult<'b>) -> Result<(), Error> {
match self.version {
SchemaVersion::V1 => v1::save_summary(self.root.as_path(), result),
}
}
pub fn path(&self) -> &Path {
self.root.as_path()
}
}
impl ArtifactSubDirectory {
pub fn new_artifact(
&mut self,
metadata: impl Into<ArtifactMetadata>,
name: impl AsRef<Path>,
) -> Result<File, Error> {
ensure_directory_exists(self.root.as_path())?;
match self.version {
SchemaVersion::V1 => {
self.artifacts.insert(name.as_ref().to_path_buf(), metadata.into());
File::create(self.root.join(name))
}
}
}
pub fn new_directory_artifact(
&mut self,
metadata: impl Into<ArtifactMetadata>,
name: impl AsRef<Path>,
) -> Result<PathBuf, Error> {
match self.version {
SchemaVersion::V1 => {
let subdir = self.root.join(name.as_ref());
ensure_directory_exists(subdir.as_path())?;
self.artifacts.insert(name.as_ref().to_path_buf(), metadata.into());
Ok(subdir)
}
}
}
pub fn path_to_artifact(&self, name: impl AsRef<Path>) -> Option<PathBuf> {
match self.version {
SchemaVersion::V1 => match self.artifacts.contains_key(name.as_ref()) {
true => Some(self.root.join(name.as_ref())),
false => None,
},
}
}
pub fn contents(&self) -> Vec<PathBuf> {
self.artifacts.keys().cloned().collect()
}
pub fn artifact_iter(&self) -> Iter<'_, PathBuf, ArtifactMetadata> {
self.artifacts.iter()
}
}
fn ensure_directory_exists(dir: &Path) -> Result<(), Error> {
match dir.exists() {
true => Ok(()),
false => DirBuilder::new().recursive(true).create(&dir),
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io::Write;
use tempfile::tempdir;
fn validate_against_schema(version: SchemaVersion, root: &Path) {
match version {
SchemaVersion::V1 => v1::validate_against_schema(root),
}
}
fn round_trip_test_all_versions<F>(produce_run_fn: F)
where
F: Fn(&OutputDirectoryBuilder) -> TestRunResult<'static>,
{
for version in SchemaVersion::all_variants() {
let dir = tempdir().expect("Create dir");
let output_dir = OutputDirectoryBuilder::new(dir.path(), version).expect("create dir");
let run_result = produce_run_fn(&output_dir);
output_dir.save_summary(&run_result).expect("save summary");
validate_against_schema(version, dir.path());
let parsed = TestRunResult::from_dir(dir.path()).expect("parse output directory");
assert_eq!(run_result, parsed, "version: {:?}", version);
}
}
#[test]
fn minimal() {
round_trip_test_all_versions(|dir_builder| TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
suites: vec![],
});
let _ = Outcome::all_variants();
}
#[test]
fn artifact_types() {
for artifact_type in ArtifactType::all_variants() {
round_trip_test_all_versions(|dir_builder| {
let mut run_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut run_artifact =
run_artifact_dir.new_artifact(artifact_type, "a.txt").expect("create artifact");
write!(run_artifact, "run contents").unwrap();
let mut suite_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut suite_artifact = suite_artifact_dir
.new_artifact(artifact_type, "a.txt")
.expect("create artifact");
write!(suite_artifact, "suite contents").unwrap();
let mut case_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut case_artifact = case_artifact_dir
.new_artifact(artifact_type, "a.txt")
.expect("create artifact");
write!(case_artifact, "case contents").unwrap();
TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: run_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
suites: vec![SuiteResult {
common: Cow::Owned(CommonResult {
name: "suite".to_string(),
artifact_dir: suite_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
tags: Cow::Owned(vec![]),
cases: vec![TestCaseResult {
common: Cow::Owned(CommonResult {
name: "case".to_string(),
artifact_dir: case_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
}],
}],
}
});
}
}
#[test]
fn artifact_types_moniker_specified() {
for artifact_type in ArtifactType::all_variants() {
round_trip_test_all_versions(|dir_builder| {
let mut run_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut run_artifact = run_artifact_dir
.new_artifact(
ArtifactMetadata {
artifact_type: artifact_type.into(),
component_moniker: Some("moniker".to_string()),
},
"a.txt",
)
.expect("create artifact");
write!(run_artifact, "run contents").unwrap();
let mut suite_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut suite_artifact = suite_artifact_dir
.new_artifact(
ArtifactMetadata {
artifact_type: artifact_type.into(),
component_moniker: Some("moniker".to_string()),
},
"a.txt",
)
.expect("create artifact");
write!(suite_artifact, "suite contents").unwrap();
let mut case_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
let mut case_artifact = case_artifact_dir
.new_artifact(
ArtifactMetadata {
artifact_type: artifact_type.into(),
component_moniker: Some("moniker".to_string()),
},
"a.txt",
)
.expect("create artifact");
write!(case_artifact, "case contents").unwrap();
TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: run_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
suites: vec![SuiteResult {
common: Cow::Owned(CommonResult {
name: "suite".to_string(),
artifact_dir: suite_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
tags: Cow::Owned(vec![]),
cases: vec![TestCaseResult {
common: Cow::Owned(CommonResult {
name: "case".to_string(),
artifact_dir: case_artifact_dir,
outcome: Outcome::Passed.into(),
start_time: None,
duration_milliseconds: None,
}),
}],
}],
}
});
}
}
#[test]
fn outcome_types() {
for outcome_type in Outcome::all_variants() {
round_trip_test_all_versions(|dir_builder| TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: outcome_type.into(),
start_time: None,
duration_milliseconds: None,
}),
suites: vec![SuiteResult {
common: Cow::Owned(CommonResult {
name: "suite".to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: outcome_type.into(),
start_time: None,
duration_milliseconds: None,
}),
tags: Cow::Owned(vec![]),
cases: vec![TestCaseResult {
common: Cow::Owned(CommonResult {
name: "case".to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: outcome_type.into(),
start_time: None,
duration_milliseconds: None,
}),
}],
}],
});
}
}
#[test]
fn timing_specified() {
round_trip_test_all_versions(|dir_builder| TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: Some(1),
duration_milliseconds: Some(2),
}),
suites: vec![SuiteResult {
common: Cow::Owned(CommonResult {
name: "suite".to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: Some(3),
duration_milliseconds: Some(4),
}),
tags: Cow::Owned(vec![]),
cases: vec![TestCaseResult {
common: Cow::Owned(CommonResult {
name: "case".to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: Some(5),
duration_milliseconds: Some(6),
}),
}],
}],
});
}
#[test]
fn tags_specified() {
round_trip_test_all_versions(|dir_builder| TestRunResult {
common: Cow::Owned(CommonResult {
name: RUN_NAME.to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: Some(1),
duration_milliseconds: Some(2),
}),
suites: vec![SuiteResult {
common: Cow::Owned(CommonResult {
name: "suite".to_string(),
artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
outcome: Outcome::Passed.into(),
start_time: Some(3),
duration_milliseconds: Some(4),
}),
tags: Cow::Owned(vec![
TestTag { key: "hermetic".to_string(), value: "false".to_string() },
TestTag { key: "realm".to_string(), value: "system".to_string() },
]),
cases: vec![],
}],
});
}
}