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