blob: 86bcb5c7f3f69728e39c658bc4c3105fea5db740 [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 serde_json::Value;
use std::collections::HashMap;
use std::io::BufReader;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::{fs, io};
use thiserror::Error;
/// Distinguished exit status used to indicate the test was run as requested but that the
/// test itself failed.
const FAIL_EXIT_STATUS: i32 = 86;
/// The body of the test output summary.
#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
pub struct Summary {
/// Properties concerning the overall test run.
#[serde(flatten)]
pub common: SummaryCommonProperties,
/// Cases by case name.
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub cases: HashMap<String, SummaryCase>,
}
impl Summary {
/// Merges `Summary` read from a file into self. Returns Ok(true) if the file was merged
/// successfully, Ok(false) if the file did not exist.
pub fn maybe_merge_file(&mut self, path: &PathBuf) -> Result<bool, TestOutputError> {
if let Ok(file) = fs::File::open(&path) {
let mut reader = BufReader::new(file);
let summary_from_file: Summary = serde_json::from_reader(&mut reader)
.map_err(|e| TestOutputError::SummaryRead { path: path.clone(), source: e })?;
self.merge(summary_from_file);
Ok(true)
} else {
Ok(false)
}
}
/// Writes this summary to a file.
pub fn write(&self, path: &PathBuf) -> Result<(), TestOutputError> {
fs::write(path, serde_json::to_string_pretty(self).unwrap())
.map_err(|e| TestOutputError::SummaryWrite { path: path.clone(), source: e })?;
Ok(())
}
/// Merges `other` into self. This is used for merging test and postprocessor summaries
/// into the aggregate summary.
fn merge(&mut self, other: Summary) {
self.common.merge(other.common);
for (other_case_name, other_case) in other.cases {
match &mut self.cases.get_mut(other_case_name.as_str()) {
Some(case_properties) => {
case_properties.merge(other_case);
}
None => {
self.cases.insert(other_case_name, other_case);
}
}
}
}
}
/// Test output regarding a single case.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct SummaryCase {
#[serde(flatten)]
pub common: SummaryCommonProperties,
}
impl SummaryCase {
/// Merges `other` into self. This is used for merging test and postprocessor summaries
/// into the aggregate summary.
fn merge(&mut self, other: SummaryCase) {
self.common.merge(other.common);
}
}
/// Properties shared by the overall summary and by cases.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct SummaryCommonProperties {
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub duration: i64,
pub outcome: SummaryOutcome,
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub artifacts: HashMap<PathBuf, SummaryArtifact>,
#[serde(flatten)]
pub extension: HashMap<String, Value>,
}
impl SummaryCommonProperties {
/// Merges `other` into self. This is used for merging test and postprocessor summaries
/// into the aggregate summary.
fn merge(&mut self, other: SummaryCommonProperties) {
if other.duration != 0 {
self.duration = other.duration;
}
self.outcome.merge(other.outcome);
for (other_artifact_name, other_artifact_properties) in other.artifacts {
match &mut self.artifacts.get_mut(&other_artifact_name) {
Some(artifact_properties) => {
artifact_properties.merge(other_artifact_properties);
}
None => {
self.artifacts.insert(other_artifact_name, other_artifact_properties);
}
}
}
for (other_extension_name, other_extension_properties) in other.extension {
self.extension.insert(other_extension_name, other_extension_properties);
}
}
}
/// Expresses the outcome of a test or test case run.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct SummaryOutcome {
/// General result of the test run. The result of a test that has one
/// or more cases should typically be the maximum of the results of
/// the cases.
#[serde(default)]
#[serde(skip_serializing_if = "is_not_specified")]
pub result: SummaryOutcomeResult,
/// Optional details describing the outcome, used primarily to
/// describe `Failed` and `Error` outcomes. The format of this field
/// is not constrained by the ABI. The intent of this field is to offer
/// a brief description of the problem for the purposes of triage,
/// debugging, and classification of failures and errors.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl SummaryOutcome {
/// Merges `other` into self. This is used for merging test and postprocessor summaries
/// into the aggregate summary.
fn merge(&mut self, other: SummaryOutcome) {
if other.result != SummaryOutcomeResult::NotSpecified {
// If other has a specified result, the merge changes self to equal other.
self.result = other.result;
self.detail = other.detail;
} else if other.detail.is_some() {
// Otherwise, if other has detail, the merge changes self's detail to match other's.
self.detail = other.detail;
}
}
/// Merges a case outcome into a test outcome (self).
pub fn merge_case_outcome(&mut self, case_outcome: SummaryOutcome) {
if (self.result as u8) < (case_outcome.result as u8) {
self.result = case_outcome.result;
self.detail = case_outcome.detail;
} else if self.result == case_outcome.result {
if self.detail.is_none() {
self.detail = case_outcome.detail;
} else if case_outcome.detail.is_some() {
let mut detail = self.detail.take().unwrap();
detail.push('\n');
detail.push_str(&case_outcome.detail.unwrap());
self.detail = Some(detail);
}
}
}
}
/// Summarizes the outcome of a test or test case run.
///
/// In general, the result of a test that has many cases will be the
/// maximum of the results of the cases. This constrains the ordering of
/// these values.
#[derive(Serialize, Deserialize, Debug, Default, Copy, Clone, PartialEq)]
#[repr(u8)]
pub enum SummaryOutcomeResult {
/// Default result value used when a test or postprocessor summary does
/// not wish to specify an outcome result. This value must never appear
/// in an aggregate summary.
#[default]
NotSpecified = 0x00,
/// Regarding a case, the case was not fully executed, because the
/// test configuration specified that it should be skipped or because
/// the case explicitly requested that it be skipped. In the latter
/// case, the detail associated with this result might give the reason
/// that the case was skipped.
///
/// Regarding a test, all cases in the test were skipped.
#[serde(rename = "skipped")]
Skipped = 0x10,
/// Regarding a case, the case was executed as requested and passed.
///
/// Regarding a test, at least one case passed, and all others passed
/// or were skipped.
#[serde(rename = "passed")]
Passed = 0x20,
/// Regarding a case, the case was not executed to completion as
/// requested, because test execution was terminated early. This can
/// occur if the client requests early termination or if the
/// configuration specifies something like 'break on failure'.
///
/// Regarding a test, at least one case was canceled, and all others
/// either were canceled, passed or were skipped.
#[serde(rename = "canceled")]
Canceled = 0x30,
/// Regarding a case, the case was executed as requested but did not
/// terminate before the timeout interval for the case or overall test
/// elapsed.
///
/// Regarding a test, at least one case timed out, and all others either
/// timed out, were canceled, passed, or were skipped. Alternatively,
/// this value may be used if a timeout applied to the overall test
/// run expired.
#[serde(rename = "timed_out")]
TimedOut = 0x40,
/// Regarding a case, the case was executed as requested and failed.
/// Failed outcomes typically include a non-empty `detail` field,
/// formatted in a manner that is specific to the case. That format
/// is not defined by the test ABI.
///
/// Regarding a test, at least one case failed, and all others either
/// failed, timed out, were canceled, passed, or were skipped. Failed
/// test outcomes typically aggregate the details of all the failed
/// cases.
#[serde(rename = "failed")]
Failed = 0x50,
/// Regarding a case, the case was not executed as requested due to
/// an error. This can occur due to resource or connectivity problems
/// or due to a bug in the test framework implementation. The associated
/// string is in a format that is specific to the test framework
/// implementation and is not part of the test ABI.
///
/// Regarding a test, at least one case resulted in an error result or
/// an error that interfered with the execution of the test occurred
/// outside the context of any given case. Typically, error results for
/// a test are accompanied by a detail string indicating the nature of
/// the error.
#[serde(rename = "error")]
Error = 0x60,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct SummaryArtifact {
#[serde(rename = "type")]
pub artifact_type: String,
}
impl SummaryArtifact {
/// Merges `other` into self. This is used for merging test and postprocessor summaries
/// into the aggregate summary.
fn merge(&mut self, other: SummaryArtifact) {
if !other.artifact_type.is_empty() {
self.artifact_type = other.artifact_type;
}
}
}
fn is_zero(to_test: &i64) -> bool {
*to_test == 0
}
fn is_not_specified(to_test: &SummaryOutcomeResult) -> bool {
*to_test == SummaryOutcomeResult::NotSpecified
}
const TEST_SUBDIR: &str = "test";
const EXECUTION_LOG_FILE_PATH: &str = "execution_log.json";
const TEST_CONFIG_FILE_PATH: &str = "test_config.json";
const OUTPUT_SUMMARY_FILE_PATH: &str = "output_summary.json";
const STDOUT_FILE_PATH: &str = "stdout.txt";
const STDERR_FILE_PATH: &str = "stderr.txt";
/// Creates path values based on output directory structure.
pub struct OutputDirectory<'a> {
path: &'a Path,
}
impl<'a> OutputDirectory<'a> {
/// Creates a new `OutputDirectory` located at `Path`.
pub fn new(path: &'a Path) -> Self {
Self { path }
}
/// Returns the full path of the specified file or subdirectory in the top-level output
/// directory.
pub fn join(&self, subdir: &str) -> PathBuf {
self.path.join(subdir)
}
/// Returns the full path of the execution log.
pub fn execution_log(&self) -> PathBuf {
self.join(EXECUTION_LOG_FILE_PATH)
}
/// Returns the full path of the main summary.
pub fn main_summary(&self) -> PathBuf {
self.join(OUTPUT_SUMMARY_FILE_PATH)
}
/// Returns the full path of the test subdirectory, where the test deposits its summary
/// and artifacts.
pub fn test_subdir(&self) -> PathBuf {
self.join(TEST_SUBDIR)
}
/// Returns the full path of the summary generated by the test.
pub fn test_summary(&self) -> PathBuf {
self.test_subdir().join(OUTPUT_SUMMARY_FILE_PATH)
}
/// Returns the full path of the test configuration.
pub fn test_config(&self) -> PathBuf {
self.join(TEST_CONFIG_FILE_PATH)
}
/// Returns the full path of the test's stdout file.
pub fn test_stdout(&self) -> PathBuf {
self.test_subdir().join(STDOUT_FILE_PATH)
}
/// Returns the full path of the test's stderr file.
pub fn test_stderr(&self) -> PathBuf {
self.test_subdir().join(STDERR_FILE_PATH)
}
/// Returns the full path of the subdirectory for the specified postprocessor.
pub fn postprocessor_subdir(&self, binary: &PathBuf) -> PathBuf {
self.join(postprocessor_name(binary))
}
/// Returns the full path of the summary for a specified postprocessor.
pub fn postprocessor_summary(&self, binary: &PathBuf) -> PathBuf {
self.postprocessor_subdir(binary).join(OUTPUT_SUMMARY_FILE_PATH)
}
/// Returns the full path of a post-processor's stdout file.
pub fn postprocessor_stdout(&self, binary: &PathBuf) -> PathBuf {
self.postprocessor_subdir(binary).join(STDOUT_FILE_PATH)
}
/// Returns the full path of a post-processor's stderr file.
pub fn postprocessor_stderr(&self, binary: &PathBuf) -> PathBuf {
self.postprocessor_subdir(binary).join(STDERR_FILE_PATH)
}
/// Converts a full path into a path relative to the output directory.
pub fn make_relative(&self, path: &PathBuf) -> Option<PathBuf> {
path.strip_prefix(self.path).ok().map(|p| p.to_path_buf())
}
}
/// Returns the name of a postprocessor subdirectory given a path to the postprocessor's binary.
fn postprocessor_name<'b>(binary: &'b PathBuf) -> &'b str {
binary.file_name().expect("binary path has file name").to_str().expect("file name is ansi")
}
/// Determines whether `exit_status` indicates the test run ran correctly, but the test itself
/// failed.
pub fn is_fail_exit_status(exit_status: std::process::ExitStatus) -> bool {
exit_status.into_raw() == FAIL_EXIT_STATUS
}
/// Returns an outcome string from an exit status.
pub fn outcome_from_exit_status(exit_status: std::process::ExitStatus) -> SummaryOutcome {
match exit_status.into_raw() {
0 => SummaryOutcome { result: SummaryOutcomeResult::Passed, detail: None },
FAIL_EXIT_STATUS => SummaryOutcome { result: SummaryOutcomeResult::Failed, detail: None },
status => SummaryOutcome {
result: SummaryOutcomeResult::Error,
detail: Some(format!("test binary failed with exit status {status}")),
},
}
}
/// Error encountered while executing test binary
#[derive(Debug, Error)]
pub enum TestOutputError {
#[error("Error reading output summary file {path}: {source}")]
SummaryRead {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("Error writing output summary file {path}: {source}")]
SummaryWrite {
path: PathBuf,
#[source]
source: io::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs::File;
use std::os::unix::process::ExitStatusExt;
use std::process::ExitStatus;
use tempfile::tempdir;
fn outcome(result: SummaryOutcomeResult, detail: &str) -> SummaryOutcome {
SummaryOutcome { result, detail: Some(String::from(detail)) }
}
fn case(result: SummaryOutcomeResult, detail: &str) -> SummaryCase {
SummaryCase {
common: SummaryCommonProperties {
outcome: outcome(result, detail),
..Default::default()
},
}
}
fn artifact(artifact_type: &str) -> SummaryArtifact {
SummaryArtifact { artifact_type: String::from(artifact_type) }
}
#[fuchsia::test]
fn test_merge_common_properties() {
let mut under_test = SummaryCommonProperties {
duration: 1,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome a"),
artifacts: [
(PathBuf::from("a"), artifact("a_type")),
(PathBuf::from("b"), artifact("b_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("x_value")),
(String::from("y"), Value::from("y_value")),
]
.into(),
};
// Non-trivial values from argument take precedent. New artifacts are inserted.
// Existing artifacts with non-trivial values get updated.
under_test.merge(SummaryCommonProperties {
duration: 3,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome b"),
artifacts: [
(PathBuf::from("a"), artifact("new_a_type")),
(PathBuf::from("c"), artifact("c_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("new_x_value")),
(String::from("z"), Value::from("z_value")),
]
.into(),
});
assert_eq!(
under_test,
SummaryCommonProperties {
duration: 3,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome b"),
artifacts: [
(PathBuf::from("a"), artifact("new_a_type")),
(PathBuf::from("b"), artifact("b_type")),
(PathBuf::from("c"), artifact("c_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("new_x_value")),
(String::from("y"), Value::from("y_value")),
(String::from("z"), Value::from("z_value")),
]
.into(),
}
);
// Trivial values from argument are not copied. For existing artifacts, trivial
// values from argument are not copied.
under_test.merge(SummaryCommonProperties {
duration: 0,
outcome: outcome(SummaryOutcomeResult::NotSpecified, "test outcome c"),
artifacts: [(PathBuf::from("a"), artifact(""))].into(),
extension: HashMap::new(),
});
assert_eq!(
under_test,
SummaryCommonProperties {
duration: 3,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome c"),
artifacts: [
(PathBuf::from("a"), artifact("new_a_type")),
(PathBuf::from("b"), artifact("b_type")),
(PathBuf::from("c"), artifact("c_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("new_x_value")),
(String::from("y"), Value::from("y_value")),
(String::from("z"), Value::from("z_value")),
]
.into(),
}
);
}
#[fuchsia::test]
fn test_merge_cases() {
let mut under_test = Summary {
common: SummaryCommonProperties::default(),
cases: [
(String::from("case_a"), case(SummaryOutcomeResult::Failed, "case_a_outcome")),
(String::from("case_b"), case(SummaryOutcomeResult::Failed, "case_b_outcome")),
]
.into(),
};
// Non-trivial values from argument take precedent. New artifacts are inserted.
// Existing artifacts with non-trivial values get updated.
under_test.merge(Summary {
common: SummaryCommonProperties::default(),
cases: [
(String::from("case_a"), case(SummaryOutcomeResult::Failed, "case_a_new_outcome")),
(String::from("case_c"), case(SummaryOutcomeResult::Failed, "case_c_outcome")),
]
.into(),
});
assert_eq!(
under_test,
Summary {
common: SummaryCommonProperties::default(),
cases: [
(
String::from("case_a"),
case(SummaryOutcomeResult::Failed, "case_a_new_outcome")
),
(String::from("case_b"), case(SummaryOutcomeResult::Failed, "case_b_outcome")),
(String::from("case_c"), case(SummaryOutcomeResult::Failed, "case_c_outcome")),
]
.into()
}
);
}
#[fuchsia::test]
fn test_merge_outcomes() {
let results = [
SummaryOutcomeResult::NotSpecified,
SummaryOutcomeResult::Skipped,
SummaryOutcomeResult::Passed,
SummaryOutcomeResult::Canceled,
SummaryOutcomeResult::TimedOut,
SummaryOutcomeResult::Failed,
SummaryOutcomeResult::Error,
];
let specified_results = [
SummaryOutcomeResult::Skipped,
SummaryOutcomeResult::Passed,
SummaryOutcomeResult::Canceled,
SummaryOutcomeResult::TimedOut,
SummaryOutcomeResult::Failed,
SummaryOutcomeResult::Error,
];
// When other is unspecified, merging leaves self unchanged.
for result in &results {
let mut under_test = outcome(result.clone(), "self");
under_test
.merge(SummaryOutcome { result: SummaryOutcomeResult::NotSpecified, detail: None });
assert_eq!(under_test, outcome(result.clone(), "self"));
}
// When other's result is specified, merging changes self to equal other.
for self_result in &results {
for other_result in &specified_results {
let mut under_test = outcome(self_result.clone(), "self");
let other = outcome(other_result.clone(), "other");
under_test.merge(other.clone());
assert_eq!(under_test, other);
// ...even if other's detail is_none.
let mut under_test = outcome(self_result.clone(), "self");
let other = SummaryOutcome { result: other_result.clone(), detail: None };
under_test.merge(other.clone());
assert_eq!(under_test, other);
}
}
// When other's result is unspecified but details is_some, merging changes self's detail
// to equal other's.
for result in &results {
let mut under_test = outcome(result.clone(), "self");
under_test.merge(outcome(SummaryOutcomeResult::NotSpecified, "other"));
assert_eq!(under_test, outcome(result.clone(), "other"));
}
}
#[fuchsia::test]
fn test_merge_case_outcomes() {
let results = [
SummaryOutcomeResult::NotSpecified,
SummaryOutcomeResult::Skipped,
SummaryOutcomeResult::Passed,
SummaryOutcomeResult::Canceled,
SummaryOutcomeResult::TimedOut,
SummaryOutcomeResult::Failed,
SummaryOutcomeResult::Error,
];
// Merging an outcome with a greater result copies the greater result.
let selves_iter = &mut results.into_iter();
while let Some(self_result) = selves_iter.next() {
let mut others_iter = selves_iter.clone();
while let Some(other_result) = others_iter.next() {
let mut self_outcome = SummaryOutcome {
result: self_result,
detail: Some(String::from("self detail")),
};
let other_outcome = SummaryOutcome {
result: other_result,
detail: Some(String::from("other detail")),
};
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(other_outcome, self_outcome);
}
}
// Merging an outcome with a lesser result has no effect.
let others_iter = &mut results.into_iter();
while let Some(other_result) = others_iter.next() {
let mut selves_iter = others_iter.clone();
while let Some(self_result) = selves_iter.next() {
let mut self_outcome = SummaryOutcome {
result: self_result,
detail: Some(String::from("self detail")),
};
let other_outcome = SummaryOutcome {
result: other_result,
detail: Some(String::from("other detail")),
};
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(
SummaryOutcome {
result: self_result,
detail: Some(String::from("self detail"))
},
self_outcome
);
}
}
// Merging an outcome with an equal result has no effect on the result and merges the
// detail.
for result in results {
// If both outcomes have no detail, the merge will have no detail.
let mut self_outcome = SummaryOutcome { result: result, detail: None };
let other_outcome = SummaryOutcome { result: result, detail: None };
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(self_outcome, SummaryOutcome { result: result, detail: None });
// If both outcomes have detail, the detail is concatenated with a newline separator.
let mut self_outcome =
SummaryOutcome { result: result, detail: Some(String::from("self detail")) };
let other_outcome =
SummaryOutcome { result: result, detail: Some(String::from("other detail")) };
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(
self_outcome,
SummaryOutcome {
result: result,
detail: Some(String::from("self detail\nother detail"))
}
);
// If only the self outcome has detail, that detail is used.
let mut self_outcome =
SummaryOutcome { result: result, detail: Some(String::from("self detail")) };
let other_outcome = SummaryOutcome { result: result, detail: None };
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(
self_outcome,
SummaryOutcome { result: result, detail: Some(String::from("self detail")) }
);
// If only the other outcome has detail, that detail is used.
let mut self_outcome = SummaryOutcome { result: result, detail: None };
let other_outcome =
SummaryOutcome { result: result, detail: Some(String::from("other detail")) };
self_outcome.merge_case_outcome(other_outcome.clone());
assert_eq!(
self_outcome,
SummaryOutcome { result: result, detail: Some(String::from("other detail")) }
);
}
}
#[fuchsia::test]
fn test_outcome_from_exit_status() {
assert_eq!(
outcome_from_exit_status(ExitStatus::from_raw(0)),
SummaryOutcome { result: SummaryOutcomeResult::Passed, detail: None }
);
assert_eq!(
outcome_from_exit_status(ExitStatus::from_raw(FAIL_EXIT_STATUS)),
SummaryOutcome { result: SummaryOutcomeResult::Failed, detail: None }
);
assert_eq!(
outcome_from_exit_status(ExitStatus::from_raw(1)),
SummaryOutcome {
result: SummaryOutcomeResult::Error,
detail: Some(String::from("test binary failed with exit status 1"))
}
);
}
#[fuchsia::test]
fn test_summary_write() {
let under_test = Summary {
common: SummaryCommonProperties {
duration: 3,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome b"),
artifacts: [
(PathBuf::from("a"), artifact("a_type")),
(PathBuf::from("b"), artifact("b_type")),
(PathBuf::from("c"), artifact("c_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("x_value")),
(String::from("y"), Value::from("y_value")),
(String::from("z"), Value::from("z_value")),
]
.into(),
},
cases: [
(String::from("case_a"), case(SummaryOutcomeResult::Failed, "case_a_outcome")),
(String::from("case_b"), case(SummaryOutcomeResult::Failed, "case_b_outcome")),
]
.into(),
};
let temp_dir = tempdir().expect("to create temporary directory");
let file_path = temp_dir.path().join("test_output_summary.json");
under_test.write(&file_path).expect("to write summary");
let file = File::open(&file_path).expect("to open summary");
let mut reader = BufReader::new(file);
let file_contents: Value = serde_json5::from_reader(&mut reader).expect("to read summary");
assert_eq!(
file_contents,
json!({
"artifacts": {
"a": {"type": "a_type"},
"b": {"type": "b_type"},
"c": {"type": "c_type"}
},
"cases": {
"case_a": {"outcome": {"result": "failed", "detail": "case_a_outcome"}},
"case_b": {"outcome": {"result": "failed", "detail": "case_b_outcome"}}
},
"duration": 3,
"outcome": {"result": "failed", "detail": "test outcome b"},
"x": "x_value",
"y": "y_value",
"z": "z_value"
})
);
temp_dir.close().expect("to close temporary directory");
}
#[fuchsia::test]
fn test_summary_maybe_merge_file() {
let temp_dir = tempdir().expect("to create temporary directory");
let mut under_test = Summary::default();
assert_eq!(
false,
under_test
.maybe_merge_file(&temp_dir.path().join("does_not_exist.json"))
.expect("success"),
);
assert_eq!(Summary::default(), under_test);
let file_path = temp_dir.path().join("test_output_summary.json");
fs::write(
&file_path,
serde_json::to_string_pretty(&json!({
"artifacts": {
"a": {"type": "a_type"},
"b": {"type": "b_type"},
"c": {"type": "c_type"}
},
"cases": {
"case_a": {"outcome": {"result": "failed", "detail": "case_a_outcome"}},
"case_b": {"outcome": {"result": "failed", "detail": "case_b_outcome"}}
},
"duration": 3,
"outcome": {"result": "failed", "detail": "test outcome b"},
"x": "x_value",
"y": "y_value",
"z": "z_value"
}))
.unwrap(),
)
.expect("json write succeeds");
assert_eq!(true, under_test.maybe_merge_file(&file_path).expect("success"),);
assert_eq!(
Summary {
common: SummaryCommonProperties {
duration: 3,
outcome: outcome(SummaryOutcomeResult::Failed, "test outcome b"),
artifacts: [
(PathBuf::from("a"), artifact("a_type")),
(PathBuf::from("b"), artifact("b_type")),
(PathBuf::from("c"), artifact("c_type")),
]
.into(),
extension: [
(String::from("x"), Value::from("x_value")),
(String::from("y"), Value::from("y_value")),
(String::from("z"), Value::from("z_value")),
]
.into(),
},
cases: [
(String::from("case_a"), case(SummaryOutcomeResult::Failed, "case_a_outcome")),
(String::from("case_b"), case(SummaryOutcomeResult::Failed, "case_b_outcome")),
]
.into(),
},
under_test
);
temp_dir.close().expect("to close temporary directory");
}
}