blob: 46189d24e8bd380f1f0be4b6f39929005029b8b9 [file] [log] [blame]
// Copyright 2024 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::{Result, anyhow};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::io::BufReader;
use std::path::Path;
use test_pilot_lib::test_output::{
OutputDirectory, Summary, SummaryArtifact, SummaryCase, SummaryCommonProperties,
SummaryOutcome, SummaryOutcomeResult,
};
const OLD_RUN_SUMMARY_FILE_NAME: &str = "run_summary.json";
/// Converts an output directory generated by run_test_suite_lib into a form compatible with
/// test_pilot.
pub fn convert_output_for_test_pilot(output_directory: &Path) -> Result<()> {
let output_directory = OutputDirectory::new(output_directory);
let run_summary_path = output_directory.join(OLD_RUN_SUMMARY_FILE_NAME);
let file = fs::File::open(&run_summary_path)?;
let mut reader = BufReader::new(file);
let run_summary_envelope: RunSummaryEnvelope = serde_json::from_reader(&mut reader)?;
let run_summary = &run_summary_envelope.data;
// We only support one suite per test run.
if run_summary.suites.len() != 1 {
return Err(anyhow!(
"{0} contains {1} suite runs, expected 1",
OLD_RUN_SUMMARY_FILE_NAME,
run_summary.suites.len(),
));
}
let suite = &run_summary.suites[0];
let mut new_summary = Summary {
common: SummaryCommonProperties {
duration: suite.entity_common.duration_milliseconds,
outcome: SummaryOutcome {
result: summary_result_from(&suite.entity_common.outcome),
..Default::default()
},
..Default::default()
},
..Default::default()
};
// Create the test directory.
let new_test_dir_path = output_directory.test_subdir();
fs::create_dir_all(new_test_dir_path.clone())?;
// Handle suite artifacts.
let new_test_dir_rel_path = output_directory
.make_relative(&new_test_dir_path)
.expect("test dir path is in output_directory");
if suite.entity_common.artifact_dir.is_empty() {
if !suite.entity_common.artifacts.is_empty() {
return Err(anyhow!("suite has artifacts but no artifact directory"));
}
} else {
let old_artifact_dir_path =
output_directory.join(suite.entity_common.artifact_dir.as_str());
for (name, metadata) in &suite.entity_common.artifacts {
let mut new_file_rel_path = new_test_dir_rel_path.to_path_buf();
new_file_rel_path.push(name);
new_summary.common.artifacts.insert(
new_file_rel_path,
SummaryArtifact { artifact_type: metadata.artifact_type.clone() },
);
// Move the artifact to <root>/test.
let mut old_file_path = old_artifact_dir_path.clone();
old_file_path.push(name);
let mut new_file_path = new_test_dir_path.clone();
new_file_path.push(name);
fs::rename(old_file_path, new_file_path)?;
}
if old_artifact_dir_path.exists() {
fs::remove_dir_all(old_artifact_dir_path)?;
}
}
let mut case_outcomes_merged =
SummaryOutcome { result: SummaryOutcomeResult::Passed, detail: None };
// Handle the cases.
for old_case in &suite.cases {
let mut case = SummaryCase {
common: SummaryCommonProperties {
duration: old_case.entity_common.duration_milliseconds,
outcome: SummaryOutcome {
result: summary_result_from(&old_case.entity_common.outcome),
..Default::default()
},
..Default::default()
},
};
case_outcomes_merged.merge_case_outcome(case.common.outcome.clone());
if old_case.entity_common.artifact_dir.is_empty() {
if !old_case.entity_common.artifacts.is_empty() {
return Err(anyhow!(
"case {} has artifacts but no artifact directory",
old_case.name.as_str()
));
}
} else {
let old_artifact_dir_path =
output_directory.join(old_case.entity_common.artifact_dir.as_str());
let mut new_artifact_dir_path = new_test_dir_path.clone();
new_artifact_dir_path.push(old_case.name.as_str());
if !old_case.entity_common.artifacts.is_empty() {
// Create the new artifact dir at <root>/test/<case name>.
fs::create_dir(new_artifact_dir_path.clone())?;
}
for (name, metadata) in &old_case.entity_common.artifacts {
let mut new_file_rel_path = new_test_dir_rel_path.clone();
new_file_rel_path.push(old_case.name.as_str());
new_file_rel_path.push(name);
case.common.artifacts.insert(
new_file_rel_path,
SummaryArtifact { artifact_type: metadata.artifact_type.clone() },
);
// Move the artifact to <root>/target/<case name>.
let mut old_file_path = old_artifact_dir_path.clone();
old_file_path.push(name);
let mut new_file_path = new_artifact_dir_path.clone();
new_file_path.push(name);
fs::rename(old_file_path, new_file_path)?;
}
if old_artifact_dir_path.exists() {
fs::remove_dir_all(old_artifact_dir_path)?;
}
}
new_summary.cases.insert(old_case.name.clone(), case);
}
if case_outcomes_merged != new_summary.common.outcome {
eprintln!(
"WARN: merged case outcome {:?} differs from original suite outcome {:?}, using merged outcome",
case_outcomes_merged, new_summary.common.outcome
);
new_summary.common.outcome = case_outcomes_merged;
}
fs::write(
output_directory.test_summary().to_str().unwrap(),
serde_json::to_string_pretty(&new_summary).unwrap(),
)?;
fs::remove_file(run_summary_path)?;
Ok(())
}
fn summary_result_from(old_result: &str) -> SummaryOutcomeResult {
match old_result {
"SKIPPED" => SummaryOutcomeResult::Skipped,
"PASSED" => SummaryOutcomeResult::Passed,
"FAILED" => SummaryOutcomeResult::Failed,
"TIMED_OUT" => SummaryOutcomeResult::TimedOut,
"ERROR" => SummaryOutcomeResult::Error,
"FINISHED" => SummaryOutcomeResult::Passed,
"DID_NOT_FINISH" => SummaryOutcomeResult::Canceled,
"STOPPED" => SummaryOutcomeResult::Canceled,
"INTERNAL_ERROR" => SummaryOutcomeResult::Error,
_ => {
eprintln!("UNRECOGNIZED LEGACY RESULT {}", old_result);
SummaryOutcomeResult::Error
}
}
}
/// Top level of the legacy run_summary.json. Identifies the schema.
#[derive(Deserialize, Debug, Default)]
struct RunSummaryEnvelope {
data: RunSummary,
#[serde(rename = "schema_id")]
_schema_id: String,
}
/// The body of the summary. The `entity_common` bit refers to the run.
#[derive(Deserialize, Debug, Default)]
struct RunSummary {
#[serde(flatten)]
#[serde(rename = "entity_common")]
_entity_common: RunSummaryEntityCommon,
suites: Vec<RunSummarySuiteEntry>,
}
/// Properties of the run, a suite, and a case.
#[derive(Deserialize, Debug, Default)]
struct RunSummaryEntityCommon {
#[serde(default)]
artifacts: HashMap<String, RunSummaryArtifactMetadata>,
#[serde(default)]
artifact_dir: String,
#[serde(default)]
duration_milliseconds: i64,
// This is the name actually used in summary files, as opposed to the name in the schema, which
// is start_time_milliseconds.
#[serde(default)]
#[serde(rename = "start_time")]
_start_time: i64,
outcome: String,
}
/// Describes an artifact.
#[derive(Deserialize, Debug, Default)]
struct RunSummaryArtifactMetadata {
artifact_type: String,
#[serde(default)]
#[serde(rename = "component_moniker")]
_component_moniker: String,
}
/// Describes a case.
#[derive(Deserialize, Debug, Default)]
struct RunSummaryCaseEntry {
#[serde(flatten)]
entity_common: RunSummaryEntityCommon,
name: String,
}
/// Describes a suite.
#[derive(Deserialize, Debug, Default)]
struct RunSummarySuiteEntry {
#[serde(flatten)]
entity_common: RunSummaryEntityCommon,
cases: Vec<RunSummaryCaseEntry>,
#[serde(rename = "name")]
_name: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_convert_output() {
// Setup
let tmp_dir = tempdir().unwrap();
let output_dir = tmp_dir.path();
// Create old run_summary.json
let run_summary_content = json!({
"schema_id": "schema_id_string",
"data": {
"outcome": "FAILED",
"suites": [
{
"name": "suite_name",
"outcome": "FAILED",
"duration_milliseconds": 123,
"artifact_dir": "suite_artifacts",
"artifacts": {
"stdout.txt": {
"artifact_type": "stdout"
}
},
"cases": [
{
"name": "case1",
"outcome": "PASSED",
"duration_milliseconds": 100,
"artifact_dir": "case1_artifacts",
"artifacts": {
"screenshot.png": {
"artifact_type": "screenshot"
}
}
},
{
"name": "case2",
"outcome": "FAILED",
"duration_milliseconds": 23,
"artifacts": {},
"artifact_dir": "case2_artifacts"
}
]
}
]
}
});
let run_summary_path = output_dir.join(OLD_RUN_SUMMARY_FILE_NAME);
fs::write(&run_summary_path, serde_json::to_string(&run_summary_content).unwrap()).unwrap();
// Create artifact directories and files
let suite_artifact_dir = output_dir.join("suite_artifacts");
fs::create_dir(&suite_artifact_dir).unwrap();
fs::write(suite_artifact_dir.join("stdout.txt"), "suite stdout").unwrap();
let case1_artifact_dir = output_dir.join("case1_artifacts");
fs::create_dir(&case1_artifact_dir).unwrap();
fs::write(case1_artifact_dir.join("screenshot.png"), "fake png").unwrap();
let case2_artifact_dir = output_dir.join("case2_artifacts");
fs::create_dir(&case2_artifact_dir).unwrap();
// Run conversion
let result = convert_output_for_test_pilot(output_dir);
assert!(result.is_ok());
// Assertions
// 1. old run_summary.json is gone
assert!(!run_summary_path.exists());
// 2. new summary.json exists and is correct
let new_summary_path = output_dir.join("test/output_summary.json");
assert!(new_summary_path.exists());
let new_summary_content = fs::read_to_string(&new_summary_path).unwrap();
let new_summary: Summary = serde_json::from_str(&new_summary_content).unwrap();
assert_eq!(new_summary.common.duration, 123);
assert_eq!(new_summary.common.outcome.result, SummaryOutcomeResult::Failed);
assert_eq!(new_summary.common.artifacts.len(), 1);
assert!(new_summary.common.artifacts.contains_key(Path::new("test/stdout.txt")));
assert_eq!(new_summary.cases.len(), 2);
let case1 = new_summary.cases.get("case1").unwrap();
assert_eq!(case1.common.duration, 100);
assert_eq!(case1.common.outcome.result, SummaryOutcomeResult::Passed);
assert_eq!(case1.common.artifacts.len(), 1);
assert!(case1.common.artifacts.contains_key(Path::new("test/case1/screenshot.png")));
let case2 = new_summary.cases.get("case2").unwrap();
assert_eq!(case2.common.duration, 23);
assert_eq!(case2.common.outcome.result, SummaryOutcomeResult::Failed);
assert!(case2.common.artifacts.is_empty());
// 3. artifacts are moved
let new_suite_artifact_path = output_dir.join("test/stdout.txt");
assert!(new_suite_artifact_path.exists());
assert_eq!(fs::read_to_string(new_suite_artifact_path).unwrap(), "suite stdout");
let new_case1_artifact_path = output_dir.join("test/case1/screenshot.png");
assert!(new_case1_artifact_path.exists());
assert_eq!(fs::read_to_string(new_case1_artifact_path).unwrap(), "fake png");
// 4. old artifact directories are gone
assert!(!suite_artifact_dir.exists());
assert!(!case1_artifact_dir.exists());
assert!(!case2_artifact_dir.exists()); // This one was empty, should be removed.
}
#[test]
fn test_convert_no_cases_no_suite_artifacts() {
let tmp_dir = tempdir().unwrap();
let output_dir = tmp_dir.path();
let run_summary_content = json!({
"schema_id": "schema_id_string",
"data": {
"outcome": "PASSED",
"suites": [
{
"name": "suite_name",
"outcome": "PASSED",
"duration_milliseconds": 42,
"cases": []
}
]
}
});
let run_summary_path = output_dir.join(OLD_RUN_SUMMARY_FILE_NAME);
fs::write(&run_summary_path, serde_json::to_string(&run_summary_content).unwrap()).unwrap();
let result = convert_output_for_test_pilot(output_dir);
assert!(result.is_ok());
assert!(!run_summary_path.exists());
let new_summary_path = output_dir.join("test/output_summary.json");
assert!(new_summary_path.exists());
let new_summary: Summary =
serde_json::from_str(&fs::read_to_string(new_summary_path).unwrap()).unwrap();
assert_eq!(new_summary.common.duration, 42);
assert_eq!(new_summary.common.outcome.result, SummaryOutcomeResult::Passed);
assert!(new_summary.common.artifacts.is_empty());
assert!(new_summary.cases.is_empty());
assert!(output_dir.exists());
}
#[test]
fn test_convert_multiple_suites_is_error() {
let tmp_dir = tempdir().unwrap();
let output_dir = tmp_dir.path();
let run_summary_content = json!({
"schema_id": "schema_id_string",
"data": {
"outcome": "PASSED",
"suites": [
{ "name": "suite1", "outcome": "PASSED", "cases": [] },
{ "name": "suite2", "outcome": "PASSED", "cases": [] }
]
}
});
let run_summary_path = output_dir.join(OLD_RUN_SUMMARY_FILE_NAME);
fs::write(&run_summary_path, serde_json::to_string(&run_summary_content).unwrap()).unwrap();
let result = convert_output_for_test_pilot(output_dir);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!("{0} contains 2 suite runs, expected 1", OLD_RUN_SUMMARY_FILE_NAME)
);
}
#[test]
fn test_summary_result_from() {
assert_eq!(summary_result_from("SKIPPED"), SummaryOutcomeResult::Skipped);
assert_eq!(summary_result_from("PASSED"), SummaryOutcomeResult::Passed);
assert_eq!(summary_result_from("FAILED"), SummaryOutcomeResult::Failed);
assert_eq!(summary_result_from("TIMED_OUT"), SummaryOutcomeResult::TimedOut);
assert_eq!(summary_result_from("ERROR"), SummaryOutcomeResult::Error);
assert_eq!(summary_result_from("FINISHED"), SummaryOutcomeResult::Passed);
assert_eq!(summary_result_from("DID_NOT_FINISH"), SummaryOutcomeResult::Canceled);
assert_eq!(summary_result_from("STOPPED"), SummaryOutcomeResult::Canceled);
assert_eq!(summary_result_from("INTERNAL_ERROR"), SummaryOutcomeResult::Error);
assert_eq!(summary_result_from("UNRECOGNIZED"), SummaryOutcomeResult::Error);
}
}