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