Skip to main content

nextest_runner/config/elements/
junit.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::config::elements::ReportSkipPolicy;
5use camino::{Utf8Path, Utf8PathBuf};
6use serde::{Deserialize, Serialize};
7
8/// Controls how flaky-fail tests are reported in JUnit XML output.
9///
10/// Flaky-fail tests are tests that eventually passed on retry but are configured
11/// with `flaky-result = "fail"`. This setting controls whether they appear as
12/// failures or successes in the JUnit report.
13#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
14#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
15#[serde(rename_all = "kebab-case")]
16#[cfg_attr(test, derive(test_strategy::Arbitrary))]
17pub enum JunitFlakyFailStatus {
18    /// Report flaky-fail tests as failures with `<failure>` and
19    /// `<flakyFailure>` elements.
20    #[default]
21    Failure,
22
23    /// Report flaky-fail tests as successes, identical to flaky-pass tests.
24    Success,
25}
26
27/// Global JUnit configuration stored within a profile.
28///
29/// Returned by an [`EvaluatableProfile`](crate::config::core::EvaluatableProfile).
30#[derive(Clone, Debug)]
31pub struct JunitConfig<'cfg> {
32    path: Utf8PathBuf,
33    report_name: &'cfg str,
34    store_success_output: bool,
35    store_failure_output: bool,
36    report_skipped: ReportSkipPolicy,
37    flaky_fail_status: JunitFlakyFailStatus,
38}
39
40impl<'cfg> JunitConfig<'cfg> {
41    pub(in crate::config) fn new(
42        store_dir: &Utf8Path,
43        settings: JunitSettings<'cfg>,
44    ) -> Option<Self> {
45        let path = settings.path?;
46        Some(Self {
47            path: store_dir.join(path),
48            report_name: settings.report_name,
49            store_success_output: settings.store_success_output,
50            store_failure_output: settings.store_failure_output,
51            report_skipped: settings.report_skipped,
52            flaky_fail_status: settings.flaky_fail_status,
53        })
54    }
55
56    /// Returns the absolute path to the JUnit report.
57    pub fn path(&self) -> &Utf8Path {
58        &self.path
59    }
60
61    /// Returns the name of the JUnit report.
62    pub fn report_name(&self) -> &'cfg str {
63        self.report_name
64    }
65
66    /// Returns true if success output should be stored.
67    pub fn store_success_output(&self) -> bool {
68        self.store_success_output
69    }
70
71    /// Returns true if failure output should be stored.
72    pub fn store_failure_output(&self) -> bool {
73        self.store_failure_output
74    }
75
76    /// Returns the policy controlling which skipped tests should be emitted as
77    /// `<testcase>` elements with a `<skipped>` child.
78    pub fn report_skipped(&self) -> ReportSkipPolicy {
79        self.report_skipped
80    }
81
82    /// Returns the flaky-fail status for JUnit reporting.
83    pub fn flaky_fail_status(&self) -> JunitFlakyFailStatus {
84        self.flaky_fail_status
85    }
86
87    /// Creates a `JunitConfig` directly for unit tests, bypassing the profile
88    /// inheritance chain.
89    #[cfg(test)]
90    pub(crate) fn new_for_test(
91        path: Utf8PathBuf,
92        report_name: &'cfg str,
93        report_skipped: ReportSkipPolicy,
94    ) -> Self {
95        Self {
96            path,
97            report_name,
98            store_success_output: false,
99            store_failure_output: false,
100            report_skipped,
101            flaky_fail_status: JunitFlakyFailStatus::Failure,
102        }
103    }
104}
105
106/// Pre-resolved JUnit settings from the profile inheritance chain.
107#[derive(Clone, Debug)]
108pub(in crate::config) struct JunitSettings<'cfg> {
109    pub(in crate::config) path: Option<&'cfg Utf8Path>,
110    pub(in crate::config) report_name: &'cfg str,
111    pub(in crate::config) store_success_output: bool,
112    pub(in crate::config) store_failure_output: bool,
113    pub(in crate::config) report_skipped: ReportSkipPolicy,
114    pub(in crate::config) flaky_fail_status: JunitFlakyFailStatus,
115}
116
117#[derive(Clone, Debug)]
118pub(in crate::config) struct DefaultJunitImpl {
119    pub(in crate::config) path: Option<Utf8PathBuf>,
120    pub(in crate::config) report_name: String,
121    pub(in crate::config) store_success_output: bool,
122    pub(in crate::config) store_failure_output: bool,
123    pub(in crate::config) report_skipped: ReportSkipPolicy,
124    pub(in crate::config) flaky_fail_status: JunitFlakyFailStatus,
125}
126
127impl DefaultJunitImpl {
128    // Default values have all fields defined on them.
129    pub(crate) fn for_default_profile(data: JunitImpl) -> Self {
130        DefaultJunitImpl {
131            path: data.path,
132            report_name: data
133                .report_name
134                .expect("junit.report present in default profile"),
135            store_success_output: data
136                .store_success_output
137                .expect("junit.store-success-output present in default profile"),
138            store_failure_output: data
139                .store_failure_output
140                .expect("junit.store-failure-output present in default profile"),
141            report_skipped: data
142                .report_skipped
143                .expect("junit.report-skipped present in default profile"),
144            flaky_fail_status: data
145                .flaky_fail_status
146                .expect("junit.flaky-fail-status present in default profile"),
147        }
148    }
149}
150
151#[derive(Clone, Debug, Default, Deserialize)]
152#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
153#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
154#[serde(rename_all = "kebab-case")]
155pub(in crate::config) struct JunitImpl {
156    /// Path to write the JUnit XML report to. If unset, JUnit reporting is
157    /// disabled.
158    #[serde(default)]
159    #[cfg_attr(
160        feature = "config-schema",
161        schemars(schema_with = "String::json_schema")
162    )]
163    pub(in crate::config) path: Option<Utf8PathBuf>,
164    /// Name for the JUnit XML report.
165    #[serde(default)]
166    pub(in crate::config) report_name: Option<String>,
167    /// Whether to store successful test output in the JUnit XML report.
168    #[serde(default)]
169    pub(in crate::config) store_success_output: Option<bool>,
170    /// Whether to store failed test output in the JUnit XML report.
171    #[serde(default)]
172    pub(in crate::config) store_failure_output: Option<bool>,
173    /// Which skipped tests to emit as `<testcase>` elements with a `<skipped>`
174    /// child in the JUnit XML report.
175    #[serde(default)]
176    pub(in crate::config) report_skipped: Option<ReportSkipPolicy>,
177    /// How flaky-fail tests are reported in the JUnit XML report.
178    #[serde(default)]
179    pub(in crate::config) flaky_fail_status: Option<JunitFlakyFailStatus>,
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::config::{core::NextestConfig, elements::ReportSkipPolicy, utils::test_helpers::*};
185    use camino_tempfile::tempdir;
186    use indoc::indoc;
187    use nextest_filtering::ParseContext;
188
189    fn report_skipped_for(config_contents: &str, profile: &str) -> ReportSkipPolicy {
190        let workspace_dir = tempdir().unwrap();
191        let graph = temp_workspace(&workspace_dir, config_contents);
192        let pcx = ParseContext::new(&graph);
193        let nextest_config = NextestConfig::from_sources(
194            graph.workspace().root(),
195            &pcx,
196            None,
197            &[][..],
198            &Default::default(),
199        )
200        .expect("config file should parse");
201
202        nextest_config
203            .profile(profile)
204            .expect("profile should exist")
205            .apply_build_platforms(&build_platforms())
206            .junit()
207            .expect("junit config should be present")
208            .report_skipped()
209    }
210
211    #[test]
212    fn report_skipped_defaults_to_none() {
213        // When only a path is set, report-skipped must default to "none" to keep
214        // machine-readable output stable.
215        let config = indoc! {r#"
216            [profile.default.junit]
217            path = "junit.xml"
218        "#};
219        assert_eq!(
220            report_skipped_for(config, "default"),
221            ReportSkipPolicy::None
222        );
223    }
224
225    #[test]
226    fn report_skipped_can_be_set_to_ignored() {
227        let config = indoc! {r#"
228            [profile.default.junit]
229            path = "junit.xml"
230            report-skipped = "ignored"
231        "#};
232        assert_eq!(
233            report_skipped_for(config, "default"),
234            ReportSkipPolicy::Ignored
235        );
236    }
237
238    #[test]
239    fn report_skipped_can_be_set_to_all() {
240        let config = indoc! {r#"
241            [profile.default.junit]
242            path = "junit.xml"
243            report-skipped = "all"
244        "#};
245        assert_eq!(report_skipped_for(config, "default"), ReportSkipPolicy::All);
246    }
247}