diff --git a/src/lib.rs b/src/lib.rs index e01580c..ee95e44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ // https://opensource.org/licenses/MIT. pub mod output; +mod spec; diff --git a/src/output/config.rs b/src/output/config.rs new file mode 100644 index 0000000..d5c27b8 --- /dev/null +++ b/src/output/config.rs @@ -0,0 +1,77 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::output::emitter; + +/// The configuration repository for the TestRun. +pub struct Config { + pub(crate) timezone: chrono_tz::Tz, + pub(crate) writer: emitter::WriterType, +} + +impl Config { + /// Creates a new [`ConfigBuilder`] + /// + /// # Examples + /// ```rust + /// # use ocptv::output::*; + /// + /// let builder = Config::builder(); + /// ``` + pub fn builder() -> ConfigBuilder { + ConfigBuilder::new() + } +} + +/// The builder for the [`Config`] object. +pub struct ConfigBuilder { + timezone: Option, + writer: Option, +} + +impl ConfigBuilder { + fn new() -> Self { + Self { + timezone: None, + writer: Some(emitter::WriterType::Stdout(emitter::StdoutWriter::new())), + } + } + + pub fn timezone(mut self, timezone: chrono_tz::Tz) -> Self { + self.timezone = Some(timezone); + self + } + + pub fn with_buffer_output(mut self, buffer: Arc>>) -> Self { + self.writer = Some(emitter::WriterType::Buffer(emitter::BufferWriter::new( + buffer, + ))); + self + } + + pub async fn with_file_output>( + mut self, + path: P, + ) -> Result { + self.writer = Some(emitter::WriterType::File( + emitter::FileWriter::new(path).await?, + )); + Ok(self) + } + + pub fn build(self) -> Config { + Config { + timezone: self.timezone.unwrap_or(chrono_tz::UTC), + writer: self + .writer + .unwrap_or(emitter::WriterType::Stdout(emitter::StdoutWriter::new())), + } + } +} diff --git a/src/output/dut.rs b/src/output/dut.rs new file mode 100644 index 0000000..b4d6c20 --- /dev/null +++ b/src/output/dut.rs @@ -0,0 +1,620 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use crate::spec; + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct DutInfo { + id: String, + name: Option, + platform_infos: Option>, + software_infos: Option>, + hardware_infos: Option>, + metadata: Option>, +} + +impl DutInfo { + pub fn builder(id: &str) -> DutInfoBuilder { + DutInfoBuilder::new(id) + } + + pub fn new(id: &str) -> DutInfo { + DutInfoBuilder::new(id).build() + } + + pub(crate) fn to_spec(&self) -> spec::DutInfo { + spec::DutInfo { + id: self.id.clone(), + name: self.name.clone(), + platform_infos: self + .platform_infos + .clone() + .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), + software_infos: self + .software_infos + .clone() + .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), + hardware_infos: self + .hardware_infos + .clone() + .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), + metadata: self.metadata.clone(), + } + } +} + +pub struct DutInfoBuilder { + id: String, + name: Option, + platform_infos: Option>, + software_infos: Option>, + hardware_infos: Option>, + metadata: Option>, +} + +impl DutInfoBuilder { + pub fn new(id: &str) -> DutInfoBuilder { + DutInfoBuilder { + id: id.to_string(), + name: None, + platform_infos: None, + software_infos: None, + hardware_infos: None, + metadata: None, + } + } + pub fn name(mut self, value: &str) -> DutInfoBuilder { + self.name = Some(value.to_string()); + self + } + + pub fn add_platform_info(mut self, platform_info: &PlatformInfo) -> DutInfoBuilder { + self.platform_infos = match self.platform_infos { + Some(mut platform_infos) => { + platform_infos.push(platform_info.clone()); + Some(platform_infos) + } + None => Some(vec![platform_info.clone()]), + }; + self + } + + pub fn add_software_info(mut self, software_info: &SoftwareInfo) -> DutInfoBuilder { + self.software_infos = match self.software_infos { + Some(mut software_infos) => { + software_infos.push(software_info.clone()); + Some(software_infos) + } + None => Some(vec![software_info.clone()]), + }; + self + } + + pub fn add_hardware_info(mut self, hardware_info: &HardwareInfo) -> DutInfoBuilder { + self.hardware_infos = match self.hardware_infos { + Some(mut hardware_infos) => { + hardware_infos.push(hardware_info.clone()); + Some(hardware_infos) + } + None => Some(vec![hardware_info.clone()]), + }; + self + } + + pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> DutInfoBuilder { + self.metadata = match self.metadata { + Some(mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + None => { + let mut metadata = serde_json::Map::new(); + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + }; + self + } + + pub fn build(self) -> DutInfo { + DutInfo { + id: self.id, + name: self.name, + platform_infos: self.platform_infos, + software_infos: self.software_infos, + hardware_infos: self.hardware_infos, + metadata: self.metadata, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct HardwareInfo { + id: String, + name: String, + version: Option, + revision: Option, + location: Option, + serial_no: Option, + part_no: Option, + manufacturer: Option, + manufacturer_part_no: Option, + odata_id: Option, + computer_system: Option, + manager: Option, +} + +impl HardwareInfo { + pub fn builder(id: &str, name: &str) -> HardwareInfoBuilder { + HardwareInfoBuilder::new(id, name) + } + + pub fn to_spec(&self) -> spec::HardwareInfo { + spec::HardwareInfo { + id: self.id.clone(), + name: self.name.clone(), + version: self.version.clone(), + revision: self.revision.clone(), + location: self.location.clone(), + serial_no: self.serial_no.clone(), + part_no: self.part_no.clone(), + manufacturer: self.manufacturer.clone(), + manufacturer_part_no: self.manufacturer_part_no.clone(), + odata_id: self.odata_id.clone(), + computer_system: self.computer_system.clone(), + manager: self.manager.clone(), + } + } + + pub fn id(&self) -> &str { + &self.id + } +} + +#[derive(Debug)] +pub struct HardwareInfoBuilder { + id: String, + name: String, + version: Option, + revision: Option, + location: Option, + serial_no: Option, + part_no: Option, + manufacturer: Option, + manufacturer_part_no: Option, + odata_id: Option, + computer_system: Option, + manager: Option, +} + +impl HardwareInfoBuilder { + fn new(id: &str, name: &str) -> Self { + HardwareInfoBuilder { + id: id.to_string(), + name: name.to_string(), + version: None, + revision: None, + location: None, + serial_no: None, + part_no: None, + manufacturer: None, + manufacturer_part_no: None, + odata_id: None, + computer_system: None, + manager: None, + } + } + pub fn version(mut self, value: &str) -> HardwareInfoBuilder { + self.version = Some(value.to_string()); + self + } + pub fn revision(mut self, value: &str) -> HardwareInfoBuilder { + self.revision = Some(value.to_string()); + self + } + pub fn location(mut self, value: &str) -> HardwareInfoBuilder { + self.location = Some(value.to_string()); + self + } + pub fn serial_no(mut self, value: &str) -> HardwareInfoBuilder { + self.serial_no = Some(value.to_string()); + self + } + pub fn part_no(mut self, value: &str) -> HardwareInfoBuilder { + self.part_no = Some(value.to_string()); + self + } + pub fn manufacturer(mut self, value: &str) -> HardwareInfoBuilder { + self.manufacturer = Some(value.to_string()); + self + } + pub fn manufacturer_part_no(mut self, value: &str) -> HardwareInfoBuilder { + self.manufacturer_part_no = Some(value.to_string()); + self + } + pub fn odata_id(mut self, value: &str) -> HardwareInfoBuilder { + self.odata_id = Some(value.to_string()); + self + } + pub fn computer_system(mut self, value: &str) -> HardwareInfoBuilder { + self.computer_system = Some(value.to_string()); + self + } + pub fn manager(mut self, value: &str) -> HardwareInfoBuilder { + self.manager = Some(value.to_string()); + self + } + + pub fn build(self) -> HardwareInfo { + HardwareInfo { + id: self.id, + name: self.name, + version: self.version, + revision: self.revision, + location: self.location, + serial_no: self.serial_no, + part_no: self.part_no, + manufacturer: self.manufacturer, + manufacturer_part_no: self.manufacturer_part_no, + odata_id: self.odata_id, + computer_system: self.computer_system, + manager: self.manager, + } + } +} + +#[derive(Debug, Clone)] +pub struct Subcomponent { + subcomponent_type: Option, + name: String, + location: Option, + version: Option, + revision: Option, +} + +impl Subcomponent { + pub fn builder(name: &str) -> SubcomponentBuilder { + SubcomponentBuilder::new(name) + } + pub fn to_spec(&self) -> spec::Subcomponent { + spec::Subcomponent { + subcomponent_type: self.subcomponent_type.clone(), + name: self.name.clone(), + location: self.location.clone(), + version: self.version.clone(), + revision: self.revision.clone(), + } + } +} + +#[derive(Debug)] +pub struct SubcomponentBuilder { + subcomponent_type: Option, + name: String, + location: Option, + version: Option, + revision: Option, +} + +impl SubcomponentBuilder { + fn new(name: &str) -> Self { + SubcomponentBuilder { + subcomponent_type: None, + name: name.to_string(), + location: None, + version: None, + revision: None, + } + } + pub fn subcomponent_type(mut self, value: spec::SubcomponentType) -> SubcomponentBuilder { + self.subcomponent_type = Some(value); + self + } + pub fn version(mut self, value: &str) -> SubcomponentBuilder { + self.version = Some(value.to_string()); + self + } + pub fn location(mut self, value: &str) -> SubcomponentBuilder { + self.location = Some(value.to_string()); + self + } + pub fn revision(mut self, value: &str) -> SubcomponentBuilder { + self.revision = Some(value.to_string()); + self + } + + pub fn build(self) -> Subcomponent { + Subcomponent { + subcomponent_type: self.subcomponent_type, + name: self.name, + location: self.location, + version: self.version, + revision: self.revision, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PlatformInfo { + info: String, +} + +impl PlatformInfo { + pub fn builder(info: &str) -> PlatformInfoBuilder { + PlatformInfoBuilder::new(info) + } + + pub fn to_spec(&self) -> spec::PlatformInfo { + spec::PlatformInfo { + info: self.info.clone(), + } + } +} + +#[derive(Debug)] +pub struct PlatformInfoBuilder { + info: String, +} + +impl PlatformInfoBuilder { + fn new(info: &str) -> Self { + PlatformInfoBuilder { + info: info.to_string(), + } + } + + pub fn build(self) -> PlatformInfo { + PlatformInfo { info: self.info } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SoftwareInfo { + id: String, + name: String, + version: Option, + revision: Option, + software_type: Option, + computer_system: Option, +} + +impl SoftwareInfo { + pub fn builder(id: &str, name: &str) -> SoftwareInfoBuilder { + SoftwareInfoBuilder::new(id, name) + } + + pub fn to_spec(&self) -> spec::SoftwareInfo { + spec::SoftwareInfo { + id: self.id.clone(), + name: self.name.clone(), + version: self.version.clone(), + revision: self.revision.clone(), + software_type: self.software_type.clone(), + computer_system: self.computer_system.clone(), + } + } +} + +#[derive(Debug)] +pub struct SoftwareInfoBuilder { + id: String, + name: String, + version: Option, + revision: Option, + software_type: Option, + computer_system: Option, +} + +impl SoftwareInfoBuilder { + fn new(id: &str, name: &str) -> Self { + SoftwareInfoBuilder { + id: id.to_string(), + name: name.to_string(), + version: None, + revision: None, + software_type: None, + computer_system: None, + } + } + pub fn version(mut self, value: &str) -> SoftwareInfoBuilder { + self.version = Some(value.to_string()); + self + } + pub fn revision(mut self, value: &str) -> SoftwareInfoBuilder { + self.revision = Some(value.to_string()); + self + } + pub fn software_type(mut self, value: spec::SoftwareType) -> SoftwareInfoBuilder { + self.software_type = Some(value); + self + } + pub fn computer_system(mut self, value: &str) -> SoftwareInfoBuilder { + self.computer_system = Some(value.to_string()); + self + } + + pub fn build(self) -> SoftwareInfo { + SoftwareInfo { + id: self.id, + name: self.name, + version: self.version, + revision: self.revision, + software_type: self.software_type, + computer_system: self.computer_system, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spec; + use anyhow::{bail, Result}; + + #[test] + fn test_dut_creation_from_builder_with_defaults() -> Result<()> { + let dut = DutInfo::builder("1234").build(); + assert_eq!(dut.id, "1234"); + Ok(()) + } + + #[test] + fn test_dut_builder() -> Result<()> { + let platform = PlatformInfo::builder("platform_info").build(); + let software = SoftwareInfo::builder("software_id", "name").build(); + let hardware = HardwareInfo::builder("hardware_id", "name").build(); + let dut = DutInfo::builder("1234") + .name("DUT") + .add_metadata("key", "value".into()) + .add_metadata("key2", "value2".into()) + .add_hardware_info(&hardware) + .add_hardware_info(&hardware) + .add_platform_info(&platform) + .add_platform_info(&platform) + .add_software_info(&software) + .add_software_info(&software) + .build(); + + let spec_dut = dut.to_spec(); + + assert_eq!(spec_dut.id, "1234"); + assert_eq!(spec_dut.name, Some("DUT".to_owned())); + + match spec_dut.metadata { + Some(m) => { + assert_eq!(m["key"], "value"); + assert_eq!(m["key2"], "value2"); + } + _ => bail!("metadata is empty"), + } + + match spec_dut.hardware_infos { + Some(infos) => match infos.first() { + Some(info) => { + assert_eq!(info.id, "hardware_id"); + } + _ => bail!("hardware_infos is empty"), + }, + _ => bail!("hardware_infos is missing"), + } + + match spec_dut.software_infos { + Some(infos) => match infos.first() { + Some(info) => { + assert_eq!(info.id, "software_id"); + } + _ => bail!("software_infos is empty"), + }, + _ => bail!("software_infos is missing"), + } + + match spec_dut.platform_infos { + Some(infos) => match infos.first() { + Some(info) => { + assert_eq!(info.info, "platform_info"); + } + _ => bail!("platform_infos is empty"), + }, + _ => bail!("platform_infos is missing"), + } + + Ok(()) + } + + #[test] + fn test_hardware_info() -> Result<()> { + let info = HardwareInfo::builder("hardware_id", "hardware_name") + .version("version") + .revision("revision") + .location("location") + .serial_no("serial_no") + .part_no("part_no") + .manufacturer("manufacturer") + .manufacturer_part_no("manufacturer_part_no") + .odata_id("odata_id") + .computer_system("computer_system") + .manager("manager") + .build(); + + let spec_hwinfo = info.to_spec(); + + assert_eq!(spec_hwinfo.id, "hardware_id"); + assert_eq!(spec_hwinfo.name, "hardware_name"); + assert_eq!(spec_hwinfo.version, Some("version".to_owned())); + assert_eq!(spec_hwinfo.revision, Some("revision".to_owned())); + assert_eq!(spec_hwinfo.location, Some("location".to_owned())); + assert_eq!(spec_hwinfo.serial_no, Some("serial_no".to_owned())); + assert_eq!(spec_hwinfo.part_no, Some("part_no".to_owned())); + assert_eq!(spec_hwinfo.manufacturer, Some("manufacturer".to_owned())); + assert_eq!( + spec_hwinfo.manufacturer_part_no, + Some("manufacturer_part_no".to_owned()) + ); + assert_eq!(spec_hwinfo.odata_id, Some("odata_id".to_owned())); + assert_eq!( + spec_hwinfo.computer_system, + Some("computer_system".to_owned()) + ); + assert_eq!(spec_hwinfo.manager, Some("manager".to_owned())); + + Ok(()) + } + + #[test] + fn test_software_info() -> Result<()> { + let info = SoftwareInfo::builder("software_id", "name") + .version("version") + .revision("revision") + .software_type(spec::SoftwareType::Application) + .computer_system("system") + .build(); + + let spec_swinfo = info.to_spec(); + + assert_eq!(spec_swinfo.id, "software_id"); + assert_eq!(spec_swinfo.name, "name"); + assert_eq!(spec_swinfo.version, Some("version".to_owned())); + assert_eq!(spec_swinfo.revision, Some("revision".to_owned())); + assert_eq!( + spec_swinfo.software_type, + Some(spec::SoftwareType::Application) + ); + assert_eq!(spec_swinfo.computer_system, Some("system".to_owned())); + + Ok(()) + } + + #[test] + fn test_platform_info() -> Result<()> { + let info = PlatformInfo::builder("info").build(); + + assert_eq!(info.to_spec().info, "info"); + Ok(()) + } + + #[test] + fn test_subcomponent() -> Result<()> { + let sub = Subcomponent::builder("sub_name") + .subcomponent_type(spec::SubcomponentType::Asic) + .version("version") + .location("location") + .revision("revision") + .build(); + + let spec_subcomponent = sub.to_spec(); + + assert_eq!(spec_subcomponent.name, "sub_name"); + assert_eq!(spec_subcomponent.version, Some("version".to_owned())); + assert_eq!(spec_subcomponent.revision, Some("revision".to_owned())); + assert_eq!(spec_subcomponent.location, Some("location".to_owned())); + assert_eq!( + spec_subcomponent.subcomponent_type, + Some(spec::SubcomponentType::Asic) + ); + + Ok(()) + } +} diff --git a/src/output/emitters.rs b/src/output/emitter.rs similarity index 86% rename from src/output/emitters.rs rename to src/output/emitter.rs index f33bb71..e26acd2 100644 --- a/src/output/emitters.rs +++ b/src/output/emitter.rs @@ -16,7 +16,7 @@ use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; -use crate::output::models; +use crate::spec; #[derive(Debug, thiserror::Error, derive_more::Display)] #[non_exhaustive] @@ -98,13 +98,13 @@ impl JsonEmitter { } } - fn serialize_artifact(&self, object: &models::OutputArtifactDescendant) -> serde_json::Value { + fn serialize_artifact(&self, object: &spec::RootArtifact) -> serde_json::Value { let now = chrono::Local::now(); let now_tz = now.with_timezone(&self.timezone); - let out_artifact = models::OutputArtifactSpec { - descendant: object.clone(), - now: now_tz, - sequence_number: self.next_sequence_no(), + let out_artifact = spec::Root { + artifact: object.clone(), + timestamp: now_tz, + seqno: self.next_sequence_no(), }; serde_json::json!(out_artifact) } @@ -114,7 +114,7 @@ impl JsonEmitter { self.sequence_no.load(atomic::Ordering::SeqCst) } - pub async fn emit(&self, object: &models::OutputArtifactDescendant) -> Result<(), WriterError> { + pub async fn emit(&self, object: &spec::RootArtifact) -> Result<(), WriterError> { let serialized = self.serialize_artifact(object); match self.writer { WriterType::File(ref file) => file.write(&serialized.to_string()).await?, @@ -127,20 +127,20 @@ impl JsonEmitter { #[cfg(test)] mod tests { - use anyhow::anyhow; - use anyhow::Result; + use anyhow::{anyhow, Result}; use assert_json_diff::assert_json_include; use serde_json::json; use super::*; - use crate::output::objects::*; + use crate::output as tv; + use tv::run::SchemaVersion; #[tokio::test] async fn test_emit_using_buffer_writer() -> Result<()> { let expected = json!({ "schemaVersion": { - "major": models::SPEC_VERSION.0, - "minor": models::SPEC_VERSION.1, + "major": spec::SPEC_VERSION.0, + "minor": spec::SPEC_VERSION.1, }, "sequenceNumber": 1 }); @@ -164,15 +164,15 @@ mod tests { async fn test_sequence_number_increments_at_each_call() -> Result<()> { let expected_1 = json!({ "schemaVersion": { - "major": models::SPEC_VERSION.0, - "minor": models::SPEC_VERSION.1, + "major": spec::SPEC_VERSION.0, + "minor": spec::SPEC_VERSION.1, }, "sequenceNumber": 1 }); let expected_2 = json!({ "schemaVersion": { - "major": models::SPEC_VERSION.0, - "minor": models::SPEC_VERSION.1, + "major": spec::SPEC_VERSION.0, + "minor": spec::SPEC_VERSION.1, }, "sequenceNumber": 2 }); diff --git a/src/output/error.rs b/src/output/error.rs new file mode 100644 index 0000000..22b1d17 --- /dev/null +++ b/src/output/error.rs @@ -0,0 +1,206 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use crate::output as tv; +use crate::spec; +use tv::dut; + +pub struct Error { + symptom: String, + message: Option, + software_infos: Option>, + source_location: Option, +} + +impl Error { + pub fn builder(symptom: &str) -> ErrorBuilder { + ErrorBuilder::new(symptom) + } + + pub fn to_artifact(&self) -> spec::Error { + spec::Error { + symptom: self.symptom.clone(), + message: self.message.clone(), + software_infos: self.software_infos.clone(), + source_location: self.source_location.clone(), + } + } +} + +#[derive(Debug)] +pub struct ErrorBuilder { + symptom: String, + message: Option, + software_infos: Option>, + source_location: Option, +} + +impl ErrorBuilder { + fn new(symptom: &str) -> Self { + ErrorBuilder { + symptom: symptom.to_string(), + message: None, + source_location: None, + software_infos: None, + } + } + pub fn message(mut self, value: &str) -> ErrorBuilder { + self.message = Some(value.to_string()); + self + } + pub fn source(mut self, file: &str, line: i32) -> ErrorBuilder { + self.source_location = Some(spec::SourceLocation { + file: file.to_string(), + line, + }); + self + } + pub fn add_software_info(mut self, software_info: &dut::SoftwareInfo) -> ErrorBuilder { + self.software_infos = match self.software_infos { + Some(mut software_infos) => { + software_infos.push(software_info.to_spec()); + Some(software_infos) + } + None => Some(vec![software_info.to_spec()]), + }; + self + } + + pub fn build(self) -> Error { + Error { + symptom: self.symptom, + message: self.message, + source_location: self.source_location, + software_infos: self.software_infos, + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use assert_json_diff::assert_json_include; + + use super::*; + use crate::output as tv; + use crate::spec; + use tv::dut; + + #[test] + fn test_error_output_as_test_run_descendant_to_artifact() -> Result<()> { + let error = Error::builder("symptom") + .message("") + .add_software_info(&dut::SoftwareInfo::builder("id", "name").build()) + .source("", 1) + .build(); + + let artifact = error.to_artifact(); + assert_eq!( + artifact, + spec::Error { + symptom: error.symptom.clone(), + message: error.message.clone(), + software_infos: error.software_infos.clone(), + source_location: error.source_location.clone(), + } + ); + + Ok(()) + } + + #[test] + fn test_error_output_as_test_step_descendant_to_artifact() -> Result<()> { + let error = Error::builder("symptom") + .message("") + .add_software_info(&dut::SoftwareInfo::builder("id", "name").build()) + .source("", 1) + .build(); + + let artifact = error.to_artifact(); + assert_eq!( + artifact, + spec::Error { + symptom: error.symptom.clone(), + message: error.message.clone(), + software_infos: error.software_infos.clone(), + source_location: error.source_location.clone(), + } + ); + + Ok(()) + } + + #[test] + fn test_error() -> Result<()> { + let expected_run = serde_json::json!({ + "message": "message", + "softwareInfoIds": [ + { + "computerSystem": null, + "name": "name", + "revision": null, + "softwareInfoId": + "software_id", + "softwareType": null, + "version": null + }, + { + "computerSystem": null, + "name": "name", + "revision": null, + "softwareInfoId": + "software_id", + "softwareType": null, + "version": null + } + ], + "sourceLocation": {"file": "file.rs", "line": 1}, + "symptom": "symptom" + }); + let expected_step = serde_json::json!({ + "message": "message", + "softwareInfoIds": [ + { + "computerSystem": null, + "name": "name", + "revision": null, + "softwareInfoId": "software_id", + "softwareType": null, + "version": null + }, + { + "computerSystem": null, + "name": "name", + "revision": null, + "softwareInfoId": "software_id", + "softwareType": null, + "version": null + } + ], + "sourceLocation": {"file":"file.rs","line":1}, + "symptom":"symptom" + }); + + let software = dut::SoftwareInfo::builder("software_id", "name").build(); + let error = ErrorBuilder::new("symptom") + .message("message") + .source("file.rs", 1) + .add_software_info(&software) + .add_software_info(&software) + .build(); + + let spec_error = error.to_artifact(); + let actual = serde_json::json!(spec_error); + assert_json_include!(actual: actual, expected: &expected_run); + + let spec_error = error.to_artifact(); + let actual = serde_json::json!(spec_error); + assert_json_include!(actual: actual, expected: &expected_step); + + Ok(()) + } +} diff --git a/src/output/log.rs b/src/output/log.rs new file mode 100644 index 0000000..baf8635 --- /dev/null +++ b/src/output/log.rs @@ -0,0 +1,109 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use crate::spec; + +pub struct Log { + severity: spec::LogSeverity, + message: String, + source_location: Option, +} + +impl Log { + pub fn builder(message: &str) -> LogBuilder { + LogBuilder::new(message) + } + + pub fn to_artifact(&self) -> spec::Log { + spec::Log { + severity: self.severity.clone(), + message: self.message.clone(), + source_location: self.source_location.clone(), + } + } +} + +#[derive(Debug)] +pub struct LogBuilder { + severity: spec::LogSeverity, + message: String, + source_location: Option, +} + +impl LogBuilder { + fn new(message: &str) -> Self { + LogBuilder { + severity: spec::LogSeverity::Info, + message: message.to_string(), + source_location: None, + } + } + pub fn severity(mut self, value: spec::LogSeverity) -> LogBuilder { + self.severity = value; + self + } + pub fn source(mut self, file: &str, line: i32) -> LogBuilder { + self.source_location = Some(spec::SourceLocation { + file: file.to_string(), + line, + }); + self + } + + pub fn build(self) -> Log { + Log { + severity: self.severity, + message: self.message, + source_location: self.source_location, + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + use crate::spec; + + #[test] + fn test_log_output_as_test_run_descendant_to_artifact() -> Result<()> { + let log = Log::builder("test") + .severity(spec::LogSeverity::Info) + .build(); + + let artifact = log.to_artifact(); + assert_eq!( + artifact, + spec::Log { + severity: log.severity.clone(), + message: log.message.clone(), + source_location: log.source_location.clone(), + }, + ); + + Ok(()) + } + + #[test] + fn test_log_output_as_test_step_descendant_to_artifact() -> Result<()> { + let log = Log::builder("test") + .severity(spec::LogSeverity::Info) + .build(); + + let artifact = log.to_artifact(); + assert_eq!( + artifact, + spec::Log { + severity: log.severity.clone(), + message: log.message.clone(), + source_location: log.source_location.clone(), + } + ); + + Ok(()) + } +} diff --git a/src/output/measurement.rs b/src/output/measurement.rs new file mode 100644 index 0000000..249df84 --- /dev/null +++ b/src/output/measurement.rs @@ -0,0 +1,1007 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std::future::Future; +use std::sync::atomic; +use std::sync::Arc; + +use chrono::DateTime; +use serde_json::Map; +use serde_json::Value; +use tokio::sync::Mutex; + +use crate::output as tv; +use crate::spec; +use tv::{dut, emitter, state}; + +/// The measurement series. +/// A Measurement Series is a time-series list of measurements. +/// +/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart +pub struct MeasurementSeries { + state: Arc>, + seq_no: Arc>, + start: MeasurementSeriesStart, +} + +impl MeasurementSeries { + pub(crate) fn new(series_id: &str, name: &str, state: Arc>) -> Self { + Self { + state, + seq_no: Arc::new(Mutex::new(atomic::AtomicU64::new(0))), + start: MeasurementSeriesStart::new(name, series_id), + } + } + + pub(crate) fn new_with_details( + start: MeasurementSeriesStart, + state: Arc>, + ) -> Self { + Self { + state, + seq_no: Arc::new(Mutex::new(atomic::AtomicU64::new(0))), + start, + } + } + + async fn current_sequence_no(&self) -> u64 { + self.seq_no.lock().await.load(atomic::Ordering::SeqCst) + } + + async fn increment_sequence_no(&self) { + self.seq_no + .lock() + .await + .fetch_add(1, atomic::Ordering::SeqCst); + } + + /// Starts the measurement series. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let series = step.measurement_series("name"); + /// series.start().await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn start(&self) -> Result<(), emitter::WriterError> { + self.state + .lock() + .await + .emitter + .emit(&self.start.to_artifact()) + .await?; + Ok(()) + } + + /// Ends the measurement series. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesend + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let series = step.measurement_series("name"); + /// series.start().await?; + /// series.end().await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn end(&self) -> Result<(), emitter::WriterError> { + let end = + MeasurementSeriesEnd::new(self.start.get_series_id(), self.current_sequence_no().await); + self.state + .lock() + .await + .emitter + .emit(&end.to_artifact()) + .await?; + Ok(()) + } + + /// Adds a measurement element to the measurement series. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementserieselement + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let series = step.measurement_series("name"); + /// series.start().await?; + /// series.add_measurement(60.into()).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn add_measurement(&self, value: Value) -> Result<(), emitter::WriterError> { + let element = MeasurementSeriesElement::new( + self.current_sequence_no().await, + value, + &self.start, + None, + ); + self.increment_sequence_no().await; + self.state + .lock() + .await + .emitter + .emit(&element.to_artifact()) + .await?; + Ok(()) + } + + /// Adds a measurement element to the measurement series. + /// This method accepts additional metadata to add to the element. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementserieselement + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let series = step.measurement_series("name"); + /// series.start().await?; + /// series.add_measurement_with_metadata(60.into(), vec![("key", "value".into())]).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn add_measurement_with_metadata( + &self, + value: Value, + metadata: Vec<(&str, Value)>, + ) -> Result<(), emitter::WriterError> { + let element = MeasurementSeriesElement::new( + self.current_sequence_no().await, + value, + &self.start, + Some(Map::from_iter( + metadata.iter().map(|(k, v)| (k.to_string(), v.clone())), + )), + ); + self.increment_sequence_no().await; + self.state + .lock() + .await + .emitter + .emit(&element.to_artifact()) + .await?; + Ok(()) + } + + /// Builds a scope in the [`MeasurementSeries`] object, taking care of starting and + /// ending it. View [`MeasurementSeries::start`] and [`MeasurementSeries::end`] methods. + /// After the scope is constructed, additional objects may be added to it. + /// This is the preferred usage for the [`MeasurementSeries`], since it guarantees + /// all the messages are emitted between the start and end messages, the order + /// is respected and no messages is lost. + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let series = step.measurement_series("name"); + /// series.start().await?; + /// series.scope(|s| async { + /// s.add_measurement(60.into()).await?; + /// s.add_measurement(70.into()).await?; + /// s.add_measurement(80.into()).await?; + /// Ok(()) + /// }).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn scope<'a, F, R>(&'a self, func: F) -> Result<(), emitter::WriterError> + where + R: Future>, + F: std::ops::FnOnce(&'a MeasurementSeries) -> R, + { + self.start().await?; + func(self).await?; + self.end().await?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct Validator { + name: Option, + validator_type: spec::ValidatorType, + value: Value, + metadata: Option>, +} + +impl Validator { + pub fn builder(validator_type: spec::ValidatorType, value: Value) -> ValidatorBuilder { + ValidatorBuilder::new(validator_type, value) + } + pub fn to_spec(&self) -> spec::Validator { + spec::Validator { + name: self.name.clone(), + validator_type: self.validator_type.clone(), + value: self.value.clone(), + metadata: self.metadata.clone(), + } + } +} + +#[derive(Debug)] +pub struct ValidatorBuilder { + name: Option, + validator_type: spec::ValidatorType, + value: Value, + metadata: Option>, +} + +impl ValidatorBuilder { + fn new(validator_type: spec::ValidatorType, value: Value) -> Self { + ValidatorBuilder { + validator_type, + value: value.clone(), + name: None, + metadata: None, + } + } + pub fn name(mut self, value: &str) -> ValidatorBuilder { + self.name = Some(value.to_string()); + self + } + pub fn add_metadata(mut self, key: &str, value: Value) -> ValidatorBuilder { + self.metadata = match self.metadata { + Some(mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + None => { + let mut metadata = Map::new(); + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + }; + self + } + + pub fn build(self) -> Validator { + Validator { + name: self.name, + validator_type: self.validator_type, + value: self.value, + metadata: self.metadata, + } + } +} + +/// This structure represents a Measurement message. +/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement +/// +/// # Examples +/// +/// ## Create a Measurement object with the `new` method +/// +/// ``` +/// use ocptv::output::Measurement; +/// use ocptv::output::Value; +/// +/// let measurement = Measurement::new("name", 50.into()); +/// ``` +/// +/// ## Create a Measurement object with the `builder` method +/// +/// ``` +/// use ocptv::output::HardwareInfo; +/// use ocptv::output::Measurement; +/// use ocptv::output::Subcomponent; +/// use ocptv::output::Validator; +/// use ocptv::output::ValidatorType; +/// use ocptv::output::Value; +/// +/// let measurement = Measurement::builder("name", 50.into()) +/// .hardware_info(&HardwareInfo::builder("id", "name").build()) +/// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) +/// .add_metadata("key", "value".into()) +/// .subcomponent(&Subcomponent::builder("name").build()) +/// .build(); +/// ``` +pub struct Measurement { + name: String, + value: Value, + unit: Option, + validators: Option>, + hardware_info: Option, + subcomponent: Option, + metadata: Option>, +} + +impl Measurement { + /// Builds a new Measurement object. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::Measurement; + /// use ocptv::output::Value; + /// + /// let measurement = Measurement::new("name", 50.into()); + /// ``` + pub fn new(name: &str, value: Value) -> Self { + Measurement { + name: name.to_string(), + value: value.clone(), + unit: None, + validators: None, + hardware_info: None, + subcomponent: None, + metadata: None, + } + } + + /// Builds a new Measurement object using [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::HardwareInfo; + /// use ocptv::output::Measurement; + /// use ocptv::output::Subcomponent; + /// use ocptv::output::Validator; + /// use ocptv::output::ValidatorType; + /// use ocptv::output::Value; + /// + /// let measurement = Measurement::builder("name", 50.into()) + /// .hardware_info(&HardwareInfo::builder("id", "name").build()) + /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) + /// .add_metadata("key", "value".into()) + /// .subcomponent(&Subcomponent::builder("name").build()) + /// .build(); + /// ``` + pub fn builder(name: &str, value: Value) -> MeasurementBuilder { + MeasurementBuilder::new(name, value) + } + + /// Creates an artifact from a Measurement object. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::Measurement; + /// use ocptv::output::Value; + /// + /// let measurement = Measurement::new("name", 50.into()); + /// let _ = measurement.to_artifact(); + /// ``` + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Measurement(spec::Measurement { + name: self.name.clone(), + unit: self.unit.clone(), + value: self.value.clone(), + validators: self + .validators + .clone() + .map(|vals| vals.iter().map(|val| val.to_spec()).collect()), + hardware_info_id: self + .hardware_info + .as_ref() + .map(|hardware_info| hardware_info.id().to_owned()), + subcomponent: self + .subcomponent + .as_ref() + .map(|subcomponent| subcomponent.to_spec()), + metadata: self.metadata.clone(), + }), + }) + } +} + +/// This structure builds a [`Measurement`] object. +/// +/// # Examples +/// +/// ``` +/// use ocptv::output::HardwareInfo; +/// use ocptv::output::Measurement; +/// use ocptv::output::MeasurementBuilder; +/// use ocptv::output::Subcomponent; +/// use ocptv::output::Validator; +/// use ocptv::output::ValidatorType; +/// use ocptv::output::Value; +/// +/// let builder = MeasurementBuilder::new("name", 50.into()) +/// .hardware_info(&HardwareInfo::builder("id", "name").build()) +/// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) +/// .add_metadata("key", "value".into()) +/// .subcomponent(&Subcomponent::builder("name").build()); +/// let measurement = builder.build(); +/// ``` +pub struct MeasurementBuilder { + name: String, + value: Value, + unit: Option, + validators: Option>, + hardware_info: Option, + subcomponent: Option, + metadata: Option>, +} + +impl MeasurementBuilder { + /// Creates a new MeasurementBuilder. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50.into()); + /// ``` + pub fn new(name: &str, value: Value) -> Self { + MeasurementBuilder { + name: name.to_string(), + value: value.clone(), + unit: None, + validators: None, + hardware_info: None, + subcomponent: None, + metadata: None, + } + } + + /// Add a [`Validator`] to a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::HardwareInfo; + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Subcomponent; + /// use ocptv::output::Validator; + /// use ocptv::output::ValidatorType; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50.into()) + /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()); + /// ``` + pub fn add_validator(mut self, validator: &Validator) -> MeasurementBuilder { + self.validators = match self.validators { + Some(mut validators) => { + validators.push(validator.clone()); + Some(validators) + } + None => Some(vec![validator.clone()]), + }; + self + } + + /// Add a [`HardwareInfo`] to a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::HardwareInfo; + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50.into()) + /// .hardware_info(&HardwareInfo::builder("id", "name").build()); + /// ``` + pub fn hardware_info(mut self, hardware_info: &dut::HardwareInfo) -> MeasurementBuilder { + self.hardware_info = Some(hardware_info.clone()); + self + } + + /// Add a [`Subcomponent`] to a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Subcomponent; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50.into()) + /// .subcomponent(&Subcomponent::builder("name").build()); + /// ``` + pub fn subcomponent(mut self, subcomponent: &dut::Subcomponent) -> MeasurementBuilder { + self.subcomponent = Some(subcomponent.clone()); + self + } + + /// Add custom metadata to a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Value; + /// + /// let builder = + /// MeasurementBuilder::new("name", 50.into()).add_metadata("key", "value".into()); + /// ``` + pub fn add_metadata(mut self, key: &str, value: Value) -> MeasurementBuilder { + match self.metadata { + Some(ref mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + } + None => { + let mut entries = serde_json::Map::new(); + entries.insert(key.to_owned(), value); + self.metadata = Some(entries); + } + }; + self + } + + /// Add measurement unit to a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50000.into()).unit("RPM"); + /// ``` + pub fn unit(mut self, unit: &str) -> MeasurementBuilder { + self.unit = Some(unit.to_string()); + self + } + + /// Builds a [`Measurement`] object from a [`MeasurementBuilder`]. + /// + /// # Examples + /// + /// ``` + /// use ocptv::output::MeasurementBuilder; + /// use ocptv::output::Value; + /// + /// let builder = MeasurementBuilder::new("name", 50.into()); + /// let measurement = builder.build(); + /// ``` + pub fn build(self) -> Measurement { + Measurement { + name: self.name, + value: self.value, + unit: self.unit, + validators: self.validators, + hardware_info: self.hardware_info, + subcomponent: self.subcomponent, + metadata: self.metadata, + } + } +} + +pub struct MeasurementSeriesStart { + name: String, + unit: Option, + series_id: String, + validators: Option>, + hardware_info: Option, + subcomponent: Option, + metadata: Option>, +} + +impl MeasurementSeriesStart { + pub fn new(name: &str, series_id: &str) -> MeasurementSeriesStart { + MeasurementSeriesStart { + name: name.to_string(), + unit: None, + series_id: series_id.to_string(), + validators: None, + hardware_info: None, + subcomponent: None, + metadata: None, + } + } + + pub fn builder(name: &str, series_id: &str) -> MeasurementSeriesStartBuilder { + MeasurementSeriesStartBuilder::new(name, series_id) + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesStart( + spec::MeasurementSeriesStart { + name: self.name.clone(), + unit: self.unit.clone(), + series_id: self.series_id.clone(), + validators: self + .validators + .clone() + .map(|vals| vals.iter().map(|val| val.to_spec()).collect()), + hardware_info: self + .hardware_info + .as_ref() + .map(|hardware_info| hardware_info.to_spec()), + subcomponent: self + .subcomponent + .as_ref() + .map(|subcomponent| subcomponent.to_spec()), + metadata: self.metadata.clone(), + }, + ), + }) + } + + pub fn get_series_id(&self) -> &str { + &self.series_id + } +} + +pub struct MeasurementSeriesStartBuilder { + name: String, + unit: Option, + series_id: String, + validators: Option>, + hardware_info: Option, + subcomponent: Option, + metadata: Option>, +} + +impl MeasurementSeriesStartBuilder { + pub fn new(name: &str, series_id: &str) -> Self { + MeasurementSeriesStartBuilder { + name: name.to_string(), + unit: None, + series_id: series_id.to_string(), + validators: None, + hardware_info: None, + subcomponent: None, + metadata: None, + } + } + pub fn add_validator(mut self, validator: &Validator) -> MeasurementSeriesStartBuilder { + self.validators = match self.validators { + Some(mut validators) => { + validators.push(validator.clone()); + Some(validators) + } + None => Some(vec![validator.clone()]), + }; + self + } + + pub fn hardware_info( + mut self, + hardware_info: &dut::HardwareInfo, + ) -> MeasurementSeriesStartBuilder { + self.hardware_info = Some(hardware_info.clone()); + self + } + + pub fn subcomponent( + mut self, + subcomponent: &dut::Subcomponent, + ) -> MeasurementSeriesStartBuilder { + self.subcomponent = Some(subcomponent.clone()); + self + } + + pub fn add_metadata(mut self, key: &str, value: Value) -> MeasurementSeriesStartBuilder { + self.metadata = match self.metadata { + Some(mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + None => { + let mut metadata = Map::new(); + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + }; + self + } + + pub fn unit(mut self, unit: &str) -> MeasurementSeriesStartBuilder { + self.unit = Some(unit.to_string()); + self + } + + pub fn build(self) -> MeasurementSeriesStart { + MeasurementSeriesStart { + name: self.name, + unit: self.unit, + series_id: self.series_id, + validators: self.validators, + hardware_info: self.hardware_info, + subcomponent: self.subcomponent, + metadata: self.metadata, + } + } +} + +pub struct MeasurementSeriesEnd { + series_id: String, + total_count: u64, +} + +impl MeasurementSeriesEnd { + pub(crate) fn new(series_id: &str, total_count: u64) -> MeasurementSeriesEnd { + MeasurementSeriesEnd { + series_id: series_id.to_string(), + total_count, + } + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesEnd( + spec::MeasurementSeriesEnd { + series_id: self.series_id.clone(), + total_count: self.total_count, + }, + ), + }) + } +} + +pub struct MeasurementSeriesElement { + index: u64, + value: Value, + timestamp: DateTime, + series_id: String, + metadata: Option>, +} + +impl MeasurementSeriesElement { + pub(crate) fn new( + index: u64, + value: Value, + series: &MeasurementSeriesStart, + metadata: Option>, + ) -> MeasurementSeriesElement { + MeasurementSeriesElement { + index, + value: value.clone(), + timestamp: chrono::Local::now().with_timezone(&chrono_tz::Tz::UTC), + series_id: series.series_id.to_string(), + metadata, + } + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesElement( + spec::MeasurementSeriesElement { + index: self.index, + value: self.value.clone(), + timestamp: self.timestamp, + series_id: self.series_id.clone(), + metadata: self.metadata.clone(), + }, + ), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::output as tv; + use crate::spec; + use tv::dut::*; + use tv::ValidatorType; + + use anyhow::{bail, Result}; + + #[test] + fn test_measurement_as_test_step_descendant_to_artifact() -> Result<()> { + let name = "name".to_owned(); + let value = Value::from(50); + let measurement = Measurement::new(&name, value.clone()); + + let artifact = measurement.to_artifact(); + assert_eq!( + artifact, + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Measurement(spec::Measurement { + name: name.to_string(), + unit: None, + value, + validators: None, + hardware_info_id: None, + subcomponent: None, + metadata: None, + }), + }) + ); + + Ok(()) + } + + #[test] + fn test_measurement_builder_as_test_step_descendant_to_artifact() -> Result<()> { + let name = "name".to_owned(); + let value = Value::from(50000); + let hardware_info = HardwareInfo::builder("id", "name").build(); + let validator = Validator::builder(spec::ValidatorType::Equal, 30.into()).build(); + + let meta_key = "key"; + let meta_value = Value::from("value"); + let mut metadata = Map::new(); + metadata.insert(meta_key.to_string(), meta_value.clone()); + metadata.insert(meta_key.to_string(), meta_value.clone()); + + let subcomponent = Subcomponent::builder("name").build(); + + let unit = "RPM"; + let measurement = Measurement::builder(&name, value.clone()) + .hardware_info(&hardware_info) + .add_validator(&validator) + .add_validator(&validator) + .add_metadata(meta_key, meta_value.clone()) + .add_metadata(meta_key, meta_value.clone()) + .subcomponent(&subcomponent) + .unit(unit) + .build(); + + let artifact = measurement.to_artifact(); + assert_eq!( + artifact, + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Measurement(spec::Measurement { + name, + unit: Some(unit.to_string()), + value, + validators: Some(vec![validator.to_spec(), validator.to_spec()]), + hardware_info_id: Some(hardware_info.to_spec().id.clone()), + subcomponent: Some(subcomponent.to_spec()), + metadata: Some(metadata), + }), + }) + ); + + Ok(()) + } + + #[test] + fn test_measurement_series_start_to_artifact() -> Result<()> { + let name = "name".to_owned(); + let series_id = "series_id".to_owned(); + let series = MeasurementSeriesStart::new(&name, &series_id); + + let artifact = series.to_artifact(); + assert_eq!( + artifact, + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesStart( + spec::MeasurementSeriesStart { + name: name.to_string(), + unit: None, + series_id: series_id.to_string(), + validators: None, + hardware_info: None, + subcomponent: None, + metadata: None, + } + ), + }) + ); + + Ok(()) + } + + #[test] + fn test_measurement_series_start_builder_to_artifact() -> Result<()> { + let name = "name".to_owned(); + let series_id = "series_id".to_owned(); + let validator = Validator::builder(spec::ValidatorType::Equal, 30.into()).build(); + let validator2 = Validator::builder(spec::ValidatorType::GreaterThen, 10.into()).build(); + let hw_info = HardwareInfo::builder("id", "name").build(); + let subcomponent = Subcomponent::builder("name").build(); + let series = MeasurementSeriesStart::builder(&name, &series_id) + .unit("unit") + .add_metadata("key", "value".into()) + .add_metadata("key2", "value2".into()) + .add_validator(&validator) + .add_validator(&validator2) + .hardware_info(&hw_info) + .subcomponent(&subcomponent) + .build(); + + let artifact = series.to_artifact(); + assert_eq!( + artifact, + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesStart( + spec::MeasurementSeriesStart { + name, + unit: Some("unit".to_string()), + series_id: series_id.to_string(), + validators: Some(vec![validator.to_spec(), validator2.to_spec()]), + hardware_info: Some(hw_info.to_spec()), + subcomponent: Some(subcomponent.to_spec()), + metadata: Some(serde_json::Map::from_iter([ + ("key".to_string(), "value".into()), + ("key2".to_string(), "value2".into()) + ])), + } + ), + }) + ); + + Ok(()) + } + + #[test] + fn test_measurement_series_end_to_artifact() -> Result<()> { + let series_id = "series_id".to_owned(); + let series = MeasurementSeriesEnd::new(&series_id, 1); + + let artifact = series.to_artifact(); + assert_eq!( + artifact, + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::MeasurementSeriesEnd( + spec::MeasurementSeriesEnd { + series_id: series_id.to_string(), + total_count: 1, + } + ), + }) + ); + + Ok(()) + } + + #[test] + fn test_validator() -> Result<()> { + let validator = Validator::builder(ValidatorType::Equal, 30.into()) + .name("validator") + .add_metadata("key", "value".into()) + .add_metadata("key2", "value2".into()) + .build(); + + let spec_validator = validator.to_spec(); + + assert_eq!(spec_validator.name, Some("validator".to_owned())); + assert_eq!(spec_validator.value, 30); + assert_eq!(spec_validator.validator_type, ValidatorType::Equal); + + match spec_validator.metadata { + Some(m) => { + assert_eq!(m["key"], "value"); + assert_eq!(m["key2"], "value2"); + } + _ => bail!("metadata is none"), + } + + Ok(()) + } +} diff --git a/src/output/mod.rs b/src/output/mod.rs index 231567f..17cd5f4 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -4,18 +4,29 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -mod emitters; +mod config; +mod dut; +mod emitter; +mod error; +mod log; mod macros; -mod models; -mod objects; -mod runner; +mod measurement; +mod run; +mod state; +mod step; + +pub use crate::spec::LogSeverity; +pub use crate::spec::TestResult; +pub use crate::spec::TestStatus; +pub use crate::spec::ValidatorType; +pub use crate::spec::SPEC_VERSION; +pub use config::*; +pub use dut::*; +pub use emitter::*; +pub use error::*; +pub use log::*; +pub use measurement::*; +pub use run::*; +pub use step::*; -pub use emitters::*; -pub use models::LogSeverity; -pub use models::TestResult; -pub use models::TestStatus; -pub use models::ValidatorType; -pub use models::SPEC_VERSION; -pub use objects::*; -pub use runner::*; pub use serde_json::Value; diff --git a/src/output/objects.rs b/src/output/objects.rs deleted file mode 100644 index 8aa115c..0000000 --- a/src/output/objects.rs +++ /dev/null @@ -1,1920 +0,0 @@ -// (c) Meta Platforms, Inc. and affiliates. -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -use chrono::DateTime; -use serde_json::Map; -use serde_json::Value; - -use crate::output::models; - -pub enum ArtifactContext { - TestRun, - TestStep, -} - -pub struct SchemaVersion { - major: i8, - minor: i8, -} - -#[allow(clippy::new_without_default)] -impl SchemaVersion { - pub fn new() -> SchemaVersion { - SchemaVersion { - major: models::SPEC_VERSION.0, - minor: models::SPEC_VERSION.1, - } - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::SchemaVersion(models::SchemaVersionSpec { - major: self.major, - minor: self.minor, - }) - } -} - -#[derive(Default, Debug, Clone, PartialEq)] -pub struct DutInfo { - id: String, - name: Option, - platform_infos: Option>, - software_infos: Option>, - hardware_infos: Option>, - metadata: Option>, -} - -impl DutInfo { - pub fn builder(id: &str) -> DutInfoBuilder { - DutInfoBuilder::new(id) - } - - pub fn new(id: &str) -> DutInfo { - DutInfoBuilder::new(id).build() - } - - pub(crate) fn to_spec(&self) -> models::DutInfoSpec { - models::DutInfoSpec { - id: self.id.clone(), - name: self.name.clone(), - platform_infos: self - .platform_infos - .clone() - .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), - software_infos: self - .software_infos - .clone() - .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), - hardware_infos: self - .hardware_infos - .clone() - .map(|infos| infos.iter().map(|info| info.to_spec()).collect()), - metadata: self.metadata.clone(), - } - } -} - -pub struct DutInfoBuilder { - id: String, - name: Option, - platform_infos: Option>, - software_infos: Option>, - hardware_infos: Option>, - metadata: Option>, -} - -impl DutInfoBuilder { - pub fn new(id: &str) -> DutInfoBuilder { - DutInfoBuilder { - id: id.to_string(), - name: None, - platform_infos: None, - software_infos: None, - hardware_infos: None, - metadata: None, - } - } - pub fn name(mut self, value: &str) -> DutInfoBuilder { - self.name = Some(value.to_string()); - self - } - - pub fn add_platform_info(mut self, platform_info: &PlatformInfo) -> DutInfoBuilder { - self.platform_infos = match self.platform_infos { - Some(mut platform_infos) => { - platform_infos.push(platform_info.clone()); - Some(platform_infos) - } - None => Some(vec![platform_info.clone()]), - }; - self - } - - pub fn add_software_info(mut self, software_info: &SoftwareInfo) -> DutInfoBuilder { - self.software_infos = match self.software_infos { - Some(mut software_infos) => { - software_infos.push(software_info.clone()); - Some(software_infos) - } - None => Some(vec![software_info.clone()]), - }; - self - } - - pub fn add_hardware_info(mut self, hardware_info: &HardwareInfo) -> DutInfoBuilder { - self.hardware_infos = match self.hardware_infos { - Some(mut hardware_infos) => { - hardware_infos.push(hardware_info.clone()); - Some(hardware_infos) - } - None => Some(vec![hardware_info.clone()]), - }; - self - } - - pub fn add_metadata(mut self, key: &str, value: Value) -> DutInfoBuilder { - self.metadata = match self.metadata { - Some(mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - None => { - let mut metadata = Map::new(); - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - }; - self - } - - pub fn build(self) -> DutInfo { - DutInfo { - id: self.id, - name: self.name, - platform_infos: self.platform_infos, - software_infos: self.software_infos, - hardware_infos: self.hardware_infos, - metadata: self.metadata, - } - } -} - -pub struct TestRunStart { - name: String, - version: String, - command_line: String, - parameters: Map, - metadata: Option>, - dut_info: DutInfo, -} - -impl TestRunStart { - pub fn builder( - name: &str, - version: &str, - command_line: &str, - parameters: &Map, - dut_info: &DutInfo, - ) -> TestRunStartBuilder { - TestRunStartBuilder::new(name, version, command_line, parameters, dut_info) - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::TestRunStart(models::TestRunStartSpec { - name: self.name.clone(), - version: self.version.clone(), - command_line: self.command_line.clone(), - parameters: self.parameters.clone(), - metadata: self.metadata.clone(), - dut_info: self.dut_info.to_spec(), - }), - }) - } -} - -pub struct TestRunStartBuilder { - name: String, - version: String, - command_line: String, - parameters: Map, - metadata: Option>, - dut_info: DutInfo, -} - -impl TestRunStartBuilder { - pub fn new( - name: &str, - version: &str, - command_line: &str, - parameters: &Map, - dut_info: &DutInfo, - ) -> TestRunStartBuilder { - TestRunStartBuilder { - name: name.to_string(), - version: version.to_string(), - command_line: command_line.to_string(), - parameters: parameters.clone(), - metadata: None, - dut_info: dut_info.clone(), - } - } - - pub fn add_metadata(mut self, key: &str, value: Value) -> TestRunStartBuilder { - self.metadata = match self.metadata { - Some(mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - None => { - let mut metadata = Map::new(); - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - }; - self - } - - pub fn build(self) -> TestRunStart { - TestRunStart { - name: self.name, - version: self.version, - command_line: self.command_line, - parameters: self.parameters, - metadata: self.metadata, - dut_info: self.dut_info, - } - } -} - -pub struct TestRunEnd { - status: models::TestStatus, - result: models::TestResult, -} - -impl TestRunEnd { - pub fn builder() -> TestRunEndBuilder { - TestRunEndBuilder::new() - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::TestRunEnd(models::TestRunEndSpec { - status: self.status.clone(), - result: self.result.clone(), - }), - }) - } -} - -#[derive(Debug)] -pub struct TestRunEndBuilder { - status: models::TestStatus, - result: models::TestResult, -} - -#[allow(clippy::new_without_default)] -impl TestRunEndBuilder { - pub fn new() -> TestRunEndBuilder { - TestRunEndBuilder { - status: models::TestStatus::Complete, - result: models::TestResult::Pass, - } - } - pub fn status(mut self, value: models::TestStatus) -> TestRunEndBuilder { - self.status = value; - self - } - - pub fn result(mut self, value: models::TestResult) -> TestRunEndBuilder { - self.result = value; - self - } - - pub fn build(self) -> TestRunEnd { - TestRunEnd { - status: self.status, - result: self.result, - } - } -} - -pub struct Log { - severity: models::LogSeverity, - message: String, - source_location: Option, -} - -impl Log { - pub fn builder(message: &str) -> LogBuilder { - LogBuilder::new(message) - } - - pub fn to_artifact(&self, context: ArtifactContext) -> models::OutputArtifactDescendant { - match context { - ArtifactContext::TestRun => { - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::Log(models::LogSpec { - severity: self.severity.clone(), - message: self.message.clone(), - source_location: self.source_location.clone(), - }), - }) - } - ArtifactContext::TestStep => { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Log(models::LogSpec { - severity: self.severity.clone(), - message: self.message.clone(), - source_location: self.source_location.clone(), - }), - }) - } - } - } -} - -#[derive(Debug)] -pub struct LogBuilder { - severity: models::LogSeverity, - message: String, - source_location: Option, -} - -impl LogBuilder { - fn new(message: &str) -> Self { - LogBuilder { - severity: models::LogSeverity::Info, - message: message.to_string(), - source_location: None, - } - } - pub fn severity(mut self, value: models::LogSeverity) -> LogBuilder { - self.severity = value; - self - } - pub fn source(mut self, file: &str, line: i32) -> LogBuilder { - self.source_location = Some(models::SourceLocationSpec { - file: file.to_string(), - line, - }); - self - } - - pub fn build(self) -> Log { - Log { - severity: self.severity, - message: self.message, - source_location: self.source_location, - } - } -} - -pub struct Error { - symptom: String, - message: Option, - software_infos: Option>, - source_location: Option, -} - -impl Error { - pub fn builder(symptom: &str) -> ErrorBuilder { - ErrorBuilder::new(symptom) - } - - pub fn to_artifact(&self, context: ArtifactContext) -> models::OutputArtifactDescendant { - match context { - ArtifactContext::TestRun => { - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::Error(models::ErrorSpec { - symptom: self.symptom.clone(), - message: self.message.clone(), - software_infos: self.software_infos.clone(), - source_location: self.source_location.clone(), - }), - }) - } - ArtifactContext::TestStep => { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Error(models::ErrorSpec { - symptom: self.symptom.clone(), - message: self.message.clone(), - software_infos: self.software_infos.clone(), - source_location: self.source_location.clone(), - }), - }) - } - } - } -} - -#[derive(Debug)] -pub struct ErrorBuilder { - symptom: String, - message: Option, - software_infos: Option>, - source_location: Option, -} - -impl ErrorBuilder { - fn new(symptom: &str) -> Self { - ErrorBuilder { - symptom: symptom.to_string(), - message: None, - source_location: None, - software_infos: None, - } - } - pub fn message(mut self, value: &str) -> ErrorBuilder { - self.message = Some(value.to_string()); - self - } - pub fn source(mut self, file: &str, line: i32) -> ErrorBuilder { - self.source_location = Some(models::SourceLocationSpec { - file: file.to_string(), - line, - }); - self - } - pub fn add_software_info(mut self, software_info: &SoftwareInfo) -> ErrorBuilder { - self.software_infos = match self.software_infos { - Some(mut software_infos) => { - software_infos.push(software_info.to_spec()); - Some(software_infos) - } - None => Some(vec![software_info.to_spec()]), - }; - self - } - - pub fn build(self) -> Error { - Error { - symptom: self.symptom, - message: self.message, - source_location: self.source_location, - software_infos: self.software_infos, - } - } -} - -pub struct TestStepStart { - name: String, -} - -impl TestStepStart { - pub fn new(name: &str) -> TestStepStart { - TestStepStart { - name: name.to_string(), - } - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::TestStepStart( - models::TestStepStartSpec { - name: self.name.clone(), - }, - ), - }) - } -} - -pub struct TestStepEnd { - status: models::TestStatus, -} - -impl TestStepEnd { - pub fn new(status: models::TestStatus) -> TestStepEnd { - TestStepEnd { status } - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::TestStepEnd(models::TestStepEndSpec { - status: self.status.clone(), - }), - }) - } -} - -#[derive(Clone)] -pub struct Validator { - name: Option, - validator_type: models::ValidatorType, - value: Value, - metadata: Option>, -} - -impl Validator { - pub fn builder(validator_type: models::ValidatorType, value: Value) -> ValidatorBuilder { - ValidatorBuilder::new(validator_type, value) - } - pub fn to_spec(&self) -> models::ValidatorSpec { - models::ValidatorSpec { - name: self.name.clone(), - validator_type: self.validator_type.clone(), - value: self.value.clone(), - metadata: self.metadata.clone(), - } - } -} - -#[derive(Debug)] -pub struct ValidatorBuilder { - name: Option, - validator_type: models::ValidatorType, - value: Value, - metadata: Option>, -} - -impl ValidatorBuilder { - fn new(validator_type: models::ValidatorType, value: Value) -> Self { - ValidatorBuilder { - validator_type, - value: value.clone(), - name: None, - metadata: None, - } - } - pub fn name(mut self, value: &str) -> ValidatorBuilder { - self.name = Some(value.to_string()); - self - } - pub fn add_metadata(mut self, key: &str, value: Value) -> ValidatorBuilder { - self.metadata = match self.metadata { - Some(mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - None => { - let mut metadata = Map::new(); - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - }; - self - } - - pub fn build(self) -> Validator { - Validator { - name: self.name, - validator_type: self.validator_type, - value: self.value, - metadata: self.metadata, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct HardwareInfo { - id: String, - name: String, - version: Option, - revision: Option, - location: Option, - serial_no: Option, - part_no: Option, - manufacturer: Option, - manufacturer_part_no: Option, - odata_id: Option, - computer_system: Option, - manager: Option, -} - -impl HardwareInfo { - pub fn builder(id: &str, name: &str) -> HardwareInfoBuilder { - HardwareInfoBuilder::new(id, name) - } - pub fn to_spec(&self) -> models::HardwareInfoSpec { - models::HardwareInfoSpec { - id: self.id.clone(), - name: self.name.clone(), - version: self.version.clone(), - revision: self.revision.clone(), - location: self.location.clone(), - serial_no: self.serial_no.clone(), - part_no: self.part_no.clone(), - manufacturer: self.manufacturer.clone(), - manufacturer_part_no: self.manufacturer_part_no.clone(), - odata_id: self.odata_id.clone(), - computer_system: self.computer_system.clone(), - manager: self.manager.clone(), - } - } -} - -#[derive(Debug)] -pub struct HardwareInfoBuilder { - id: String, - name: String, - version: Option, - revision: Option, - location: Option, - serial_no: Option, - part_no: Option, - manufacturer: Option, - manufacturer_part_no: Option, - odata_id: Option, - computer_system: Option, - manager: Option, -} - -impl HardwareInfoBuilder { - fn new(id: &str, name: &str) -> Self { - HardwareInfoBuilder { - id: id.to_string(), - name: name.to_string(), - version: None, - revision: None, - location: None, - serial_no: None, - part_no: None, - manufacturer: None, - manufacturer_part_no: None, - odata_id: None, - computer_system: None, - manager: None, - } - } - pub fn version(mut self, value: &str) -> HardwareInfoBuilder { - self.version = Some(value.to_string()); - self - } - pub fn revision(mut self, value: &str) -> HardwareInfoBuilder { - self.revision = Some(value.to_string()); - self - } - pub fn location(mut self, value: &str) -> HardwareInfoBuilder { - self.location = Some(value.to_string()); - self - } - pub fn serial_no(mut self, value: &str) -> HardwareInfoBuilder { - self.serial_no = Some(value.to_string()); - self - } - pub fn part_no(mut self, value: &str) -> HardwareInfoBuilder { - self.part_no = Some(value.to_string()); - self - } - pub fn manufacturer(mut self, value: &str) -> HardwareInfoBuilder { - self.manufacturer = Some(value.to_string()); - self - } - pub fn manufacturer_part_no(mut self, value: &str) -> HardwareInfoBuilder { - self.manufacturer_part_no = Some(value.to_string()); - self - } - pub fn odata_id(mut self, value: &str) -> HardwareInfoBuilder { - self.odata_id = Some(value.to_string()); - self - } - pub fn computer_system(mut self, value: &str) -> HardwareInfoBuilder { - self.computer_system = Some(value.to_string()); - self - } - pub fn manager(mut self, value: &str) -> HardwareInfoBuilder { - self.manager = Some(value.to_string()); - self - } - - pub fn build(self) -> HardwareInfo { - HardwareInfo { - id: self.id, - name: self.name, - version: self.version, - revision: self.revision, - location: self.location, - serial_no: self.serial_no, - part_no: self.part_no, - manufacturer: self.manufacturer, - manufacturer_part_no: self.manufacturer_part_no, - odata_id: self.odata_id, - computer_system: self.computer_system, - manager: self.manager, - } - } -} - -#[derive(Debug, Clone)] -pub struct Subcomponent { - subcomponent_type: Option, - name: String, - location: Option, - version: Option, - revision: Option, -} - -impl Subcomponent { - pub fn builder(name: &str) -> SubcomponentBuilder { - SubcomponentBuilder::new(name) - } - pub fn to_spec(&self) -> models::SubcomponentSpec { - models::SubcomponentSpec { - subcomponent_type: self.subcomponent_type.clone(), - name: self.name.clone(), - location: self.location.clone(), - version: self.version.clone(), - revision: self.revision.clone(), - } - } -} - -#[derive(Debug)] -pub struct SubcomponentBuilder { - subcomponent_type: Option, - name: String, - location: Option, - version: Option, - revision: Option, -} - -impl SubcomponentBuilder { - fn new(name: &str) -> Self { - SubcomponentBuilder { - subcomponent_type: None, - name: name.to_string(), - location: None, - version: None, - revision: None, - } - } - pub fn subcomponent_type(mut self, value: models::SubcomponentType) -> SubcomponentBuilder { - self.subcomponent_type = Some(value); - self - } - pub fn version(mut self, value: &str) -> SubcomponentBuilder { - self.version = Some(value.to_string()); - self - } - pub fn location(mut self, value: &str) -> SubcomponentBuilder { - self.location = Some(value.to_string()); - self - } - pub fn revision(mut self, value: &str) -> SubcomponentBuilder { - self.revision = Some(value.to_string()); - self - } - - pub fn build(self) -> Subcomponent { - Subcomponent { - subcomponent_type: self.subcomponent_type, - name: self.name, - location: self.location, - version: self.version, - revision: self.revision, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct PlatformInfo { - info: String, -} - -impl PlatformInfo { - pub fn builder(info: &str) -> PlatformInfoBuilder { - PlatformInfoBuilder::new(info) - } - - pub fn to_spec(&self) -> models::PlatformInfoSpec { - models::PlatformInfoSpec { - info: self.info.clone(), - } - } -} - -#[derive(Debug)] -pub struct PlatformInfoBuilder { - info: String, -} - -impl PlatformInfoBuilder { - fn new(info: &str) -> Self { - PlatformInfoBuilder { - info: info.to_string(), - } - } - - pub fn build(self) -> PlatformInfo { - PlatformInfo { info: self.info } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct SoftwareInfo { - id: String, - name: String, - version: Option, - revision: Option, - software_type: Option, - computer_system: Option, -} - -impl SoftwareInfo { - pub fn builder(id: &str, name: &str) -> SoftwareInfoBuilder { - SoftwareInfoBuilder::new(id, name) - } - - pub fn to_spec(&self) -> models::SoftwareInfoSpec { - models::SoftwareInfoSpec { - id: self.id.clone(), - name: self.name.clone(), - version: self.version.clone(), - revision: self.revision.clone(), - software_type: self.software_type.clone(), - computer_system: self.computer_system.clone(), - } - } -} - -#[derive(Debug)] -pub struct SoftwareInfoBuilder { - id: String, - name: String, - version: Option, - revision: Option, - software_type: Option, - computer_system: Option, -} - -impl SoftwareInfoBuilder { - fn new(id: &str, name: &str) -> Self { - SoftwareInfoBuilder { - id: id.to_string(), - name: name.to_string(), - version: None, - revision: None, - software_type: None, - computer_system: None, - } - } - pub fn version(mut self, value: &str) -> SoftwareInfoBuilder { - self.version = Some(value.to_string()); - self - } - pub fn revision(mut self, value: &str) -> SoftwareInfoBuilder { - self.revision = Some(value.to_string()); - self - } - pub fn software_type(mut self, value: models::SoftwareType) -> SoftwareInfoBuilder { - self.software_type = Some(value); - self - } - pub fn computer_system(mut self, value: &str) -> SoftwareInfoBuilder { - self.computer_system = Some(value.to_string()); - self - } - - pub fn build(self) -> SoftwareInfo { - SoftwareInfo { - id: self.id, - name: self.name, - version: self.version, - revision: self.revision, - software_type: self.software_type, - computer_system: self.computer_system, - } - } -} - -/// This structure represents a Measurement message. -/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement -/// -/// # Examples -/// -/// ## Create a Measurement object with the `new` method -/// -/// ``` -/// use ocptv::output::Measurement; -/// use ocptv::output::Value; -/// -/// let measurement = Measurement::new("name", 50.into()); -/// ``` -/// -/// ## Create a Measurement object with the `builder` method -/// -/// ``` -/// use ocptv::output::HardwareInfo; -/// use ocptv::output::Measurement; -/// use ocptv::output::Subcomponent; -/// use ocptv::output::Validator; -/// use ocptv::output::ValidatorType; -/// use ocptv::output::Value; -/// -/// let measurement = Measurement::builder("name", 50.into()) -/// .hardware_info(&HardwareInfo::builder("id", "name").build()) -/// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) -/// .add_metadata("key", "value".into()) -/// .subcomponent(&Subcomponent::builder("name").build()) -/// .build(); -/// ``` -pub struct Measurement { - name: String, - value: Value, - unit: Option, - validators: Option>, - hardware_info: Option, - subcomponent: Option, - metadata: Option>, -} - -impl Measurement { - /// Builds a new Measurement object. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::Measurement; - /// use ocptv::output::Value; - /// - /// let measurement = Measurement::new("name", 50.into()); - /// ``` - pub fn new(name: &str, value: Value) -> Self { - Measurement { - name: name.to_string(), - value: value.clone(), - unit: None, - validators: None, - hardware_info: None, - subcomponent: None, - metadata: None, - } - } - - /// Builds a new Measurement object using [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::HardwareInfo; - /// use ocptv::output::Measurement; - /// use ocptv::output::Subcomponent; - /// use ocptv::output::Validator; - /// use ocptv::output::ValidatorType; - /// use ocptv::output::Value; - /// - /// let measurement = Measurement::builder("name", 50.into()) - /// .hardware_info(&HardwareInfo::builder("id", "name").build()) - /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) - /// .add_metadata("key", "value".into()) - /// .subcomponent(&Subcomponent::builder("name").build()) - /// .build(); - /// ``` - pub fn builder(name: &str, value: Value) -> MeasurementBuilder { - MeasurementBuilder::new(name, value) - } - - /// Creates an artifact from a Measurement object. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::Measurement; - /// use ocptv::output::Value; - /// - /// let measurement = Measurement::new("name", 50.into()); - /// let _ = measurement.to_artifact(); - /// ``` - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Measurement(models::MeasurementSpec { - name: self.name.clone(), - unit: self.unit.clone(), - value: self.value.clone(), - validators: self - .validators - .clone() - .map(|vals| vals.iter().map(|val| val.to_spec()).collect()), - hardware_info_id: self - .hardware_info - .as_ref() - .map(|hardware_info| hardware_info.id.clone()), - subcomponent: self - .subcomponent - .as_ref() - .map(|subcomponent| subcomponent.to_spec()), - metadata: self.metadata.clone(), - }), - }) - } -} - -/// This structure builds a [`Measurement`] object. -/// -/// # Examples -/// -/// ``` -/// use ocptv::output::HardwareInfo; -/// use ocptv::output::Measurement; -/// use ocptv::output::MeasurementBuilder; -/// use ocptv::output::Subcomponent; -/// use ocptv::output::Validator; -/// use ocptv::output::ValidatorType; -/// use ocptv::output::Value; -/// -/// let builder = MeasurementBuilder::new("name", 50.into()) -/// .hardware_info(&HardwareInfo::builder("id", "name").build()) -/// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) -/// .add_metadata("key", "value".into()) -/// .subcomponent(&Subcomponent::builder("name").build()); -/// let measurement = builder.build(); -/// ``` -pub struct MeasurementBuilder { - name: String, - value: Value, - unit: Option, - validators: Option>, - hardware_info: Option, - subcomponent: Option, - metadata: Option>, -} - -impl MeasurementBuilder { - /// Creates a new MeasurementBuilder. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50.into()); - /// ``` - pub fn new(name: &str, value: Value) -> Self { - MeasurementBuilder { - name: name.to_string(), - value: value.clone(), - unit: None, - validators: None, - hardware_info: None, - subcomponent: None, - metadata: None, - } - } - - /// Add a [`Validator`] to a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::HardwareInfo; - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Subcomponent; - /// use ocptv::output::Validator; - /// use ocptv::output::ValidatorType; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50.into()) - /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()); - /// ``` - pub fn add_validator(mut self, validator: &Validator) -> MeasurementBuilder { - self.validators = match self.validators { - Some(mut validators) => { - validators.push(validator.clone()); - Some(validators) - } - None => Some(vec![validator.clone()]), - }; - self - } - - /// Add a [`HardwareInfo`] to a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::HardwareInfo; - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50.into()) - /// .hardware_info(&HardwareInfo::builder("id", "name").build()); - /// ``` - pub fn hardware_info(mut self, hardware_info: &HardwareInfo) -> MeasurementBuilder { - self.hardware_info = Some(hardware_info.clone()); - self - } - - /// Add a [`Subcomponent`] to a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Subcomponent; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50.into()) - /// .subcomponent(&Subcomponent::builder("name").build()); - /// ``` - pub fn subcomponent(mut self, subcomponent: &Subcomponent) -> MeasurementBuilder { - self.subcomponent = Some(subcomponent.clone()); - self - } - - /// Add custom metadata to a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Value; - /// - /// let builder = - /// MeasurementBuilder::new("name", 50.into()).add_metadata("key", "value".into()); - /// ``` - pub fn add_metadata(mut self, key: &str, value: Value) -> MeasurementBuilder { - match self.metadata { - Some(ref mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - } - None => { - let mut entries = serde_json::Map::new(); - entries.insert(key.to_owned(), value); - self.metadata = Some(entries); - } - }; - self - } - - /// Add measurement unit to a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50000.into()).unit("RPM"); - /// ``` - pub fn unit(mut self, unit: &str) -> MeasurementBuilder { - self.unit = Some(unit.to_string()); - self - } - - /// Builds a [`Measurement`] object from a [`MeasurementBuilder`]. - /// - /// # Examples - /// - /// ``` - /// use ocptv::output::MeasurementBuilder; - /// use ocptv::output::Value; - /// - /// let builder = MeasurementBuilder::new("name", 50.into()); - /// let measurement = builder.build(); - /// ``` - pub fn build(self) -> Measurement { - Measurement { - name: self.name, - value: self.value, - unit: self.unit, - validators: self.validators, - hardware_info: self.hardware_info, - subcomponent: self.subcomponent, - metadata: self.metadata, - } - } -} - -pub struct MeasurementSeriesStart { - name: String, - unit: Option, - series_id: String, - validators: Option>, - hardware_info: Option, - subcomponent: Option, - metadata: Option>, -} - -impl MeasurementSeriesStart { - pub fn new(name: &str, series_id: &str) -> MeasurementSeriesStart { - MeasurementSeriesStart { - name: name.to_string(), - unit: None, - series_id: series_id.to_string(), - validators: None, - hardware_info: None, - subcomponent: None, - metadata: None, - } - } - - pub fn builder(name: &str, series_id: &str) -> MeasurementSeriesStartBuilder { - MeasurementSeriesStartBuilder::new(name, series_id) - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesStart( - models::MeasurementSeriesStartSpec { - name: self.name.clone(), - unit: self.unit.clone(), - series_id: self.series_id.clone(), - validators: self - .validators - .clone() - .map(|vals| vals.iter().map(|val| val.to_spec()).collect()), - hardware_info: self - .hardware_info - .as_ref() - .map(|hardware_info| hardware_info.to_spec()), - subcomponent: self - .subcomponent - .as_ref() - .map(|subcomponent| subcomponent.to_spec()), - metadata: self.metadata.clone(), - }, - ), - }) - } - - pub fn get_series_id(&self) -> &str { - &self.series_id - } -} - -pub struct MeasurementSeriesStartBuilder { - name: String, - unit: Option, - series_id: String, - validators: Option>, - hardware_info: Option, - subcomponent: Option, - metadata: Option>, -} - -impl MeasurementSeriesStartBuilder { - pub fn new(name: &str, series_id: &str) -> Self { - MeasurementSeriesStartBuilder { - name: name.to_string(), - unit: None, - series_id: series_id.to_string(), - validators: None, - hardware_info: None, - subcomponent: None, - metadata: None, - } - } - pub fn add_validator(mut self, validator: &Validator) -> MeasurementSeriesStartBuilder { - self.validators = match self.validators { - Some(mut validators) => { - validators.push(validator.clone()); - Some(validators) - } - None => Some(vec![validator.clone()]), - }; - self - } - - pub fn hardware_info(mut self, hardware_info: &HardwareInfo) -> MeasurementSeriesStartBuilder { - self.hardware_info = Some(hardware_info.clone()); - self - } - - pub fn subcomponent(mut self, subcomponent: &Subcomponent) -> MeasurementSeriesStartBuilder { - self.subcomponent = Some(subcomponent.clone()); - self - } - - pub fn add_metadata(mut self, key: &str, value: Value) -> MeasurementSeriesStartBuilder { - self.metadata = match self.metadata { - Some(mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - None => { - let mut metadata = Map::new(); - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - }; - self - } - - pub fn unit(mut self, unit: &str) -> MeasurementSeriesStartBuilder { - self.unit = Some(unit.to_string()); - self - } - - pub fn build(self) -> MeasurementSeriesStart { - MeasurementSeriesStart { - name: self.name, - unit: self.unit, - series_id: self.series_id, - validators: self.validators, - hardware_info: self.hardware_info, - subcomponent: self.subcomponent, - metadata: self.metadata, - } - } -} - -pub struct MeasurementSeriesEnd { - series_id: String, - total_count: u64, -} - -impl MeasurementSeriesEnd { - pub(crate) fn new(series_id: &str, total_count: u64) -> MeasurementSeriesEnd { - MeasurementSeriesEnd { - series_id: series_id.to_string(), - total_count, - } - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesEnd( - models::MeasurementSeriesEndSpec { - series_id: self.series_id.clone(), - total_count: self.total_count, - }, - ), - }) - } -} - -pub struct MeasurementSeriesElement { - index: u64, - value: Value, - timestamp: DateTime, - series_id: String, - metadata: Option>, -} - -impl MeasurementSeriesElement { - pub(crate) fn new( - index: u64, - value: Value, - series: &MeasurementSeriesStart, - metadata: Option>, - ) -> MeasurementSeriesElement { - MeasurementSeriesElement { - index, - value: value.clone(), - timestamp: chrono::Local::now().with_timezone(&chrono_tz::Tz::UTC), - series_id: series.series_id.to_string(), - metadata, - } - } - - pub fn to_artifact(&self) -> models::OutputArtifactDescendant { - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesElement( - models::MeasurementSeriesElementSpec { - index: self.index, - value: self.value.clone(), - timestamp: self.timestamp, - series_id: self.series_id.clone(), - metadata: self.metadata.clone(), - }, - ), - }) - } -} - -#[cfg(test)] -mod tests { - use anyhow::bail; - use anyhow::Result; - - use assert_json_diff::assert_json_include; - use serde_json::Map; - use serde_json::Value; - - use super::*; - use crate::output::models; - use crate::output::models::ValidatorType; - - #[test] - fn test_schema_creation_from_builder() -> Result<()> { - let version = SchemaVersion::new(); - assert_eq!(version.major, models::SPEC_VERSION.0); - assert_eq!(version.minor, models::SPEC_VERSION.1); - Ok(()) - } - - #[test] - fn test_dut_creation_from_builder_with_defaults() -> Result<()> { - let dut = DutInfo::builder("1234").build(); - assert_eq!(dut.id, "1234"); - Ok(()) - } - - #[test] - fn test_log_output_as_test_run_descendant_to_artifact() -> Result<()> { - let log = Log::builder("test") - .severity(models::LogSeverity::Info) - .build(); - - let artifact = log.to_artifact(ArtifactContext::TestRun); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::Log(models::LogSpec { - severity: log.severity.clone(), - message: log.message.clone(), - source_location: log.source_location.clone(), - }), - }) - ); - - Ok(()) - } - - #[test] - fn test_log_output_as_test_step_descendant_to_artifact() -> Result<()> { - let log = Log::builder("test") - .severity(models::LogSeverity::Info) - .build(); - - let artifact = log.to_artifact(ArtifactContext::TestStep); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Log(models::LogSpec { - severity: log.severity.clone(), - message: log.message.clone(), - source_location: log.source_location.clone(), - }), - }) - ); - - Ok(()) - } - - #[test] - fn test_error_output_as_test_run_descendant_to_artifact() -> Result<()> { - let error = Error::builder("symptom") - .message("") - .add_software_info(&SoftwareInfo::builder("id", "name").build()) - .source("", 1) - .build(); - - let artifact = error.to_artifact(ArtifactContext::TestRun); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestRunArtifact(models::TestRunArtifactSpec { - descendant: models::TestRunArtifactDescendant::Error(models::ErrorSpec { - symptom: error.symptom.clone(), - message: error.message.clone(), - software_infos: error.software_infos.clone(), - source_location: error.source_location.clone(), - }), - }) - ); - - Ok(()) - } - - #[test] - fn test_error_output_as_test_step_descendant_to_artifact() -> Result<()> { - let error = Error::builder("symptom") - .message("") - .add_software_info(&SoftwareInfo::builder("id", "name").build()) - .source("", 1) - .build(); - - let artifact = error.to_artifact(ArtifactContext::TestStep); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Error(models::ErrorSpec { - symptom: error.symptom.clone(), - message: error.message.clone(), - software_infos: error.software_infos.clone(), - source_location: error.source_location.clone(), - }), - }) - ); - - Ok(()) - } - - #[test] - fn test_measurement_as_test_step_descendant_to_artifact() -> Result<()> { - let name = "name".to_owned(); - let value = Value::from(50); - let measurement = Measurement::new(&name, value.clone()); - - let artifact = measurement.to_artifact(); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Measurement( - models::MeasurementSpec { - name: name.to_string(), - unit: None, - value, - validators: None, - hardware_info_id: None, - subcomponent: None, - metadata: None, - } - ), - }) - ); - - Ok(()) - } - - #[test] - fn test_measurement_builder_as_test_step_descendant_to_artifact() -> Result<()> { - let name = "name".to_owned(); - let value = Value::from(50000); - let hardware_info = HardwareInfo::builder("id", "name").build(); - let validator = Validator::builder(models::ValidatorType::Equal, 30.into()).build(); - - let meta_key = "key"; - let meta_value = Value::from("value"); - let mut metadata = Map::new(); - metadata.insert(meta_key.to_string(), meta_value.clone()); - metadata.insert(meta_key.to_string(), meta_value.clone()); - - let subcomponent = Subcomponent::builder("name").build(); - - let unit = "RPM"; - let measurement = Measurement::builder(&name, value.clone()) - .hardware_info(&hardware_info) - .add_validator(&validator) - .add_validator(&validator) - .add_metadata(meta_key, meta_value.clone()) - .add_metadata(meta_key, meta_value.clone()) - .subcomponent(&subcomponent) - .unit(unit) - .build(); - - let artifact = measurement.to_artifact(); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::Measurement( - models::MeasurementSpec { - name, - unit: Some(unit.to_string()), - value, - validators: Some(vec![validator.to_spec(), validator.to_spec()]), - hardware_info_id: Some(hardware_info.to_spec().id.clone()), - subcomponent: Some(subcomponent.to_spec()), - metadata: Some(metadata), - } - ), - }) - ); - - Ok(()) - } - - #[test] - fn test_measurement_series_start_to_artifact() -> Result<()> { - let name = "name".to_owned(); - let series_id = "series_id".to_owned(); - let series = MeasurementSeriesStart::new(&name, &series_id); - - let artifact = series.to_artifact(); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesStart( - models::MeasurementSeriesStartSpec { - name: name.to_string(), - unit: None, - series_id: series_id.to_string(), - validators: None, - hardware_info: None, - subcomponent: None, - metadata: None, - } - ), - }) - ); - - Ok(()) - } - - #[test] - fn test_measurement_series_start_builder_to_artifact() -> Result<()> { - let name = "name".to_owned(); - let series_id = "series_id".to_owned(); - let validator = Validator::builder(models::ValidatorType::Equal, 30.into()).build(); - let validator2 = Validator::builder(models::ValidatorType::GreaterThen, 10.into()).build(); - let hw_info = HardwareInfo::builder("id", "name").build(); - let subcomponent = Subcomponent::builder("name").build(); - let series = MeasurementSeriesStart::builder(&name, &series_id) - .unit("unit") - .add_metadata("key", "value".into()) - .add_metadata("key2", "value2".into()) - .add_validator(&validator) - .add_validator(&validator2) - .hardware_info(&hw_info) - .subcomponent(&subcomponent) - .build(); - - let artifact = series.to_artifact(); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesStart( - models::MeasurementSeriesStartSpec { - name, - unit: Some("unit".to_string()), - series_id: series_id.to_string(), - validators: Some(vec![validator.to_spec(), validator2.to_spec()]), - hardware_info: Some(hw_info.to_spec()), - subcomponent: Some(subcomponent.to_spec()), - metadata: Some(serde_json::Map::from_iter([ - ("key".to_string(), "value".into()), - ("key2".to_string(), "value2".into()) - ])), - } - ), - }) - ); - - Ok(()) - } - - #[test] - fn test_measurement_series_end_to_artifact() -> Result<()> { - let series_id = "series_id".to_owned(); - let series = MeasurementSeriesEnd::new(&series_id, 1); - - let artifact = series.to_artifact(); - assert_eq!( - artifact, - models::OutputArtifactDescendant::TestStepArtifact(models::TestStepArtifactSpec { - descendant: models::TestStepArtifactDescendant::MeasurementSeriesEnd( - models::MeasurementSeriesEndSpec { - series_id: series_id.to_string(), - total_count: 1, - } - ), - }) - ); - - Ok(()) - } - - #[test] - fn test_dut_builder() -> Result<()> { - let platform = PlatformInfo::builder("platform_info").build(); - let software = SoftwareInfo::builder("software_id", "name").build(); - let hardware = HardwareInfo::builder("hardware_id", "name").build(); - let dut = DutInfo::builder("1234") - .name("DUT") - .add_metadata("key", "value".into()) - .add_metadata("key2", "value2".into()) - .add_hardware_info(&hardware) - .add_hardware_info(&hardware) - .add_platform_info(&platform) - .add_platform_info(&platform) - .add_software_info(&software) - .add_software_info(&software) - .build(); - - let spec_dut = dut.to_spec(); - - assert_eq!(spec_dut.id, "1234"); - assert_eq!(spec_dut.name, Some("DUT".to_owned())); - - match spec_dut.metadata { - Some(m) => { - assert_eq!(m["key"], "value"); - assert_eq!(m["key2"], "value2"); - } - _ => bail!("metadata is empty"), - } - - match spec_dut.hardware_infos { - Some(infos) => match infos.first() { - Some(info) => { - assert_eq!(info.id, "hardware_id"); - } - _ => bail!("hardware_infos is empty"), - }, - _ => bail!("hardware_infos is missing"), - } - - match spec_dut.software_infos { - Some(infos) => match infos.first() { - Some(info) => { - assert_eq!(info.id, "software_id"); - } - _ => bail!("software_infos is empty"), - }, - _ => bail!("software_infos is missing"), - } - - match spec_dut.platform_infos { - Some(infos) => match infos.first() { - Some(info) => { - assert_eq!(info.info, "platform_info"); - } - _ => bail!("platform_infos is empty"), - }, - _ => bail!("platform_infos is missing"), - } - - Ok(()) - } - - #[test] - fn test_error() -> Result<()> { - let expected_run = serde_json::json!({ - "testRunArtifact": { - "error": { - "message": "message", - "softwareInfoIds": [ - { - "computerSystem": null, - "name": "name", - "revision": null, - "softwareInfoId": - "software_id", - "softwareType": null, - "version": null - }, - { - "computerSystem": null, - "name": "name", - "revision": null, - "softwareInfoId": - "software_id", - "softwareType": null, - "version": null - } - ], - "sourceLocation": {"file": "file.rs", "line": 1}, - "symptom": "symptom" - } - } - }); - let expected_step = serde_json::json!({ - "testStepArtifact": { - "error": { - "message": "message", - "softwareInfoIds": [ - { - "computerSystem": null, - "name": "name", - "revision": null, - "softwareInfoId": "software_id", - "softwareType": null, - "version": null - }, - { - "computerSystem": null, - "name": "name", - "revision": null, - "softwareInfoId": "software_id", - "softwareType": null, - "version": null - } - ], - "sourceLocation": {"file":"file.rs","line":1}, - "symptom":"symptom" - } - } - }); - - let software = SoftwareInfo::builder("software_id", "name").build(); - let error = ErrorBuilder::new("symptom") - .message("message") - .source("file.rs", 1) - .add_software_info(&software) - .add_software_info(&software) - .build(); - - let spec_error = error.to_artifact(ArtifactContext::TestRun); - let actual = serde_json::json!(spec_error); - assert_json_include!(actual: actual, expected: &expected_run); - - let spec_error = error.to_artifact(ArtifactContext::TestStep); - let actual = serde_json::json!(spec_error); - assert_json_include!(actual: actual, expected: &expected_step); - - Ok(()) - } - - #[test] - fn test_validator() -> Result<()> { - let validator = Validator::builder(ValidatorType::Equal, 30.into()) - .name("validator") - .add_metadata("key", "value".into()) - .add_metadata("key2", "value2".into()) - .build(); - - let spec_validator = validator.to_spec(); - - assert_eq!(spec_validator.name, Some("validator".to_owned())); - assert_eq!(spec_validator.value, 30); - assert_eq!(spec_validator.validator_type, ValidatorType::Equal); - - match spec_validator.metadata { - Some(m) => { - assert_eq!(m["key"], "value"); - assert_eq!(m["key2"], "value2"); - } - _ => bail!("metadata is none"), - } - - Ok(()) - } - - #[test] - fn test_hardware_info() -> Result<()> { - let info = HardwareInfo::builder("hardware_id", "hardware_name") - .version("version") - .revision("revision") - .location("location") - .serial_no("serial_no") - .part_no("part_no") - .manufacturer("manufacturer") - .manufacturer_part_no("manufacturer_part_no") - .odata_id("odata_id") - .computer_system("computer_system") - .manager("manager") - .build(); - - let spec_hwinfo = info.to_spec(); - - assert_eq!(spec_hwinfo.id, "hardware_id"); - assert_eq!(spec_hwinfo.name, "hardware_name"); - assert_eq!(spec_hwinfo.version, Some("version".to_owned())); - assert_eq!(spec_hwinfo.revision, Some("revision".to_owned())); - assert_eq!(spec_hwinfo.location, Some("location".to_owned())); - assert_eq!(spec_hwinfo.serial_no, Some("serial_no".to_owned())); - assert_eq!(spec_hwinfo.part_no, Some("part_no".to_owned())); - assert_eq!(spec_hwinfo.manufacturer, Some("manufacturer".to_owned())); - assert_eq!( - spec_hwinfo.manufacturer_part_no, - Some("manufacturer_part_no".to_owned()) - ); - assert_eq!(spec_hwinfo.odata_id, Some("odata_id".to_owned())); - assert_eq!( - spec_hwinfo.computer_system, - Some("computer_system".to_owned()) - ); - assert_eq!(spec_hwinfo.manager, Some("manager".to_owned())); - - Ok(()) - } - - #[test] - fn test_subcomponent() -> Result<()> { - let sub = Subcomponent::builder("sub_name") - .subcomponent_type(models::SubcomponentType::Asic) - .version("version") - .location("location") - .revision("revision") - .build(); - - let spec_subcomponent = sub.to_spec(); - - assert_eq!(spec_subcomponent.name, "sub_name"); - assert_eq!(spec_subcomponent.version, Some("version".to_owned())); - assert_eq!(spec_subcomponent.revision, Some("revision".to_owned())); - assert_eq!(spec_subcomponent.location, Some("location".to_owned())); - assert_eq!( - spec_subcomponent.subcomponent_type, - Some(models::SubcomponentType::Asic) - ); - - Ok(()) - } - - #[test] - fn test_platform_info() -> Result<()> { - let info = PlatformInfo::builder("info").build(); - - assert_eq!(info.to_spec().info, "info"); - Ok(()) - } - - #[test] - fn test_software_info() -> Result<()> { - let info = SoftwareInfo::builder("software_id", "name") - .version("version") - .revision("revision") - .software_type(models::SoftwareType::Application) - .computer_system("system") - .build(); - - let spec_swinfo = info.to_spec(); - - assert_eq!(spec_swinfo.id, "software_id"); - assert_eq!(spec_swinfo.name, "name"); - assert_eq!(spec_swinfo.version, Some("version".to_owned())); - assert_eq!(spec_swinfo.revision, Some("revision".to_owned())); - assert_eq!( - spec_swinfo.software_type, - Some(models::SoftwareType::Application) - ); - assert_eq!(spec_swinfo.computer_system, Some("system".to_owned())); - - Ok(()) - } -} diff --git a/src/output/run.rs b/src/output/run.rs new file mode 100644 index 0000000..c3cc844 --- /dev/null +++ b/src/output/run.rs @@ -0,0 +1,689 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std::env; +use std::sync::Arc; + +use serde_json::Map; +use serde_json::Value; +use tokio::sync::Mutex; + +use crate::output as tv; +use crate::spec; +use tv::step::TestStep; +use tv::{config, dut, emitter, error, log, run, state}; + +/// The outcome of a TestRun. +/// It's returned when the scope method of the [`TestRun`] object is used. +pub struct TestRunOutcome { + /// Reports the execution status of the test + pub status: spec::TestStatus, + /// Reports the result of the test + pub result: spec::TestResult, +} + +/// The main diag test run. +/// +/// This object describes a single run instance of the diag, and therefore drives the test session. +pub struct TestRun { + name: String, + version: String, + parameters: Map, + dut: dut::DutInfo, + command_line: String, + metadata: Option>, + state: Arc>, +} + +impl TestRun { + /// Creates a new [`TestRunBuilder`] object. + /// + /// # Examples + /// + /// ```rust + /// # use ocptv::output::*; + /// + /// let dut = DutInfo::builder("my_dut").build(); + /// let builder = TestRun::builder("run_name", &dut, "1.0"); + /// ``` + pub fn builder(name: &str, dut: &dut::DutInfo, version: &str) -> TestRunBuilder { + TestRunBuilder::new(name, dut, version) + } + + /// Creates a new [`TestRun`] object. + /// + /// # Examples + /// + /// ```rust + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); + /// ``` + pub fn new(name: &str, dut_id: &str, version: &str) -> TestRun { + let dut = dut::DutInfo::new(dut_id); + TestRunBuilder::new(name, &dut, version).build() + } + + /// Starts the test run. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#schemaversion + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); + /// run.start().await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn start(self) -> Result { + let version = SchemaVersion::new(); + self.state + .lock() + .await + .emitter + .emit(&version.to_artifact()) + .await?; + + let mut builder = run::TestRunStart::builder( + &self.name, + &self.version, + &self.command_line, + &self.parameters, + &self.dut, + ); + + if let Some(m) = &self.metadata { + for m in m { + builder = builder.add_metadata(m.0, m.1.clone()) + } + } + + let start = builder.build(); + self.state + .lock() + .await + .emitter + .emit(&start.to_artifact()) + .await?; + + Ok(StartedTestRun { run: self }) + } + + // disabling this for the moment so we don't publish api that's unusable. + // see: https://github.com/rust-lang/rust/issues/70263 + // + // /// Builds a scope in the [`TestRun`] object, taking care of starting and + // /// ending it. View [`TestRun::start`] and [`TestRun::end`] methods. + // /// After the scope is constructed, additional objects may be added to it. + // /// This is the preferred usage for the [`TestRun`], since it guarantees + // /// all the messages are emitted between the start and end messages, the order + // /// is respected and no messages is lost. + // /// + // /// # Examples + // /// + // /// ```rust + // /// # tokio_test::block_on(async { + // /// # use ocptv::output::*; + // /// + // /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); + // /// run.scope(|r| async { + // /// r.log(LogSeverity::Info, "First message").await?; + // /// Ok(TestRunOutcome { + // /// status: TestStatus::Complete, + // /// result: TestResult::Pass, + // /// }) + // /// }).await?; + // /// + // /// # Ok::<(), WriterError>(()) + // /// # }); + // /// ``` + // pub async fn scope(self, func: F) -> Result<(), emitters::WriterError> + // where + // R: Future>, + // for<'a> F: Fut2<'a, R>, + // { + // let run = self.start().await?; + // let outcome = func(&run).await?; + // run.end(outcome.status, outcome.result).await?; + + // Ok(()) + // } +} + +/// Builder for the [`TestRun`] object. +pub struct TestRunBuilder { + name: String, + dut: dut::DutInfo, + version: String, + parameters: Map, + command_line: String, + metadata: Option>, + config: Option, +} + +impl TestRunBuilder { + pub fn new(name: &str, dut: &dut::DutInfo, version: &str) -> Self { + Self { + name: name.to_string(), + dut: dut.clone(), + version: version.to_string(), + parameters: Map::new(), + command_line: env::args().collect::>()[1..].join(" "), + metadata: None, + config: None, + } + } + + /// Adds a user defined parameter to the future [`TestRun`] object. + /// + /// # Examples + /// + /// ```rust + /// # use ocptv::output::*; + /// + /// let dut = DutInfo::builder("dut_id").build(); + /// let run = TestRunBuilder::new("run_name", &dut, "1.0") + /// .add_parameter("param1", "value1".into()) + /// .build(); + /// ``` + pub fn add_parameter(mut self, key: &str, value: Value) -> TestRunBuilder { + self.parameters.insert(key.to_string(), value.clone()); + self + } + + /// Adds the command line used to run the test session to the future + /// [`TestRun`] object. + /// + /// # Examples + /// + /// ```rust + /// # use ocptv::output::*; + /// + /// let dut = DutInfo::builder("dut_id").build(); + /// let run = TestRunBuilder::new("run_name", &dut, "1.0") + /// .command_line("my_diag --arg value") + /// .build(); + /// ``` + pub fn command_line(mut self, cmd: &str) -> TestRunBuilder { + self.command_line = cmd.to_string(); + self + } + + /// Adds the configuration for the test session to the future [`TestRun`] object + /// + /// # Examples + /// + /// ```rust + /// use ocptv::output::{Config, TestRunBuilder, DutInfo}; + /// + /// let dut = DutInfo::builder("dut_id").build(); + /// let run = TestRunBuilder::new("run_name", &dut, "1.0") + /// .config(Config::builder().build()) + /// .build(); + /// ``` + pub fn config(mut self, value: config::Config) -> TestRunBuilder { + self.config = Some(value); + self + } + + /// Adds user defined metadata to the future [`TestRun`] object + /// + /// # Examples + /// + /// ```rust + /// # use ocptv::output::*; + /// + /// let dut = DutInfo::builder("dut_id").build(); + /// let run = TestRunBuilder::new("run_name", &dut, "1.0") + /// .add_metadata("meta1", "value1".into()) + /// .build(); + /// ``` + pub fn add_metadata(mut self, key: &str, value: Value) -> TestRunBuilder { + self.metadata = match self.metadata { + Some(mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + None => { + let mut metadata = Map::new(); + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + }; + self + } + + pub fn build(self) -> TestRun { + let config = self.config.unwrap_or(config::Config::builder().build()); + let emitter = emitter::JsonEmitter::new(config.timezone, config.writer); + let state = state::TestState::new(emitter); + TestRun { + name: self.name, + dut: self.dut, + version: self.version, + parameters: self.parameters, + command_line: self.command_line, + metadata: self.metadata, + state: Arc::new(Mutex::new(state)), + } + } +} + +/// A test run that was started. +/// +/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart +pub struct StartedTestRun { + run: TestRun, +} + +impl StartedTestRun { + /// Ends the test run. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunend + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn end( + &self, + status: spec::TestStatus, + result: spec::TestResult, + ) -> Result<(), emitter::WriterError> { + let end = run::TestRunEnd::builder() + .status(status) + .result(result) + .build(); + + let emitter = &self.run.state.lock().await.emitter; + + emitter.emit(&end.to_artifact()).await?; + Ok(()) + } + + /// Emits a Log message. + /// This method accepts a [`models::LogSeverity`] to define the severity + /// and a [`std::string::String`] for the message. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.log( + /// LogSeverity::Info, + /// "This is a log message with INFO severity", + /// ).await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn log( + &self, + severity: spec::LogSeverity, + msg: &str, + ) -> Result<(), emitter::WriterError> { + let log = log::Log::builder(msg).severity(severity).build(); + + let emitter = &self.run.state.lock().await.emitter; + + let artifact = spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::Log(log.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestRunArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Log message. + /// This method accepts a [`objects::Log`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.log_with_details( + /// &Log::builder("This is a log message with INFO severity") + /// .severity(LogSeverity::Info) + /// .source("file", 1) + /// .build(), + /// ).await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn log_with_details(&self, log: &log::Log) -> Result<(), emitter::WriterError> { + let emitter = &self.run.state.lock().await.emitter; + + let artifact = spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::Log(log.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestRunArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Error message. + /// This method accepts a [`std::string::String`] to define the symptom. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.error("symptom").await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error(&self, symptom: &str) -> Result<(), emitter::WriterError> { + let error = error::Error::builder(symptom).build(); + let emitter = &self.run.state.lock().await.emitter; + + let artifact = spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestRunArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Error message. + /// This method accepts a [`std::string::String`] to define the symptom and + /// another [`std::string::String`] as error message. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.error_with_msg("symptom", "error messasge").await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error_with_msg( + &self, + symptom: &str, + msg: &str, + ) -> Result<(), emitter::WriterError> { + let error = error::Error::builder(symptom).message(msg).build(); + let emitter = &self.run.state.lock().await.emitter; + + let artifact = spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestRunArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Error message. + /// This method acceps a [`objects::Error`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// run.error_with_details( + /// &Error::builder("symptom") + /// .message("Error message") + /// .source("file", 1) + /// .add_software_info(&SoftwareInfo::builder("id", "name").build()) + /// .build(), + /// ).await?; + /// run.end(TestStatus::Complete, TestResult::Pass).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error_with_details( + &self, + error: &error::Error, + ) -> Result<(), emitter::WriterError> { + let emitter = &self.run.state.lock().await.emitter; + + let artifact = spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestRunArtifact(artifact)) + .await?; + + Ok(()) + } + + pub fn step(&self, name: &str) -> TestStep { + TestStep::new(name, self.run.state.clone()) + } +} + +pub struct TestRunStart { + name: String, + version: String, + command_line: String, + parameters: Map, + metadata: Option>, + dut_info: dut::DutInfo, +} + +impl TestRunStart { + pub fn builder( + name: &str, + version: &str, + command_line: &str, + parameters: &Map, + dut_info: &dut::DutInfo, + ) -> TestRunStartBuilder { + TestRunStartBuilder::new(name, version, command_line, parameters, dut_info) + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestRunArtifact(spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::TestRunStart(spec::TestRunStart { + name: self.name.clone(), + version: self.version.clone(), + command_line: self.command_line.clone(), + parameters: self.parameters.clone(), + metadata: self.metadata.clone(), + dut_info: self.dut_info.to_spec(), + }), + }) + } +} + +pub struct TestRunStartBuilder { + name: String, + version: String, + command_line: String, + parameters: Map, + metadata: Option>, + dut_info: dut::DutInfo, +} + +impl TestRunStartBuilder { + pub fn new( + name: &str, + version: &str, + command_line: &str, + parameters: &Map, + dut_info: &dut::DutInfo, + ) -> TestRunStartBuilder { + TestRunStartBuilder { + name: name.to_string(), + version: version.to_string(), + command_line: command_line.to_string(), + parameters: parameters.clone(), + metadata: None, + dut_info: dut_info.clone(), + } + } + + pub fn add_metadata(mut self, key: &str, value: Value) -> TestRunStartBuilder { + self.metadata = match self.metadata { + Some(mut metadata) => { + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + None => { + let mut metadata = Map::new(); + metadata.insert(key.to_string(), value.clone()); + Some(metadata) + } + }; + self + } + + pub fn build(self) -> TestRunStart { + TestRunStart { + name: self.name, + version: self.version, + command_line: self.command_line, + parameters: self.parameters, + metadata: self.metadata, + dut_info: self.dut_info, + } + } +} + +pub struct TestRunEnd { + status: spec::TestStatus, + result: spec::TestResult, +} + +impl TestRunEnd { + pub fn builder() -> TestRunEndBuilder { + TestRunEndBuilder::new() + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestRunArtifact(spec::TestRunArtifact { + artifact: spec::TestRunArtifactDescendant::TestRunEnd(spec::TestRunEnd { + status: self.status.clone(), + result: self.result.clone(), + }), + }) + } +} + +#[derive(Debug)] +pub struct TestRunEndBuilder { + status: spec::TestStatus, + result: spec::TestResult, +} + +#[allow(clippy::new_without_default)] +impl TestRunEndBuilder { + pub fn new() -> TestRunEndBuilder { + TestRunEndBuilder { + status: spec::TestStatus::Complete, + result: spec::TestResult::Pass, + } + } + pub fn status(mut self, value: spec::TestStatus) -> TestRunEndBuilder { + self.status = value; + self + } + + pub fn result(mut self, value: spec::TestResult) -> TestRunEndBuilder { + self.result = value; + self + } + + pub fn build(self) -> TestRunEnd { + TestRunEnd { + status: self.status, + result: self.result, + } + } +} + +// TODO: this likely will go into the emitter since it's not the run's job to emit the schema version +pub struct SchemaVersion { + major: i8, + minor: i8, +} + +#[allow(clippy::new_without_default)] +impl SchemaVersion { + pub fn new() -> SchemaVersion { + SchemaVersion { + major: spec::SPEC_VERSION.0, + minor: spec::SPEC_VERSION.1, + } + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::SchemaVersion(spec::SchemaVersion { + major: self.major, + minor: self.minor, + }) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + use crate::spec; + + #[test] + fn test_schema_creation_from_builder() -> Result<()> { + let version = SchemaVersion::new(); + assert_eq!(version.major, spec::SPEC_VERSION.0); + assert_eq!(version.minor, spec::SPEC_VERSION.1); + Ok(()) + } +} diff --git a/src/output/runner.rs b/src/output/runner.rs deleted file mode 100644 index 681e565..0000000 --- a/src/output/runner.rs +++ /dev/null @@ -1,1291 +0,0 @@ -// (c) Meta Platforms, Inc. and affiliates. -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -//! OCPTV library runner -//! -//! This module contains the main entry point for the test runner. This is the -//! main object the user will interact with. - -use std::env; -use std::future::Future; -use std::path::Path; -use std::sync::atomic; -use std::sync::Arc; - -use serde_json::Map; -use serde_json::Value; -use tokio::sync::Mutex; - -use crate::output::emitters; -use crate::output::models; -use crate::output::objects; - -/// The configuration repository for the TestRun. -pub struct Config { - timezone: chrono_tz::Tz, - writer: emitters::WriterType, -} - -impl Config { - /// Creates a new [`ConfigBuilder`] - /// - /// # Examples - /// ```rust - /// # use ocptv::output::*; - /// - /// let builder = Config::builder(); - /// ``` - pub fn builder() -> ConfigBuilder { - ConfigBuilder::new() - } -} - -/// The builder for the [`Config`] object. -pub struct ConfigBuilder { - timezone: Option, - writer: Option, -} - -impl ConfigBuilder { - fn new() -> Self { - Self { - timezone: None, - writer: Some(emitters::WriterType::Stdout(emitters::StdoutWriter::new())), - } - } - - pub fn timezone(mut self, timezone: chrono_tz::Tz) -> Self { - self.timezone = Some(timezone); - self - } - - pub fn with_buffer_output(mut self, buffer: Arc>>) -> Self { - self.writer = Some(emitters::WriterType::Buffer(emitters::BufferWriter::new( - buffer, - ))); - self - } - - pub async fn with_file_output>( - mut self, - path: P, - ) -> Result { - self.writer = Some(emitters::WriterType::File( - emitters::FileWriter::new(path).await?, - )); - Ok(self) - } - - pub fn build(self) -> Config { - Config { - timezone: self.timezone.unwrap_or(chrono_tz::UTC), - writer: self - .writer - .unwrap_or(emitters::WriterType::Stdout(emitters::StdoutWriter::new())), - } - } -} - -/// The outcome of a TestRun. -/// It's returned when the scope method of the [`TestRun`] object is used. -pub struct TestRunOutcome { - /// Reports the execution status of the test - pub status: models::TestStatus, - /// Reports the result of the test - pub result: models::TestResult, -} - -struct TestState { - emitter: emitters::JsonEmitter, -} - -impl TestState { - fn new(emitter: emitters::JsonEmitter) -> TestState { - TestState { emitter } - } -} - -/// The main diag test run. -/// -/// This object describes a single run instance of the diag, and therefore drives the test session. -pub struct TestRun { - name: String, - version: String, - parameters: Map, - dut: objects::DutInfo, - command_line: String, - metadata: Option>, - state: Arc>, -} - -impl TestRun { - /// Creates a new [`TestRunBuilder`] object. - /// - /// # Examples - /// - /// ```rust - /// # use ocptv::output::*; - /// - /// let dut = DutInfo::builder("my_dut").build(); - /// let builder = TestRun::builder("run_name", &dut, "1.0"); - /// ``` - pub fn builder(name: &str, dut: &objects::DutInfo, version: &str) -> TestRunBuilder { - TestRunBuilder::new(name, dut, version) - } - - /// Creates a new [`TestRun`] object. - /// - /// # Examples - /// - /// ```rust - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); - /// ``` - pub fn new(name: &str, dut_id: &str, version: &str) -> TestRun { - let dut = objects::DutInfo::new(dut_id); - TestRunBuilder::new(name, &dut, version).build() - } - - /// Starts the test run. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#schemaversion - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); - /// run.start().await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn start(self) -> Result { - let version = objects::SchemaVersion::new(); - self.state - .lock() - .await - .emitter - .emit(&version.to_artifact()) - .await?; - - let mut builder = objects::TestRunStart::builder( - &self.name, - &self.version, - &self.command_line, - &self.parameters, - &self.dut, - ); - - if let Some(m) = &self.metadata { - for m in m { - builder = builder.add_metadata(m.0, m.1.clone()) - } - } - - let start = builder.build(); - self.state - .lock() - .await - .emitter - .emit(&start.to_artifact()) - .await?; - - Ok(StartedTestRun { run: self }) - } - - // disabling this for the moment so we don't publish api that's unusable. - // see: https://github.com/rust-lang/rust/issues/70263 - // - // /// Builds a scope in the [`TestRun`] object, taking care of starting and - // /// ending it. View [`TestRun::start`] and [`TestRun::end`] methods. - // /// After the scope is constructed, additional objects may be added to it. - // /// This is the preferred usage for the [`TestRun`], since it guarantees - // /// all the messages are emitted between the start and end messages, the order - // /// is respected and no messages is lost. - // /// - // /// # Examples - // /// - // /// ```rust - // /// # tokio_test::block_on(async { - // /// # use ocptv::output::*; - // /// - // /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0"); - // /// run.scope(|r| async { - // /// r.log(LogSeverity::Info, "First message").await?; - // /// Ok(TestRunOutcome { - // /// status: TestStatus::Complete, - // /// result: TestResult::Pass, - // /// }) - // /// }).await?; - // /// - // /// # Ok::<(), WriterError>(()) - // /// # }); - // /// ``` - // pub async fn scope(self, func: F) -> Result<(), emitters::WriterError> - // where - // R: Future>, - // for<'a> F: Fut2<'a, R>, - // { - // let run = self.start().await?; - // let outcome = func(&run).await?; - // run.end(outcome.status, outcome.result).await?; - - // Ok(()) - // } -} - -/// Builder for the [`TestRun`] object. -pub struct TestRunBuilder { - name: String, - dut: objects::DutInfo, - version: String, - parameters: Map, - command_line: String, - metadata: Option>, - config: Option, -} - -impl TestRunBuilder { - pub fn new(name: &str, dut: &objects::DutInfo, version: &str) -> Self { - Self { - name: name.to_string(), - dut: dut.clone(), - version: version.to_string(), - parameters: Map::new(), - command_line: env::args().collect::>()[1..].join(" "), - metadata: None, - config: None, - } - } - - /// Adds a user defined parameter to the future [`TestRun`] object. - /// - /// # Examples - /// - /// ```rust - /// # use ocptv::output::*; - /// - /// let dut = DutInfo::builder("dut_id").build(); - /// let run = TestRunBuilder::new("run_name", &dut, "1.0") - /// .add_parameter("param1", "value1".into()) - /// .build(); - /// ``` - pub fn add_parameter(mut self, key: &str, value: Value) -> TestRunBuilder { - self.parameters.insert(key.to_string(), value.clone()); - self - } - - /// Adds the command line used to run the test session to the future - /// [`TestRun`] object. - /// - /// # Examples - /// - /// ```rust - /// # use ocptv::output::*; - /// - /// let dut = DutInfo::builder("dut_id").build(); - /// let run = TestRunBuilder::new("run_name", &dut, "1.0") - /// .command_line("my_diag --arg value") - /// .build(); - /// ``` - pub fn command_line(mut self, cmd: &str) -> TestRunBuilder { - self.command_line = cmd.to_string(); - self - } - - /// Adds the configuration for the test session to the future [`TestRun`] object - /// - /// # Examples - /// - /// ```rust - /// use ocptv::output::{Config, TestRunBuilder, DutInfo}; - /// - /// let dut = DutInfo::builder("dut_id").build(); - /// let run = TestRunBuilder::new("run_name", &dut, "1.0") - /// .config(Config::builder().build()) - /// .build(); - /// ``` - pub fn config(mut self, value: Config) -> TestRunBuilder { - self.config = Some(value); - self - } - - /// Adds user defined metadata to the future [`TestRun`] object - /// - /// # Examples - /// - /// ```rust - /// # use ocptv::output::*; - /// - /// let dut = DutInfo::builder("dut_id").build(); - /// let run = TestRunBuilder::new("run_name", &dut, "1.0") - /// .add_metadata("meta1", "value1".into()) - /// .build(); - /// ``` - pub fn add_metadata(mut self, key: &str, value: Value) -> TestRunBuilder { - self.metadata = match self.metadata { - Some(mut metadata) => { - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - None => { - let mut metadata = Map::new(); - metadata.insert(key.to_string(), value.clone()); - Some(metadata) - } - }; - self - } - - pub fn build(self) -> TestRun { - let config = self.config.unwrap_or(Config::builder().build()); - let emitter = emitters::JsonEmitter::new(config.timezone, config.writer); - let state = TestState::new(emitter); - TestRun { - name: self.name, - dut: self.dut, - version: self.version, - parameters: self.parameters, - command_line: self.command_line, - metadata: self.metadata, - state: Arc::new(Mutex::new(state)), - } - } -} - -/// A test run that was started. -/// -/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart -pub struct StartedTestRun { - run: TestRun, -} - -impl StartedTestRun { - /// Ends the test run. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunend - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn end( - &self, - status: models::TestStatus, - result: models::TestResult, - ) -> Result<(), emitters::WriterError> { - let end = objects::TestRunEnd::builder() - .status(status) - .result(result) - .build(); - - let emitter = &self.run.state.lock().await.emitter; - - emitter.emit(&end.to_artifact()).await?; - Ok(()) - } - - /// Emits a Log message. - /// This method accepts a [`models::LogSeverity`] to define the severity - /// and a [`std::string::String`] for the message. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.log( - /// LogSeverity::Info, - /// "This is a log message with INFO severity", - /// ).await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn log( - &self, - severity: models::LogSeverity, - msg: &str, - ) -> Result<(), emitters::WriterError> { - let log = objects::Log::builder(msg).severity(severity).build(); - - let emitter = &self.run.state.lock().await.emitter; - - emitter - .emit(&log.to_artifact(objects::ArtifactContext::TestRun)) - .await?; - Ok(()) - } - - /// Emits a Log message. - /// This method accepts a [`objects::Log`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.log_with_details( - /// &Log::builder("This is a log message with INFO severity") - /// .severity(LogSeverity::Info) - /// .source("file", 1) - /// .build(), - /// ).await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn log_with_details(&self, log: &objects::Log) -> Result<(), emitters::WriterError> { - let emitter = &self.run.state.lock().await.emitter; - - emitter - .emit(&log.to_artifact(objects::ArtifactContext::TestRun)) - .await?; - Ok(()) - } - - /// Emits a Error message. - /// This method accepts a [`std::string::String`] to define the symptom. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.error("symptom").await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error(&self, symptom: &str) -> Result<(), emitters::WriterError> { - let error = objects::Error::builder(symptom).build(); - let emitter = &self.run.state.lock().await.emitter; - - emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestRun)) - .await?; - Ok(()) - } - - /// Emits a Error message. - /// This method accepts a [`std::string::String`] to define the symptom and - /// another [`std::string::String`] as error message. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.error_with_msg("symptom", "error messasge").await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error_with_msg( - &self, - symptom: &str, - msg: &str, - ) -> Result<(), emitters::WriterError> { - let error = objects::Error::builder(symptom).message(msg).build(); - let emitter = &self.run.state.lock().await.emitter; - - emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestRun)) - .await?; - Ok(()) - } - - /// Emits a Error message. - /// This method acceps a [`objects::Error`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// run.error_with_details( - /// &Error::builder("symptom") - /// .message("Error message") - /// .source("file", 1) - /// .add_software_info(&SoftwareInfo::builder("id", "name").build()) - /// .build(), - /// ).await?; - /// run.end(TestStatus::Complete, TestResult::Pass).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error_with_details( - &self, - error: &objects::Error, - ) -> Result<(), emitters::WriterError> { - let emitter = &self.run.state.lock().await.emitter; - - emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestRun)) - .await?; - Ok(()) - } - - pub fn step(&self, name: &str) -> TestStep { - TestStep::new(name, self.run.state.clone()) - } -} - -/// A single test step in the scope of a [`TestRun`]. -/// -/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#test-step-artifacts -pub struct TestStep { - name: String, - state: Arc>, -} - -impl TestStep { - fn new(name: &str, state: Arc>) -> TestStep { - TestStep { - name: name.to_string(), - state, - } - } - - /// Starts the test step. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#teststepstart - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn start(self) -> Result { - let start = objects::TestStepStart::new(&self.name); - self.state - .lock() - .await - .emitter - .emit(&start.to_artifact()) - .await?; - - Ok(StartedTestStep { - step: self, - measurement_id_no: Arc::new(atomic::AtomicU64::new(0)), - }) - } - - // /// Builds a scope in the [`TestStep`] object, taking care of starting and - // /// ending it. View [`TestStep::start`] and [`TestStep::end`] methods. - // /// After the scope is constructed, additional objects may be added to it. - // /// This is the preferred usage for the [`TestStep`], since it guarantees - // /// all the messages are emitted between the start and end messages, the order - // /// is respected and no messages is lost. - // /// - // /// # Examples - // /// - // /// ```rust - // /// # tokio_test::block_on(async { - // /// # use ocptv::output::*; - // /// - // /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - // /// - // /// let step = run.step("first step")?; - // /// step.scope(|s| async { - // /// s.log( - // /// LogSeverity::Info, - // /// "This is a log message with INFO severity", - // /// ).await?; - // /// Ok(TestStatus::Complete) - // /// }).await?; - // /// - // /// # Ok::<(), WriterError>(()) - // /// # }); - // /// ``` - // pub async fn scope<'a, F, R>(&'a self, func: F) -> Result<(), emitters::WriterError> - // where - // R: Future>, - // F: std::ops::FnOnce(&'a TestStep) -> R, - // { - // self.start().await?; - // let status = func(self).await?; - // self.end(status).await?; - // Ok(()) - // } -} - -pub struct StartedTestStep { - step: TestStep, - measurement_id_no: Arc, -} - -impl StartedTestStep { - /// Ends the test step. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#teststepend - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn end(&self, status: models::TestStatus) -> Result<(), emitters::WriterError> { - let end = objects::TestStepEnd::new(status); - self.step - .state - .lock() - .await - .emitter - .emit(&end.to_artifact()) - .await?; - Ok(()) - } - - /// Eemits Log message. - /// This method accepts a [`models::LogSeverity`] to define the severity - /// and a [`std::string::String`] for the message. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.log( - /// LogSeverity::Info, - /// "This is a log message with INFO severity", - /// ).await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - /// ## Using macros - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// use ocptv::ocptv_log_info; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// ocptv_log_info!(step, "This is a log message with INFO severity").await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn log( - &self, - severity: models::LogSeverity, - msg: &str, - ) -> Result<(), emitters::WriterError> { - let log = objects::Log::builder(msg).severity(severity).build(); - self.step - .state - .lock() - .await - .emitter - .emit(&log.to_artifact(objects::ArtifactContext::TestStep)) - .await?; - Ok(()) - } - - /// Emits Log message. - /// This method accepts a [`objects::Log`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.log_with_details( - /// &Log::builder("This is a log message with INFO severity") - /// .severity(LogSeverity::Info) - /// .source("file", 1) - /// .build(), - /// ).await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn log_with_details(&self, log: &objects::Log) -> Result<(), emitters::WriterError> { - self.step - .state - .lock() - .await - .emitter - .emit(&log.to_artifact(objects::ArtifactContext::TestStep)) - .await?; - Ok(()) - } - - /// Emits an Error symptom. - /// This method accepts a [`std::string::String`] to define the symptom. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.error("symptom").await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - /// - /// ## Using macros - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// use ocptv::ocptv_error; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// ocptv_error!(step, "symptom").await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error(&self, symptom: &str) -> Result<(), emitters::WriterError> { - let error = objects::Error::builder(symptom).build(); - self.step - .state - .lock() - .await - .emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestStep)) - .await?; - Ok(()) - } - - /// Emits an Error message. - /// This method accepts a [`std::string::String`] to define the symptom and - /// another [`std::string::String`] as error message. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.error_with_msg("symptom", "error message").await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - /// - /// ## Using macros - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// use ocptv::ocptv_error; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// ocptv_error!(step, "symptom", "error message").await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error_with_msg( - &self, - symptom: &str, - msg: &str, - ) -> Result<(), emitters::WriterError> { - let error = objects::Error::builder(symptom).message(msg).build(); - self.step - .state - .lock() - .await - .emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestStep)) - .await?; - Ok(()) - } - - /// Emits a Error message. - /// This method accepts a [`objects::Error`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.error_with_details( - /// &Error::builder("symptom") - /// .message("Error message") - /// .source("file", 1) - /// .add_software_info(&SoftwareInfo::builder("id", "name").build()) - /// .build(), - /// ).await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn error_with_details( - &self, - error: &objects::Error, - ) -> Result<(), emitters::WriterError> { - self.step - .state - .lock() - .await - .emitter - .emit(&error.to_artifact(objects::ArtifactContext::TestStep)) - .await?; - Ok(()) - } - - /// Emits a Measurement message. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// - /// let step = run.step("step_name").start().await?; - /// step.add_measurement("name", 50.into()).await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn add_measurement( - &self, - name: &str, - value: Value, - ) -> Result<(), emitters::WriterError> { - let measurement = objects::Measurement::new(name, value); - self.step - .state - .lock() - .await - .emitter - .emit(&measurement.to_artifact()) - .await?; - Ok(()) - } - - /// Emits a Measurement message. - /// This method accepts a [`objects::Error`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let hwinfo = HardwareInfo::builder("id", "fan").build(); - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let measurement = Measurement::builder("name", 5000.into()) - /// .hardware_info(&hwinfo) - /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) - /// .add_metadata("key", "value".into()) - /// .subcomponent(&Subcomponent::builder("name").build()) - /// .build(); - /// step.add_measurement_with_details(&measurement).await?; - /// step.end(TestStatus::Complete).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn add_measurement_with_details( - &self, - measurement: &objects::Measurement, - ) -> Result<(), emitters::WriterError> { - self.step - .state - .lock() - .await - .emitter - .emit(&measurement.to_artifact()) - .await?; - Ok(()) - } - - /// Starts a Measurement Series (a time-series list of measurements). - /// This method accepts a [`std::string::String`] as series ID and - /// a [`std::string::String`] as series name. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// let series = step.measurement_series("name"); - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub fn measurement_series(&self, name: &str) -> MeasurementSeries { - self.measurement_id_no - .fetch_add(1, atomic::Ordering::SeqCst); - let series_id: String = format!( - "series_{}", - self.measurement_id_no.load(atomic::Ordering::SeqCst) - ); - - MeasurementSeries::new(&series_id, name, self.step.state.clone()) - } - - /// Starts a Measurement Series (a time-series list of measurements). - /// This method accepts a [`objects::MeasurementSeriesStart`] object. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// let series = - /// step.measurement_series_with_details(MeasurementSeriesStart::new("name", "series_id")); - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub fn measurement_series_with_details( - &self, - start: objects::MeasurementSeriesStart, - ) -> MeasurementSeries { - MeasurementSeries::new_with_details(start, self.step.state.clone()) - } -} - -/// The measurement series. -/// A Measurement Series is a time-series list of measurements. -/// -/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart -pub struct MeasurementSeries { - state: Arc>, - seq_no: Arc>, - start: objects::MeasurementSeriesStart, -} - -impl MeasurementSeries { - fn new(series_id: &str, name: &str, state: Arc>) -> Self { - Self { - state, - seq_no: Arc::new(Mutex::new(atomic::AtomicU64::new(0))), - start: objects::MeasurementSeriesStart::new(name, series_id), - } - } - - fn new_with_details( - start: objects::MeasurementSeriesStart, - state: Arc>, - ) -> Self { - Self { - state, - seq_no: Arc::new(Mutex::new(atomic::AtomicU64::new(0))), - start, - } - } - - async fn current_sequence_no(&self) -> u64 { - self.seq_no.lock().await.load(atomic::Ordering::SeqCst) - } - - async fn increment_sequence_no(&self) { - self.seq_no - .lock() - .await - .fetch_add(1, atomic::Ordering::SeqCst); - } - - /// Starts the measurement series. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let series = step.measurement_series("name"); - /// series.start().await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn start(&self) -> Result<(), emitters::WriterError> { - self.state - .lock() - .await - .emitter - .emit(&self.start.to_artifact()) - .await?; - Ok(()) - } - - /// Ends the measurement series. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesend - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let series = step.measurement_series("name"); - /// series.start().await?; - /// series.end().await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn end(&self) -> Result<(), emitters::WriterError> { - let end = objects::MeasurementSeriesEnd::new( - self.start.get_series_id(), - self.current_sequence_no().await, - ); - self.state - .lock() - .await - .emitter - .emit(&end.to_artifact()) - .await?; - Ok(()) - } - - /// Adds a measurement element to the measurement series. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementserieselement - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let series = step.measurement_series("name"); - /// series.start().await?; - /// series.add_measurement(60.into()).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn add_measurement(&self, value: Value) -> Result<(), emitters::WriterError> { - let element = objects::MeasurementSeriesElement::new( - self.current_sequence_no().await, - value, - &self.start, - None, - ); - self.increment_sequence_no().await; - self.state - .lock() - .await - .emitter - .emit(&element.to_artifact()) - .await?; - Ok(()) - } - - /// Adds a measurement element to the measurement series. - /// This method accepts additional metadata to add to the element. - /// - /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementserieselement - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let series = step.measurement_series("name"); - /// series.start().await?; - /// series.add_measurement_with_metadata(60.into(), vec![("key", "value".into())]).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn add_measurement_with_metadata( - &self, - value: Value, - metadata: Vec<(&str, Value)>, - ) -> Result<(), emitters::WriterError> { - let element = objects::MeasurementSeriesElement::new( - self.current_sequence_no().await, - value, - &self.start, - Some(Map::from_iter( - metadata.iter().map(|(k, v)| (k.to_string(), v.clone())), - )), - ); - self.increment_sequence_no().await; - self.state - .lock() - .await - .emitter - .emit(&element.to_artifact()) - .await?; - Ok(()) - } - - /// Builds a scope in the [`MeasurementSeries`] object, taking care of starting and - /// ending it. View [`MeasurementSeries::start`] and [`MeasurementSeries::end`] methods. - /// After the scope is constructed, additional objects may be added to it. - /// This is the preferred usage for the [`MeasurementSeries`], since it guarantees - /// all the messages are emitted between the start and end messages, the order - /// is respected and no messages is lost. - /// - /// # Examples - /// - /// ```rust - /// # tokio_test::block_on(async { - /// # use ocptv::output::*; - /// - /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; - /// let step = run.step("step_name").start().await?; - /// - /// let series = step.measurement_series("name"); - /// series.start().await?; - /// series.scope(|s| async { - /// s.add_measurement(60.into()).await?; - /// s.add_measurement(70.into()).await?; - /// s.add_measurement(80.into()).await?; - /// Ok(()) - /// }).await?; - /// - /// # Ok::<(), WriterError>(()) - /// # }); - /// ``` - pub async fn scope<'a, F, R>(&'a self, func: F) -> Result<(), emitters::WriterError> - where - R: Future>, - F: std::ops::FnOnce(&'a MeasurementSeries) -> R, - { - self.start().await?; - func(self).await?; - self.end().await?; - Ok(()) - } -} diff --git a/src/output/state.rs b/src/output/state.rs new file mode 100644 index 0000000..df1fe96 --- /dev/null +++ b/src/output/state.rs @@ -0,0 +1,18 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use crate::output::emitter; + +// TODO: will prob need some redesign +pub struct TestState { + pub emitter: emitter::JsonEmitter, +} + +impl TestState { + pub fn new(emitter: emitter::JsonEmitter) -> TestState { + TestState { emitter } + } +} diff --git a/src/output/step.rs b/src/output/step.rs new file mode 100644 index 0000000..20fa755 --- /dev/null +++ b/src/output/step.rs @@ -0,0 +1,559 @@ +// (c) Meta Platforms, Inc. and affiliates. +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use serde_json::Value; +use std::sync::atomic; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::output as tv; +use crate::spec; +use tv::measurement::MeasurementSeries; +use tv::{emitter, error, log, measurement, state, step}; + +/// A single test step in the scope of a [`TestRun`]. +/// +/// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#test-step-artifacts +pub struct TestStep { + name: String, + state: Arc>, +} + +impl TestStep { + pub(crate) fn new(name: &str, state: Arc>) -> TestStep { + TestStep { + name: name.to_string(), + state, + } + } + + /// Starts the test step. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#teststepstart + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn start(self) -> Result { + let start = step::TestStepStart::new(&self.name); + self.state + .lock() + .await + .emitter + .emit(&start.to_artifact()) + .await?; + + Ok(StartedTestStep { + step: self, + measurement_id_no: Arc::new(atomic::AtomicU64::new(0)), + }) + } + + // /// Builds a scope in the [`TestStep`] object, taking care of starting and + // /// ending it. View [`TestStep::start`] and [`TestStep::end`] methods. + // /// After the scope is constructed, additional objects may be added to it. + // /// This is the preferred usage for the [`TestStep`], since it guarantees + // /// all the messages are emitted between the start and end messages, the order + // /// is respected and no messages is lost. + // /// + // /// # Examples + // /// + // /// ```rust + // /// # tokio_test::block_on(async { + // /// # use ocptv::output::*; + // /// + // /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + // /// + // /// let step = run.step("first step")?; + // /// step.scope(|s| async { + // /// s.log( + // /// LogSeverity::Info, + // /// "This is a log message with INFO severity", + // /// ).await?; + // /// Ok(TestStatus::Complete) + // /// }).await?; + // /// + // /// # Ok::<(), WriterError>(()) + // /// # }); + // /// ``` + // pub async fn scope<'a, F, R>(&'a self, func: F) -> Result<(), emitters::WriterError> + // where + // R: Future>, + // F: std::ops::FnOnce(&'a TestStep) -> R, + // { + // self.start().await?; + // let status = func(self).await?; + // self.end(status).await?; + // Ok(()) + // } +} + +pub struct StartedTestStep { + step: TestStep, + measurement_id_no: Arc, +} + +impl StartedTestStep { + /// Ends the test step. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#teststepend + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn end(&self, status: spec::TestStatus) -> Result<(), emitter::WriterError> { + let end = step::TestStepEnd::new(status); + self.step + .state + .lock() + .await + .emitter + .emit(&end.to_artifact()) + .await?; + Ok(()) + } + + /// Eemits Log message. + /// This method accepts a [`models::LogSeverity`] to define the severity + /// and a [`std::string::String`] for the message. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.log( + /// LogSeverity::Info, + /// "This is a log message with INFO severity", + /// ).await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + /// ## Using macros + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// use ocptv::ocptv_log_info; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// ocptv_log_info!(step, "This is a log message with INFO severity").await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn log( + &self, + severity: spec::LogSeverity, + msg: &str, + ) -> Result<(), emitter::WriterError> { + let log = log::Log::builder(msg).severity(severity).build(); + let emitter = &self.step.state.lock().await.emitter; + + let artifact = spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Log(log.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestStepArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits Log message. + /// This method accepts a [`objects::Log`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.log_with_details( + /// &Log::builder("This is a log message with INFO severity") + /// .severity(LogSeverity::Info) + /// .source("file", 1) + /// .build(), + /// ).await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn log_with_details(&self, log: &log::Log) -> Result<(), emitter::WriterError> { + let emitter = &self.step.state.lock().await.emitter; + + let artifact = spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Log(log.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestStepArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits an Error symptom. + /// This method accepts a [`std::string::String`] to define the symptom. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.error("symptom").await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + /// + /// ## Using macros + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// use ocptv::ocptv_error; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// ocptv_error!(step, "symptom").await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error(&self, symptom: &str) -> Result<(), emitter::WriterError> { + let error = error::Error::builder(symptom).build(); + let emitter = &self.step.state.lock().await.emitter; + + let artifact = spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestStepArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits an Error message. + /// This method accepts a [`std::string::String`] to define the symptom and + /// another [`std::string::String`] as error message. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.error_with_msg("symptom", "error message").await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + /// + /// ## Using macros + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// use ocptv::ocptv_error; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// ocptv_error!(step, "symptom", "error message").await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error_with_msg( + &self, + symptom: &str, + msg: &str, + ) -> Result<(), emitter::WriterError> { + let error = error::Error::builder(symptom).message(msg).build(); + let emitter = &self.step.state.lock().await.emitter; + + let artifact = spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestStepArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Error message. + /// This method accepts a [`objects::Error`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.error_with_details( + /// &Error::builder("symptom") + /// .message("Error message") + /// .source("file", 1) + /// .add_software_info(&SoftwareInfo::builder("id", "name").build()) + /// .build(), + /// ).await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn error_with_details( + &self, + error: &error::Error, + ) -> Result<(), emitter::WriterError> { + let emitter = &self.step.state.lock().await.emitter; + + let artifact = spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::Error(error.to_artifact()), + }; + emitter + .emit(&spec::RootArtifact::TestStepArtifact(artifact)) + .await?; + + Ok(()) + } + + /// Emits a Measurement message. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// + /// let step = run.step("step_name").start().await?; + /// step.add_measurement("name", 50.into()).await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn add_measurement( + &self, + name: &str, + value: Value, + ) -> Result<(), emitter::WriterError> { + let measurement = measurement::Measurement::new(name, value); + self.step + .state + .lock() + .await + .emitter + .emit(&measurement.to_artifact()) + .await?; + Ok(()) + } + + /// Emits a Measurement message. + /// This method accepts a [`objects::Error`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurement + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let hwinfo = HardwareInfo::builder("id", "fan").build(); + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// + /// let measurement = Measurement::builder("name", 5000.into()) + /// .hardware_info(&hwinfo) + /// .add_validator(&Validator::builder(ValidatorType::Equal, 30.into()).build()) + /// .add_metadata("key", "value".into()) + /// .subcomponent(&Subcomponent::builder("name").build()) + /// .build(); + /// step.add_measurement_with_details(&measurement).await?; + /// step.end(TestStatus::Complete).await?; + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub async fn add_measurement_with_details( + &self, + measurement: &measurement::Measurement, + ) -> Result<(), emitter::WriterError> { + self.step + .state + .lock() + .await + .emitter + .emit(&measurement.to_artifact()) + .await?; + Ok(()) + } + + /// Starts a Measurement Series (a time-series list of measurements). + /// This method accepts a [`std::string::String`] as series ID and + /// a [`std::string::String`] as series name. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// let series = step.measurement_series("name"); + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub fn measurement_series(&self, name: &str) -> MeasurementSeries { + self.measurement_id_no + .fetch_add(1, atomic::Ordering::SeqCst); + let series_id: String = format!( + "series_{}", + self.measurement_id_no.load(atomic::Ordering::SeqCst) + ); + + MeasurementSeries::new(&series_id, name, self.step.state.clone()) + } + + /// Starts a Measurement Series (a time-series list of measurements). + /// This method accepts a [`objects::MeasurementSeriesStart`] object. + /// + /// ref: https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#measurementseriesstart + /// + /// # Examples + /// + /// ```rust + /// # tokio_test::block_on(async { + /// # use ocptv::output::*; + /// + /// let run = TestRun::new("diagnostic_name", "my_dut", "1.0").start().await?; + /// let step = run.step("step_name").start().await?; + /// let series = + /// step.measurement_series_with_details(MeasurementSeriesStart::new("name", "series_id")); + /// + /// # Ok::<(), WriterError>(()) + /// # }); + /// ``` + pub fn measurement_series_with_details( + &self, + start: measurement::MeasurementSeriesStart, + ) -> MeasurementSeries { + MeasurementSeries::new_with_details(start, self.step.state.clone()) + } +} + +pub struct TestStepStart { + name: String, +} + +impl TestStepStart { + pub fn new(name: &str) -> TestStepStart { + TestStepStart { + name: name.to_string(), + } + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::TestStepStart(spec::TestStepStart { + name: self.name.clone(), + }), + }) + } +} + +pub struct TestStepEnd { + status: spec::TestStatus, +} + +impl TestStepEnd { + pub fn new(status: spec::TestStatus) -> TestStepEnd { + TestStepEnd { status } + } + + pub fn to_artifact(&self) -> spec::RootArtifact { + spec::RootArtifact::TestStepArtifact(spec::TestStepArtifact { + descendant: spec::TestStepArtifactDescendant::TestStepEnd(spec::TestStepEnd { + status: self.status.clone(), + }), + }) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/output/models.rs b/src/spec.rs similarity index 91% rename from src/output/models.rs rename to src/spec.rs index 48a2493..d60baae 100644 --- a/src/output/models.rs +++ b/src/spec.rs @@ -41,50 +41,65 @@ mod rfc3339_format { #[derive(Debug, Serialize, PartialEq, Clone)] pub enum TestRunArtifactDescendant { #[serde(rename = "testRunStart")] - TestRunStart(TestRunStartSpec), + TestRunStart(TestRunStart), + #[serde(rename = "testRunEnd")] - TestRunEnd(TestRunEndSpec), + TestRunEnd(TestRunEnd), + #[serde(rename = "log")] - Log(LogSpec), + Log(Log), + #[serde(rename = "error")] - Error(ErrorSpec), + Error(Error), } #[derive(Debug, Serialize, PartialEq, Clone)] -pub enum OutputArtifactDescendant { +pub enum RootArtifact { #[serde(rename = "schemaVersion")] - SchemaVersion(SchemaVersionSpec), + SchemaVersion(SchemaVersion), + #[serde(rename = "testRunArtifact")] - TestRunArtifact(TestRunArtifactSpec), + TestRunArtifact(TestRunArtifact), + #[serde(rename = "testStepArtifact")] - TestStepArtifact(TestStepArtifactSpec), + TestStepArtifact(TestStepArtifact), } #[allow(clippy::large_enum_variant)] #[derive(Debug, Serialize, PartialEq, Clone)] pub enum TestStepArtifactDescendant { #[serde(rename = "testStepStart")] - TestStepStart(TestStepStartSpec), + TestStepStart(TestStepStart), + #[serde(rename = "testStepEnd")] - TestStepEnd(TestStepEndSpec), + TestStepEnd(TestStepEnd), + #[serde(rename = "measurement")] - Measurement(MeasurementSpec), + Measurement(Measurement), + #[serde(rename = "measurementSeriesStart")] - MeasurementSeriesStart(MeasurementSeriesStartSpec), + MeasurementSeriesStart(MeasurementSeriesStart), + #[serde(rename = "measurementSeriesEnd")] - MeasurementSeriesEnd(MeasurementSeriesEndSpec), + MeasurementSeriesEnd(MeasurementSeriesEnd), + #[serde(rename = "measurementSeriesElement")] - MeasurementSeriesElement(MeasurementSeriesElementSpec), + MeasurementSeriesElement(MeasurementSeriesElement), + #[serde(rename = "diagnosis")] - Diagnosis(DiagnosisSpec), + Diagnosis(Diagnosis), + #[serde(rename = "log")] - Log(LogSpec), + Log(Log), + #[serde(rename = "error")] - Error(ErrorSpec), + Error(Error), + #[serde(rename = "file")] - File(FileSpec), + File(File), + #[serde(rename = "extension")] - Extension(ExtensionSpec), + Extension(Extension), } #[derive(Debug, Serialize, Clone, PartialEq)] @@ -127,6 +142,7 @@ pub enum SubcomponentType { Connector, } +// TODO: this should be better typed #[derive(Debug, Serialize, PartialEq, Clone)] pub enum ExtensionContentType { #[serde(rename = "float")] @@ -218,15 +234,17 @@ pub enum SoftwareType { } #[derive(Debug, Serialize, Clone)] -pub struct OutputArtifactSpec { +pub struct Root { #[serde(flatten)] - pub descendant: OutputArtifactDescendant, + pub artifact: RootArtifact, + // TODO : manage different timezones #[serde(rename = "timestamp")] #[serde(with = "rfc3339_format")] - pub now: DateTime, + pub timestamp: DateTime, + #[serde(rename = "sequenceNumber")] - pub sequence_number: u64, + pub seqno: u64, } /// Low-level model for the `schemaVersion` spec object. @@ -236,9 +254,10 @@ pub struct OutputArtifactSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/output/$defs/schemaVersion #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "schemaVersion")] -pub struct SchemaVersionSpec { +pub struct SchemaVersion { #[serde(rename = "major")] pub major: i8, + #[serde(rename = "minor")] pub minor: i8, } @@ -249,9 +268,9 @@ pub struct SchemaVersionSpec { /// schema url: https://github.com/opencomputeproject/ocp-diag-core/blob/main/json_spec/output/test_run_artifact.json /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testRunArtifact #[derive(Debug, Serialize, PartialEq, Clone)] -pub struct TestRunArtifactSpec { +pub struct TestRunArtifact { #[serde(flatten)] - pub descendant: TestRunArtifactDescendant, + pub artifact: TestRunArtifactDescendant, } /// Low-level model for the `testRunStart` spec object. @@ -261,17 +280,22 @@ pub struct TestRunArtifactSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testRunStart #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "testRunStart")] -pub struct TestRunStartSpec { +pub struct TestRunStart { #[serde(rename = "name")] pub name: String, + #[serde(rename = "version")] pub version: String, + #[serde(rename = "commandLine")] pub command_line: String, + #[serde(rename = "parameters")] pub parameters: Map, + #[serde(rename = "dutInfo")] - pub dut_info: DutInfoSpec, + pub dut_info: DutInfo, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -283,17 +307,22 @@ pub struct TestRunStartSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/dutInfo #[derive(Debug, Serialize, Default, Clone, PartialEq)] #[serde(rename = "dutInfo")] -pub struct DutInfoSpec { +pub struct DutInfo { #[serde(rename = "dutInfoId")] pub id: String, + #[serde(rename = "name")] pub name: Option, + #[serde(rename = "platformInfos")] - pub platform_infos: Option>, + pub platform_infos: Option>, + #[serde(rename = "softwareInfos")] - pub software_infos: Option>, + pub software_infos: Option>, + #[serde(rename = "hardwareInfos")] - pub hardware_infos: Option>, + pub hardware_infos: Option>, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -305,7 +334,7 @@ pub struct DutInfoSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/dutInfo/$defs/platformInfo #[derive(Debug, Serialize, Default, Clone, PartialEq)] #[serde(rename = "platformInfo")] -pub struct PlatformInfoSpec { +pub struct PlatformInfo { #[serde(rename = "info")] pub info: String, } @@ -317,17 +346,22 @@ pub struct PlatformInfoSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/dutInfo/$defs/softwareInfo #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "softwareInfo")] -pub struct SoftwareInfoSpec { +pub struct SoftwareInfo { #[serde(rename = "softwareInfoId")] pub id: String, + #[serde(rename = "name")] pub name: String, + #[serde(rename = "version")] pub version: Option, + #[serde(rename = "revision")] pub revision: Option, + #[serde(rename = "softwareType")] pub software_type: Option, + #[serde(rename = "computerSystem")] pub computer_system: Option, } @@ -339,29 +373,40 @@ pub struct SoftwareInfoSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/dutInfo/$defs/hardwareInfo #[derive(Debug, Serialize, Default, Clone, PartialEq)] #[serde(rename = "hardwareInfo")] -pub struct HardwareInfoSpec { +pub struct HardwareInfo { #[serde(rename = "hardwareInfoId")] pub id: String, + #[serde(rename = "name")] pub name: String, + #[serde(rename = "version")] pub version: Option, + #[serde(rename = "revision")] pub revision: Option, + #[serde(rename = "location")] pub location: Option, + #[serde(rename = "serialNumber")] pub serial_no: Option, + #[serde(rename = "partNumber")] pub part_no: Option, + #[serde(rename = "manufacturer")] pub manufacturer: Option, + #[serde(rename = "manufacturerPartNumber")] pub manufacturer_part_no: Option, + #[serde(rename = "odataId")] pub odata_id: Option, + #[serde(rename = "computerSystem")] pub computer_system: Option, + #[serde(rename = "manager")] pub manager: Option, } @@ -373,9 +418,10 @@ pub struct HardwareInfoSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testRunEnd #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "testRunEnd")] -pub struct TestRunEndSpec { +pub struct TestRunEnd { #[serde(rename = "status")] pub status: TestStatus, + #[serde(rename = "result")] pub result: TestResult, } @@ -388,16 +434,19 @@ pub struct TestRunEndSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/error #[derive(Debug, Serialize, Default, Clone, PartialEq)] #[serde(rename = "error")] -pub struct ErrorSpec { +pub struct Error { #[serde(rename = "symptom")] pub symptom: String, + #[serde(rename = "message")] pub message: Option, + // TODO: support this field during serialization to print only the id of SoftwareInfo struct #[serde(rename = "softwareInfoIds")] - pub software_infos: Option>, + pub software_infos: Option>, + #[serde(rename = "sourceLocation")] - pub source_location: Option, + pub source_location: Option, } /// Low-level model for `log` spec object. @@ -407,13 +456,15 @@ pub struct ErrorSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/log #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "log")] -pub struct LogSpec { +pub struct Log { #[serde(rename = "severity")] pub severity: LogSeverity, + #[serde(rename = "message")] pub message: String, + #[serde(rename = "sourceLocation")] - pub source_location: Option, + pub source_location: Option, } /// Provides information about which file/line of the source code in @@ -423,9 +474,10 @@ pub struct LogSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/sourceLocation #[derive(Debug, Serialize, Clone, Default, PartialEq)] #[serde(rename = "sourceLocation")] -pub struct SourceLocationSpec { +pub struct SourceLocation { #[serde(rename = "file")] pub file: String, + #[serde(rename = "line")] pub line: i32, } @@ -436,7 +488,7 @@ pub struct SourceLocationSpec { /// schema url: https://github.com/opencomputeproject/ocp-diag-core/blob/main/json_spec/output/test_step_artifact.json /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testStepArtifact #[derive(Debug, Serialize, PartialEq, Clone)] -pub struct TestStepArtifactSpec { +pub struct TestStepArtifact { #[serde(flatten)] pub descendant: TestStepArtifactDescendant, } @@ -448,7 +500,7 @@ pub struct TestStepArtifactSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testStepStart #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "testStepStart")] -pub struct TestStepStartSpec { +pub struct TestStepStart { #[serde(rename = "name")] pub name: String, } @@ -460,7 +512,7 @@ pub struct TestStepStartSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testStepEnd #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "testStepEnd")] -pub struct TestStepEndSpec { +pub struct TestStepEnd { #[serde(rename = "status")] pub status: TestStatus, } @@ -472,19 +524,25 @@ pub struct TestStepEndSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/measurement #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "measurement")] -pub struct MeasurementSpec { +pub struct Measurement { #[serde(rename = "name")] pub name: String, + #[serde(rename = "value")] pub value: Value, + #[serde(rename = "unit")] pub unit: Option, + #[serde(rename = "validators")] - pub validators: Option>, + pub validators: Option>, + #[serde(rename = "hardwareInfoId")] pub hardware_info_id: Option, + #[serde(rename = "subcomponent")] - pub subcomponent: Option, + pub subcomponent: Option, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -496,13 +554,16 @@ pub struct MeasurementSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/validator #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "validator")] -pub struct ValidatorSpec { +pub struct Validator { #[serde(rename = "name")] pub name: Option, + #[serde(rename = "type")] pub validator_type: ValidatorType, + #[serde(rename = "value")] pub value: Value, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -514,15 +575,19 @@ pub struct ValidatorSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/subcomponent #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename = "subcomponent")] -pub struct SubcomponentSpec { +pub struct Subcomponent { #[serde(rename = "type")] pub subcomponent_type: Option, + #[serde(rename = "name")] pub name: String, + #[serde(rename = "location")] pub location: Option, + #[serde(rename = "version")] pub version: Option, + #[serde(rename = "revision")] pub revision: Option, } @@ -534,19 +599,25 @@ pub struct SubcomponentSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/measurementSeriesStart #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "measurementSeriesStart")] -pub struct MeasurementSeriesStartSpec { +pub struct MeasurementSeriesStart { #[serde(rename = "name")] pub name: String, + #[serde(rename = "unit")] pub unit: Option, + #[serde(rename = "measurementSeriesId")] pub series_id: String, + #[serde(rename = "validators")] - pub validators: Option>, + pub validators: Option>, + #[serde(rename = "hardwareInfoId")] - pub hardware_info: Option, + pub hardware_info: Option, + #[serde(rename = "subComponent")] - pub subcomponent: Option, + pub subcomponent: Option, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -558,9 +629,10 @@ pub struct MeasurementSeriesStartSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/measurementSeriesEnd #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "measurementSeriesEnd")] -pub struct MeasurementSeriesEndSpec { +pub struct MeasurementSeriesEnd { #[serde(rename = "measurementSeriesId")] pub series_id: String, + #[serde(rename = "totalCount")] pub total_count: u64, } @@ -572,15 +644,19 @@ pub struct MeasurementSeriesEndSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/measurementSeriesElement #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[serde(rename = "measurementSeriesElement")] -pub struct MeasurementSeriesElementSpec { +pub struct MeasurementSeriesElement { #[serde(rename = "index")] pub index: u64, + #[serde(rename = "value")] pub value: Value, + #[serde(with = "rfc3339_format")] pub timestamp: DateTime, + #[serde(rename = "measurementSeriesId")] pub series_id: String, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -592,19 +668,24 @@ pub struct MeasurementSeriesElementSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/diagnosis #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "diagnosis")] -pub struct DiagnosisSpec { +pub struct Diagnosis { #[serde(rename = "verdict")] pub verdict: String, + #[serde(rename = "type")] pub diagnosis_type: DiagnosisType, + #[serde(rename = "message")] pub message: Option, + #[serde(rename = "validators")] - pub hardware_info: Option, + pub hardware_info: Option, + #[serde(rename = "subComponent")] - pub subcomponent: Option, + pub subcomponent: Option, + #[serde(rename = "sourceLocation")] - pub source_location: Option, + pub source_location: Option, } /// Low-level model for the `file` spec object. @@ -614,17 +695,22 @@ pub struct DiagnosisSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/file #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "file")] -pub struct FileSpec { +pub struct File { #[serde(rename = "name")] pub name: String, + #[serde(rename = "uri")] pub uri: String, + #[serde(rename = "isSnapshot")] pub is_snapshot: bool, + #[serde(rename = "description")] pub description: Option, + #[serde(rename = "contentType")] pub content_type: Option, + #[serde(rename = "metadata")] pub metadata: Option>, } @@ -636,9 +722,10 @@ pub struct FileSpec { /// schema ref: https://github.com/opencomputeproject/ocp-diag-core/testStepArtifact/$defs/extension #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename = "extension")] -pub struct ExtensionSpec { +pub struct Extension { #[serde(rename = "name")] pub name: String, + #[serde(rename = "content")] pub content: ExtensionContentType, } @@ -655,7 +742,7 @@ mod tests { #[test] fn test_rfc3339_format_serialize() -> Result<()> { let test_date = "2022-01-01T00:00:00.000Z"; - let msr = MeasurementSeriesElementSpec { + let msr = MeasurementSeriesElement { index: 0, value: 1.0.into(), timestamp: DateTime::parse_from_rfc3339(test_date)?.with_timezone(&chrono_tz::UTC), @@ -675,7 +762,7 @@ mod tests { let test_date = "2022-01-01T00:00:00.000Z"; let json = json!({"index":0,"measurementSeriesId":"test","metadata":null,"timestamp":"2022-01-01T00:00:00.000Z","value":1.0}); - let msr = serde_json::from_value::(json)?; + let msr = serde_json::from_value::(json)?; assert_eq!( msr.timestamp.to_rfc3339_opts(SecondsFormat::Millis, true), test_date