Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tracing integration for writer::JUnit and writer::Json (#213) #261

Merged
merged 41 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a61b42d
WIP
ilslv Feb 21, 2023
c063a85
Implement `tracing` events capturing and postponed output in `writer:…
ilslv Feb 22, 2023
6d9a4d7
Minor corrections
ilslv Feb 22, 2023
1358d55
Corrections, docs and clippy
ilslv Feb 23, 2023
864d1df
Add optimisation on PostponedWriter relying on fact, that we get full…
ilslv Feb 23, 2023
14af39c
Forward traces without Scenario Span to all active Scenarios
ilslv Feb 24, 2023
200c3b1
Docs and clippy
ilslv Feb 24, 2023
ea72445
Expose tracing public API to Cucumber
ilslv Feb 24, 2023
e968140
Corrections
ilslv Feb 24, 2023
5cc3068
Corrections
ilslv Feb 24, 2023
1444b28
Correct MSRV
ilslv Feb 24, 2023
e82d2ee
Merge branch 'main' into 213-tracing-integration
ilslv Feb 27, 2023
4d54d6f
tracing output test
ilslv Feb 27, 2023
3f6244d
Corrections
ilslv Feb 27, 2023
ff44f20
Corrections
ilslv Feb 27, 2023
915a30a
Corrections
ilslv Feb 27, 2023
bc323f7
Corrections
ilslv Feb 27, 2023
140f097
Corrections
ilslv Feb 27, 2023
7692a15
Corrections
ilslv Feb 27, 2023
a58cd25
Corrections
ilslv Feb 27, 2023
7fd71dc
Corrections
ilslv Feb 27, 2023
317ed7e
Corrections
ilslv Feb 27, 2023
67005d6
Corrections
ilslv Feb 27, 2023
43f5c20
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
b080a7a
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
860f510
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
878e8e6
Corrections
ilslv Feb 28, 2023
42b8202
Clippy
ilslv Feb 28, 2023
620408e
Clippy
ilslv Feb 28, 2023
59a83e4
Clippy
ilslv Feb 28, 2023
23ff3f7
Clippy
ilslv Feb 28, 2023
01ff07e
Corrections
ilslv Mar 1, 2023
f63f995
Corrections and update GIFs
ilslv Mar 1, 2023
2c7ca52
Corrections
ilslv Mar 1, 2023
4a5f626
Implement `event::Scenario::Log` support for `writer::JUnit`
ilslv Mar 1, 2023
420b7d2
Implement `event::Scenario::Log` support for `writer::Json`
ilslv Mar 2, 2023
f9bd81b
Corrections
ilslv Mar 2, 2023
1636e81
Corrections
ilslv Mar 2, 2023
8fd6760
Merge branch 'main' into 213-tracing-writer-integrations
ilslv Mar 6, 2023
f9f4dfb
Corrections
ilslv Mar 6, 2023
3a67f3b
Corrections
tyranron Mar 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
### BC Breaks

- Added `Log` variant to `event::Scenario`. ([#258])
- Added `embeddings` field to `writer::json::Step` and `writer::json::HookResult`. ([#261])

### Added

- [`tracing`] crate integration behind the `tracing` feature flag. ([#213], [#258])
- [`tracing`] crate integration behind the `tracing` feature flag. ([#213], [#258], [#261])

[#213]: /../../issues/213
[#258]: /../../pull/258
[#261]: /../../pull/261



Expand Down
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ libtest = ["dep:serde", "dep:serde_json", "timestamps"]
# Enables step attributes and auto-wiring.
macros = ["dep:anyhow", "dep:cucumber-codegen", "dep:cucumber-expressions", "dep:inventory"]
# Enables support for outputting in Cucumber JSON format.
output-json = ["dep:Inflector", "dep:serde", "dep:serde_json", "timestamps"]
output-json = ["dep:base64", "dep:Inflector", "dep:mime", "dep:serde", "dep:serde_json", "timestamps"]
# Enables support for outputting JUnit XML report.
output-junit = ["dep:junit-report", "timestamps"]
# Enables timestamps collecting for all events.
Expand Down Expand Up @@ -67,9 +67,11 @@ cucumber-expressions = { version = "0.2.1", features = ["into-regex"], optional
inventory = { version = "0.3", optional = true }

# "output-json" and/or "libtest" features dependencies.
base64 = { version = "0.21", optional = true }
Inflector = { version = "0.11", default-features = false, optional = true }
mime = { version = "0.3.16", optional = true }
serde = { version = "1.0.103", features = ["derive"], optional = true }
serde_json = { version = "1.0.18", optional = true }
Inflector = { version = "0.11", default-features = false, optional = true }

# "output-junit" feature dependencies.
junit-report = { version = "0.8", optional = true }
Expand All @@ -87,11 +89,11 @@ tokio = { version = "1.12", features = ["macros", "rt-multi-thread", "sync", "ti

[[test]]
name = "json"
required-features = ["output-json"]
required-features = ["output-json", "tracing"]

[[test]]
name = "junit"
required-features = ["output-junit"]
required-features = ["output-junit", "tracing"]

[[test]]
name = "libtest"
Expand Down
171 changes: 151 additions & 20 deletions src/writer/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@
//!
//! [1]: https://github.com/cucumber/cucumber-json-schema

use std::{fmt::Debug, io, time::SystemTime};
use std::{
fmt::{self, Debug},
io, mem,
time::SystemTime,
};

use async_trait::async_trait;
use base64::Engine as _;
use derive_more::Display;
use inflector::Inflector as _;
use serde::Serialize;
use mime::Mime;
use once_cell::sync::Lazy;
use serde::{Serialize, Serializer};

use crate::{
cli, event,
Expand Down Expand Up @@ -55,9 +63,13 @@ pub struct Json<Out: io::Write> {

/// [`SystemTime`] when the current [`Hook`]/[`Step`] has started.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Hook`]: event::Hook
started: Option<SystemTime>,

