Skip to content

No default features #430

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ jobs:
run: cargo build
- name: Run tests
run: cargo nextest run
- name: Run tests (without default features)
run: cargo nextest run --no-default-features
14 changes: 10 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ categories = ["encoding", "development-tools"]
edition = "2021"
rust-version = "1.70"

[features]
default = ["timestamps", "uuids", "strip-ansi"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make timestamps default and the other two non-default.

timestamps = ["dep:chrono"]
uuids = ["dep:uuid", "dep:newtype-uuid"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this feature uuid.

strip-ansi = ["dep:strip-ansi-escapes"]

[dependencies]
chrono = { version = "0.4.41", default-features = false, features = ["std"] }
chrono = { version = "0.4.41", default-features = false, features = ["std"], optional = true }
indexmap = "2.7.1"
quick-xml = "0.38.1"
newtype-uuid = "1.2.4"
newtype-uuid = { version = "1.2.4", optional = true }
thiserror = "2.0.12"
strip-ansi-escapes = "0.2.1"
uuid = "1.17.0"
strip-ansi-escapes = { version = "0.2.1", optional = true }
uuid = { version = "1.17.0", optional = true }

[dev-dependencies]
goldenfile = "1.7.3"
Expand Down
23 changes: 23 additions & 0 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::{serialize::serialize_report, SerializeError};
#[cfg(feature = "timestamps")]
use chrono::{DateTime, FixedOffset};
use indexmap::map::IndexMap;
#[cfg(feature = "uuids")]
use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
#[cfg(feature = "uuids")]
use uuid::Uuid;

/// A tag indicating the kind of report.
#[cfg(feature = "uuids")]
pub enum ReportKind {}

#[cfg(feature = "uuids")]
impl TypedUuidKind for ReportKind {
fn tag() -> TypedUuidTag {
const TAG: TypedUuidTag = TypedUuidTag::new("quick-junit-report");
Expand All @@ -19,6 +24,7 @@ impl TypedUuidKind for ReportKind {
}

/// A unique identifier associated with a report.
#[cfg(feature = "uuids")]
pub type ReportUuid = TypedUuid<ReportKind>;

/// The root element of a JUnit report.
Expand All @@ -30,11 +36,13 @@ pub struct Report {
/// A unique identifier associated with this report.
///
/// This is an extension to the spec that's used by nextest.
#[cfg(feature = "uuids")]
pub uuid: Option<ReportUuid>,

/// The time at which the first test in this report began execution.
///
/// This is not part of the JUnit spec, but may be useful for some tools.
#[cfg(feature = "timestamps")]
pub timestamp: Option<DateTime<FixedOffset>>,

/// The overall time taken by the test suite.
Expand All @@ -60,7 +68,9 @@ impl Report {
pub fn new(name: impl Into<XmlString>) -> Self {
Self {
name: name.into(),
#[cfg(feature = "uuids")]
uuid: None,
#[cfg(feature = "timestamps")]
timestamp: None,
time: None,
tests: 0,
Expand All @@ -73,6 +83,7 @@ impl Report {
/// Sets a unique ID for this `Report`.
///
/// This is an extension that's used by nextest.
#[cfg(feature = "uuids")]
pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
self.uuid = Some(uuid);
self
Expand All @@ -81,12 +92,14 @@ impl Report {
/// Sets a unique ID for this `Report` from an untyped [`Uuid`].
///
/// This is an extension that's used by nextest.
#[cfg(feature = "uuids")]
pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
self
}

/// Sets the start timestamp for the report.
#[cfg(feature = "timestamps")]
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
Expand Down Expand Up @@ -165,6 +178,7 @@ pub struct TestSuite {
pub failures: usize,

/// The time at which the TestSuite began execution.
#[cfg(feature = "timestamps")]
pub timestamp: Option<DateTime<FixedOffset>>,

/// The overall time taken by the TestSuite.
Expand Down Expand Up @@ -192,6 +206,7 @@ impl TestSuite {
Self {
name: name.into(),
time: None,
#[cfg(feature = "timestamps")]
timestamp: None,
tests: 0,
disabled: 0,
Expand All @@ -206,6 +221,7 @@ impl TestSuite {
}

/// Sets the start timestamp for the TestSuite.
#[cfg(feature = "timestamps")]
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
Expand Down Expand Up @@ -309,6 +325,7 @@ pub struct TestCase {
/// The time at which this test case began execution.
///
/// This is not part of the JUnit spec, but may be useful for some tools.
#[cfg(feature = "timestamps")]
pub timestamp: Option<DateTime<FixedOffset>>,

/// The time it took to execute this test case.
Expand Down Expand Up @@ -337,6 +354,7 @@ impl TestCase {
name: name.into(),
classname: None,
assertions: None,
#[cfg(feature = "timestamps")]
timestamp: None,
time: None,
status,
Expand All @@ -360,6 +378,7 @@ impl TestCase {
}

/// Sets the start timestamp for the test case.
#[cfg(feature = "timestamps")]
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
Expand Down Expand Up @@ -548,6 +567,7 @@ pub struct TestRerun {
/// The time at which this rerun began execution.
///
/// This is not part of the JUnit spec, but may be useful for some tools.
#[cfg(feature = "timestamps")]
pub timestamp: Option<DateTime<FixedOffset>>,

/// The time it took to execute this rerun.
Expand Down Expand Up @@ -581,6 +601,7 @@ impl TestRerun {
pub fn new(kind: NonSuccessKind) -> Self {
TestRerun {
kind,
#[cfg(feature = "timestamps")]
timestamp: None,
time: None,
message: None,
Expand All @@ -593,6 +614,7 @@ impl TestRerun {
}

/// Sets the start timestamp for this rerun.
#[cfg(feature = "timestamps")]
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
Expand Down Expand Up @@ -718,6 +740,7 @@ impl XmlString {
/// Creates a new `XmlString`, removing any ANSI escapes and non-printable characters from it.
pub fn new(data: impl AsRef<str>) -> Self {
let data = data.as_ref();
#[cfg(feature = "strip-ansi")]
let data = strip_ansi_escapes::strip_str(data);
Comment on lines +743 to 744
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't right -- enabling a feature shouldn't cause this kind of behavior change. Instead I'd recommend adding an alternative constructor.

Copy link
Author

@mhils mhils Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, you mean something like XmlString::escape_ansi() -> Self? XmlString::new would not do automatic stripping and consumers would use this alternative constructor if they need to strip ANSI escapes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility, I think new should retain the strip-ansi behavior, and there should be a separate constructor that only removes bytes illegal in XML without stripping ANSI codes. The latter could be called new_minimal or something. (This also suggests that the strip-ansi feature should be turned on by default.)

Oh it looks like in the loop below, it's quick-junit that's doing the filtering of illegal bytes.

let data = data
.replace(
Expand Down
12 changes: 12 additions & 0 deletions src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
NonSuccessKind, Property, Report, SerializeError, TestCase, TestCaseStatus, TestRerun,
TestSuite, XmlString,
};
#[cfg(feature = "timestamps")]
use chrono::{DateTime, FixedOffset};
use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
Expand Down Expand Up @@ -52,7 +53,9 @@ pub(crate) fn serialize_report_impl(
// Use the destructuring syntax to ensure that all fields are handled.
let Report {
name,
#[cfg(feature = "uuids")]
uuid,
#[cfg(feature = "timestamps")]
timestamp,
time,
tests,
Expand All @@ -68,9 +71,11 @@ pub(crate) fn serialize_report_impl(
("failures", failures.to_string().as_str()),
("errors", errors.to_string().as_str()),
]);
#[cfg(feature = "uuids")]
if let Some(uuid) = uuid {
testsuites_tag.push_attribute(("uuid", uuid.to_string().as_str()));
}
#[cfg(feature = "timestamps")]
if let Some(timestamp) = timestamp {
serialize_timestamp(&mut testsuites_tag, timestamp);
}
Expand Down Expand Up @@ -101,6 +106,7 @@ pub(crate) fn serialize_test_suite(
errors,
failures,
time,
#[cfg(feature = "timestamps")]
timestamp,
test_cases,
properties,
Expand All @@ -118,6 +124,7 @@ pub(crate) fn serialize_test_suite(
("failures", failures.to_string().as_str()),
]);

#[cfg(feature = "timestamps")]
if let Some(timestamp) = timestamp {
serialize_timestamp(&mut test_suite_tag, timestamp);
}
Expand Down Expand Up @@ -172,6 +179,7 @@ fn serialize_test_case(
name,
classname,
assertions,
#[cfg(feature = "timestamps")]
timestamp,
time,
status,
Expand All @@ -190,6 +198,7 @@ fn serialize_test_case(
testcase_tag.push_attribute(("assertions", format!("{assertions}").as_str()));
}

#[cfg(feature = "timestamps")]
if let Some(timestamp) = timestamp {
serialize_timestamp(&mut testcase_tag, timestamp);
}
Expand Down Expand Up @@ -306,6 +315,7 @@ fn serialize_rerun(
writer: &mut Writer<impl io::Write>,
) -> quick_xml::Result<()> {
let TestRerun {
#[cfg(feature = "timestamps")]
timestamp,
time,
kind,
Expand All @@ -325,6 +335,7 @@ fn serialize_rerun(
};

let mut tag = BytesStart::new(tag_name);
#[cfg(feature = "timestamps")]
if let Some(timestamp) = timestamp {
serialize_timestamp(&mut tag, timestamp);
}
Expand Down Expand Up @@ -409,6 +420,7 @@ fn serialize_end_tag(
writer.write_event(Event::End(end_tag))
}

#[cfg(feature = "timestamps")]
fn serialize_timestamp(tag: &mut BytesStart<'_>, timestamp: &DateTime<FixedOffset>) {
// The format string is obtained from https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html#fn8.
// The only change is that this only prints timestamps up to 3 decimal places (to match times).
Expand Down
14 changes: 13 additions & 1 deletion tests/fixture_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) The nextest Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

#[cfg(feature = "timestamps")]
use chrono::DateTime;
use goldenfile::Mint;
use owo_colors::OwoColorize;
Expand All @@ -13,9 +14,14 @@ use std::time::Duration;
fn fixtures() {
let mut mint = Mint::new("tests/fixtures");

#[cfg(all(feature = "timestamps", feature = "uuids", feature = "strip-ansi"))]
let f = mint
.new_goldenfile("basic_report.xml")
.expect("creating new goldenfile succeeds");
#[cfg(not(any(feature = "timestamps", feature = "uuids", feature = "strip-ansi")))]
let f = mint
.new_goldenfile("basic_report_no_default_features.xml")
.expect("creating new goldenfile succeeds");

let basic_report = basic_report();
basic_report
Expand All @@ -25,13 +31,15 @@ fn fixtures() {

fn basic_report() -> Report {
let mut report = Report::new("my-test-run");
#[cfg(feature = "timestamps")]
report.set_timestamp(
DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 10:52:37 -0800")
.expect("valid RFC2822 datetime"),
);
report.set_time(Duration::new(42, 234_567_890));

let mut test_suite = TestSuite::new("testsuite0");
#[cfg(feature = "timestamps")]
test_suite.set_timestamp(
DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 10:52:39 -0800")
.expect("valid RFC2822 datetime"),
Expand Down Expand Up @@ -74,11 +82,13 @@ fn basic_report() -> Report {
.set_message("skipped message");
// no description to test that.
let mut test_case = TestCase::new("testcase3", test_case_status);
#[cfg(feature = "timestamps")]
test_case
.set_timestamp(
DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 11:52:41 -0700")
.expect("valid RFC2822 datetime"),
)
);
test_case
.set_assertions(20)
.set_system_out("testcase3 output")
.set_system_err("testcase3 error");
Expand Down Expand Up @@ -138,6 +148,8 @@ fn basic_report() -> Report {
test_suite.add_property(Property::new("env", "FOOBAR"));

report.add_test_suite(test_suite);
#[cfg(feature = "uuids")]
report.set_uuid(uuid::Uuid::parse_str("0500990f-0df3-4722-bbeb-90a75b8aa6bd").expect("uuid parsing succeeds"));

report
}
2 changes: 1 addition & 1 deletion tests/fixtures/basic_report.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="my-test-run" tests="7" failures="2" errors="1" timestamp="2021-04-01T10:52:37.000-08:00" time="42.235">
<testsuites name="my-test-run" tests="7" failures="2" errors="1" uuid="0500990f-0df3-4722-bbeb-90a75b8aa6bd" timestamp="2021-04-01T10:52:37.000-08:00" time="42.235">
<testsuite name="testsuite0" tests="7" disabled="1" errors="1" failures="2" timestamp="2021-04-01T10:52:39.000-08:00">
<properties>
<property name="env" value="FOOBAR"/>
Expand Down
45 changes: 45 additions & 0 deletions tests/fixtures/basic_report_no_default_features.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="my-test-run" tests="7" failures="2" errors="1" time="42.235">
<testsuite name="testsuite0" tests="7" disabled="1" errors="1" failures="2">
<properties>
<property name="env" value="FOOBAR"/>
</properties>
<testcase name="testcase0">
<system-out>testcase0-output</system-out>
</testcase>
<testcase name="testcase1" time="4.242">
<failure message="testcase1-message">this is the failure description</failure>
<system-err>some sort of failure output</system-err>
</testcase>
<testcase name="testcase2" time="0.000">
<error type="error type">testcase2 error description</error>
</testcase>
<testcase name="testcase3" assertions="20">
<skipped message="skipped message" type="skipped type"/>
<system-out>testcase3 output</system-out>
<system-err>testcase3 error</system-err>
</testcase>
<testcase name="testcase4" time="661.661">
<flakyFailure type="flaky failure type">this is a flaky failure description</flakyFailure>
<flakyError type="flaky error type">flaky error description
<stackTrace>flaky stack trace</stackTrace>
<system-out>flaky system output</system-out>
<system-err>flaky system error with [34mANSI escape codes[39m</system-err>
Copy link
Author

@mhils mhils Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the ansi-escape feature flag, ANSI escape codes are just added to the XML. It looks like there is actually no additional escaping necessary to be valid XML (but if it were, quick-xml would take care of it).

Of course we shouldn't have unescaped ANSI codes in here, but in many cases you may know that the target application is never producing them in the first place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting -- I think a newer version of quick-xml might be doing some filtering here.

</flakyError>
</testcase>
<testcase name="testcase5" time="0.156">
<failure>main test failure description</failure>
<rerunFailure type="retry failure type">
</rerunFailure>
<rerunError type="retry error type">
<stackTrace>retry error stack trace</stackTrace>
<system-out>retry error system output</system-out>
</rerunError>
</testcase>
<testcase name="testcase6">
<properties>
<property name="step" value="foobar"/>
</properties>
</testcase>
</testsuite>
</testsuites>