use time::{Duration, OffsetDateTime};
use crate::path::RelativePathBuf;
use crate::{ConversionError, FmtHash};
use crate::{Line, Origin, Properties, Revision, requirements::ReqId};
pub fn test_date_from_str(date: &str) -> Result<OffsetDateTime, time::error::Parse> {
OffsetDateTime::parse(
date,
&time::format_description::well_known::Iso8601::PARSING,
)
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TestRunSchema {
#[serde(serialize_with = "crate::serialize_schema_version")]
pub schema_version: Option<String>,
pub test_runs: Vec<TestRun>,
pub test_run_properties: Option<Properties>,
pub test_case_properties: Option<Properties>,
pub origin: Option<Origin>,
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TestRun {
pub name: String,
#[serde(with = "time::serde::iso8601")]
#[schemars(with = "String")]
pub utc_date: time::OffsetDateTime,
pub description: Option<String>,
pub revisions: Option<Vec<Revision>>,
pub origin: Option<Origin>,
#[serde(alias = "nr_of_tests")]
pub nr_of_test_cases: u32,
pub properties: Option<Properties>,
#[schemars(with = "Option<f64>")]
#[serde(with = "duration_as_saturating_seconds_f64", default)]
pub duration_sec: Option<Duration>,
pub logs: Option<Vec<LogOutput>>,
#[serde(alias = "tests")]
pub test_cases: Vec<TestCase>,
#[serde(default)]
pub covered_files: Vec<CoveredFile>,
#[serde(default)]
pub test_runs: Vec<TestRun>,
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TestCase {
pub name: String,
pub description: Option<String>,
pub state: TestCaseState,
pub state_properties: Option<Properties>,
pub location: Option<TestCaseLocation>,
#[serde(with = "time::serde::iso8601::option")]
#[schemars(with = "String")]
#[serde(default)] pub utc_date: Option<time::OffsetDateTime>,
#[schemars(with = "Option<f64>")]
#[serde(with = "duration_as_saturating_seconds_f64", default)]
pub duration_sec: Option<Duration>,
pub properties: Option<Properties>,
pub logs: Option<Vec<LogOutput>>,
#[serde(default)]
pub verified_reqs: Vec<ReqId>,
#[serde(default)]
pub covered_files: Vec<CoveredFile>,
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TestCaseLocation {
#[schemars(with = "String")]
pub filepath: RelativePathBuf,
#[schemars(with = "String")]
pub file_hash: Option<FmtHash>,
pub line: Line,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum TestCaseState {
Failed = 0,
Passed = 1,
Skipped = 2,
Unknown = 3,
}
impl TestCaseState {
pub fn as_nr(&self) -> i32 {
*self as i32
}
}
impl TryFrom<i64> for TestCaseState {
type Error = ConversionError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if value == TestCaseState::Failed.as_nr() as i64 {
Ok(TestCaseState::Failed)
} else if value == TestCaseState::Skipped.as_nr() as i64 {
Ok(TestCaseState::Skipped)
} else if value == TestCaseState::Passed.as_nr() as i64 {
Ok(TestCaseState::Passed)
} else if value == TestCaseState::Unknown.as_nr() as i64 {
Ok(TestCaseState::Unknown)
} else {
Err(ConversionError::UnknownState)
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct CoveredFile {
#[schemars(with = "String")]
pub filepath: RelativePathBuf,
pub file_hash: Option<FmtHash>,
#[serde(default)]
pub lines: Vec<CoveredLine>,
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct CoveredLine {
pub nr: Line,
pub hits: Option<i64>,
}
impl std::cmp::PartialOrd for CoveredLine {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for CoveredLine {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.nr.cmp(&other.nr) {
std::cmp::Ordering::Equal => self.hits.cmp(&other.hits),
cmp => cmp,
}
}
}
#[derive(
Debug,
Clone,
PartialEq,
PartialOrd,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct LogOutput {
pub source: LogSource,
pub content: String,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
schemars::JsonSchema,
)]
pub enum LogSource {
Stdout = 0,
Stderr = 1,
}
impl LogSource {
pub fn as_nr(&self) -> i32 {
*self as i32
}
}
impl std::fmt::Display for LogSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogSource::Stdout => write!(f, "stdout"),
LogSource::Stderr => write!(f, "stderr"),
}
}
}
impl TryFrom<i64> for LogSource {
type Error = ConversionError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if value == LogSource::Stdout.as_nr() as i64 {
Ok(LogSource::Stdout)
} else if value == LogSource::Stderr.as_nr() as i64 {
Ok(LogSource::Stderr)
} else {
Err(ConversionError::UnknownState)
}
}
}
pub mod duration_as_saturating_seconds_f64 {
use serde::de::Visitor;
use time::Duration;
pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match duration {
Some(value) => serializer.serialize_f64(value.as_seconds_f64()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_option(OptionDurationVisitor)
}
struct OptionDurationVisitor;
impl<'de> Visitor<'de> for OptionDurationVisitor {
type Value = Option<Duration>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "optional duration as f64 seconds.")
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_f64(DurationVisitor).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
struct DurationVisitor;
impl<'de> Visitor<'de> for DurationVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "duration as f64 seconds.")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(time::Duration::saturating_seconds_f64(v))
}
}
}