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

Add libtest json output option #1086

Merged
merged 6 commits into from
Dec 10, 2023
Merged
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
56 changes: 54 additions & 2 deletions cargo-nextest/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
partition::PartitionerBuilder,
platform::BuildPlatforms,
reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay, TestReporterBuilder},
reporter::{structured, FinalStatusLevel, StatusLevel, TestOutputDisplay, TestReporterBuilder},
reuse_build::{archive_to_file, ArchiveReporter, MetadataOrPath, PathMapper, ReuseBuildInfo},
runner::{configure_handle_inheritance, RunStatsFailureKind, TestRunnerBuilder},
show_config::{ShowNextestVersion, ShowTestGroupSettings, ShowTestGroups, ShowTestGroupsMode},
Expand Down Expand Up @@ -730,6 +730,18 @@
All,
}

#[derive(Clone, Copy, Debug, ValueEnum, Default)]
enum MessageFormat {
/// The default output format
#[default]
Human,

Check warning on line 737 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L737

Added line #L737 was not covered by tests
/// Output test information in the same format as libtest itself
LibtestJson,
/// Output test information in the same format as libtest itself, with an
/// `nextest` subobject that includes additional metadata

Check warning on line 741 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L741

Added line #L741 was not covered by tests
LibtestJsonPlus,
}

#[derive(Debug, Default, Args)]
#[command(next_help_heading = "Reporter options")]
struct TestReporterOpts {
Expand Down Expand Up @@ -777,6 +789,32 @@
/// Do not display the progress bar
#[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR")]
hide_progress_bar: bool,

/// The format to use for outputting test results
#[arg(
long,
name = "message-format",
value_enum,
default_value_t,
conflicts_with = "no-run",
value_name = "FORMAT",
env = "NEXTEST_MESSAGE_FORMAT"
)]
message_format: MessageFormat,

/// The specific version of the `message-format` to use
///
/// This version string allows the machine-readable formats to use a stable
/// structure for consistent consumption across changes to nextest. If not
/// specified the latest version for the selected message format is used
#[arg(
long,
conflicts_with = "no-run",
requires = "message-format",
value_name = "VERSION",
env = "NEXTEST_MESSAGE_FORMAT_VERSION"
)]
message_format_version: Option<String>,
}

impl TestReporterOpts {
Expand Down Expand Up @@ -1463,10 +1501,24 @@
let output = output_writer.reporter_output();
let profile = profile.apply_build_platforms(&build_platforms);

let structured_reporter = match reporter_opts.message_format {
MessageFormat::Human => structured::StructuredReporter::Disabled,
MessageFormat::LibtestJson | MessageFormat::LibtestJsonPlus => {
structured::StructuredReporter::Libtest(structured::LibtestReporter::new(
reporter_opts.message_format_version.as_deref(),
if matches!(reporter_opts.message_format, MessageFormat::LibtestJsonPlus) {
structured::EmitNextestObject::Yes
} else {
structured::EmitNextestObject::No
},
)?)
}
};

let mut reporter = reporter_opts
.to_builder(no_capture)
.set_verbose(self.base.output.verbose)
.build(&test_list, &profile, output);
.build(&test_list, &profile, output, structured_reporter);
if self
.base
.output
Expand Down
12 changes: 11 additions & 1 deletion cargo-nextest/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@
#[source]
err: std::io::Error,
},
#[error("message format version is not valid")]
InvalidMessageFormatVersion {
#[from]
err: FormatVersionError,
},
}

impl ExpectedError {
Expand Down Expand Up @@ -394,7 +399,8 @@
| Self::TestBinaryArgsParseError { .. }
| Self::DialoguerError { .. }
| Self::SignalHandlerSetupError { .. }
| Self::ShowTestGroupsError { .. } => NextestExitCode::SETUP_ERROR,
| Self::ShowTestGroupsError { .. }
| Self::InvalidMessageFormatVersion { .. } => NextestExitCode::SETUP_ERROR,

Check warning on line 403 in cargo-nextest/src/errors.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/errors.rs#L403

Added line #L403 was not covered by tests
Self::ConfigParseError { err } => {
// Experimental features not being enabled are their own error.
match err.kind() {
Expand Down Expand Up @@ -835,6 +841,10 @@
log::error!("[double-spawn] failed to exec `{command:?}`");
Some(err as &dyn Error)
}
Self::InvalidMessageFormatVersion { err } => {
log::error!("error parsing message format version`");
Some(err as &dyn Error)

Check warning on line 846 in cargo-nextest/src/errors.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/errors.rs#L844-L846

Added lines #L844 - L846 were not covered by tests
}
};

while let Some(err) = next_error {
Expand Down
1 change: 1 addition & 0 deletions nextest-runner/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,5 +1396,6 @@ mod self_update_errors {
}
}

pub use crate::reporter::structured::FormatVersionError;
#[cfg(feature = "self-update")]
pub use self_update_errors::*;
18 changes: 16 additions & 2 deletions nextest-runner/src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! The main structure in this module is [`TestReporter`].

mod aggregator;
pub mod structured;

use crate::{
config::{NextestProfile, ScriptId},
errors::WriteEventError,
Expand Down Expand Up @@ -161,6 +163,7 @@ pub struct TestReporterBuilder {
success_output: Option<TestOutputDisplay>,
status_level: Option<StatusLevel>,
final_status_level: Option<FinalStatusLevel>,

verbose: bool,
hide_progress_bar: bool,
}
Expand Down Expand Up @@ -220,6 +223,7 @@ impl TestReporterBuilder {
test_list: &TestList,
profile: &NextestProfile<'a>,
output: ReporterStderr<'a>,
structured_reporter: structured::StructuredReporter<'a>,
) -> TestReporter<'a> {
let styles = Box::default();
let binary_id_width = test_list
Expand Down Expand Up @@ -319,6 +323,7 @@ impl TestReporterBuilder {
final_outputs: DebugIgnore(vec![]),
},
stderr,
structured_reporter,
metadata_reporter: aggregator,
}
}
Expand All @@ -330,11 +335,15 @@ enum ReporterStderrImpl<'a> {
Buffer(&'a mut Vec<u8>),
}

