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 {
205 return Err(serde::de::Error::custom(
206 "`jitter` cannot be true if `delay` isn't specified or is zero",
207 ));
208 }
209 Some(RetryPolicy::Fixed { .. }) => {}
210 Some(RetryPolicy::Exponential {
211 count,
212 delay,
213 jitter: _,
214 max_delay,
215 }) => {
216 if *count == 0 {
218 return Err(serde::de::Error::custom(
219 "`count` cannot be zero with exponential backoff",
220 ));
221 }
222 if delay.is_zero() {
224 return Err(serde::de::Error::custom(
225 "`delay` cannot be zero with exponential backoff",
226 ));
227 }
228 if max_delay.is_some_and(|f| f.is_zero()) {
230 return Err(serde::de::Error::custom(
231 "`max-delay` cannot be zero with exponential backoff",
232 ));
233 }
234 if max_delay.is_some_and(|max_delay| max_delay < *delay) {
236 return Err(serde::de::Error::custom(
237 "`max-delay` cannot be less than delay with exponential backoff",
238 ));
239 }
240 }
241 None => {}
242 }
243
244 Ok(policy)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::{
251 config::{core::NextestConfig, utils::test_helpers::*},
252 errors::ConfigParseErrorKind,
253 run_mode::NextestRunMode,
254 };
255 use camino_tempfile::tempdir;
256 use config::ConfigError;
257 use guppy::graph::cargo::BuildPlatform;
258 use indoc::indoc;
259 use nextest_filtering::{ParseContext, TestQuery};
260 use nextest_metadata::TestCaseName;
261 use test_case::test_case;
262
263 #[test]
264 fn parse_retries_valid() {
265 let config_contents = indoc! {r#"
266 [profile.default]
267 retries = { backoff = "fixed", count = 3 }
268
269 [profile.no-retries]
270 retries = 0
271
272 [profile.fixed-with-delay]
273 retries = { backoff = "fixed", count = 3, delay = "1s" }
274
275 [profile.exp]
276 retries = { backoff = "exponential", count = 4, delay = "2s" }
277
278 [profile.exp-with-max-delay]
279 retries = { backoff = "exponential", count = 5, delay = "3s", max-delay = "10s" }
280
281 [profile.exp-with-max-delay-and-jitter]
282 retries = { backoff = "exponential", count = 6, delay = "4s", max-delay = "1m", jitter = true }
283
284 [profile.with-flaky-result-fail]
285 retries = { backoff = "fixed", count = 2 }
286 flaky-result = "fail"
287
288 [profile.with-flaky-result-pass]
289 retries = { backoff = "fixed", count = 2 }
290 flaky-result = "pass"
291
292 [profile.exp-with-flaky-result-fail]
293 retries = { backoff = "exponential", count = 3, delay = "1s" }
294 flaky-result = "fail"
295
296 [profile.flaky-result-only]
297 flaky-result = "fail"
298 "#};
299
300 let workspace_dir = tempdir().unwrap();
301
302 let graph = temp_workspace(&workspace_dir, config_contents);
303 let pcx = ParseContext::new(&graph);
304
305 let config = NextestConfig::from_sources(
306 graph.workspace().root(),
307 &pcx,
308 None,
309 [],
310 &Default::default(),
311 )
312 .expect("config is valid");
313
314 let default_profile = config
315 .profile("default")
316 .expect("default profile exists")
317 .apply_build_platforms(&build_platforms());
318 assert_eq!(
319 default_profile.retries(),
320 RetryPolicy::Fixed {
321 count: 3,
322 delay: Duration::ZERO,
323 jitter: false,
324 },
325 "default retries matches"
326 );
327 assert_eq!(
328 default_profile.flaky_result(),
329 FlakyResult::Pass,
330 "default flaky_result matches"
331 );
332
333 assert_eq!(
334 config
335 .profile("no-retries")
336 .expect("profile exists")
337 .apply_build_platforms(&build_platforms())
338 .retries(),
339 RetryPolicy::new_without_delay(0),
340 "no-retries retries matches"
341 );
342
343 assert_eq!(
344 config
345 .profile("fixed-with-delay")
346 .expect("profile exists")
347 .apply_build_platforms(&build_platforms())
348 .retries(),
349 RetryPolicy::Fixed {
350 count: 3,
351 delay: Duration::from_secs(1),
352 jitter: false,
353 },
354 "fixed-with-delay retries matches"
355 );
356
357 assert_eq!(
358 config
359 .profile("exp")
360 .expect("profile exists")
361 .apply_build_platforms(&build_platforms())
362 .retries(),
363 RetryPolicy::Exponential {
364 count: 4,
365 delay: Duration::from_secs(2),
366 jitter: false,
367 max_delay: None,
368 },
369 "exp retries matches"
370 );
371
372 assert_eq!(
373 config
374 .profile("exp-with-max-delay")
375 .expect("profile exists")
376 .apply_build_platforms(&build_platforms())
377 .retries(),
378 RetryPolicy::Exponential {
379 count: 5,
380 delay: Duration::from_secs(3),
381 jitter: false,
382 max_delay: Some(Duration::from_secs(10)),
383 },
384 "exp-with-max-delay retries matches"
385 );
386
387 assert_eq!(
388 config
389 .profile("exp-with-max-delay-and-jitter")
390 .expect("profile exists")
391 .apply_build_platforms(&build_platforms())
392 .retries(),
393 RetryPolicy::Exponential {
394 count: 6,
395 delay: Duration::from_secs(4),
396 jitter: true,
397 max_delay: Some(Duration::from_secs(60)),
398 },
399 "exp-with-max-delay-and-jitter retries matches"
400 );
401
402 let with_flaky_result_fail = config
403 .profile("with-flaky-result-fail")
404 .expect("profile exists")
405 .apply_build_platforms(&build_platforms());
406 assert_eq!(
407 with_flaky_result_fail.retries(),
408 RetryPolicy::new_without_delay(2),
409 "with-flaky-result-fail retries matches"
410 );
411 assert_eq!(
412 with_flaky_result_fail.flaky_result(),
413 FlakyResult::Fail,
414 "with-flaky-result-fail flaky_result matches"
415 );
416
417 let with_flaky_result_pass = config
418 .profile("with-flaky-result-pass")
419 .expect("profile exists")
420 .apply_build_platforms(&build_platforms());
421 assert_eq!(
422 with_flaky_result_pass.retries(),
423 RetryPolicy::new_without_delay(2),
424 "with-flaky-result-pass retries matches"
425 );
426 assert_eq!(
427 with_flaky_result_pass.flaky_result(),
428 FlakyResult::Pass,
429 "with-flaky-result-pass flaky_result matches"
430 );
431
432 let exp_with_flaky_result_fail = config
433 .profile("exp-with-flaky-result-fail")
434 .expect("profile exists")
435 .apply_build_platforms(&build_platforms());
436 assert_eq!(
437 exp_with_flaky_result_fail.retries(),
438 RetryPolicy::Exponential {
439 count: 3,
440 delay: Duration::from_secs(1),
441 jitter: false,
442 max_delay: None,
443 },
444 "exp-with-flaky-result-fail retries matches"
445 );
446 assert_eq!(
447 exp_with_flaky_result_fail.flaky_result(),
448 FlakyResult::Fail,
449 "exp-with-flaky-result-fail flaky_result matches"
450 );
451
452 let flaky_result_only = config
455 .profile("flaky-result-only")
456 .expect("profile exists")
457 .apply_build_platforms(&build_platforms());
458 assert_eq!(
459 flaky_result_only.retries(),
460 RetryPolicy::Fixed {
461 count: 3,
462 delay: Duration::ZERO,
463 jitter: false,
464 },
465 "flaky-result-only retries inherited from default"
466 );
467 assert_eq!(
468 flaky_result_only.flaky_result(),
469 FlakyResult::Fail,
470 "flaky-result-only flaky_result matches"
471 );
472 }
473
474 #[test_case(
475 indoc!{r#"
476 [profile.default]
477 retries = { backoff = "foo" }
478 "#},
479 ConfigErrorKind::Message,
480 "unknown variant `foo`, expected `fixed` or `exponential`"
481 ; "invalid value for backoff")]
482 #[test_case(
483 indoc!{r#"
484 [profile.default]
485 retries = { backoff = "fixed" }
486 "#},
487 ConfigErrorKind::NotFound,
488 "profile.default.retries.count"
489 ; "fixed specified without count")]
490 #[test_case(
491 indoc!{r#"
492 [profile.default]
493 retries = { backoff = "fixed", count = 1, delay = "foobar" }
494 "#},
495 ConfigErrorKind::Message,
496 "invalid value: string \"foobar\", expected a duration"
497 ; "delay is not a valid duration")]
498 #[test_case(
499 indoc!{r#"
500 [profile.default]
501 retries = { backoff = "fixed", count = 1, jitter = true }
502 "#},
503 ConfigErrorKind::Message,
504 "`jitter` cannot be true if `delay` isn't specified or is zero"
505 ; "jitter specified without delay")]
506 #[test_case(
507 indoc!{r#"
508 [profile.default]
509 retries = { backoff = "fixed", count = 1, max-delay = "10s" }
510 "#},
511 ConfigErrorKind::Message,
512 "unknown field `max-delay`, expected one of `count`, `delay`, `jitter`"
513 ; "max-delay is incompatible with fixed backoff")]
514 #[test_case(
515 indoc!{r#"
516 [profile.default]
517 retries = { backoff = "exponential", count = 1 }
518 "#},
519 ConfigErrorKind::NotFound,
520 "profile.default.retries.delay"
521 ; "exponential backoff must specify delay")]
522 #[test_case(
523 indoc!{r#"
524 [profile.default]
525 retries = { backoff = "exponential", delay = "1s" }
526 "#},
527 ConfigErrorKind::NotFound,
528 "profile.default.retries.count"
529 ; "exponential backoff must specify count")]
530 #[test_case(
531 indoc!{r#"
532 [profile.default]
533 retries = { backoff = "exponential", count = 0, delay = "1s" }
534 "#},
535 ConfigErrorKind::Message,
536 "`count` cannot be zero with exponential backoff"
537 ; "exponential backoff must have a non-zero count")]
538 #[test_case(
539 indoc!{r#"
540 [profile.default]
541 retries = { backoff = "exponential", count = 1, delay = "0s" }
542 "#},
543 ConfigErrorKind::Message,
544 "`delay` cannot be zero with exponential backoff"
545 ; "exponential backoff must have a non-zero delay")]
546 #[test_case(
547 indoc!{r#"
548 [profile.default]
549 retries = { backoff = "exponential", count = 1, delay = "1s", max-delay = "0s" }
550 "#},
551 ConfigErrorKind::Message,
552 "`max-delay` cannot be zero with exponential backoff"
553 ; "exponential backoff must have a non-zero max delay")]
554 #[test_case(
555 indoc!{r#"
556 [profile.default]
557 retries = { backoff = "exponential", count = 1, delay = "4s", max-delay = "2s", jitter = true }
558 "#},
559 ConfigErrorKind::Message,
560 "`max-delay` cannot be less than delay"
561 ; "max-delay greater than delay")]
562 #[test_case(
563 indoc!{r#"
564 [profile.default]
565 flaky-result = "unknown"
566 "#},
567 ConfigErrorKind::Message,
568 "enum FlakyResult does not have variant constructor unknown"
569 ; "invalid flaky-result value")]
570 fn parse_retries_invalid(
571 config_contents: &str,
572 expected_kind: ConfigErrorKind,
573 expected_message: &str,
574 ) {
575 let workspace_dir = tempdir().unwrap();
576
577 let graph = temp_workspace(&workspace_dir, config_contents);
578 let pcx = ParseContext::new(&graph);
579
580 let config_err = NextestConfig::from_sources(
581 graph.workspace().root(),
582 &pcx,
583 None,
584 [],
585 &Default::default(),
586 )
587 .expect_err("config expected to be invalid");
588
589 let message = match config_err.kind() {
590 ConfigParseErrorKind::DeserializeError(path_error) => {
591 match (path_error.inner(), expected_kind) {
592 (ConfigError::Message(message), ConfigErrorKind::Message) => message,
593 (ConfigError::NotFound(message), ConfigErrorKind::NotFound) => message,
594 (other, expected) => {
595 panic!(
596 "for config error {config_err:?}, expected \
597 ConfigErrorKind::{expected:?} for inner error {other:?}"
598 );
599 }
600 }
601 }
602 other => {
603 panic!(
604 "for config error {other:?}, expected ConfigParseErrorKind::DeserializeError"
605 );
606 }
607 };
608
609 assert!(
610 message.contains(expected_message),
611 "expected message \"{message}\" to contain \"{expected_message}\""
612 );
613 }
614
615 #[test_case(
616 indoc! {r#"
617 [[profile.default.overrides]]
618 filter = "test(=my_test)"
619 retries = 2
620
621 [profile.ci]
622 "#},
623 BuildPlatform::Target,
624 RetryPolicy::new_without_delay(2)
625
626 ; "my_test matches exactly"
627 )]
628 #[test_case(
629 indoc! {r#"
630 [[profile.default.overrides]]
631 filter = "!test(=my_test)"
632 retries = 2
633
634 [profile.ci]
635 "#},
636 BuildPlatform::Target,
637 RetryPolicy::new_without_delay(0)
638
639 ; "not match"
640 )]
641 #[test_case(
642 indoc! {r#"
643 [[profile.default.overrides]]
644 filter = "test(=my_test)"
645
646 [profile.ci]
647 "#},
648 BuildPlatform::Target,
649 RetryPolicy::new_without_delay(0)
650
651 ; "no retries specified"
652 )]
653 #[test_case(
654 indoc! {r#"
655 [[profile.default.overrides]]
656 filter = "test(test)"
657 retries = 2
658
659 [[profile.default.overrides]]
660 filter = "test(=my_test)"
661 retries = 3
662
663 [profile.ci]
664 "#},
665 BuildPlatform::Target,
666 RetryPolicy::new_without_delay(2)
667
668 ; "earlier configs override later ones"
669 )]
670 #[test_case(
671 indoc! {r#"
672 [[profile.default.overrides]]
673 filter = "test(test)"
674 retries = 2
675
676 [profile.ci]
677
678 [[profile.ci.overrides]]
679 filter = "test(=my_test)"
680 retries = 3
681 "#},
682 BuildPlatform::Target,
683 RetryPolicy::new_without_delay(3)
684
685 ; "profile-specific configs override default ones"
686 )]
687 #[test_case(
688 indoc! {r#"
689 [[profile.default.overrides]]
690 filter = "(!package(test-package)) and test(test)"
691 retries = 2
692
693 [profile.ci]
694
695 [[profile.ci.overrides]]
696 filter = "!test(=my_test_2)"
697 retries = 3
698 "#},
699 BuildPlatform::Target,
700 RetryPolicy::new_without_delay(3)
701
702 ; "no overrides match my_test exactly"
703 )]
704 #[test_case(
705 indoc! {r#"
706 [[profile.default.overrides]]
707 platform = "x86_64-unknown-linux-gnu"
708 filter = "test(test)"
709 retries = 2
710
711 [[profile.default.overrides]]
712 filter = "test(=my_test)"
713 retries = 3
714
715 [profile.ci]
716 "#},
717 BuildPlatform::Host,
718 RetryPolicy::new_without_delay(2)
719
720 ; "earlier config applied because it matches host triple"
721 )]
722 #[test_case(
723 indoc! {r#"
724 [[profile.default.overrides]]
725 platform = "aarch64-apple-darwin"
726 filter = "test(test)"
727 retries = 2
728
729 [[profile.default.overrides]]
730 filter = "test(=my_test)"
731 retries = 3
732
733 [profile.ci]
734 "#},
735 BuildPlatform::Host,
736 RetryPolicy::new_without_delay(3)
737
738 ; "earlier config ignored because it doesn't match host triple"
739 )]
740 #[test_case(
741 indoc! {r#"
742 [[profile.default.overrides]]
743 platform = "aarch64-apple-darwin"
744 filter = "test(test)"
745 retries = 2
746
747 [[profile.default.overrides]]
748 filter = "test(=my_test)"
749 retries = 3
750
751 [profile.ci]
752 "#},
753 BuildPlatform::Target,
754 RetryPolicy::new_without_delay(2)
755
756 ; "earlier config applied because it matches target triple"
757 )]
758 #[test_case(
759 indoc! {r#"
760 [[profile.default.overrides]]
761 platform = "x86_64-unknown-linux-gnu"
762 filter = "test(test)"
763 retries = 2
764
765 [[profile.default.overrides]]
766 filter = "test(=my_test)"
767 retries = 3
768
769 [profile.ci]
770 "#},
771 BuildPlatform::Target,
772 RetryPolicy::new_without_delay(3)
773
774 ; "earlier config ignored because it doesn't match target triple"
775 )]
776 #[test_case(
777 indoc! {r#"
778 [[profile.default.overrides]]
779 platform = 'cfg(target_os = "macos")'
780 filter = "test(test)"
781 retries = 2
782
783 [[profile.default.overrides]]
784 filter = "test(=my_test)"
785 retries = 3
786
787 [profile.ci]
788 "#},
789 BuildPlatform::Target,
790 RetryPolicy::new_without_delay(2)
791
792 ; "earlier config applied because it matches target cfg expr"
793 )]
794 #[test_case(
795 indoc! {r#"
796 [[profile.default.overrides]]
797 platform = 'cfg(target_arch = "x86_64")'
798 filter = "test(test)"
799 retries = 2
800
801 [[profile.default.overrides]]
802 filter = "test(=my_test)"
803 retries = 3
804
805 [profile.ci]
806 "#},
807 BuildPlatform::Target,
808 RetryPolicy::new_without_delay(3)
809
810 ; "earlier config ignored because it doesn't match target cfg expr"
811 )]
812 fn overrides_retries(
813 config_contents: &str,
814 build_platform: BuildPlatform,
815 retries: RetryPolicy,
816 ) {
817 let workspace_dir = tempdir().unwrap();
818
819 let graph = temp_workspace(&workspace_dir, config_contents);
820 let package_id = graph.workspace().iter().next().unwrap().id();
821 let pcx = ParseContext::new(&graph);
822
823 let config = NextestConfig::from_sources(
824 graph.workspace().root(),
825 &pcx,
826 None,
827 &[][..],
828 &Default::default(),
829 )
830 .unwrap();
831 let binary_query = binary_query(&graph, package_id, "lib", "my-binary", build_platform);
832 let test_name = TestCaseName::new("my_test");
833 let query = TestQuery {
834 binary_query: binary_query.to_query(),
835 test_name: &test_name,
836 };
837 let profile = config
838 .profile("ci")
839 .expect("ci profile is defined")
840 .apply_build_platforms(&build_platforms());
841 let settings_for = profile.settings_for(NextestRunMode::Test, &query);
842 assert_eq!(
843 settings_for.retries(),
844 retries,
845 "actual retries don't match expected retries"
846 );
847 }
848
849 #[test]
850 fn overrides_flaky_result() {
851 let config_contents = indoc! {r#"
852 [[profile.default.overrides]]
853 filter = "test(=my_test)"
854 retries = { backoff = "fixed", count = 3 }
855 flaky-result = "fail"
856
857 [[profile.default.overrides]]
858 filter = "test(=other_test)"
859 retries = 2
860
861 [profile.ci]
862 "#};
863 let workspace_dir = tempdir().unwrap();
864
865 let graph = temp_workspace(&workspace_dir, config_contents);
866 let package_id = graph.workspace().iter().next().unwrap().id();
867 let pcx = ParseContext::new(&graph);
868
869 let config = NextestConfig::from_sources(
870 graph.workspace().root(),
871 &pcx,
872 None,
873 &[][..],
874 &Default::default(),
875 )
876 .unwrap();
877
878 let profile = config
879 .profile("ci")
880 .expect("ci profile is defined")
881 .apply_build_platforms(&build_platforms());
882
883 let binary_query = binary_query(
885 &graph,
886 package_id,
887 "lib",
888 "my-binary",
889 BuildPlatform::Target,
890 );
891 let test_name = TestCaseName::new("my_test");
892 let query = TestQuery {
893 binary_query: binary_query.to_query(),
894 test_name: &test_name,
895 };
896 let settings = profile.settings_for(NextestRunMode::Test, &query);
897 assert_eq!(
898 settings.flaky_result(),
899 FlakyResult::Fail,
900 "my_test flaky_result is fail"
901 );
902
903 let test_name = TestCaseName::new("other_test");
906 let query = TestQuery {
907 binary_query: binary_query.to_query(),
908 test_name: &test_name,
909 };
910 let settings = profile.settings_for(NextestRunMode::Test, &query);
911 assert_eq!(
912 settings.flaky_result(),
913 FlakyResult::Pass,
914 "other_test flaky_result defaults to pass"
915 );
916 }
917
918 #[test]
922 fn overrides_flaky_result_independent_resolution() {
923 let config_contents = indoc! {r#"
924 # Override 1: sets retries count only.
925 [[profile.default.overrides]]
926 filter = "test(=my_test)"
927 retries = 5
928
929 # Override 2: sets retries with flaky-result = "fail".
930 [[profile.default.overrides]]
931 filter = "all()"
932 retries = { backoff = "fixed", count = 2 }
933 flaky-result = "fail"
934
935 [profile.ci]
936 "#};
937 let workspace_dir = tempdir().unwrap();
938
939 let graph = temp_workspace(&workspace_dir, config_contents);
940 let package_id = graph.workspace().iter().next().unwrap().id();
941 let pcx = ParseContext::new(&graph);
942
943 let config = NextestConfig::from_sources(
944 graph.workspace().root(),
945 &pcx,
946 None,
947 &[][..],
948 &Default::default(),
949 )
950 .unwrap();
951
952 let profile = config
953 .profile("ci")
954 .expect("ci profile is defined")
955 .apply_build_platforms(&build_platforms());
956
957 let binary_query = binary_query(
958 &graph,
959 package_id,
960 "lib",
961 "my-binary",
962 BuildPlatform::Target,
963 );
964 let test_name = TestCaseName::new("my_test");
965 let query = TestQuery {
966 binary_query: binary_query.to_query(),
967 test_name: &test_name,
968 };
969 let settings = profile.settings_for(NextestRunMode::Test, &query);
970
971 assert_eq!(
973 settings.retries(),
974 RetryPolicy::new_without_delay(5),
975 "retries count from first override"
976 );
977 assert_eq!(
979 settings.flaky_result(),
980 FlakyResult::Fail,
981 "flaky_result from second override"
982 );
983 }
984
985 #[test]
988 fn overrides_flaky_result_only() {
989 let config_contents = indoc! {r#"
990 # Override 1: sets only flaky-result, no retry policy.
991 [[profile.default.overrides]]
992 filter = "test(=my_test)"
993 flaky-result = "fail"
994
995 # Override 2: sets retries count for all tests.
996 [[profile.default.overrides]]
997 filter = "all()"
998 retries = 3
999
1000 [profile.ci]
1001 "#};
1002 let workspace_dir = tempdir().unwrap();
1003
1004 let graph = temp_workspace(&workspace_dir, config_contents);
1005 let package_id = graph.workspace().iter().next().unwrap().id();
1006 let pcx = ParseContext::new(&graph);
1007
1008 let config = NextestConfig::from_sources(
1009 graph.workspace().root(),
1010 &pcx,
1011 None,
1012 &[][..],
1013 &Default::default(),
1014 )
1015 .unwrap();
1016
1017 let profile = config
1018 .profile("ci")
1019 .expect("ci profile is defined")
1020 .apply_build_platforms(&build_platforms());
1021
1022 let binary_query = binary_query(
1023 &graph,
1024 package_id,
1025 "lib",
1026 "my-binary",
1027 BuildPlatform::Target,
1028 );
1029 let test_name = TestCaseName::new("my_test");
1030 let query = TestQuery {
1031 binary_query: binary_query.to_query(),
1032 test_name: &test_name,
1033 };
1034 let settings = profile.settings_for(NextestRunMode::Test, &query);
1035
1036 assert_eq!(
1038 settings.retries(),
1039 RetryPolicy::new_without_delay(3),
1040 "retries from second override"
1041 );
1042 assert_eq!(
1044 settings.flaky_result(),
1045 FlakyResult::Fail,
1046 "flaky_result from first override"
1047 );
1048
1049 let test_name = TestCaseName::new("other_test");
1052 let query = TestQuery {
1053 binary_query: binary_query.to_query(),
1054 test_name: &test_name,
1055 };
1056 let settings = profile.settings_for(NextestRunMode::Test, &query);
1057 assert_eq!(
1058 settings.retries(),
1059 RetryPolicy::new_without_delay(3),
1060 "other_test retries from second override"
1061 );
1062 assert_eq!(
1063 settings.flaky_result(),
1064 FlakyResult::Pass,
1065 "other_test flaky_result defaults to pass"
1066 );
1067 }
1068}