1use serde::{Deserialize, Serialize};
5use std::{cmp::Ordering, fmt, time::Duration};
6
7#[derive(Debug, Copy, Clone, PartialEq, Eq)]
10pub enum RetryPolicy {
11 Fixed {
13 count: u32,
15
16 delay: Duration,
18
19 jitter: bool,
21 },
22
23 Exponential {
25 count: u32,
27
28 delay: Duration,
30
31 jitter: bool,
33
34 max_delay: Option<Duration>,
36 },
37}
38
39impl Default for RetryPolicy {
40 #[inline]
41 fn default() -> Self {
42 Self::new_without_delay(0)
43 }
44}
45
46impl RetryPolicy {
47 pub fn new_without_delay(count: u32) -> Self {
49 Self::Fixed {
50 count,
51 delay: Duration::ZERO,
52 jitter: false,
53 }
54 }
55
56 pub fn count(&self) -> u32 {
58 match self {
59 Self::Fixed { count, .. } | Self::Exponential { count, .. } => *count,
60 }
61 }
62}
63
64#[cfg(feature = "config-schema")]
65impl schemars::JsonSchema for RetryPolicy {
66 fn schema_name() -> std::borrow::Cow<'static, str> {
67 "RetryPolicy".into()
68 }
69
70 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
71 schemars::json_schema!({
72 "title": "RetryPolicy",
73 "oneOf": [
74 { "type": "integer", "minimum": 0 },
75 {
76 "type": "object",
77 "properties": {
78 "backoff": { "type": "string", "const": "fixed" },
79 "count": { "type": "integer", "minimum": 0 },
80 "delay": generator.subschema_for::<String>(),
81 "jitter": generator.subschema_for::<bool>(),
82 },
83 "required": ["backoff", "count"],
84 "additionalProperties": false,
85 },
86 {
87 "type": "object",
88 "properties": {
89 "backoff": { "type": "string", "const": "exponential" },
90 "count": { "type": "integer", "minimum": 0 },
91 "delay": generator.subschema_for::<String>(),
92 "jitter": generator.subschema_for::<bool>(),
93 "max-delay": generator.subschema_for::<String>(),
94 },
95 "required": ["backoff", "count", "delay"],
96 "additionalProperties": false,
97 }
98 ]
99 })
100 }
101}
102
103#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
105#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
106#[serde(rename_all = "kebab-case")]
107#[cfg_attr(test, derive(test_strategy::Arbitrary))]
108pub enum FlakyResult {
109 Fail,
111
112 #[default]
114 Pass,
115}
116
117impl FlakyResult {
118 pub fn fail_message(self, attempt: u32, total_attempts: u32) -> Option<String> {
124 match self {
125 Self::Fail => Some(format!(
126 "test passed on attempt {attempt}/{total_attempts} \
127 but is configured to fail when flaky",
128 )),
129 Self::Pass => None,
130 }
131 }
132}
133
134#[derive(Debug, Copy, Clone, Deserialize)]
137#[serde(tag = "backoff", rename_all = "kebab-case", deny_unknown_fields)]
138enum RetryPolicySerde {
139 #[serde(rename_all = "kebab-case")]
140 Fixed {
141 count: u32,
142 #[serde(default, with = "humantime_serde")]
143 delay: Duration,
144 #[serde(default)]
145 jitter: bool,
146 },
147 #[serde(rename_all = "kebab-case")]
148 Exponential {
149 count: u32,
150 #[serde(with = "humantime_serde")]
151 delay: Duration,
152 #[serde(default)]
153 jitter: bool,
154 #[serde(default, with = "humantime_serde")]
155 max_delay: Option<Duration>,
156 },
157}
158
159impl RetryPolicySerde {
160 fn into_policy(self) -> RetryPolicy {
161 match self {
162 RetryPolicySerde::Fixed {
163 count,
164 delay,
165 jitter,
166 } => RetryPolicy::Fixed {
167 count,
168 delay,
169 jitter,
170 },
171 RetryPolicySerde::Exponential {
172 count,
173 delay,
174 jitter,
175 max_delay,
176 } => RetryPolicy::Exponential {
177 count,
178 delay,
179 jitter,
180 max_delay,
181 },
182 }
183 }
184}
185
186pub(in crate::config) fn deserialize_retry_policy<'de, D>(
187 deserializer: D,
188) -> Result<Option<RetryPolicy>, D::Error>
189where
190 D: serde::Deserializer<'de>,
191{
192 struct V;
193
194 impl<'de2> serde::de::Visitor<'de2> for V {
195 type Value = Option<RetryPolicy>;
196
197 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
198 write!(
199 formatter,
200 "a table ({{ backoff = \"fixed\", count = 5 }}) or a number (5)"
201 )
202 }
203
204 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
206 where
207 E: serde::de::Error,
208 {
209 match v.cmp(&0) {
210 Ordering::Greater | Ordering::Equal => {
211 let v = u32::try_from(v).map_err(|_| {
212 serde::de::Error::invalid_value(
213 serde::de::Unexpected::Signed(v),
214 &"a positive u32",
215 )
216 })?;
217 Ok(Some(RetryPolicy::new_without_delay(v)))
218 }
219 Ordering::Less => Err(serde::de::Error::invalid_value(
220 serde::de::Unexpected::Signed(v),
221 &self,
222 )),
223 }
224 }
225
226 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
227 where
228 A: serde::de::MapAccess<'de2>,
229 {
230 RetryPolicySerde::deserialize(serde::de::value::MapAccessDeserializer::new(map))
231 .map(|s| Some(s.into_policy()))
232 }
233 }
234
235 let policy = deserializer.deserialize_any(V)?;
237 match &policy {
238 Some(RetryPolicy::Fixed {
239 count: _,
240 delay,
241 jitter,
242 })
243 if delay.is_zero() && *jitter =>
245 {
246 return Err(serde::de::Error::custom(
247 "`jitter` cannot be true if `delay` isn't specified or is zero",
248 ));
249 }
250 Some(RetryPolicy::Fixed { .. }) => {}
251 Some(RetryPolicy::Exponential {
252 count,
253 delay,
254 jitter: _,
255 max_delay,
256 }) => {
257 if *count == 0 {
259 return Err(serde::de::Error::custom(
260 "`count` cannot be zero with exponential backoff",
261 ));
262 }
263 if delay.is_zero() {
265 return Err(serde::de::Error::custom(
266 "`delay` cannot be zero with exponential backoff",
267 ));
268 }
269 if max_delay.is_some_and(|f| f.is_zero()) {
271 return Err(serde::de::Error::custom(
272 "`max-delay` cannot be zero with exponential backoff",
273 ));
274 }
275 if max_delay.is_some_and(|max_delay| max_delay < *delay) {
277 return Err(serde::de::Error::custom(
278 "`max-delay` cannot be less than delay with exponential backoff",
279 ));
280 }
281 }
282 None => {}
283 }
284
285 Ok(policy)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::{
292 config::{core::NextestConfig, utils::test_helpers::*},
293 errors::ConfigParseErrorKind,
294 run_mode::NextestRunMode,
295 };
296 use camino_tempfile::tempdir;
297 use config::ConfigError;
298 use guppy::graph::cargo::BuildPlatform;
299 use indoc::indoc;
300 use nextest_filtering::{ParseContext, TestQuery};
301 use nextest_metadata::TestCaseName;
302 use test_case::test_case;
303
304 #[test]
305 fn parse_retries_valid() {
306 let config_contents = indoc! {r#"
307 [profile.default]
308 retries = { backoff = "fixed", count = 3 }
309
310 [profile.no-retries]
311 retries = 0
312
313 [profile.fixed-with-delay]
314 retries = { backoff = "fixed", count = 3, delay = "1s" }
315
316 [profile.exp]
317 retries = { backoff = "exponential", count = 4, delay = "2s" }
318
319 [profile.exp-with-max-delay]
320 retries = { backoff = "exponential", count = 5, delay = "3s", max-delay = "10s" }
321
322 [profile.exp-with-max-delay-and-jitter]
323 retries = { backoff = "exponential", count = 6, delay = "4s", max-delay = "1m", jitter = true }
324
325 [profile.with-flaky-result-fail]
326 retries = { backoff = "fixed", count = 2 }
327 flaky-result = "fail"
328
329 [profile.with-flaky-result-pass]
330 retries = { backoff = "fixed", count = 2 }
331 flaky-result = "pass"
332
333 [profile.exp-with-flaky-result-fail]
334 retries = { backoff = "exponential", count = 3, delay = "1s" }
335 flaky-result = "fail"
336
337 [profile.flaky-result-only]
338 flaky-result = "fail"
339 "#};
340
341 let workspace_dir = tempdir().unwrap();
342
343 let graph = temp_workspace(&workspace_dir, config_contents);
344 let pcx = ParseContext::new(&graph);
345
346 let config = NextestConfig::from_sources(
347 graph.workspace().root(),
348 &pcx,
349 None,
350 [],
351 &Default::default(),
352 )
353 .expect("config is valid");
354
355 let default_profile = config
356 .profile("default")
357 .expect("default profile exists")
358 .apply_build_platforms(&build_platforms());
359 assert_eq!(
360 default_profile.retries(),
361 RetryPolicy::Fixed {
362 count: 3,
363 delay: Duration::ZERO,
364 jitter: false,
365 },
366 "default retries matches"
367 );
368 assert_eq!(
369 default_profile.flaky_result(),
370 FlakyResult::Pass,
371 "default flaky_result matches"
372 );
373
374 assert_eq!(
375 config
376 .profile("no-retries")
377 .expect("profile exists")
378 .apply_build_platforms(&build_platforms())
379 .retries(),
380 RetryPolicy::new_without_delay(0),
381 "no-retries retries matches"
382 );
383
384 assert_eq!(
385 config
386 .profile("fixed-with-delay")
387 .expect("profile exists")
388 .apply_build_platforms(&build_platforms())
389 .retries(),
390 RetryPolicy::Fixed {
391 count: 3,
392 delay: Duration::from_secs(1),
393 jitter: false,
394 },
395 "fixed-with-delay retries matches"
396 );
397
398 assert_eq!(
399 config
400 .profile("exp")
401 .expect("profile exists")
402 .apply_build_platforms(&build_platforms())
403 .retries(),
404 RetryPolicy::Exponential {
405 count: 4,
406 delay: Duration::from_secs(2),
407 jitter: false,
408 max_delay: None,
409 },
410 "exp retries matches"
411 );
412
413 assert_eq!(
414 config
415 .profile("exp-with-max-delay")
416 .expect("profile exists")
417 .apply_build_platforms(&build_platforms())
418 .retries(),
419 RetryPolicy::Exponential {
420 count: 5,
421 delay: Duration::from_secs(3),
422 jitter: false,
423 max_delay: Some(Duration::from_secs(10)),
424 },
425 "exp-with-max-delay retries matches"
426 );
427
428 assert_eq!(
429 config
430 .profile("exp-with-max-delay-and-jitter")
431 .expect("profile exists")
432 .apply_build_platforms(&build_platforms())
433 .retries(),
434 RetryPolicy::Exponential {
435 count: 6,
436 delay: Duration::from_secs(4),
437 jitter: true,
438 max_delay: Some(Duration::from_secs(60)),
439 },
440 "exp-with-max-delay-and-jitter retries matches"
441 );
442
443 let with_flaky_result_fail = config
444 .profile("with-flaky-result-fail")
445 .expect("profile exists")
446 .apply_build_platforms(&build_platforms());
447 assert_eq!(
448 with_flaky_result_fail.retries(),
449 RetryPolicy::new_without_delay(2),
450 "with-flaky-result-fail retries matches"
451 );
452 assert_eq!(
453 with_flaky_result_fail.flaky_result(),
454 FlakyResult::Fail,
455 "with-flaky-result-fail flaky_result matches"
456 );
457
458 let with_flaky_result_pass = config
459 .profile("with-flaky-result-pass")
460 .expect("profile exists")
461 .apply_build_platforms(&build_platforms());
462 assert_eq!(
463 with_flaky_result_pass.retries(),
464 RetryPolicy::new_without_delay(2),
465 "with-flaky-result-pass retries matches"
466 );
467 assert_eq!(
468 with_flaky_result_pass.flaky_result(),
469 FlakyResult::Pass,
470 "with-flaky-result-pass flaky_result matches"
471 );
472
473 let exp_with_flaky_result_fail = config
474 .profile("exp-with-flaky-result-fail")
475 .expect("profile exists")
476 .apply_build_platforms(&build_platforms());
477 assert_eq!(
478 exp_with_flaky_result_fail.retries(),
479 RetryPolicy::Exponential {
480 count: 3,
481 delay: Duration::from_secs(1),
482 jitter: false,
483 max_delay: None,
484 },
485 "exp-with-flaky-result-fail retries matches"
486 );
487 assert_eq!(
488 exp_with_flaky_result_fail.flaky_result(),
489 FlakyResult::Fail,
490 "exp-with-flaky-result-fail flaky_result matches"
491 );
492
493 let flaky_result_only = config
496 .profile("flaky-result-only")
497 .expect("profile exists")
498 .apply_build_platforms(&build_platforms());
499 assert_eq!(
500 flaky_result_only.retries(),
501 RetryPolicy::Fixed {
502 count: 3,
503 delay: Duration::ZERO,
504 jitter: false,
505 },
506 "flaky-result-only retries inherited from default"
507 );
508 assert_eq!(
509 flaky_result_only.flaky_result(),
510 FlakyResult::Fail,
511 "flaky-result-only flaky_result matches"
512 );
513 }
514
515 #[test_case(
516 indoc!{r#"
517 [profile.default]
518 retries = { backoff = "foo" }
519 "#},
520 ConfigErrorKind::Message,
521 "unknown variant `foo`, expected `fixed` or `exponential`"
522 ; "invalid value for backoff")]
523 #[test_case(
524 indoc!{r#"
525 [profile.default]
526 retries = { backoff = "fixed" }
527 "#},
528 ConfigErrorKind::NotFound,
529 "profile.default.retries.count"
530 ; "fixed specified without count")]
531 #[test_case(
532 indoc!{r#"
533 [profile.default]
534 retries = { backoff = "fixed", count = 1, delay = "foobar" }
535 "#},
536 ConfigErrorKind::Message,
537 "invalid value: string \"foobar\", expected a duration"
538 ; "delay is not a valid duration")]
539 #[test_case(
540 indoc!{r#"
541 [profile.default]
542 retries = { backoff = "fixed", count = 1, jitter = true }
543 "#},
544 ConfigErrorKind::Message,
545 "`jitter` cannot be true if `delay` isn't specified or is zero"
546 ; "jitter specified without delay")]
547 #[test_case(
548 indoc!{r#"
549 [profile.default]
550 retries = { backoff = "fixed", count = 1, max-delay = "10s" }
551 "#},
552 ConfigErrorKind::Message,
553 "unknown field `max-delay`, expected one of `count`, `delay`, `jitter`"
554 ; "max-delay is incompatible with fixed backoff")]
555 #[test_case(
556 indoc!{r#"
557 [profile.default]
558 retries = { backoff = "exponential", count = 1 }
559 "#},
560 ConfigErrorKind::NotFound,
561 "profile.default.retries.delay"
562 ; "exponential backoff must specify delay")]
563 #[test_case(
564 indoc!{r#"
565 [profile.default]
566 retries = { backoff = "exponential", delay = "1s" }
567 "#},
568 ConfigErrorKind::NotFound,
569 "profile.default.retries.count"
570 ; "exponential backoff must specify count")]
571 #[test_case(
572 indoc!{r#"
573 [profile.default]
574 retries = { backoff = "exponential", count = 0, delay = "1s" }
575 "#},
576 ConfigErrorKind::Message,
577 "`count` cannot be zero with exponential backoff"
578 ; "exponential backoff must have a non-zero count")]
579 #[test_case(
580 indoc!{r#"
581 [profile.default]
582 retries = { backoff = "exponential", count = 1, delay = "0s" }
583 "#},
584 ConfigErrorKind::Message,
585 "`delay` cannot be zero with exponential backoff"
586 ; "exponential backoff must have a non-zero delay")]
587 #[test_case(
588 indoc!{r#"
589 [profile.default]
590 retries = { backoff = "exponential", count = 1, delay = "1s", max-delay = "0s" }
591 "#},
592 ConfigErrorKind::Message,
593 "`max-delay` cannot be zero with exponential backoff"
594 ; "exponential backoff must have a non-zero max delay")]
595 #[test_case(
596 indoc!{r#"
597 [profile.default]
598 retries = { backoff = "exponential", count = 1, delay = "4s", max-delay = "2s", jitter = true }
599 "#},
600 ConfigErrorKind::Message,
601 "`max-delay` cannot be less than delay"
602 ; "max-delay greater than delay")]
603 #[test_case(
604 indoc!{r#"
605 [profile.default]
606 flaky-result = "unknown"
607 "#},
608 ConfigErrorKind::Message,
609 "enum FlakyResult does not have variant constructor unknown"
610 ; "invalid flaky-result value")]
611 fn parse_retries_invalid(
612 config_contents: &str,
613 expected_kind: ConfigErrorKind,
614 expected_message: &str,
615 ) {
616 let workspace_dir = tempdir().unwrap();
617
618 let graph = temp_workspace(&workspace_dir, config_contents);
619 let pcx = ParseContext::new(&graph);
620
621 let config_err = NextestConfig::from_sources(
622 graph.workspace().root(),
623 &pcx,
624 None,
625 [],
626 &Default::default(),
627 )
628 .expect_err("config expected to be invalid");
629
630 let message = match config_err.kind() {
631 ConfigParseErrorKind::DeserializeError(path_error) => {
632 match (path_error.inner(), expected_kind) {
633 (ConfigError::Message(message), ConfigErrorKind::Message) => message,
634 (ConfigError::NotFound(message), ConfigErrorKind::NotFound) => message,
635 (other, expected) => {
636 panic!(
637 "for config error {config_err:?}, expected \
638 ConfigErrorKind::{expected:?} for inner error {other:?}"
639 );
640 }
641 }
642 }
643 other => {
644 panic!(
645 "for config error {other:?}, expected ConfigParseErrorKind::DeserializeError"
646 );
647 }
648 };
649
650 assert!(
651 message.contains(expected_message),
652 "expected message \"{message}\" to contain \"{expected_message}\""
653 );
654 }
655
656 #[test_case(
657 indoc! {r#"
658 [[profile.default.overrides]]
659 filter = "test(=my_test)"
660 retries = 2
661
662 [profile.ci]
663 "#},
664 BuildPlatform::Target,
665 RetryPolicy::new_without_delay(2)
666
667 ; "my_test matches exactly"
668 )]
669 #[test_case(
670 indoc! {r#"
671 [[profile.default.overrides]]
672 filter = "!test(=my_test)"
673 retries = 2
674
675 [profile.ci]
676 "#},
677 BuildPlatform::Target,
678 RetryPolicy::new_without_delay(0)
679
680 ; "not match"
681 )]
682 #[test_case(
683 indoc! {r#"
684 [[profile.default.overrides]]
685 filter = "test(=my_test)"
686
687 [profile.ci]
688 "#},
689 BuildPlatform::Target,
690 RetryPolicy::new_without_delay(0)
691
692 ; "no retries specified"
693 )]
694 #[test_case(
695 indoc! {r#"
696 [[profile.default.overrides]]
697 filter = "test(test)"
698 retries = 2
699
700 [[profile.default.overrides]]
701 filter = "test(=my_test)"
702 retries = 3
703
704 [profile.ci]
705 "#},
706 BuildPlatform::Target,
707 RetryPolicy::new_without_delay(2)
708
709 ; "earlier configs override later ones"
710 )]
711 #[test_case(
712 indoc! {r#"
713 [[profile.default.overrides]]
714 filter = "test(test)"
715 retries = 2
716
717 [profile.ci]
718
719 [[profile.ci.overrides]]
720 filter = "test(=my_test)"
721 retries = 3
722 "#},
723 BuildPlatform::Target,
724 RetryPolicy::new_without_delay(3)
725
726 ; "profile-specific configs override default ones"
727 )]
728 #[test_case(
729 indoc! {r#"
730 [[profile.default.overrides]]
731 filter = "(!package(test-package)) and test(test)"
732 retries = 2
733
734 [profile.ci]
735
736 [[profile.ci.overrides]]
737 filter = "!test(=my_test_2)"
738 retries = 3
739 "#},
740 BuildPlatform::Target,
741 RetryPolicy::new_without_delay(3)
742
743 ; "no overrides match my_test exactly"
744 )]
745 #[test_case(
746 indoc! {r#"
747 [[profile.default.overrides]]
748 platform = "x86_64-unknown-linux-gnu"
749 filter = "test(test)"
750 retries = 2
751
752 [[profile.default.overrides]]
753 filter = "test(=my_test)"
754 retries = 3
755
756 [profile.ci]
757 "#},
758 BuildPlatform::Host,
759 RetryPolicy::new_without_delay(2)
760
761 ; "earlier config applied because it matches host triple"
762 )]
763 #[test_case(
764 indoc! {r#"
765 [[profile.default.overrides]]
766 platform = "aarch64-apple-darwin"
767 filter = "test(test)"
768 retries = 2
769
770 [[profile.default.overrides]]
771 filter = "test(=my_test)"
772 retries = 3
773
774 [profile.ci]
775 "#},
776 BuildPlatform::Host,
777 RetryPolicy::new_without_delay(3)
778
779 ; "earlier config ignored because it doesn't match host triple"
780 )]
781 #[test_case(
782 indoc! {r#"
783 [[profile.default.overrides]]
784 platform = "aarch64-apple-darwin"
785 filter = "test(test)"
786 retries = 2
787
788 [[profile.default.overrides]]
789 filter = "test(=my_test)"
790 retries = 3
791
792 [profile.ci]
793 "#},
794 BuildPlatform::Target,
795 RetryPolicy::new_without_delay(2)
796
797 ; "earlier config applied because it matches target triple"
798 )]
799 #[test_case(
800 indoc! {r#"
801 [[profile.default.overrides]]
802 platform = "x86_64-unknown-linux-gnu"
803 filter = "test(test)"
804 retries = 2
805
806 [[profile.default.overrides]]
807 filter = "test(=my_test)"
808 retries = 3
809
810 [profile.ci]
811 "#},
812 BuildPlatform::Target,
813 RetryPolicy::new_without_delay(3)
814
815 ; "earlier config ignored because it doesn't match target triple"
816 )]
817 #[test_case(
818 indoc! {r#"
819 [[profile.default.overrides]]
820 platform = 'cfg(target_os = "macos")'
821 filter = "test(test)"
822 retries = 2
823
824 [[profile.default.overrides]]
825 filter = "test(=my_test)"
826 retries = 3
827
828 [profile.ci]
829 "#},
830 BuildPlatform::Target,
831 RetryPolicy::new_without_delay(2)
832
833 ; "earlier config applied because it matches target cfg expr"
834 )]
835 #[test_case(
836 indoc! {r#"
837 [[profile.default.overrides]]
838 platform = 'cfg(target_arch = "x86_64")'
839 filter = "test(test)"
840 retries = 2
841
842 [[profile.default.overrides]]
843 filter = "test(=my_test)"
844 retries = 3
845
846 [profile.ci]
847 "#},
848 BuildPlatform::Target,
849 RetryPolicy::new_without_delay(3)
850
851 ; "earlier config ignored because it doesn't match target cfg expr"
852 )]
853 fn overrides_retries(
854 config_contents: &str,
855 build_platform: BuildPlatform,
856 retries: RetryPolicy,
857 ) {
858 let workspace_dir = tempdir().unwrap();
859
860 let graph = temp_workspace(&workspace_dir, config_contents);
861 let package_id = graph.workspace().iter().next().unwrap().id();
862 let pcx = ParseContext::new(&graph);
863
864 let config = NextestConfig::from_sources(
865 graph.workspace().root(),
866 &pcx,
867 None,
868 &[][..],
869 &Default::default(),
870 )
871 .unwrap();
872 let binary_query = binary_query(&graph, package_id, "lib", "my-binary", build_platform);
873 let test_name = TestCaseName::new("my_test");
874 let query = TestQuery {
875 binary_query: binary_query.to_query(),
876 test_name: &test_name,
877 };
878 let profile = config
879 .profile("ci")
880 .expect("ci profile is defined")
881 .apply_build_platforms(&build_platforms());
882 let settings_for = profile.settings_for(NextestRunMode::Test, &query);
883 assert_eq!(
884 settings_for.retries(),
885 retries,
886 "actual retries don't match expected retries"
887 );
888 }
889
890 #[test]
891 fn overrides_flaky_result() {
892 let config_contents = indoc! {r#"
893 [[profile.default.overrides]]
894 filter = "test(=my_test)"
895 retries = { backoff = "fixed", count = 3 }
896 flaky-result = "fail"
897
898 [[profile.default.overrides]]
899 filter = "test(=other_test)"
900 retries = 2
901
902 [profile.ci]
903 "#};
904 let workspace_dir = tempdir().unwrap();
905
906 let graph = temp_workspace(&workspace_dir, config_contents);
907 let package_id = graph.workspace().iter().next().unwrap().id();
908 let pcx = ParseContext::new(&graph);
909
910 let config = NextestConfig::from_sources(
911 graph.workspace().root(),
912 &pcx,
913 None,
914 &[][..],
915 &Default::default(),
916 )
917 .unwrap();
918
919 let profile = config
920 .profile("ci")
921 .expect("ci profile is defined")
922 .apply_build_platforms(&build_platforms());
923
924 let binary_query = binary_query(
926 &graph,
927 package_id,
928 "lib",
929 "my-binary",
930 BuildPlatform::Target,
931 );
932 let test_name = TestCaseName::new("my_test");
933 let query = TestQuery {
934 binary_query: binary_query.to_query(),
935 test_name: &test_name,
936 };
937 let settings = profile.settings_for(NextestRunMode::Test, &query);
938 assert_eq!(
939 settings.flaky_result(),
940 FlakyResult::Fail,
941 "my_test flaky_result is fail"
942 );
943
944 let test_name = TestCaseName::new("other_test");
947 let query = TestQuery {
948 binary_query: binary_query.to_query(),
949 test_name: &test_name,
950 };
951 let settings = profile.settings_for(NextestRunMode::Test, &query);
952 assert_eq!(
953 settings.flaky_result(),
954 FlakyResult::Pass,
955 "other_test flaky_result defaults to pass"
956 );
957 }
958
959 #[test]
963 fn overrides_flaky_result_independent_resolution() {
964 let config_contents = indoc! {r#"
965 # Override 1: sets retries count only.
966 [[profile.default.overrides]]
967 filter = "test(=my_test)"
968 retries = 5
969
970 # Override 2: sets retries with flaky-result = "fail".
971 [[profile.default.overrides]]
972 filter = "all()"
973 retries = { backoff = "fixed", count = 2 }
974 flaky-result = "fail"
975
976 [profile.ci]
977 "#};
978 let workspace_dir = tempdir().unwrap();
979
980 let graph = temp_workspace(&workspace_dir, config_contents);
981 let package_id = graph.workspace().iter().next().unwrap().id();
982 let pcx = ParseContext::new(&graph);
983
984 let config = NextestConfig::from_sources(
985 graph.workspace().root(),
986 &pcx,
987 None,
988 &[][..],
989 &Default::default(),
990 )
991 .unwrap();
992
993 let profile = config
994 .profile("ci")
995 .expect("ci profile is defined")
996 .apply_build_platforms(&build_platforms());
997
998 let binary_query = binary_query(
999 &graph,
1000 package_id,
1001 "lib",
1002 "my-binary",
1003 BuildPlatform::Target,
1004 );
1005 let test_name = TestCaseName::new("my_test");
1006 let query = TestQuery {
1007 binary_query: binary_query.to_query(),
1008 test_name: &test_name,
1009 };
1010 let settings = profile.settings_for(NextestRunMode::Test, &query);
1011
1012 assert_eq!(
1014 settings.retries(),
1015 RetryPolicy::new_without_delay(5),
1016 "retries count from first override"
1017 );
1018 assert_eq!(
1020 settings.flaky_result(),
1021 FlakyResult::Fail,
1022 "flaky_result from second override"
1023 );
1024 }
1025
1026 #[test]
1029 fn overrides_flaky_result_only() {
1030 let config_contents = indoc! {r#"
1031 # Override 1: sets only flaky-result, no retry policy.
1032 [[profile.default.overrides]]
1033 filter = "test(=my_test)"
1034 flaky-result = "fail"
1035
1036 # Override 2: sets retries count for all tests.
1037 [[profile.default.overrides]]
1038 filter = "all()"
1039 retries = 3
1040
1041 [profile.ci]
1042 "#};
1043 let workspace_dir = tempdir().unwrap();
1044
1045 let graph = temp_workspace(&workspace_dir, config_contents);
1046 let package_id = graph.workspace().iter().next().unwrap().id();
1047 let pcx = ParseContext::new(&graph);
1048
1049 let config = NextestConfig::from_sources(
1050 graph.workspace().root(),
1051 &pcx,
1052 None,
1053 &[][..],
1054 &Default::default(),
1055 )
1056 .unwrap();
1057
1058 let profile = config
1059 .profile("ci")
1060 .expect("ci profile is defined")
1061 .apply_build_platforms(&build_platforms());
1062
1063 let binary_query = binary_query(
1064 &graph,
1065 package_id,
1066 "lib",
1067 "my-binary",
1068 BuildPlatform::Target,
1069 );
1070 let test_name = TestCaseName::new("my_test");
1071 let query = TestQuery {
1072 binary_query: binary_query.to_query(),
1073 test_name: &test_name,
1074 };
1075 let settings = profile.settings_for(NextestRunMode::Test, &query);
1076
1077 assert_eq!(
1079 settings.retries(),
1080 RetryPolicy::new_without_delay(3),
1081 "retries from second override"
1082 );
1083 assert_eq!(
1085 settings.flaky_result(),
1086 FlakyResult::Fail,
1087 "flaky_result from first override"
1088 );
1089
1090 let test_name = TestCaseName::new("other_test");
1093 let query = TestQuery {
1094 binary_query: binary_query.to_query(),
1095 test_name: &test_name,
1096 };
1097 let settings = profile.settings_for(NextestRunMode::Test, &query);
1098 assert_eq!(
1099 settings.retries(),
1100 RetryPolicy::new_without_delay(3),
1101 "other_test retries from second override"
1102 );
1103 assert_eq!(
1104 settings.flaky_result(),
1105 FlakyResult::Pass,
1106 "other_test flaky_result defaults to pass"
1107 );
1108 }
1109}