blob: 47be049a452fe3a0ad286915c9804ac16e55c9e2 [file]
// Copyright 2026 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 std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TestStatus {
#[serde(rename = "PASS")]
Pass,
#[serde(rename = "FAIL")]
Fail,
#[serde(rename = "ABORT")]
Abort,
#[serde(rename = "SKIP")]
Skip,
#[serde(rename = "INFRA_FAIL")]
InfraFail,
#[serde(rename = "EXONERATED")]
Exonerated,
}
impl TestStatus {
/// Returns true if the status represents a failure condition.
/// Note: This considers infra failures and aborts (timeouts) as failures,
/// matching ResultDB ingestion requirements (only PASS and SKIP are non-failures).
pub fn is_failure(&self) -> bool {
!matches!(self, TestStatus::Pass | TestStatus::Skip)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FailureReasonError {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FailureReason {
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<FailureReasonError>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncated_errors_count: Option<i32>,
}
impl FailureReason {
pub fn from_message(message: &str) -> Option<Self> {
let trimmed_message = message.trim();
if trimmed_message.is_empty() {
None
} else {
Some(Self {
errors: Some(vec![FailureReasonError {
message: Some(trimmed_message.to_string()),
trace: None,
}]),
truncated_errors_count: None,
})
}
}
}
fn serialize_duration_nanos<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u128(duration.as_nanos())
}
fn deserialize_duration_nanos<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
let nanos = u128::deserialize(deserializer)?;
let secs = (nanos / 1_000_000_000) as u64;
let subsec_nanos = (nanos % 1_000_000_000) as u32;
Ok(Duration::new(secs, subsec_nanos))
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestCaseResult {
pub display_name: String,
pub suite_name: String,
pub case_name: String,
pub status: TestStatus,
#[serde(
rename = "duration_nanos",
serialize_with = "serialize_duration_nanos",
deserialize_with = "deserialize_duration_nanos"
)]
pub duration: Duration,
pub format: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<FailureReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_files: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_dir: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestResult {
pub cases: Vec<TestCaseResult>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_serialization() {
let case_result = TestCaseResult {
display_name: "Suite.Case".to_string(),
suite_name: "Suite".to_string(),
case_name: "Case".to_string(),
status: TestStatus::Pass,
duration: Duration::from_millis(4),
format: "GoogleTest".to_string(),
failure_reason: None,
output_files: None,
output_dir: None,
};
let json_value = serde_json::to_value(&case_result).expect("serialization succeeds");
assert_eq!(json_value["display_name"], "Suite.Case");
assert_eq!(json_value["status"], "PASS");
assert_eq!(json_value["duration_nanos"], 4_000_000);
assert_eq!(json_value["format"], "GoogleTest");
}
}