/// Functionality to report test results to stderr and JUnit
/// Functionality to report test results to stderr, JUnit, and/or structured,
/// machine-readable results to stdout
pub struct TestReporter<'a> {
inner: TestReporterImpl<'a>,
stderr: ReporterStderrImpl<'a>,
/// Used to aggregate events for JUnit reports written to disk
metadata_reporter: EventAggregator<'a>,
/// Used to emit test events in machine-readable format(s) to stdout
structured_reporter: structured::StructuredReporter<'a>,
}

impl<'a> TestReporter<'a> {
Expand Down Expand Up @@ -383,6 +392,8 @@ impl<'a> TestReporter<'a> {
.map_err(WriteEventError::Io)?;
}
}

self.structured_reporter.write_event(&event)?;
self.metadata_reporter.write_event(event)?;
Ok(())
}
Expand Down Expand Up @@ -1808,7 +1819,9 @@ impl Styles {
#[cfg(test)]
mod tests {
use super::*;
use crate::{config::NextestConfig, platform::BuildPlatforms};
use crate::{
config::NextestConfig, platform::BuildPlatforms, reporter::structured::StructuredReporter,
};

#[test]
fn no_capture_settings() {
Expand All @@ -1830,6 +1843,7 @@ mod tests {
&test_list,
&profile.apply_build_platforms(&build_platforms),
output,
StructuredReporter::Disabled,
);
assert!(reporter.inner.no_capture, "no_capture is true");
assert_eq!(
Expand Down
70 changes: 70 additions & 0 deletions nextest-runner/src/reporter/structured.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Functionality for emitting structured, machine readable output in different
//! formats

mod libtest;

use super::*;
pub use libtest::{EmitNextestObject, LibtestReporter};

/// Error returned when a user-supplied format version fails to be parsed to a
/// valid and supported version
#[derive(Clone, Debug, thiserror::Error)]

Check warning on line 11 in nextest-runner/src/reporter/structured.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/structured.rs#L11

Added line #L11 was not covered by tests
#[error("invalid format version: {input}")]
pub struct FormatVersionError {
/// The input that failed to parse.
pub input: String,
/// The underlying error
#[source]
pub err: FormatVersionErrorInner,
}

/// The different errors that can occur when parsing and validating a format version
#[derive(Clone, Debug, thiserror::Error)]

Check warning on line 22 in nextest-runner/src/reporter/structured.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/structured.rs#L22

Added line #L22 was not covered by tests
pub enum FormatVersionErrorInner {
/// The input did not have a valid syntax
#[error("expected format version in form of `{expected}`")]
InvalidFormat {
/// The expected pseudo format
expected: &'static str,
},
/// A decimal integer was expected but could not be parsed
#[error("version component `{which}` could not be parsed as an integer")]
InvalidInteger {
/// Which component was invalid
which: &'static str,
/// The parse failure
#[source]
err: std::num::ParseIntError,
},
/// The version component was not within th expected range
#[error("version component `{which}` value {value} is out of range {range:?}")]
InvalidValue {
/// The component which was out of range
which: &'static str,
/// The value that was parsed
value: u8,
/// The range of valid values for the component
range: std::ops::Range<u8>,
},
}

/// A reporter for structured, machine readable, output based on the user's
/// preference
pub enum StructuredReporter<'a> {
/// Libtest compatible output
Libtest(LibtestReporter<'a>),
// TODO: make a custom format that is easier to consume than libtest's
//Json(json::JsonReporter<'a>),
/// Variant that doesn't actually emit anything
Disabled,
}

impl<'a> StructuredReporter<'a> {
#[inline]
pub(super) fn write_event(&mut self, event: &TestEvent<'a>) -> Result<(), WriteEventError> {
match self {
Self::Disabled => Ok(()),
Self::Libtest(ltr) => ltr.write_event(event),

Check warning on line 67 in nextest-runner/src/reporter/structured.rs

View check run for this annotation

Codecov / codecov/patch

nextest-runner/src/reporter/structured.rs#L67

Added line #L67 was not covered by tests
}
}
}
Loading