1use crate::{
5 config::{
6 core::{
7 EvaluatableProfile, FinalConfig, NextestConfig, NextestConfigImpl, PreBuildPlatform,
8 },
9 elements::{
10 FlakyResult, JunitFlakyFailStatus, LeakTimeout, ReportSkipPolicy, RetryPolicy,
11 SlowTimeout, TestGroup, TestPriority, ThreadsRequired,
12 },
13 scripts::{
14 CompiledProfileScripts, DeserializedProfileScriptConfig, ScriptId, WrapperScriptConfig,
15 },
16 },
17 errors::{
18 ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
19 },
20 platform::BuildPlatforms,
21 reporter::TestOutputDisplay,
22 run_mode::NextestRunMode,
23};
24use guppy::graph::cargo::BuildPlatform;
25use nextest_filtering::{
26 BinaryQuery, CompiledExpr, Filterset, FiltersetKind, KnownGroups, ParseContext, TestQuery,
27};
28use owo_colors::{OwoColorize, Style};
29use serde::{Deserialize, Deserializer};
30use smol_str::SmolStr;
31use std::collections::HashMap;
32use target_spec::{Platform, TargetSpec};
33
34#[derive(Clone, Debug)]
36pub struct ListSettings<'p, Source = ()> {
37 list_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
38}
39
40impl<'p, Source: Copy> ListSettings<'p, Source> {
41 pub(in crate::config) fn new(
42 profile: &'p EvaluatableProfile<'_>,
43 query: &BinaryQuery<'_>,
44 ) -> Self
45 where
46 Source: TrackSource<'p>,
47 {
48 let ecx = profile.filterset_ecx();
49
50 let mut list_wrapper = None;
51
52 for override_ in &profile.compiled_data.scripts {
53 if let Some(wrapper) = &override_.list_wrapper
54 && list_wrapper.is_none()
55 {
56 let (wrapper, source) =
57 map_wrapper_script(profile, Source::track_script(wrapper.clone(), override_));
58
59 if !override_
60 .is_enabled_binary(query, &ecx)
61 .expect("test() in list-time scripts should have been rejected")
62 {
63 continue;
64 }
65
66 list_wrapper = Some((wrapper, source));
67 }
68 }
69
70 Self { list_wrapper }
71 }
72}
73
74impl<'p> ListSettings<'p> {
75 pub fn debug_empty() -> Self {
79 Self { list_wrapper: None }
80 }
81
82 pub fn debug_set_list_wrapper(&mut self, wrapper: &'p WrapperScriptConfig) -> &mut Self {
86 self.list_wrapper = Some((wrapper, ()));
87 self
88 }
89
90 pub fn list_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
92 self.list_wrapper.as_ref().map(|(wrapper, _)| *wrapper)
93 }
94}
95
96#[derive(Clone, Debug)]
103pub struct TestSettings<'p, Source = ()> {
104 priority: (TestPriority, Source),
105 threads_required: (ThreadsRequired, Source),
106 run_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
107 run_extra_args: (&'p [String], Source),
108 retries: (RetryPolicy, Source),
109 flaky_result: (FlakyResult, Source),
110 slow_timeout: (SlowTimeout, Source),
111 leak_timeout: (LeakTimeout, Source),
112 test_group: (TestGroup, Source),
113 success_output: (TestOutputDisplay, Source),
114 failure_output: (TestOutputDisplay, Source),
115 junit_store_success_output: (bool, Source),
116 junit_store_failure_output: (bool, Source),
117 junit_report_skipped: (ReportSkipPolicy, Source),
118 junit_flaky_fail_status: (JunitFlakyFailStatus, Source),
119}
120
121pub(crate) trait TrackSource<'p>: Sized {
122 fn track_default<T>(value: T) -> (T, Self);
123 fn track_profile<T>(value: T) -> (T, Self);
124 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self);
125 fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self);
126}
127
128impl<'p> TrackSource<'p> for () {
129 fn track_default<T>(value: T) -> (T, Self) {
130 (value, ())
131 }
132
133 fn track_profile<T>(value: T) -> (T, Self) {
134 (value, ())
135 }
136
137 fn track_override<T>(value: T, _source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
138 (value, ())
139 }
140
141 fn track_script<T>(value: T, _source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
142 (value, ())
143 }
144}
145
146#[derive(Copy, Clone, Debug)]
147pub(crate) enum SettingSource<'p> {
148 Default,
151
152 Profile,
154
155 Override(&'p CompiledOverride<FinalConfig>),
157
158 #[expect(dead_code)]
160 Script(&'p CompiledProfileScripts<FinalConfig>),
161}
162
163impl<'p> TrackSource<'p> for SettingSource<'p> {
164 fn track_default<T>(value: T) -> (T, Self) {
165 (value, SettingSource::Default)
166 }
167
168 fn track_profile<T>(value: T) -> (T, Self) {
169 (value, SettingSource::Profile)
170 }
171
172 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
173 (value, SettingSource::Override(source))
174 }
175
176 fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
177 (value, SettingSource::Script(source))
178 }
179}
180
181impl<'p> TestSettings<'p> {
182 pub fn priority(&self) -> TestPriority {
184 self.priority.0
185 }
186
187 pub fn threads_required(&self) -> ThreadsRequired {
189 self.threads_required.0
190 }
191
192 pub fn run_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
194 self.run_wrapper.map(|(script, _)| script)
195 }
196
197 pub fn run_extra_args(&self) -> &'p [String] {
199 self.run_extra_args.0
200 }
201
202 pub fn retries(&self) -> RetryPolicy {
204 self.retries.0
205 }
206
207 pub fn flaky_result(&self) -> FlakyResult {
209 self.flaky_result.0
210 }
211
212 pub fn slow_timeout(&self) -> SlowTimeout {
214 self.slow_timeout.0
215 }
216
217 pub fn leak_timeout(&self) -> LeakTimeout {
219 self.leak_timeout.0
220 }
221
222 pub fn test_group(&self) -> &TestGroup {
224 &self.test_group.0
225 }
226
227 pub fn success_output(&self) -> TestOutputDisplay {
229 self.success_output.0
230 }
231
232 pub fn failure_output(&self) -> TestOutputDisplay {
234 self.failure_output.0
235 }
236
237 pub fn junit_store_success_output(&self) -> bool {
239 self.junit_store_success_output.0
240 }
241
242 pub fn junit_store_failure_output(&self) -> bool {
244 self.junit_store_failure_output.0
245 }
246
247 pub fn junit_report_skipped(&self) -> ReportSkipPolicy {
249 self.junit_report_skipped.0
250 }
251
252 pub fn junit_flaky_fail_status(&self) -> JunitFlakyFailStatus {
254 self.junit_flaky_fail_status.0
255 }
256}
257
258#[expect(dead_code)]
259impl<'p, Source: Copy> TestSettings<'p, Source> {
260 pub(in crate::config) fn new(
261 profile: &'p EvaluatableProfile<'_>,
262 run_mode: NextestRunMode,
263 query: &TestQuery<'_>,
264 ) -> Self
265 where
266 Source: TrackSource<'p>,
267 {
268 let ecx = profile.filterset_ecx();
269
270 let mut priority = None;
271 let mut threads_required = None;
272 let mut run_wrapper = None;
273 let mut run_extra_args = None;
274 let mut retries = None;
275 let mut flaky_result = None;
276 let mut slow_timeout = None;
277 let mut leak_timeout = None;
278 let mut test_group = None;
279 let mut success_output = None;
280 let mut failure_output = None;
281 let mut junit_store_success_output = None;
282 let mut junit_store_failure_output = None;
283 let mut junit_report_skipped = None;
284 let mut junit_flaky_fail_status = None;
285
286 for override_ in &profile.compiled_data.overrides {
287 if !override_.matches_test_query(query, &ecx) {
288 continue;
289 }
290
291 if priority.is_none()
292 && let Some(p) = override_.data.priority
293 {
294 priority = Some(Source::track_override(p, override_));
295 }
296 if threads_required.is_none()
297 && let Some(t) = override_.data.threads_required
298 {
299 threads_required = Some(Source::track_override(t, override_));
300 }
301 if run_extra_args.is_none()
302 && let Some(r) = override_.data.run_extra_args.as_deref()
303 {
304 run_extra_args = Some(Source::track_override(r, override_));
305 }
306 if retries.is_none()
307 && let Some(r) = override_.data.retries
308 {
309 retries = Some(Source::track_override(r, override_));
310 }
311 if flaky_result.is_none()
312 && let Some(fr) = override_.data.flaky_result
313 {
314 flaky_result = Some(Source::track_override(fr, override_));
315 }
316 if slow_timeout.is_none() {
317 let timeout_for_mode = match run_mode {
320 NextestRunMode::Test => override_.data.slow_timeout,
321 NextestRunMode::Benchmark => override_.data.bench_slow_timeout,
322 };
323 if let Some(s) = timeout_for_mode {
324 slow_timeout = Some(Source::track_override(s, override_));
325 }
326 }
327 if leak_timeout.is_none()
328 && let Some(l) = override_.data.leak_timeout
329 {
330 leak_timeout = Some(Source::track_override(l, override_));
331 }
332 if test_group.is_none()
333 && let Some(t) = &override_.data.test_group
334 {
335 test_group = Some(Source::track_override(t.clone(), override_));
336 }
337 if success_output.is_none()
338 && let Some(s) = override_.data.success_output
339 {
340 success_output = Some(Source::track_override(s, override_));
341 }
342 if failure_output.is_none()
343 && let Some(f) = override_.data.failure_output
344 {
345 failure_output = Some(Source::track_override(f, override_));
346 }
347 if junit_store_success_output.is_none()
348 && let Some(s) = override_.data.junit.store_success_output
349 {
350 junit_store_success_output = Some(Source::track_override(s, override_));
351 }
352 if junit_store_failure_output.is_none()
353 && let Some(f) = override_.data.junit.store_failure_output
354 {
355 junit_store_failure_output = Some(Source::track_override(f, override_));
356 }
357 if junit_report_skipped.is_none()
358 && let Some(s) = override_.data.junit.report_skipped
359 {
360 junit_report_skipped = Some(Source::track_override(s, override_));
361 }
362 if junit_flaky_fail_status.is_none()
363 && let Some(s) = override_.data.junit.flaky_fail_status
364 {
365 junit_flaky_fail_status = Some(Source::track_override(s, override_));
366 }
367 }
368
369 for override_ in &profile.compiled_data.scripts {
370 if !override_.is_enabled(query, &ecx) {
371 continue;
372 }
373
374 if run_wrapper.is_none()
375 && let Some(wrapper) = &override_.run_wrapper
376 {
377 run_wrapper = Some(Source::track_script(wrapper.clone(), override_));
378 }
379 }
380
381 let priority = priority.unwrap_or_else(|| Source::track_default(TestPriority::default()));
383 let threads_required =
384 threads_required.unwrap_or_else(|| Source::track_profile(profile.threads_required()));
385 let run_wrapper = run_wrapper.map(|wrapper| map_wrapper_script(profile, wrapper));
386 let run_extra_args =
387 run_extra_args.unwrap_or_else(|| Source::track_profile(profile.run_extra_args()));
388 let retries = retries.unwrap_or_else(|| Source::track_profile(profile.retries()));
389 let flaky_result =
390 flaky_result.unwrap_or_else(|| Source::track_profile(profile.flaky_result()));
391 let slow_timeout =
392 slow_timeout.unwrap_or_else(|| Source::track_profile(profile.slow_timeout(run_mode)));
393 let leak_timeout =
394 leak_timeout.unwrap_or_else(|| Source::track_profile(profile.leak_timeout()));
395 let test_group = test_group.unwrap_or_else(|| Source::track_profile(TestGroup::Global));
396 let success_output =
397 success_output.unwrap_or_else(|| Source::track_profile(profile.success_output()));
398 let failure_output =
399 failure_output.unwrap_or_else(|| Source::track_profile(profile.failure_output()));
400 let junit_store_success_output = junit_store_success_output.unwrap_or_else(|| {
401 Source::track_profile(profile.junit().is_some_and(|j| j.store_success_output()))
403 });
404 let junit_store_failure_output = junit_store_failure_output.unwrap_or_else(|| {
405 Source::track_profile(profile.junit().is_some_and(|j| j.store_failure_output()))
407 });
408 let junit_report_skipped = junit_report_skipped.unwrap_or_else(|| {
409 Source::track_profile(
410 profile
411 .junit()
412 .map_or(ReportSkipPolicy::default(), |j| j.report_skipped()),
413 )
414 });
415 let junit_flaky_fail_status = junit_flaky_fail_status.unwrap_or_else(|| {
416 Source::track_profile(
417 profile
418 .junit()
419 .map_or(JunitFlakyFailStatus::default(), |j| j.flaky_fail_status()),
420 )
421 });
422
423 TestSettings {
424 threads_required,
425 run_extra_args,
426 run_wrapper,
427 retries,
428 flaky_result,
429 priority,
430 slow_timeout,
431 leak_timeout,
432 test_group,
433 success_output,
434 failure_output,
435 junit_store_success_output,
436 junit_store_failure_output,
437 junit_report_skipped,
438 junit_flaky_fail_status,
439 }
440 }
441
442 pub(crate) fn threads_required_with_source(&self) -> (ThreadsRequired, Source) {
444 self.threads_required
445 }
446
447 pub(crate) fn retries_with_source(&self) -> (RetryPolicy, Source) {
449 self.retries
450 }
451
452 pub(crate) fn slow_timeout_with_source(&self) -> (SlowTimeout, Source) {
454 self.slow_timeout
455 }
456
457 pub(crate) fn leak_timeout_with_source(&self) -> (LeakTimeout, Source) {
459 self.leak_timeout
460 }
461
462 pub(crate) fn test_group_with_source(&self) -> &(TestGroup, Source) {
464 &self.test_group
465 }
466}
467
468fn map_wrapper_script<'p, Source>(
469 profile: &'p EvaluatableProfile<'_>,
470 (script, source): (ScriptId, Source),
471) -> (&'p WrapperScriptConfig, Source)
472where
473 Source: TrackSource<'p>,
474{
475 let wrapper_config = profile
476 .script_config()
477 .wrapper
478 .get(&script)
479 .unwrap_or_else(|| {
480 panic!(
481 "wrapper script {script} not found \
482 (should have been checked while reading config)"
483 )
484 });
485 (wrapper_config, source)
486}
487
488#[derive(Clone, Debug)]
489pub(in crate::config) struct CompiledByProfile {
490 pub(in crate::config) default: CompiledData<PreBuildPlatform>,
491 pub(in crate::config) other: HashMap<String, CompiledData<PreBuildPlatform>>,
492}
493
494impl CompiledByProfile {
495 pub(in crate::config) fn new(
496 pcx: &ParseContext<'_>,
497 config: &NextestConfigImpl,
498 ) -> Result<Self, ConfigParseErrorKind> {
499 let mut errors = vec![];
500 let default = CompiledData::new(
501 pcx,
502 "default",
503 Some(config.default_profile().default_filter()),
504 config.default_profile().overrides(),
505 config.default_profile().setup_scripts(),
506 &mut errors,
507 );
508 let other: HashMap<_, _> = config
509 .other_profiles()
510 .map(|(profile_name, profile)| {
511 (
512 profile_name.to_owned(),
513 CompiledData::new(
514 pcx,
515 profile_name,
516 profile.default_filter(),
517 profile.overrides(),
518 profile.scripts(),
519 &mut errors,
520 ),
521 )
522 })
523 .collect();
524
525 if errors.is_empty() {
526 Ok(Self { default, other })
527 } else {
528 Err(ConfigParseErrorKind::CompileErrors(errors))
529 }
530 }
531
532 pub(in crate::config) fn for_default_config() -> Self {
538 Self {
539 default: CompiledData {
540 profile_default_filter: Some(CompiledDefaultFilter::for_default_config()),
541 overrides: vec![],
542 scripts: vec![],
543 },
544 other: HashMap::new(),
545 }
546 }
547}
548
549#[derive(Clone, Debug)]
553pub struct CompiledDefaultFilter {
554 pub expr: CompiledExpr,
565
566 pub profile: String,
568
569 pub section: CompiledDefaultFilterSection,
571}
572
573impl CompiledDefaultFilter {
574 pub(crate) fn for_default_config() -> Self {
575 Self {
576 expr: CompiledExpr::ALL,
577 profile: NextestConfig::DEFAULT_PROFILE.to_owned(),
578 section: CompiledDefaultFilterSection::Profile,
579 }
580 }
581
582 pub fn display_config(&self, bold_style: Style) -> String {
584 match &self.section {
585 CompiledDefaultFilterSection::Profile => {
586 format!("profile.{}.default-filter", self.profile)
587 .style(bold_style)
588 .to_string()
589 }
590 CompiledDefaultFilterSection::Override(_) => {
591 format!(
592 "default-filter in {}",
593 format!("profile.{}.overrides", self.profile).style(bold_style)
594 )
595 }
596 }
597 }
598}
599
600#[derive(Clone, Copy, Debug)]
603pub enum CompiledDefaultFilterSection {
604 Profile,
606
607 Override(usize),
609}
610
611#[derive(Clone, Debug)]
612pub(in crate::config) struct CompiledData<State> {
613 pub(in crate::config) profile_default_filter: Option<CompiledDefaultFilter>,
618 pub(in crate::config) overrides: Vec<CompiledOverride<State>>,
619 pub(in crate::config) scripts: Vec<CompiledProfileScripts<State>>,
620}
621
622impl CompiledData<PreBuildPlatform> {
623 fn new(
624 pcx: &ParseContext<'_>,
625 profile_name: &str,
626 profile_default_filter: Option<&str>,
627 overrides: &[DeserializedOverride],
628 scripts: &[DeserializedProfileScriptConfig],
629 errors: &mut Vec<ConfigCompileError>,
630 ) -> Self {
631 let profile_default_filter =
632 profile_default_filter.and_then(|filter| {
633 match Filterset::parse(
634 filter.to_owned(),
635 pcx,
636 FiltersetKind::DefaultFilter,
637 &KnownGroups::Unavailable,
638 ) {
639 Ok(expr) => Some(CompiledDefaultFilter {
640 expr: expr.compiled,
641 profile: profile_name.to_owned(),
642 section: CompiledDefaultFilterSection::Profile,
643 }),
644 Err(err) => {
645 errors.push(ConfigCompileError {
646 profile_name: profile_name.to_owned(),
647 section: ConfigCompileSection::DefaultFilter,
648 kind: ConfigCompileErrorKind::Parse {
649 host_parse_error: None,
650 target_parse_error: None,
651 filter_parse_errors: vec![err],
652 },
653 });
654 None
655 }
656 }
657 });
658
659 let overrides = overrides
660 .iter()
661 .enumerate()
662 .filter_map(|(index, source)| {
663 CompiledOverride::new(pcx, profile_name, index, source, errors)
664 })
665 .collect();
666 let scripts = scripts
667 .iter()
668 .enumerate()
669 .filter_map(|(index, source)| {
670 CompiledProfileScripts::new(pcx, profile_name, index, source, errors)
671 })
672 .collect();
673 Self {
674 profile_default_filter,
675 overrides,
676 scripts,
677 }
678 }
679
680 pub(in crate::config) fn extend_reverse(&mut self, other: Self) {
681 if other.profile_default_filter.is_some() {
683 self.profile_default_filter = other.profile_default_filter;
684 }
685 self.overrides.extend(other.overrides.into_iter().rev());
686 self.scripts.extend(other.scripts.into_iter().rev());
687 }
688
689 pub(in crate::config) fn reverse(&mut self) {
690 self.overrides.reverse();
691 self.scripts.reverse();
692 }
693
694 pub(in crate::config) fn chain(self, other: Self) -> Self {
696 let profile_default_filter = self.profile_default_filter.or(other.profile_default_filter);
697 let mut overrides = self.overrides;
698 let mut scripts = self.scripts;
699 overrides.extend(other.overrides);
700 scripts.extend(other.scripts);
701 Self {
702 profile_default_filter,
703 overrides,
704 scripts,
705 }
706 }
707
708 pub(in crate::config) fn apply_build_platforms(
709 self,
710 build_platforms: &BuildPlatforms,
711 ) -> CompiledData<FinalConfig> {
712 let profile_default_filter = self.profile_default_filter;
713 let overrides = self
714 .overrides
715 .into_iter()
716 .map(|override_| override_.apply_build_platforms(build_platforms))
717 .collect();
718 let setup_scripts = self
719 .scripts
720 .into_iter()
721 .map(|setup_script| setup_script.apply_build_platforms(build_platforms))
722 .collect();
723 CompiledData {
724 profile_default_filter,
725 overrides,
726 scripts: setup_scripts,
727 }
728 }
729}
730
731#[derive(Clone, Debug)]
732pub(crate) struct CompiledOverride<State> {
733 id: OverrideId,
734 state: State,
735 pub(in crate::config) data: ProfileOverrideData,
736}
737
738impl<State> CompiledOverride<State> {
739 pub(crate) fn id(&self) -> &OverrideId {
740 &self.id
741 }
742}
743
744#[derive(Clone, Debug, Eq, Hash, PartialEq)]
745pub(crate) struct OverrideId {
746 pub(crate) profile_name: SmolStr,
747 index: usize,
748}
749
750#[derive(Clone, Debug)]
751pub(in crate::config) struct ProfileOverrideData {
752 host_spec: MaybeTargetSpec,
753 target_spec: MaybeTargetSpec,
754 filter: Option<FilterOrDefaultFilter>,
755 priority: Option<TestPriority>,
756 threads_required: Option<ThreadsRequired>,
757 run_extra_args: Option<Vec<String>>,
758 retries: Option<RetryPolicy>,
759 flaky_result: Option<FlakyResult>,
760 slow_timeout: Option<SlowTimeout>,
761 bench_slow_timeout: Option<SlowTimeout>,
762 leak_timeout: Option<LeakTimeout>,
763 pub(in crate::config) test_group: Option<TestGroup>,
764 success_output: Option<TestOutputDisplay>,
765 failure_output: Option<TestOutputDisplay>,
766 junit: DeserializedJunitOutput,
767}
768
769impl CompiledOverride<PreBuildPlatform> {
770 fn new(
771 pcx: &ParseContext<'_>,
772 profile_name: &str,
773 index: usize,
774 source: &DeserializedOverride,
775 errors: &mut Vec<ConfigCompileError>,
776 ) -> Option<Self> {
777 if source.platform.host.is_none()
778 && source.platform.target.is_none()
779 && source.filter.is_none()
780 {
781 errors.push(ConfigCompileError {
782 profile_name: profile_name.to_owned(),
783 section: ConfigCompileSection::Override(index),
784 kind: ConfigCompileErrorKind::ConstraintsNotSpecified {
785 default_filter_specified: source.default_filter.is_some(),
786 },
787 });
788 return None;
789 }
790
791 let host_spec = MaybeTargetSpec::new(source.platform.host.as_deref());
792 let target_spec = MaybeTargetSpec::new(source.platform.target.as_deref());
793 let filter = source.filter.as_ref().map_or(Ok(None), |filter| {
794 Some(Filterset::parse(
795 filter.clone(),
796 pcx,
797 FiltersetKind::OverrideFilter,
798 &KnownGroups::Unavailable,
799 ))
800 .transpose()
801 });
802 let default_filter = source.default_filter.as_ref().map_or(Ok(None), |filter| {
803 Some(Filterset::parse(
804 filter.clone(),
805 pcx,
806 FiltersetKind::DefaultFilter,
807 &KnownGroups::Unavailable,
808 ))
809 .transpose()
810 });
811
812 match (host_spec, target_spec, filter, default_filter) {
813 (Ok(host_spec), Ok(target_spec), Ok(filter), Ok(default_filter)) => {
814 let filter = match (filter, default_filter) {
816 (Some(_), Some(_)) => {
817 errors.push(ConfigCompileError {
818 profile_name: profile_name.to_owned(),
819 section: ConfigCompileSection::Override(index),
820 kind: ConfigCompileErrorKind::FilterAndDefaultFilterSpecified,
821 });
822 return None;
823 }
824 (Some(filter), None) => Some(FilterOrDefaultFilter::Filter(filter)),
825 (None, Some(default_filter)) => {
826 let compiled = CompiledDefaultFilter {
827 expr: default_filter.compiled,
828 profile: profile_name.to_owned(),
829 section: CompiledDefaultFilterSection::Override(index),
830 };
831 Some(FilterOrDefaultFilter::DefaultFilter(compiled))
832 }
833 (None, None) => None,
834 };
835
836 Some(Self {
837 id: OverrideId {
838 profile_name: profile_name.into(),
839 index,
840 },
841 state: PreBuildPlatform {},
842 data: ProfileOverrideData {
843 host_spec,
844 target_spec,
845 filter,
846 priority: source.priority,
847 threads_required: source.threads_required,
848 run_extra_args: source.run_extra_args.clone(),
849 retries: source.retries,
850 flaky_result: source.flaky_result,
851 slow_timeout: source.slow_timeout,
852 bench_slow_timeout: source.bench.slow_timeout,
853 leak_timeout: source.leak_timeout,
854 test_group: source.test_group.clone(),
855 success_output: source.success_output,
856 failure_output: source.failure_output,
857 junit: source.junit,
858 },
859 })
860 }
861 (maybe_host_err, maybe_target_err, maybe_filter_err, maybe_default_filter_err) => {
862 let host_parse_error = maybe_host_err.err();
863 let target_parse_error = maybe_target_err.err();
864 let filter_parse_errors = maybe_filter_err
865 .err()
866 .into_iter()
867 .chain(maybe_default_filter_err.err())
868 .collect();
869
870 errors.push(ConfigCompileError {
871 profile_name: profile_name.to_owned(),
872 section: ConfigCompileSection::Override(index),
873 kind: ConfigCompileErrorKind::Parse {
874 host_parse_error,
875 target_parse_error,
876 filter_parse_errors,
877 },
878 });
879 None
880 }
881 }
882 }
883
884 pub(in crate::config) fn apply_build_platforms(
885 self,
886 build_platforms: &BuildPlatforms,
887 ) -> CompiledOverride<FinalConfig> {
888 let host_eval = self.data.host_spec.eval(&build_platforms.host.platform);
889 let host_test_eval = self.data.target_spec.eval(&build_platforms.host.platform);
890 let target_eval = build_platforms
891 .target
892 .as_ref()
893 .map_or(host_test_eval, |target| {
894 self.data.target_spec.eval(&target.triple.platform)
895 });
896
897 CompiledOverride {
898 id: self.id,
899 state: FinalConfig {
900 host_eval,
901 host_test_eval,
902 target_eval,
903 },
904 data: self.data,
905 }
906 }
907}
908
909impl CompiledOverride<FinalConfig> {
910 pub(crate) fn target_spec(&self) -> &MaybeTargetSpec {
912 &self.data.target_spec
913 }
914
915 pub(crate) fn filter(&self) -> Option<&Filterset> {
917 match self.data.filter.as_ref() {
918 Some(FilterOrDefaultFilter::Filter(filter)) => Some(filter),
919 _ => None,
920 }
921 }
922
923 pub(in crate::config) fn matches_test_query(
926 &self,
927 query: &TestQuery<'_>,
928 ecx: &nextest_filtering::EvalContext<'_>,
929 ) -> bool {
930 if !self.state.host_eval {
931 return false;
932 }
933 if query.binary_query.platform == BuildPlatform::Host && !self.state.host_test_eval {
934 return false;
935 }
936 if query.binary_query.platform == BuildPlatform::Target && !self.state.target_eval {
937 return false;
938 }
939 if let Some(expr) = self.filter()
941 && !expr.matches_test(query, ecx)
942 {
943 return false;
944 }
945 true
946 }
947
948 pub(crate) fn default_filter_if_matches_platform(&self) -> Option<&CompiledDefaultFilter> {
950 match self.data.filter.as_ref() {
951 Some(FilterOrDefaultFilter::DefaultFilter(filter)) => {
952 (self.state.host_eval && self.state.target_eval).then_some(filter)
959 }
960 _ => None,
961 }
962 }
963}
964
965#[derive(Clone, Debug, Default)]
967pub(crate) enum MaybeTargetSpec {
968 Provided(TargetSpec),
969 #[default]
970 Any,
971}
972
973impl MaybeTargetSpec {
974 pub(in crate::config) fn new(platform_str: Option<&str>) -> Result<Self, target_spec::Error> {
975 Ok(match platform_str {
976 Some(platform_str) => {
977 MaybeTargetSpec::Provided(TargetSpec::new(platform_str.to_owned())?)
978 }
979 None => MaybeTargetSpec::Any,
980 })
981 }
982
983 pub(in crate::config) fn eval(&self, platform: &Platform) -> bool {
984 match self {
985 MaybeTargetSpec::Provided(spec) => spec
986 .eval(platform)
987 .unwrap_or(true),
988 MaybeTargetSpec::Any => true,
989 }
990 }
991}
992
993#[derive(Clone, Debug)]
997pub(crate) enum FilterOrDefaultFilter {
998 Filter(Filterset),
999 DefaultFilter(CompiledDefaultFilter),
1000}
1001
1002#[derive(Clone, Debug, Deserialize)]
1004#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1005#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1006#[serde(rename_all = "kebab-case")]
1007pub(in crate::config) struct DeserializedOverride {
1008 #[serde(default)]
1010 platform: PlatformStrings,
1011 #[serde(default)]
1013 filter: Option<String>,
1014 #[serde(default)]
1021 priority: Option<TestPriority>,
1022 #[serde(default)]
1025 default_filter: Option<String>,
1026 #[serde(default)]
1028 threads_required: Option<ThreadsRequired>,
1029 #[serde(default)]
1031 run_extra_args: Option<Vec<String>>,
1032 #[serde(
1034 default,
1035 deserialize_with = "crate::config::elements::deserialize_retry_policy"
1036 )]
1037 retries: Option<RetryPolicy>,
1038 #[serde(default)]
1040 flaky_result: Option<FlakyResult>,
1041 #[serde(
1043 default,
1044 deserialize_with = "crate::config::elements::deserialize_slow_timeout"
1045 )]
1046 slow_timeout: Option<SlowTimeout>,
1047 #[serde(
1049 default,
1050 deserialize_with = "crate::config::elements::deserialize_leak_timeout"
1051 )]
1052 leak_timeout: Option<LeakTimeout>,
1053 #[serde(default)]
1055 test_group: Option<TestGroup>,
1056 #[serde(default)]
1058 success_output: Option<TestOutputDisplay>,
1059 #[serde(default)]
1061 failure_output: Option<TestOutputDisplay>,
1062 #[serde(default)]
1064 junit: DeserializedJunitOutput,
1065 #[serde(default)]
1067 bench: DeserializedOverrideBench,
1068}
1069
1070#[derive(Copy, Clone, Debug, Default, Deserialize)]
1071#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1072#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1073#[serde(rename_all = "kebab-case")]
1074pub(in crate::config) struct DeserializedJunitOutput {
1075 store_success_output: Option<bool>,
1078 store_failure_output: Option<bool>,
1081 report_skipped: Option<ReportSkipPolicy>,
1083 flaky_fail_status: Option<JunitFlakyFailStatus>,
1085}
1086
1087#[derive(Clone, Debug, Default, Deserialize)]
1089#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
1090#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
1091#[serde(rename_all = "kebab-case")]
1092pub(in crate::config) struct DeserializedOverrideBench {
1093 #[serde(
1095 default,
1096 deserialize_with = "crate::config::elements::deserialize_slow_timeout"
1097 )]
1098 slow_timeout: Option<SlowTimeout>,
1099}
1100
1101#[derive(Clone, Debug, Default)]
1102pub(in crate::config) struct PlatformStrings {
1103 pub(in crate::config) host: Option<String>,
1104 pub(in crate::config) target: Option<String>,
1105}
1106
1107#[cfg(feature = "config-schema")]
1108impl schemars::JsonSchema for PlatformStrings {
1109 fn schema_name() -> std::borrow::Cow<'static, str> {
1110 "PlatformStrings".into()
1111 }
1112
1113 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1114 schemars::json_schema!({
1115 "oneOf": [
1116 generator.subschema_for::<String>(),
1117 {
1118 "type": "object",
1119 "properties": {
1120 "host": {
1121 "type": ["string", "null"],
1122 },
1123 "target": {
1124 "type": ["string", "null"],
1125 },
1126 },
1127 "additionalProperties": false,
1128 }
1129 ]
1130 })
1131 }
1132}
1133
1134impl<'de> Deserialize<'de> for PlatformStrings {
1135 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1136 struct V;
1137
1138 impl<'de2> serde::de::Visitor<'de2> for V {
1139 type Value = PlatformStrings;
1140
1141 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1142 formatter.write_str(
1143 "a table ({ host = \"x86_64-apple-darwin\", \
1144 target = \"cfg(windows)\" }) \
1145 or a string (\"x86_64-unknown-gnu-linux\")",
1146 )
1147 }
1148
1149 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1150 where
1151 E: serde::de::Error,
1152 {
1153 Ok(PlatformStrings {
1154 host: None,
1155 target: Some(v.to_owned()),
1156 })
1157 }
1158
1159 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1160 where
1161 A: serde::de::MapAccess<'de2>,
1162 {
1163 #[derive(Deserialize)]
1164 struct PlatformStringsInner {
1165 #[serde(default)]
1166 host: Option<String>,
1167 #[serde(default)]
1168 target: Option<String>,
1169 }
1170
1171 let inner = PlatformStringsInner::deserialize(
1172 serde::de::value::MapAccessDeserializer::new(map),
1173 )?;
1174 Ok(PlatformStrings {
1175 host: inner.host,
1176 target: inner.target,
1177 })
1178 }
1179 }
1180
1181 deserializer.deserialize_any(V)
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use super::*;
1188 use crate::config::{
1189 core::NextestConfig,
1190 elements::{LeakTimeoutResult, SlowTimeoutResult},
1191 utils::test_helpers::*,
1192 };
1193 use camino_tempfile::tempdir;
1194 use indoc::indoc;
1195 use nextest_metadata::TestCaseName;
1196 use std::{num::NonZeroUsize, time::Duration};
1197 use test_case::test_case;
1198
1199 #[test]
1201 fn test_overrides_basic() {
1202 let config_contents = indoc! {r#"
1203 # Override 1
1204 [[profile.default.overrides]]
1205 platform = 'aarch64-apple-darwin' # this is the target platform
1206 filter = "test(test)"
1207 retries = { backoff = "exponential", count = 20, delay = "1s", max-delay = "20s" }
1208 slow-timeout = { period = "120s", terminate-after = 1, grace-period = "0s" }
1209 success-output = "immediate-final"
1210 junit = { store-success-output = true }
1211
1212 # Override 2
1213 [[profile.default.overrides]]
1214 filter = "test(test)"
1215 threads-required = 8
1216 retries = 3
1217 slow-timeout = "60s"
1218 leak-timeout = "300ms"
1219 test-group = "my-group"
1220 failure-output = "final"
1221 junit = { store-failure-output = false, report-skipped = "none" }
1222
1223 # Override 3
1224 [[profile.default.overrides]]
1225 platform = { host = "cfg(unix)" }
1226 filter = "test(override3)"
1227 retries = 5
1228
1229 # Override 4 -- host not matched
1230 [[profile.default.overrides]]
1231 platform = { host = 'aarch64-apple-darwin' }
1232 retries = 10
1233
1234 # Override 5 -- no filter provided, just platform
1235 [[profile.default.overrides]]
1236 platform = { host = 'cfg(target_os = "linux")', target = 'aarch64-apple-darwin' }
1237 filter = "test(override5)"
1238 retries = 8
1239
1240 # Override 6 -- timeout result success
1241 [[profile.default.overrides]]
1242 filter = "test(timeout_success)"
1243 slow-timeout = { period = "30s", on-timeout = "pass" }
1244
1245 [profile.default.junit]
1246 path = "my-path.xml"
1247 report-skipped = "all"
1248
1249 [test-groups.my-group]
1250 max-threads = 20
1251 "#};
1252
1253 let workspace_dir = tempdir().unwrap();
1254
1255 let graph = temp_workspace(&workspace_dir, config_contents);
1256 let package_id = graph.workspace().iter().next().unwrap().id();
1257
1258 let pcx = ParseContext::new(&graph);
1259
1260 let nextest_config_result = NextestConfig::from_sources(
1261 graph.workspace().root(),
1262 &pcx,
1263 None,
1264 &[][..],
1265 &Default::default(),
1266 )
1267 .expect("config is valid");
1268 let profile = nextest_config_result
1269 .profile("default")
1270 .expect("valid profile name")
1271 .apply_build_platforms(&build_platforms());
1272
1273 let host_binary_query =
1275 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1276 let test_name = TestCaseName::new("test");
1277 let query = TestQuery {
1278 binary_query: host_binary_query.to_query(),
1279 test_name: &test_name,
1280 };
1281 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1282
1283 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1284 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(3));
1285 assert_eq!(
1286 overrides.slow_timeout(),
1287 SlowTimeout {
1288 period: Duration::from_secs(60),
1289 on_timeout: SlowTimeoutResult::default(),
1290 terminate_after: None,
1291 grace_period: Duration::from_secs(10),
1292 }
1293 );
1294 assert_eq!(
1295 overrides.leak_timeout(),
1296 LeakTimeout {
1297 period: Duration::from_millis(300),
1298 result: LeakTimeoutResult::Pass,
1299 }
1300 );
1301 assert_eq!(overrides.test_group(), &test_group("my-group"));
1302 assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
1303 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1304 #[expect(clippy::bool_assert_comparison)]
1306 {
1307 assert_eq!(overrides.junit_store_success_output(), false);
1308 assert_eq!(overrides.junit_store_failure_output(), false);
1309 }
1310 assert_eq!(overrides.junit_report_skipped(), ReportSkipPolicy::None);
1311
1312 let target_binary_query = binary_query(
1314 &graph,
1315 package_id,
1316 "lib",
1317 "my-binary",
1318 BuildPlatform::Target,
1319 );
1320 let test_name = TestCaseName::new("test");
1321 let query = TestQuery {
1322 binary_query: target_binary_query.to_query(),
1323 test_name: &test_name,
1324 };
1325 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1326
1327 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1328 assert_eq!(
1329 overrides.retries(),
1330 RetryPolicy::Exponential {
1331 count: 20,
1332 delay: Duration::from_secs(1),
1333 jitter: false,
1334 max_delay: Some(Duration::from_secs(20)),
1335 }
1336 );
1337 assert_eq!(
1338 overrides.slow_timeout(),
1339 SlowTimeout {
1340 period: Duration::from_secs(120),
1341 terminate_after: Some(NonZeroUsize::new(1).unwrap()),
1342 grace_period: Duration::ZERO,
1343 on_timeout: SlowTimeoutResult::default(),
1344 }
1345 );
1346 assert_eq!(
1347 overrides.leak_timeout(),
1348 LeakTimeout {
1349 period: Duration::from_millis(300),
1350 result: LeakTimeoutResult::Pass,
1351 }
1352 );
1353 assert_eq!(overrides.test_group(), &test_group("my-group"));
1354 assert_eq!(
1355 overrides.success_output(),
1356 TestOutputDisplay::ImmediateFinal
1357 );
1358 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1359 #[expect(clippy::bool_assert_comparison)]
1361 {
1362 assert_eq!(overrides.junit_store_success_output(), true);
1363 assert_eq!(overrides.junit_store_failure_output(), false);
1364 }
1365
1366 let test_name = TestCaseName::new("override3");
1368 let query = TestQuery {
1369 binary_query: target_binary_query.to_query(),
1370 test_name: &test_name,
1371 };
1372 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1373 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(5));
1374
1375 let test_name = TestCaseName::new("override5");
1377 let query = TestQuery {
1378 binary_query: target_binary_query.to_query(),
1379 test_name: &test_name,
1380 };
1381 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1382 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(8));
1383
1384 let test_name = TestCaseName::new("timeout_success");
1386 let query = TestQuery {
1387 binary_query: target_binary_query.to_query(),
1388 test_name: &test_name,
1389 };
1390 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1391 assert_eq!(
1392 overrides.slow_timeout(),
1393 SlowTimeout {
1394 period: Duration::from_secs(30),
1395 on_timeout: SlowTimeoutResult::Pass,
1396 terminate_after: None,
1397 grace_period: Duration::from_secs(10),
1398 }
1399 );
1400
1401 let test_name = TestCaseName::new("no_match");
1403 let query = TestQuery {
1404 binary_query: target_binary_query.to_query(),
1405 test_name: &test_name,
1406 };
1407 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1408 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(0));
1409 assert_eq!(overrides.junit_report_skipped(), ReportSkipPolicy::All);
1410 }
1411
1412 #[test]
1414 fn test_overrides_bench_slow_timeout() {
1415 let config_contents = indoc! {r#"
1416 # Profile-level benchmark slow-timeout (used as fallback).
1417 [profile.default]
1418 bench.slow-timeout = { period = "30y" }
1419
1420 # Override 1: Both test and bench slow-timeout specified.
1421 [[profile.default.overrides]]
1422 filter = "test(both_specified)"
1423 slow-timeout = "60s"
1424 bench.slow-timeout = { period = "5m", terminate-after = 2 }
1425
1426 # Override 2: Only test slow-timeout specified.
1427 [[profile.default.overrides]]
1428 filter = "test(test_only)"
1429 slow-timeout = "90s"
1430
1431 # Override 3: Only bench slow-timeout specified.
1432 [[profile.default.overrides]]
1433 filter = "test(bench_only)"
1434 bench.slow-timeout = "10m"
1435 "#};
1436
1437 let workspace_dir = tempdir().unwrap();
1438 let graph = temp_workspace(&workspace_dir, config_contents);
1439 let package_id = graph.workspace().iter().next().unwrap().id();
1440 let pcx = ParseContext::new(&graph);
1441
1442 let nextest_config_result = NextestConfig::from_sources(
1443 graph.workspace().root(),
1444 &pcx,
1445 None,
1446 &[][..],
1447 &Default::default(),
1448 )
1449 .expect("config is valid");
1450 let profile = nextest_config_result
1451 .profile("default")
1452 .expect("valid profile name")
1453 .apply_build_platforms(&build_platforms());
1454
1455 let host_binary_query =
1456 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1457
1458 let test_name = TestCaseName::new("both_specified");
1461 let query = TestQuery {
1462 binary_query: host_binary_query.to_query(),
1463 test_name: &test_name,
1464 };
1465
1466 let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1467 assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(60));
1468
1469 let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1470 assert_eq!(
1471 bench_settings.slow_timeout(),
1472 SlowTimeout {
1473 period: Duration::from_secs(5 * 60),
1474 terminate_after: Some(NonZeroUsize::new(2).unwrap()),
1475 grace_period: Duration::from_secs(10),
1476 on_timeout: SlowTimeoutResult::default(),
1477 }
1478 );
1479
1480 let test_name = TestCaseName::new("test_only");
1484 let query = TestQuery {
1485 binary_query: host_binary_query.to_query(),
1486 test_name: &test_name,
1487 };
1488
1489 let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1490 assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(90));
1491
1492 let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1493 assert!(
1497 bench_settings.slow_timeout().period >= SlowTimeout::VERY_LARGE.period,
1498 "should be >= VERY_LARGE, got {:?}",
1499 bench_settings.slow_timeout().period
1500 );
1501
1502 let test_name = TestCaseName::new("bench_only");
1505 let query = TestQuery {
1506 binary_query: host_binary_query.to_query(),
1507 test_name: &test_name,
1508 };
1509
1510 let test_settings = profile.settings_for(NextestRunMode::Test, &query);
1511 assert_eq!(test_settings.slow_timeout().period, Duration::from_secs(60));
1513
1514 let bench_settings = profile.settings_for(NextestRunMode::Benchmark, &query);
1515 assert_eq!(
1516 bench_settings.slow_timeout().period,
1517 Duration::from_secs(10 * 60)
1518 );
1519 }
1520
1521 #[test_case(
1522 indoc! {r#"
1523 [[profile.default.overrides]]
1524 retries = 2
1525 "#},
1526 "default",
1527 &[MietteJsonReport {
1528 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1529 labels: vec![],
1530 }]
1531
1532 ; "neither platform nor filter specified"
1533 )]
1534 #[test_case(
1535 indoc! {r#"
1536 [[profile.default.overrides]]
1537 default-filter = "test(test1)"
1538 retries = 2
1539 "#},
1540 "default",
1541 &[MietteJsonReport {
1542 message: "for override with `default-filter`, `platform` must also be specified".to_owned(),
1543 labels: vec![],
1544 }]
1545
1546 ; "default-filter without platform"
1547 )]
1548 #[test_case(
1549 indoc! {r#"
1550 [[profile.default.overrides]]
1551 platform = 'cfg(unix)'
1552 default-filter = "not default()"
1553 retries = 2
1554 "#},
1555 "default",
1556 &[MietteJsonReport {
1557 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1558 labels: vec![
1559 MietteJsonLabel {
1560 label: "default() causes infinite recursion".to_owned(),
1561 span: MietteJsonSpan { offset: 4, length: 9 },
1562 },
1563 ],
1564 }]
1565
1566 ; "default filterset in default-filter"
1567 )]
1568 #[test_case(
1569 indoc! {r#"
1570 [[profile.default.overrides]]
1571 filter = 'test(test1)'
1572 default-filter = "test(test2)"
1573 retries = 2
1574 "#},
1575 "default",
1576 &[MietteJsonReport {
1577 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1578 labels: vec![],
1579 }]
1580
1581 ; "both filter and default-filter specified"
1582 )]
1583 #[test_case(
1584 indoc! {r#"
1585 [[profile.default.overrides]]
1586 filter = 'test(test1)'
1587 platform = 'cfg(unix)'
1588 default-filter = "test(test2)"
1589 retries = 2
1590 "#},
1591 "default",
1592 &[MietteJsonReport {
1593 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1594 labels: vec![],
1595 }]
1596
1597 ; "both filter and default-filter specified with platform"
1598 )]
1599 #[test_case(
1600 indoc! {r#"
1601 [[profile.default.overrides]]
1602 platform = {}
1603 retries = 2
1604 "#},
1605 "default",
1606 &[MietteJsonReport {
1607 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1608 labels: vec![],
1609 }]
1610
1611 ; "empty platform map"
1612 )]
1613 #[test_case(
1614 indoc! {r#"
1615 [[profile.ci.overrides]]
1616 platform = 'cfg(target_os = "macos)'
1617 retries = 2
1618 "#},
1619 "ci",
1620 &[MietteJsonReport {
1621 message: "error parsing cfg() expression".to_owned(),
1622 labels: vec![
1623 MietteJsonLabel { label: "unclosed quotes".to_owned(), span: MietteJsonSpan { offset: 16, length: 6 } }
1624 ]
1625 }]
1626
1627 ; "invalid platform expression"
1628 )]
1629 #[test_case(
1630 indoc! {r#"
1631 [[profile.ci.overrides]]
1632 filter = 'test(/foo)'
1633 retries = 2
1634 "#},
1635 "ci",
1636 &[MietteJsonReport {
1637 message: "expected close regex".to_owned(),
1638 labels: vec![
1639 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1640 ]
1641 }]
1642
1643 ; "invalid filterset"
1644 )]
1645 #[test_case(
1646 indoc! {r#"
1648 [profile.ci]
1649 default-filter = "test(foo) or default()"
1650 "#},
1651 "ci",
1652 &[MietteJsonReport {
1653 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1654 labels: vec![
1655 MietteJsonLabel { label: "default() causes infinite recursion".to_owned(), span: MietteJsonSpan { offset: 13, length: 9 } }
1656 ]
1657 }]
1658
1659 ; "default-filter with default"
1660 )]
1661 fn parse_overrides_invalid(
1662 config_contents: &str,
1663 faulty_profile: &str,
1664 expected_reports: &[MietteJsonReport],
1665 ) {
1666 let workspace_dir = tempdir().unwrap();
1667
1668 let graph = temp_workspace(&workspace_dir, config_contents);
1669 let pcx = ParseContext::new(&graph);
1670
1671 let err = NextestConfig::from_sources(
1672 graph.workspace().root(),
1673 &pcx,
1674 None,
1675 [],
1676 &Default::default(),
1677 )
1678 .expect_err("config is invalid");
1679 match err.kind() {
1680 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1681 assert_eq!(
1682 compile_errors.len(),
1683 1,
1684 "exactly one override error must be produced"
1685 );
1686 let error = compile_errors.first().unwrap();
1687 assert_eq!(
1688 error.profile_name, faulty_profile,
1689 "compile error profile matches"
1690 );
1691 let handler = miette::JSONReportHandler::new();
1692 let reports = error
1693 .kind
1694 .reports()
1695 .map(|report| {
1696 let mut out = String::new();
1697 handler.render_report(&mut out, report.as_ref()).unwrap();
1698
1699 let json_report: MietteJsonReport = serde_json::from_str(&out)
1700 .unwrap_or_else(|err| {
1701 panic!(
1702 "failed to deserialize JSON message produced by miette: {err}"
1703 )
1704 });
1705 json_report
1706 })
1707 .collect::<Vec<_>>();
1708 assert_eq!(&reports, expected_reports, "reports match");
1709 }
1710 other => {
1711 panic!(
1712 "for config error {other:?}, expected ConfigParseErrorKind::FiltersetOrCfgParseError"
1713 );
1714 }
1715 };
1716 }
1717
1718 #[test]
1722 fn cfg_unix_with_custom_platform() {
1723 let config_contents = indoc! {r#"
1724 [[profile.default.overrides]]
1725 platform = { host = "cfg(unix)" }
1726 filter = "test(test)"
1727 retries = 5
1728 "#};
1729
1730 let workspace_dir = tempdir().unwrap();
1731
1732 let graph = temp_workspace(&workspace_dir, config_contents);
1733 let package_id = graph.workspace().iter().next().unwrap().id();
1734 let pcx = ParseContext::new(&graph);
1735
1736 let nextest_config = NextestConfig::from_sources(
1737 graph.workspace().root(),
1738 &pcx,
1739 None,
1740 &[][..],
1741 &Default::default(),
1742 )
1743 .expect("config is valid");
1744
1745 let build_platforms = custom_build_platforms(workspace_dir.path());
1746
1747 let profile = nextest_config
1748 .profile("default")
1749 .expect("valid profile name")
1750 .apply_build_platforms(&build_platforms);
1751
1752 let target_binary_query = binary_query(
1754 &graph,
1755 package_id,
1756 "lib",
1757 "my-binary",
1758 BuildPlatform::Target,
1759 );
1760 let test_name = TestCaseName::new("test");
1761 let query = TestQuery {
1762 binary_query: target_binary_query.to_query(),
1763 test_name: &test_name,
1764 };
1765 let overrides = profile.settings_for(NextestRunMode::Test, &query);
1766 assert_eq!(
1767 overrides.retries(),
1768 RetryPolicy::new_without_delay(5),
1769 "retries applied to custom platform"
1770 );
1771 }
1772}