quick_junit/
proptest_impls.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Proptest `Arbitrary` implementations for quick-junit types.
5//!
6//! These implementations enable property-based testing of serialization and deserialization.
7
8use crate::{
9    FlakyOrRerun, NonSuccessKind, NonSuccessReruns, Property, Report, ReportUuid, TestCase,
10    TestCaseStatus, TestRerun, TestSuite, XmlString,
11};
12use chrono::{DateTime, FixedOffset};
13use proptest::{
14    arbitrary::Arbitrary,
15    collection, option,
16    prelude::*,
17    strategy::{BoxedStrategy, Map, Strategy},
18};
19use std::time::Duration;
20
21impl Arbitrary for XmlString {
22    type Parameters = <String as Arbitrary>::Parameters;
23    type Strategy = Map<<String as Arbitrary>::Strategy, fn(String) -> XmlString>;
24
25    fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
26        String::arbitrary_with(args).prop_map(|s| {
27            // Strip leading and trailing whitespace since XML isn't intended to
28            // preserve that.
29            XmlString::new(s.trim())
30        })
31    }
32}
33
34pub(crate) fn text_node_strategy() -> impl Strategy<Value = XmlString> {
35    any::<XmlString>().prop_filter("Non-empty string", |s| !s.is_empty())
36}
37
38/// Strategy for generating realistic test case names like "module::submodule::test_name"
39pub(crate) fn test_name_strategy() -> impl Strategy<Value = XmlString> {
40    // Generate alphanumeric identifier
41    let ident = "[a-z][a-z0-9_]{0,15}";
42
43    // Generate 1-4 segments joined by ::
44    collection::vec(ident, 1..=4).prop_map(|segments| XmlString::new(segments.join("::")))
45}
46
47/// Strategy for generating valid XML attribute names (alphanumeric, no special chars)
48pub(crate) fn xml_attr_name_strategy() -> impl Strategy<Value = XmlString> {
49    // XML attribute names: must start with letter or underscore, followed by letters, digits, hyphens, underscores, or periods
50    "[a-zA-Z_][a-zA-Z0-9_.-]{0,15}".prop_map(XmlString::new)
51}
52
53/// Strategy for generating arbitrary DateTime<FixedOffset>
54pub(crate) fn datetime_strategy() -> impl Strategy<Value = DateTime<FixedOffset>> {
55    // Generate timestamps within a reasonable range (2000-2100)
56    // to avoid edge cases with very old or very future dates
57    // Generate offsets in minute increments only (RFC 3339 doesn't preserve seconds in offsets)
58    (946684800i64..4102444800i64, -1440i32..1440i32).prop_map(|(secs, offset_minutes)| {
59        let offset_secs = offset_minutes * 60;
60        let offset =
61            FixedOffset::east_opt(offset_secs).unwrap_or(FixedOffset::east_opt(0).unwrap());
62        DateTime::from_timestamp(secs, 0)
63            .unwrap()
64            .with_timezone(&offset)
65    })
66}
67
68/// Strategy for generating arbitrary Duration
69pub(crate) fn duration_strategy() -> impl Strategy<Value = Duration> {
70    // Generate durations up to 1 hour, in milliseconds to avoid precision issues
71    (0u64..3_600_000u64).prop_map(Duration::from_millis)
72}
73
74/// Strategy for generating an IndexMap with XML attribute names as keys
75pub(crate) fn xml_attr_index_map_strategy(
76) -> impl Strategy<Value = indexmap::IndexMap<XmlString, XmlString>> {
77    collection::hash_map(xml_attr_name_strategy(), any::<XmlString>(), 0..3)
78        .prop_map(|hm| hm.into_iter().collect())
79}
80
81impl Arbitrary for NonSuccessReruns {
82    type Parameters = ();
83    type Strategy = BoxedStrategy<Self>;
84
85    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
86        (
87            any::<FlakyOrRerun>(),
88            collection::vec(any::<TestRerun>(), 0..5),
89        )
90            .prop_map(|(kind, runs)| {
91                // Normalize: empty runs always use Rerun, since the kind is
92                // unobservable in serialized XML when there are no elements.
93                let kind = if runs.is_empty() {
94                    FlakyOrRerun::Rerun
95                } else {
96                    kind
97                };
98                NonSuccessReruns { kind, runs }
99            })
100            .boxed()
101    }
102}
103
104impl Arbitrary for TestSuite {
105    type Parameters = ();
106    type Strategy = BoxedStrategy<Self>;
107
108    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
109        (
110            test_name_strategy(),
111            option::of(datetime_strategy()),
112            option::of(duration_strategy()),
113            collection::vec(any::<TestCase>(), 0..10),
114            collection::vec(any::<Property>(), 0..5),
115            any::<Option<XmlString>>(),
116            any::<Option<XmlString>>(),
117            collection::hash_map(xml_attr_name_strategy(), any::<XmlString>(), 0..5),
118        )
119            .prop_map(
120                |(name, timestamp, time, test_cases, properties, system_out, system_err, extra)| {
121                    // Compute counts from test_cases
122                    let tests = test_cases.len();
123                    let mut failures = 0;
124                    let mut errors = 0;
125                    let mut disabled = 0;
126
127                    for test_case in &test_cases {
128                        match &test_case.status {
129                            TestCaseStatus::Success { .. } => {}
130                            TestCaseStatus::NonSuccess { kind, .. } => match kind {
131                                NonSuccessKind::Failure => failures += 1,
132                                NonSuccessKind::Error => errors += 1,
133                            },
134                            TestCaseStatus::Skipped { .. } => disabled += 1,
135                        }
136                    }
137
138                    TestSuite {
139                        name,
140                        tests,
141                        disabled,
142                        errors,
143                        failures,
144                        timestamp,
145                        time,
146                        test_cases,
147                        properties,
148                        system_out,
149                        system_err,
150                        extra: extra.into_iter().collect(),
151                    }
152                },
153            )
154            .boxed()
155    }
156}
157
158impl Arbitrary for Report {
159    type Parameters = ();
160    type Strategy = BoxedStrategy<Self>;
161
162    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
163        (
164            test_name_strategy(),
165            any::<Option<ReportUuid>>(),
166            option::of(datetime_strategy()),
167            option::of(duration_strategy()),
168            collection::vec(any::<TestSuite>(), 0..5),
169        )
170            .prop_map(|(name, uuid, timestamp, time, test_suites)| {
171                // Compute counts from test_suites
172                let tests = test_suites.iter().map(|ts| ts.tests).sum();
173                let failures = test_suites.iter().map(|ts| ts.failures).sum();
174                let errors = test_suites.iter().map(|ts| ts.errors).sum();
175
176                Report {
177                    name,
178                    uuid,
179                    timestamp,
180                    time,
181                    tests,
182                    failures,
183                    errors,
184                    test_suites,
185                }
186            })
187            .boxed()
188    }
189}