blob: 7e86b5a2e3bf2f5e72ea455d6e1dd8f0f91c3368 [file] [log] [blame]
// Copyright 2025 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 serde::{Deserialize, Serialize};
use std::{fs, io};
use test_pilot_lib::test_output::{Summary, SummaryCase, SummaryOutcomeResult};
const FAILED_EXIT_CODE: i32 = 1;
const TEST_CASE_RESULT_FORMAT: &str = "FTF";
fn main() {
let mut args = std::env::args();
if args.len() != 4 {
usage(FAILED_EXIT_CODE);
}
let _ = args.next();
let config = args.next().unwrap();
let from = args.next().unwrap();
let to = args.next().unwrap();
if !validate_existent_file_path(&config) {
usage(FAILED_EXIT_CODE);
}
if !validate_existent_file_path(&from) {
usage(FAILED_EXIT_CODE);
}
if !validate_nonexistent_path(&to) {
usage(FAILED_EXIT_CODE);
}
match fs::File::open(&config) {
Ok(file) => {
let mut reader = io::BufReader::new(file);
let test_config: TestConfig = serde_json::from_reader(&mut reader).unwrap();
match fs::File::open(&from) {
Ok(file) => {
let mut reader = io::BufReader::new(file);
let pilot_summary: Summary = serde_json::from_reader(&mut reader).unwrap();
let botanist_summary = convert(pilot_summary, test_config);
if let Err(err) =
fs::write(&to, serde_json::to_string_pretty(&botanist_summary).unwrap())
{
eprintln!("failed to write new summary file {to}: {err}");
std::process::exit(FAILED_EXIT_CODE);
}
}
Err(err) => {
eprintln!("failed to read summary file {from}: {err}");
std::process::exit(FAILED_EXIT_CODE);
}
}
}
Err(err) => {
eprintln!("failed to open summary file {from}: {err}");
std::process::exit(FAILED_EXIT_CODE);
}
}
}
fn usage(exit_code: i32) {
eprintln!("usage: resummarize <config> <from> <to>");
std::process::exit(exit_code);
}
// Validates `path`, verifying that the path references an existing file. Returns true if and onlyf
// if `path` is valid.
fn validate_existent_file_path(path: &str) -> bool {
match fs::exists(path) {
Ok(true) => match fs::metadata(path) {
Ok(metadata) => {
if metadata.is_file() {
true
} else {
eprintln!("path {path} is a directory, expected a file");
false
}
}
Err(err) => {
eprintln!("failed to determine whether {path} is a file: {err}");
false
}
},
Ok(false) => {
eprintln!("input file {path} does not exist");
false
}
Err(err) => {
eprintln!("failed to determine if {path} exists: {err}");
false
}
}
}
// Validates `path`, verifying that the file/path does not exist. Returns true if and only if
// `path` is valid.
fn validate_nonexistent_path(path: &str) -> bool {
match fs::exists(path) {
Ok(true) => {
eprintln!("path {path} references existing file or directory, should not exist");
false
}
Ok(false) => true,
Err(err) => {
eprintln!("failed to determine if {path} exists: {err}");
false
}
}
}
// Converts a `Summary` into a `TestResult`
fn convert(pilot_summary: Summary, test_config: TestConfig) -> TestResult {
TestResult {
output_files: pilot_summary
.common
.artifacts
.into_iter()
.map(|(path, _)| path.to_string_lossy().to_string())
.collect(),
output_dir: test_config.output_directory.clone(),
cases: pilot_summary
.cases
.into_iter()
.map(|(name, case)| convert_case(name, case, &test_config.output_directory))
.collect(),
error_line: pilot_summary.common.outcome.detail.unwrap_or_default(),
}
}
fn convert_case(name: String, case: SummaryCase, output_directory: &str) -> TestCaseResult {
TestCaseResult {
display_name: name.clone(),
suite_name: "".to_string(),
case_name: name,
status: convert_result(case.common.outcome.result),
duration_nanos: case.common.duration * 1000000,
format: TEST_CASE_RESULT_FORMAT.to_string(),
fail_reason: case.common.outcome.detail.unwrap_or_default(),
output_files: case
.common
.artifacts
.into_iter()
.map(|(path, _)| path.to_string_lossy().to_string())
.collect(),
output_dir: output_directory.to_string(),
tags: vec![TestTag {
key: "test_outcome".to_string(),
value: convert_result_for_tag(case.common.outcome.result),
}],
}
}
fn convert_result(result: SummaryOutcomeResult) -> String {
match result {
SummaryOutcomeResult::NotSpecified => "NOT_SPECIFIED".to_string(),
SummaryOutcomeResult::Skipped => "SKIP".to_string(),
SummaryOutcomeResult::Passed => "PASS".to_string(),
SummaryOutcomeResult::Canceled => "ABORT".to_string(),
SummaryOutcomeResult::TimedOut => "FAIL".to_string(),
SummaryOutcomeResult::Failed => "FAIL".to_string(),
SummaryOutcomeResult::Error => "CRASH".to_string(),
}
}
fn convert_result_for_tag(result: SummaryOutcomeResult) -> String {
match result {
SummaryOutcomeResult::NotSpecified => "NOT_SPECIFIED".to_string(),
SummaryOutcomeResult::Skipped => "SKIPPED".to_string(),
SummaryOutcomeResult::Passed => "PASSED".to_string(),
SummaryOutcomeResult::Canceled => "CANCELED".to_string(),
SummaryOutcomeResult::TimedOut => "TIMED_OUT".to_string(),
SummaryOutcomeResult::Failed => "FAILED".to_string(),
SummaryOutcomeResult::Error => "ERROR".to_string(),
}
}
/// Parameters describing a test to be run.
#[derive(Deserialize, Debug)]
struct TestConfig {
pub output_directory: String,
}
/// Body of the summary in the format used by botanist.
#[derive(Serialize, Debug)]
struct TestResult {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub output_files: Vec<String>,
pub output_dir: String,
pub cases: Vec<TestCaseResult>,
#[serde(default)]
pub error_line: String,
}
/// Case summary in the format used by botanist.
#[derive(Serialize, Debug)]
struct TestCaseResult {
pub display_name: String,
pub suite_name: String,
pub case_name: String,
pub status: String,
pub duration_nanos: i64,
pub format: String,
pub fail_reason: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub output_files: Vec<String>,
#[serde(default)]
#[serde(skip_serializing_if = "String::is_empty")]
pub output_dir: String,
pub tags: Vec<TestTag>,
}
#[derive(Serialize, Deserialize, Debug)]
struct TestTag {
pub key: String,
pub value: String,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::{NamedTempFile, tempdir};
use test_pilot_lib::test_output::{SummaryCommonProperties, SummaryOutcome};
#[test]
fn test_convert() {
let pilot_summary = Summary {
common: SummaryCommonProperties {
duration: 123, // milliseconds
outcome: SummaryOutcome {
result: SummaryOutcomeResult::Failed,
detail: Some("Overall test failed".to_string()),
},
artifacts: [(PathBuf::from("path/to/artifact1.txt"), Default::default())].into(),
..Default::default()
},
cases: HashMap::from([
(
"case1".to_string(),
SummaryCase {
common: SummaryCommonProperties {
duration: 50, // milliseconds
outcome: SummaryOutcome {
result: SummaryOutcomeResult::Passed,
detail: None,
},
..Default::default()
},
},
),
(
"case2".to_string(),
SummaryCase {
common: SummaryCommonProperties {
duration: 73, // milliseconds
outcome: SummaryOutcome {
result: SummaryOutcomeResult::Failed,
detail: Some("assertion failed".to_string()),
},
artifacts: [(PathBuf::from("case2/log.txt"), Default::default())]
.into(),
..Default::default()
},
},
),
]),
};
let test_config = TestConfig { output_directory: "/path/to/output/directory".to_string() };
let botanist_summary = convert(pilot_summary, test_config);
assert_eq!(botanist_summary.output_files, vec!["path/to/artifact1.txt"]);
assert_eq!(botanist_summary.output_dir, "/path/to/output/directory".to_string());
assert_eq!(botanist_summary.cases.len(), 2);
assert_eq!(botanist_summary.error_line, "Overall test failed");
// The order of cases from a HashMap is not guaranteed, so we find them.
let case1 = botanist_summary.cases.iter().find(|c| c.case_name == "case1").unwrap();
assert_eq!(case1.display_name, "case1");
assert_eq!(case1.suite_name, "");
assert_eq!(case1.status, "PASS");
assert_eq!(case1.duration_nanos, 50_000_000);
assert_eq!(case1.format, "FTF");
assert_eq!(case1.fail_reason, "");
assert!(case1.output_files.is_empty());
assert_eq!(case1.output_dir, "/path/to/output/directory");
let case2 = botanist_summary.cases.iter().find(|c| c.case_name == "case2").unwrap();
assert_eq!(case2.display_name, "case2");
assert_eq!(case1.suite_name, "");
assert_eq!(case2.status, "FAIL");
assert_eq!(case2.duration_nanos, 73_000_000);
assert_eq!(case2.format, "FTF");
assert_eq!(case2.fail_reason, "assertion failed");
assert_eq!(case2.output_files, vec!["case2/log.txt"]);
assert_eq!(case2.output_dir, "/path/to/output/directory");
}
#[test]
fn test_convert_result() {
assert_eq!(convert_result(SummaryOutcomeResult::Passed), "PASS");
assert_eq!(convert_result(SummaryOutcomeResult::Failed), "FAIL");
assert_eq!(convert_result(SummaryOutcomeResult::Skipped), "SKIP");
assert_eq!(convert_result(SummaryOutcomeResult::TimedOut), "FAIL");
assert_eq!(convert_result(SummaryOutcomeResult::Canceled), "ABORT");
assert_eq!(convert_result(SummaryOutcomeResult::Error), "CRASH");
assert_eq!(convert_result(SummaryOutcomeResult::NotSpecified), "NOT_SPECIFIED");
}
#[test]
fn test_convert_result_for_tag() {
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::Passed), "PASSED");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::Failed), "FAILED");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::Skipped), "SKIPPED");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::TimedOut), "TIMED_OUT");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::Canceled), "CANCELED");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::Error), "ERROR");
assert_eq!(convert_result_for_tag(SummaryOutcomeResult::NotSpecified), "NOT_SPECIFIED");
}
#[test]
fn test_validate_existent_file_path() {
// Existent file.
let temp_file = NamedTempFile::new().expect("to create temporary file");
let temp_file_path = temp_file.path().to_str().unwrap().to_string();
assert!(validate_existent_file_path(temp_file_path.as_str()));
temp_file.close().expect("to close temporary file");
// Non-existent file.
assert!(!validate_existent_file_path("/non_existent_file"));
// Existent directory.
let temp_dir = tempdir().expect("to create temporary directory");
let temp_dir_path = temp_dir.path().to_str().unwrap().to_string();
assert!(!validate_existent_file_path(temp_dir_path.as_str()));
temp_dir.close().expect("to close temporary directory");
}
#[test]
fn test_validate_nonexistent_path() {
// Existent file.
let temp_file = NamedTempFile::new().expect("to create temporary file");
let temp_file_path = temp_file.path().to_str().unwrap().to_string();
assert!(!validate_nonexistent_path(temp_file_path.as_str()));
temp_file.close().expect("to close temporary file");
// Non-existent file.
assert!(validate_nonexistent_path("/non_existent_file"));
// Existent directory.
let temp_dir = tempdir().expect("to create temporary directory");
let temp_dir_path = temp_dir.path().to_str().unwrap().to_string();
assert!(!validate_nonexistent_path(temp_dir_path.as_str()));
temp_dir.close().expect("to close temporary directory");
}
}