diff --git a/CHANGELOG.md b/CHANGELOG.md index 0759858a..c045c7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,3 +38,9 @@ - Updated `derive_builder` to the latest version. - Made use of `matches!` macros where possible. - Fixed some tests + +## [0.15.1] - 2022-10-13 + +### Added + +- Added `TestMessage`, `TestEvent`, `SuiteEvent` for parsing the `cargo test -- --format json` output. diff --git a/Cargo.toml b/Cargo.toml index 722d7b73..8138918a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo_metadata" -version = "0.18.0" +version = "0.18.1" authors = ["Oliver Schneider "] repository = "https://github.com/oli-obk/cargo_metadata" description = "structured access to the output of `cargo metadata`" @@ -21,6 +21,7 @@ thiserror = "1.0.31" [features] default = [] builder = ["derive_builder"] +unstable = [] [package.metadata.cargo_metadata_test] some_field = true diff --git a/src/lib.rs b/src/lib.rs index d7c3413c..061fd8bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,8 @@ pub use dependency::DependencyBuilder; pub use dependency::{Dependency, DependencyKind}; use diagnostic::Diagnostic; pub use errors::{Error, Result}; +#[cfg(feature = "unstable")] +pub use libtest::TestMessage; #[allow(deprecated)] pub use messages::parse_messages; pub use messages::{ @@ -115,6 +117,8 @@ use serde::{Deserialize, Deserializer, Serialize}; mod dependency; pub mod diagnostic; mod errors; +#[cfg(feature = "unstable")] +pub mod libtest; mod messages; /// An "opaque" identifier for a package. diff --git a/src/libtest.rs b/src/libtest.rs new file mode 100644 index 00000000..4de86e7d --- /dev/null +++ b/src/libtest.rs @@ -0,0 +1,165 @@ +//! Parses output of [libtest](https://github.com/rust-lang/rust/blob/master/library/test/src/formatters/json.rs). +//! +//! Since this module parses output in an unstable format, all structs in this module may change at any time, and are exempt from semver guarantees. +use serde::{Deserialize, Serialize}; + +/// Suite related event +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(tag = "event")] +#[serde(rename_all = "lowercase")] +/// Suite event +pub enum SuiteEvent { + /// emitted on the start of a test run, and the start of the doctests + Started { + /// number of tests in this suite + test_count: usize, + }, + /// the suite has finished + Ok { + /// the number of tests that passed + passed: usize, + /// the number of tests that failed + failed: usize, + /// number of tests that were ignored + ignored: usize, + /// number of benchmarks run + measured: usize, + /// i think this is based on what you specify in the cargo test argument + filtered_out: usize, + /// how long the suite took to run + exec_time: f32, + }, + /// the suite has at least one failing test + Failed { + /// the number of tests that passed + passed: usize, + /// the number of tests that failed + failed: usize, + /// number of tests that were ignored + ignored: usize, + /// i think its something to do with benchmarks? + measured: usize, + /// i think this is based on what you specify in the cargo test argument + filtered_out: usize, + /// how long the suite took to run + exec_time: f32, + }, +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(tag = "event")] +#[serde(rename_all = "lowercase")] +/// Test event +pub enum TestEvent { + /// a new test starts + Started { + /// the name of this test + name: String, + }, + /// the test has finished + Ok { + /// which one + name: String, + /// in how long + exec_time: f32, + /// what did it say? + stdout: Option, + }, + /// the test has failed + Failed { + /// which one + name: String, + /// in how long + exec_time: f32, + /// why? + stdout: Option, + /// it timed out? + reason: Option, + /// what message + message: Option, + }, + /// the test has been ignored + Ignored { + /// which one + name: String, + }, + /// the test has timed out + Timeout { + /// which one + name: String, + }, +} + +impl TestEvent { + /// Get the name of this test + pub fn name(&self) -> &str { + let (Self::Started { name } + | Self::Ok { name, .. } + | Self::Ignored { name } + | Self::Failed { name, .. } + | Self::Timeout { name }) = self; + name + } + + /// Get the stdout of this test, if available. + pub fn stdout(&self) -> Option<&str> { + match self { + Self::Ok { stdout, .. } | Self::Failed { stdout, .. } => stdout.as_deref(), + _ => None, + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +/// Represents the output of `cargo test -- -Zunstable-options --report-time --show-output --format json`. +/// +/// requires --report-time +/// +/// # Stability +/// +/// As this struct is for interfacing with the unstable libtest json output, this struct may change at any time, without semver guarantees. +#[serde(tag = "type")] +#[serde(rename_all = "lowercase")] +pub enum TestMessage { + /// suite related message + Suite(SuiteEvent), + /// test related message + Test(TestEvent), + /// bench related message + Bench { + /// name of benchmark + name: String, + /// distribution + median: f32, + /// deviation + deviation: f32, + /// thruput in MiB per second + mib_per_second: Option, + }, +} + +#[test] +fn deser() { + macro_rules! run { + ($($input:literal parses to $output:expr),+) => { + $(assert_eq!(dbg!(serde_json::from_str::($input)).unwrap(), $output);)+ + }; + } + run![ + r#"{ "type": "suite", "event": "started", "test_count": 2 }"# parses to TestMessage::Suite(SuiteEvent::Started { test_count: 2 }), + r#"{ "type": "test", "event": "started", "name": "fail" }"# parses to TestMessage::Test(TestEvent::Started { name: "fail".into() }), + r#"{ "type": "test", "name": "fail", "event": "ok", "exec_time": 0.000003428, "stdout": "hello world" }"# parses to TestMessage::Test(TestEvent::Ok { name: "fail".into(), exec_time: 0.000003428, stdout: Some("hello world".into()) }), + r#"{ "type": "test", "event": "started", "name": "nope" }"# parses to TestMessage::Test(TestEvent::Started { name: "nope".into() }), + r#"{ "type": "test", "name": "nope", "event": "ignored" }"# parses to TestMessage::Test(TestEvent::Ignored { name: "nope".into() }), + r#"{ "type": "suite", "event": "ok", "passed": 1, "failed": 0, "ignored": 1, "measured": 0, "filtered_out": 0, "exec_time": 0.000684028 }"# parses to TestMessage::Suite(SuiteEvent::Ok { passed: 1, failed: 0, ignored: 1, measured: 0, filtered_out: 0, exec_time: 0.000684028 }) + ]; + + run![ + r#"{ "type": "suite", "event": "started", "test_count": 2 }"# parses to TestMessage::Suite(SuiteEvent::Started { test_count: 2 }), + r#"{ "type": "test", "event": "started", "name": "fail" }"# parses to TestMessage::Test(TestEvent::Started { name: "fail".into() }), + r#"{ "type": "test", "event": "started", "name": "benc" }"# parses to TestMessage::Test(TestEvent::Started { name: "benc".into() }), + r#"{ "type": "bench", "name": "benc", "median": 0, "deviation": 0 }"# parses to TestMessage::Bench { name: "benc".into(), median: 0., deviation: 0., mib_per_second: None }, + r#"{ "type": "test", "name": "fail", "event": "failed", "exec_time": 0.000081092, "stdout": "thread 'fail' panicked" }"# parses to TestMessage::Test(TestEvent::Failed { name: "fail".into(), exec_time: 0.000081092, stdout: Some("thread 'fail' panicked".into()), reason: None, message: None} ), + r#"{ "type": "suite", "event": "failed", "passed": 0, "failed": 1, "ignored": 0, "measured": 1, "filtered_out": 0, "exec_time": 0.000731068 }"# parses to TestMessage::Suite(SuiteEvent::Failed { passed: 0, failed: 1, ignored: 0, measured: 1, filtered_out: 0, exec_time: 0.000731068 }) + ]; +}