/// [`event::Scenario::Log`]s of the current [`Hook`]/[`Step`].
///
/// [`Hook`]: event::Hook
logs: Vec<String>,
}

#[async_trait(?Send)]
Expand Down Expand Up @@ -153,8 +165,9 @@ impl<Out: io::Write> Json<Out> {
pub const fn raw(output: Out) -> Self {
Self {
output,
features: Vec::new(),
features: vec![],
started: None,
logs: vec![],
}
}

Expand All @@ -170,6 +183,7 @@ impl<Out: io::Write> Json<Out> {
use event::Scenario;

match ev {
Scenario::Started => {}
Scenario::Hook(ty, ev) => {
self.handle_hook_event(feature, rule, scenario, ty, ev, meta);
}
Expand All @@ -189,8 +203,12 @@ impl<Out: io::Write> Json<Out> {
feature, rule, scenario, "scenario", &st, ev, meta,
);
}
// TODO: Report logs for each `Scenario`.
Scenario::Started | Scenario::Finished | Scenario::Log(_) => {}
Scenario::Log(msg) => {
self.logs.push(msg);
}
Scenario::Finished => {
self.logs.clear();
}
}
}

Expand Down Expand Up @@ -233,13 +251,21 @@ impl<Out: io::Write> Json<Out> {
duration: duration(),
error_message: None,
},
embeddings: mem::take(&mut self.logs)
.into_iter()
.map(Embedding::from_log)
.collect(),
},
Hook::Failed(_, info) => HookResult {
result: RunResult {
status: Status::Failed,
duration: duration(),
error_message: Some(coerce_error(&info).into_owned()),
},
embeddings: mem::take(&mut self.logs)
.into_iter()
.map(Embedding::from_log)
.collect(),
},
};

Expand Down Expand Up @@ -316,14 +342,19 @@ impl<Out: io::Write> Json<Out> {
},
};

let el = self.mut_or_insert_element(feature, rule, scenario, ty);
el.steps.push(Step {
let step = Step {
keyword: step.keyword.clone(),
line: step.position.line,
name: step.value.clone(),
hidden: false,
result,
});
embeddings: mem::take(&mut self.logs)
.into_iter()
.map(Embedding::from_log)
.collect(),
};
let el = self.mut_or_insert_element(feature, rule, scenario, ty);
el.steps.push(step);
}

/// Inserts the given `scenario`, if not present, and then returns a mutable
Expand Down Expand Up @@ -370,6 +401,69 @@ impl<Out: io::Write> Json<Out> {
}
}

/// [`base64`] encoded data.
#[derive(Clone, Debug, Display, Serialize)]
#[serde(transparent)]
pub struct Base64(String);

impl Base64 {
/// Used [`base64::engine`].
const ENGINE: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;

/// Encodes `bytes` as [`base64`].
#[must_use]
pub fn encode(bytes: impl AsRef<[u8]>) -> Self {
Self(Self::ENGINE.encode(bytes))
}

/// Decodes this [`base64`] encoded data.
#[must_use]
pub fn decode(&self) -> Vec<u8> {
Self::ENGINE.decode(&self.0).unwrap_or_else(|_| {
unreachable!(
"the only way to construct this type is `Base64::encode`, so \
should contain a valid `base64` encoded `String`",
)
})
}
}

/// Data embedded to [Cucumber JSON format][1] output.
///
/// [1]: https://github.com/cucumber/cucumber-json-schema
#[derive(Clone, Debug, Serialize)]
pub struct Embedding {
/// [`base64`] encoded data.
pub data: Base64,

/// [`Mime`] of this [`Embedding::data`].
#[serde(serialize_with = "serialize_display")]
pub mime_type: Mime,

/// Optional name of the [`Embedding`].
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}

impl Embedding {
/// Creates [`Embedding`] from the provided [`event::Scenario::Log`].
fn from_log(msg: impl AsRef<str>) -> Self {
/// [`Mime`] of the [`event::Scenario::Log`] [`Embedding`].
static LOG_MIME: Lazy<Mime> = Lazy::new(|| {
"text/x.cucumber.log+plain"
.parse()
.unwrap_or_else(|_| unreachable!("valid MIME"))
});

Self {
data: Base64::encode(msg.as_ref()),
mime_type: LOG_MIME.clone(),
name: None,
}
}
}

/// [`Serialize`]able tag of a [`gherkin::Feature`] or a [`gherkin::Scenario`].
#[derive(Clone, Debug, Serialize)]
pub struct Tag {
Expand Down Expand Up @@ -445,6 +539,18 @@ pub struct Step {

/// [`RunResult`] of this [`Step`].
pub result: RunResult,

/// [`Embedding`]s of this [`Step`].
///
/// Although this field isn't present in the [JSON schema][1], all major
/// implementations have it (see [Java], [JavaScript], [Ruby]).
///
/// [1]: https://github.com/cucumber/cucumber-json-schema
/// [Java]: https://bit.ly/3J66vxT
/// [JavaScript]: https://bit.ly/41HSTAf
/// [Ruby]: https://bit.ly/3kAJRof
#[serde(skip_serializing_if = "Vec::is_empty")]
pub embeddings: Vec<Embedding>,
}

/// [`Serialize`]able result of running a [`Before`] or [`After`] hook.
Expand All @@ -455,6 +561,19 @@ pub struct Step {
pub struct HookResult {
/// [`RunResult`] of the hook.
pub result: RunResult,

/// [`Embedding`]s of this [`Hook`].
///
/// Although this field isn't present in [JSON schema][1], all major
/// implementations have it (see [Java], [JavaScript], [Ruby]).
///
/// [`Hook`]: event::Hook
/// [1]: https://github.com/cucumber/cucumber-json-schema
/// [Java]: https://bit.ly/3J66vxT
/// [JavaScript]: https://bit.ly/41HSTAf
/// [Ruby]: https://bit.ly/3kAJRof
#[serde(skip_serializing_if = "Vec::is_empty")]
pub embeddings: Vec<Embedding>,
}

/// [`Serialize`]able [`gherkin::Background`] or [`gherkin::Scenario`].
Expand Down Expand Up @@ -518,8 +637,8 @@ impl Element {
ty: &'static str,
) -> Self {
Self {
after: Vec::new(),
before: Vec::new(),
after: vec![],
before: vec![],
keyword: (ty == "background")
.then(|| feature.background.as_ref().map(|bg| &bg.keyword))
.flatten()
Expand Down Expand Up @@ -547,7 +666,7 @@ impl Element {
line: scenario.position.line,
})
.collect(),
steps: Vec::new(),
steps: vec![],
}
}
}
Expand Down Expand Up @@ -590,7 +709,7 @@ impl Feature {
line: feature.position.line,
})
.collect(),
elements: Vec::new(),
elements: vec![],
}
}

Expand All @@ -604,10 +723,10 @@ impl Feature {
.map(str::to_owned),
keyword: String::new(),
name: String::new(),
tags: Vec::new(),
tags: vec![],
elements: vec![Element {
after: Vec::new(),
before: Vec::new(),
after: vec![],
before: vec![],
keyword: String::new(),
r#type: "scenario",
id: format!(
Expand All @@ -619,7 +738,7 @@ impl Feature {
),
line: 0,
name: String::new(),
tags: Vec::new(),
tags: vec![],
steps: vec![Step {
keyword: String::new(),
line: err.pos.line,
Expand All @@ -630,6 +749,7 @@ impl Feature {
duration: 0,
error_message: Some(err.to_string()),
},
embeddings: vec![],
}],
}],
}
Expand All @@ -651,8 +771,8 @@ impl Feature {
name: String::new(),
tags: vec![],
elements: vec![Element {
after: Vec::new(),
before: Vec::new(),
after: vec![],
before: vec![],
keyword: String::new(),
r#type: "scenario",
id: format!(
Expand All @@ -661,7 +781,7 @@ impl Feature {
),
line: 0,
name: String::new(),
tags: Vec::new(),
tags: vec![],
steps: vec![Step {
keyword: String::new(),
line: 0,
Expand All @@ -672,6 +792,7 @@ impl Feature {
duration: 0,
error_message: Some(err.to_string()),
},
embeddings: vec![],
}],
}],
}
Expand All @@ -693,3 +814,13 @@ impl PartialEq<gherkin::Feature> for Feature {
&& self.name == feature.name
}
}

/// Helper to use `#[serde(serialize_with = "serialize_display")]` with any type
/// implementing [`fmt::Display`].
fn serialize_display<T, S>(display: &T, ser: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
format_args!("{display}").serialize(ser)
}
Loading