1use super::{
5 CompiledProfileScripts, DeserializedProfileScriptConfig, EvaluatableProfile, LeakTimeout,
6 NextestConfig, NextestConfigImpl, TestPriority, WrapperScriptConfig,
7};
8use crate::{
9 config::{
10 FinalConfig, PreBuildPlatform, RetryPolicy, ScriptId, SlowTimeout, TestGroup,
11 ThreadsRequired,
12 },
13 errors::{
14 ConfigCompileError, ConfigCompileErrorKind, ConfigCompileSection, ConfigParseErrorKind,
15 },
16 platform::BuildPlatforms,
17 reporter::TestOutputDisplay,
18};
19use guppy::graph::cargo::BuildPlatform;
20use nextest_filtering::{
21 BinaryQuery, CompiledExpr, Filterset, FiltersetKind, ParseContext, TestQuery,
22};
23use owo_colors::{OwoColorize, Style};
24use serde::{Deserialize, Deserializer};
25use smol_str::SmolStr;
26use std::collections::HashMap;
27use target_spec::{Platform, TargetSpec};
28
29#[derive(Clone, Debug)]
31pub struct ListSettings<'p, Source = ()> {
32 list_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
33}
34
35impl<'p, Source: Copy> ListSettings<'p, Source> {
36 pub(super) fn new(profile: &'p EvaluatableProfile<'_>, query: &BinaryQuery<'_>) -> Self
37 where
38 Source: TrackSource<'p>,
39 {
40 let ecx = profile.filterset_ecx();
41
42 let mut list_wrapper = None;
43
44 for override_ in &profile.compiled_data.scripts {
45 if let Some(wrapper) = &override_.list_wrapper {
46 if list_wrapper.is_none() {
47 let (wrapper, source) = map_wrapper_script(
48 profile,
49 Source::track_script(wrapper.clone(), override_),
50 );
51
52 if !override_
53 .is_enabled_binary(query, &ecx)
54 .expect("test() in list-time scripts should have been rejected")
55 {
56 continue;
57 }
58
59 list_wrapper = Some((wrapper, source));
60 }
61 }
62 }
63
64 Self { list_wrapper }
65 }
66}
67
68impl<'p> ListSettings<'p> {
69 pub fn debug_empty() -> Self {
73 Self { list_wrapper: None }
74 }
75
76 pub fn debug_set_list_wrapper(&mut self, wrapper: &'p WrapperScriptConfig) -> &mut Self {
80 self.list_wrapper = Some((wrapper, ()));
81 self
82 }
83
84 pub fn list_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
86 self.list_wrapper.as_ref().map(|(wrapper, _)| *wrapper)
87 }
88}
89
90#[derive(Clone, Debug)]
97pub struct TestSettings<'p, Source = ()> {
98 priority: (TestPriority, Source),
99 threads_required: (ThreadsRequired, Source),
100 run_wrapper: Option<(&'p WrapperScriptConfig, Source)>,
101 run_extra_args: (&'p [String], Source),
102 retries: (RetryPolicy, Source),
103 slow_timeout: (SlowTimeout, Source),
104 leak_timeout: (LeakTimeout, Source),
105 test_group: (TestGroup, Source),
106 success_output: (TestOutputDisplay, Source),
107 failure_output: (TestOutputDisplay, Source),
108 junit_store_success_output: (bool, Source),
109 junit_store_failure_output: (bool, Source),
110}
111
112pub(crate) trait TrackSource<'p>: Sized {
113 fn track_default<T>(value: T) -> (T, Self);
114 fn track_profile<T>(value: T) -> (T, Self);
115 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self);
116 fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self);
117}
118
119impl<'p> TrackSource<'p> for () {
120 fn track_default<T>(value: T) -> (T, Self) {
121 (value, ())
122 }
123
124 fn track_profile<T>(value: T) -> (T, Self) {
125 (value, ())
126 }
127
128 fn track_override<T>(value: T, _source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
129 (value, ())
130 }
131
132 fn track_script<T>(value: T, _source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
133 (value, ())
134 }
135}
136
137#[derive(Copy, Clone, Debug)]
138pub(crate) enum SettingSource<'p> {
139 Default,
142
143 Profile,
145
146 Override(&'p CompiledOverride<FinalConfig>),
148
149 #[expect(dead_code)]
151 Script(&'p CompiledProfileScripts<FinalConfig>),
152}
153
154impl<'p> TrackSource<'p> for SettingSource<'p> {
155 fn track_default<T>(value: T) -> (T, Self) {
156 (value, SettingSource::Default)
157 }
158
159 fn track_profile<T>(value: T) -> (T, Self) {
160 (value, SettingSource::Profile)
161 }
162
163 fn track_override<T>(value: T, source: &'p CompiledOverride<FinalConfig>) -> (T, Self) {
164 (value, SettingSource::Override(source))
165 }
166
167 fn track_script<T>(value: T, source: &'p CompiledProfileScripts<FinalConfig>) -> (T, Self) {
168 (value, SettingSource::Script(source))
169 }
170}
171
172impl<'p> TestSettings<'p> {
173 pub fn priority(&self) -> TestPriority {
175 self.priority.0
176 }
177
178 pub fn threads_required(&self) -> ThreadsRequired {
180 self.threads_required.0
181 }
182
183 pub fn run_wrapper(&self) -> Option<&'p WrapperScriptConfig> {
185 self.run_wrapper.map(|(script, _)| script)
186 }
187
188 pub fn run_extra_args(&self) -> &'p [String] {
190 self.run_extra_args.0
191 }
192
193 pub fn retries(&self) -> RetryPolicy {
195 self.retries.0
196 }
197
198 pub fn slow_timeout(&self) -> SlowTimeout {
200 self.slow_timeout.0
201 }
202
203 pub fn leak_timeout(&self) -> LeakTimeout {
205 self.leak_timeout.0
206 }
207
208 pub fn test_group(&self) -> &TestGroup {
210 &self.test_group.0
211 }
212
213 pub fn success_output(&self) -> TestOutputDisplay {
215 self.success_output.0
216 }
217
218 pub fn failure_output(&self) -> TestOutputDisplay {
220 self.failure_output.0
221 }
222
223 pub fn junit_store_success_output(&self) -> bool {
225 self.junit_store_success_output.0
226 }
227
228 pub fn junit_store_failure_output(&self) -> bool {
230 self.junit_store_failure_output.0
231 }
232}
233
234#[expect(dead_code)]
235impl<'p, Source: Copy> TestSettings<'p, Source> {
236 pub(super) fn new(profile: &'p EvaluatableProfile<'_>, query: &TestQuery<'_>) -> Self
237 where
238 Source: TrackSource<'p>,
239 {
240 let ecx = profile.filterset_ecx();
241
242 let mut priority = None;
243 let mut threads_required = None;
244 let mut run_wrapper = None;
245 let mut run_extra_args = None;
246 let mut retries = None;
247 let mut slow_timeout = None;
248 let mut leak_timeout = None;
249 let mut test_group = None;
250 let mut success_output = None;
251 let mut failure_output = None;
252 let mut junit_store_success_output = None;
253 let mut junit_store_failure_output = None;
254
255 for override_ in &profile.compiled_data.overrides {
256 if !override_.state.host_eval {
257 continue;
258 }
259 if query.binary_query.platform == BuildPlatform::Host && !override_.state.host_test_eval
260 {
261 continue;
262 }
263 if query.binary_query.platform == BuildPlatform::Target && !override_.state.target_eval
264 {
265 continue;
266 }
267
268 if let Some(expr) = &override_.filter() {
269 if !expr.matches_test(query, &ecx) {
270 continue;
271 }
272 }
274
275 if priority.is_none() {
276 if let Some(p) = override_.data.priority {
277 priority = Some(Source::track_override(p, override_));
278 }
279 }
280 if threads_required.is_none() {
281 if let Some(t) = override_.data.threads_required {
282 threads_required = Some(Source::track_override(t, override_));
283 }
284 }
285 if run_extra_args.is_none() {
286 if let Some(r) = override_.data.run_extra_args.as_deref() {
287 run_extra_args = Some(Source::track_override(r, override_));
288 }
289 }
290 if retries.is_none() {
291 if let Some(r) = override_.data.retries {
292 retries = Some(Source::track_override(r, override_));
293 }
294 }
295 if slow_timeout.is_none() {
296 if let Some(s) = override_.data.slow_timeout {
297 slow_timeout = Some(Source::track_override(s, override_));
298 }
299 }
300 if leak_timeout.is_none() {
301 if let Some(l) = override_.data.leak_timeout {
302 leak_timeout = Some(Source::track_override(l, override_));
303 }
304 }
305 if test_group.is_none() {
306 if let Some(t) = &override_.data.test_group {
307 test_group = Some(Source::track_override(t.clone(), override_));
308 }
309 }
310 if success_output.is_none() {
311 if let Some(s) = override_.data.success_output {
312 success_output = Some(Source::track_override(s, override_));
313 }
314 }
315 if failure_output.is_none() {
316 if let Some(f) = override_.data.failure_output {
317 failure_output = Some(Source::track_override(f, override_));
318 }
319 }
320 if junit_store_success_output.is_none() {
321 if let Some(s) = override_.data.junit.store_success_output {
322 junit_store_success_output = Some(Source::track_override(s, override_));
323 }
324 }
325 if junit_store_failure_output.is_none() {
326 if let Some(f) = override_.data.junit.store_failure_output {
327 junit_store_failure_output = Some(Source::track_override(f, override_));
328 }
329 }
330 }
331
332 for override_ in &profile.compiled_data.scripts {
333 if !override_.is_enabled(query, &ecx) {
334 continue;
335 }
336
337 if run_wrapper.is_none() {
338 if let Some(wrapper) = &override_.run_wrapper {
339 run_wrapper = Some(Source::track_script(wrapper.clone(), override_));
340 }
341 }
342 }
343
344 let priority = priority.unwrap_or_else(|| Source::track_default(TestPriority::default()));
346 let threads_required =
347 threads_required.unwrap_or_else(|| Source::track_profile(profile.threads_required()));
348 let run_wrapper = run_wrapper.map(|wrapper| map_wrapper_script(profile, wrapper));
349 let run_extra_args =
350 run_extra_args.unwrap_or_else(|| Source::track_profile(profile.run_extra_args()));
351 let retries = retries.unwrap_or_else(|| Source::track_profile(profile.retries()));
352 let slow_timeout =
353 slow_timeout.unwrap_or_else(|| Source::track_profile(profile.slow_timeout()));
354 let leak_timeout =
355 leak_timeout.unwrap_or_else(|| Source::track_profile(profile.leak_timeout()));
356 let test_group = test_group.unwrap_or_else(|| Source::track_profile(TestGroup::Global));
357 let success_output =
358 success_output.unwrap_or_else(|| Source::track_profile(profile.success_output()));
359 let failure_output =
360 failure_output.unwrap_or_else(|| Source::track_profile(profile.failure_output()));
361 let junit_store_success_output = junit_store_success_output.unwrap_or_else(|| {
362 Source::track_profile(profile.junit().is_some_and(|j| j.store_success_output()))
364 });
365 let junit_store_failure_output = junit_store_failure_output.unwrap_or_else(|| {
366 Source::track_profile(profile.junit().is_some_and(|j| j.store_failure_output()))
368 });
369
370 TestSettings {
371 threads_required,
372 run_extra_args,
373 run_wrapper,
374 retries,
375 priority,
376 slow_timeout,
377 leak_timeout,
378 test_group,
379 success_output,
380 failure_output,
381 junit_store_success_output,
382 junit_store_failure_output,
383 }
384 }
385
386 pub(crate) fn threads_required_with_source(&self) -> (ThreadsRequired, Source) {
388 self.threads_required
389 }
390
391 pub(crate) fn retries_with_source(&self) -> (RetryPolicy, Source) {
393 self.retries
394 }
395
396 pub(crate) fn slow_timeout_with_source(&self) -> (SlowTimeout, Source) {
398 self.slow_timeout
399 }
400
401 pub(crate) fn leak_timeout_with_source(&self) -> (LeakTimeout, Source) {
403 self.leak_timeout
404 }
405
406 pub(crate) fn test_group_with_source(&self) -> &(TestGroup, Source) {
408 &self.test_group
409 }
410}
411
412fn map_wrapper_script<'p, Source>(
413 profile: &'p EvaluatableProfile<'_>,
414 (script, source): (ScriptId, Source),
415) -> (&'p WrapperScriptConfig, Source)
416where
417 Source: TrackSource<'p>,
418{
419 let wrapper_config = profile
420 .script_config()
421 .wrapper
422 .get(&script)
423 .unwrap_or_else(|| {
424 panic!(
425 "wrapper script {script} not found \
426 (should have been checked while reading config)"
427 )
428 });
429 (wrapper_config, source)
430}
431
432#[derive(Clone, Debug)]
433pub(super) struct CompiledByProfile {
434 pub(super) default: CompiledData<PreBuildPlatform>,
435 pub(super) other: HashMap<String, CompiledData<PreBuildPlatform>>,
436}
437
438impl CompiledByProfile {
439 pub(super) fn new(
440 pcx: &ParseContext<'_>,
441 config: &NextestConfigImpl,
442 ) -> Result<Self, ConfigParseErrorKind> {
443 let mut errors = vec![];
444 let default = CompiledData::new(
445 pcx,
446 "default",
447 Some(config.default_profile().default_filter()),
448 config.default_profile().overrides(),
449 config.default_profile().setup_scripts(),
450 &mut errors,
451 );
452 let other: HashMap<_, _> = config
453 .other_profiles()
454 .map(|(profile_name, profile)| {
455 (
456 profile_name.to_owned(),
457 CompiledData::new(
458 pcx,
459 profile_name,
460 profile.default_filter(),
461 profile.overrides(),
462 profile.scripts(),
463 &mut errors,
464 ),
465 )
466 })
467 .collect();
468
469 if errors.is_empty() {
470 Ok(Self { default, other })
471 } else {
472 Err(ConfigParseErrorKind::CompileErrors(errors))
473 }
474 }
475
476 pub(super) fn for_default_config() -> Self {
482 Self {
483 default: CompiledData {
484 profile_default_filter: Some(CompiledDefaultFilter::for_default_config()),
485 overrides: vec![],
486 scripts: vec![],
487 },
488 other: HashMap::new(),
489 }
490 }
491}
492
493#[derive(Clone, Debug)]
497pub struct CompiledDefaultFilter {
498 pub expr: CompiledExpr,
509
510 pub profile: String,
512
513 pub section: CompiledDefaultFilterSection,
515}
516
517impl CompiledDefaultFilter {
518 pub(crate) fn for_default_config() -> Self {
519 Self {
520 expr: CompiledExpr::ALL,
521 profile: NextestConfig::DEFAULT_PROFILE.to_owned(),
522 section: CompiledDefaultFilterSection::Profile,
523 }
524 }
525
526 pub fn display_config(&self, bold_style: Style) -> String {
528 match &self.section {
529 CompiledDefaultFilterSection::Profile => {
530 format!("profile.{}.default-filter", self.profile)
531 .style(bold_style)
532 .to_string()
533 }
534 CompiledDefaultFilterSection::Override(_) => {
535 format!(
536 "default-filter in {}",
537 format!("profile.{}.overrides", self.profile).style(bold_style)
538 )
539 }
540 }
541 }
542}
543
544#[derive(Clone, Copy, Debug)]
547pub enum CompiledDefaultFilterSection {
548 Profile,
550
551 Override(usize),
553}
554
555#[derive(Clone, Debug)]
556pub(super) struct CompiledData<State> {
557 pub(super) profile_default_filter: Option<CompiledDefaultFilter>,
562 pub(super) overrides: Vec<CompiledOverride<State>>,
563 pub(super) scripts: Vec<CompiledProfileScripts<State>>,
564}
565
566impl CompiledData<PreBuildPlatform> {
567 fn new(
568 pcx: &ParseContext<'_>,
569 profile_name: &str,
570 profile_default_filter: Option<&str>,
571 overrides: &[DeserializedOverride],
572 scripts: &[DeserializedProfileScriptConfig],
573 errors: &mut Vec<ConfigCompileError>,
574 ) -> Self {
575 let profile_default_filter =
576 profile_default_filter.and_then(|filter| {
577 match Filterset::parse(filter.to_owned(), pcx, FiltersetKind::DefaultFilter) {
578 Ok(expr) => Some(CompiledDefaultFilter {
579 expr: expr.compiled,
580 profile: profile_name.to_owned(),
581 section: CompiledDefaultFilterSection::Profile,
582 }),
583 Err(err) => {
584 errors.push(ConfigCompileError {
585 profile_name: profile_name.to_owned(),
586 section: ConfigCompileSection::DefaultFilter,
587 kind: ConfigCompileErrorKind::Parse {
588 host_parse_error: None,
589 target_parse_error: None,
590 filter_parse_errors: vec![err],
591 },
592 });
593 None
594 }
595 }
596 });
597
598 let overrides = overrides
599 .iter()
600 .enumerate()
601 .filter_map(|(index, source)| {
602 CompiledOverride::new(pcx, profile_name, index, source, errors)
603 })
604 .collect();
605 let scripts = scripts
606 .iter()
607 .enumerate()
608 .filter_map(|(index, source)| {
609 CompiledProfileScripts::new(pcx, profile_name, index, source, errors)
610 })
611 .collect();
612 Self {
613 profile_default_filter,
614 overrides,
615 scripts,
616 }
617 }
618
619 pub(super) fn extend_reverse(&mut self, other: Self) {
620 if other.profile_default_filter.is_some() {
622 self.profile_default_filter = other.profile_default_filter;
623 }
624 self.overrides.extend(other.overrides.into_iter().rev());
625 self.scripts.extend(other.scripts.into_iter().rev());
626 }
627
628 pub(super) fn reverse(&mut self) {
629 self.overrides.reverse();
630 self.scripts.reverse();
631 }
632
633 pub(super) fn chain(self, other: Self) -> Self {
635 let profile_default_filter = self.profile_default_filter.or(other.profile_default_filter);
636 let mut overrides = self.overrides;
637 let mut scripts = self.scripts;
638 overrides.extend(other.overrides);
639 scripts.extend(other.scripts);
640 Self {
641 profile_default_filter,
642 overrides,
643 scripts,
644 }
645 }
646
647 pub(super) fn apply_build_platforms(
648 self,
649 build_platforms: &BuildPlatforms,
650 ) -> CompiledData<FinalConfig> {
651 let profile_default_filter = self.profile_default_filter;
652 let overrides = self
653 .overrides
654 .into_iter()
655 .map(|override_| override_.apply_build_platforms(build_platforms))
656 .collect();
657 let setup_scripts = self
658 .scripts
659 .into_iter()
660 .map(|setup_script| setup_script.apply_build_platforms(build_platforms))
661 .collect();
662 CompiledData {
663 profile_default_filter,
664 overrides,
665 scripts: setup_scripts,
666 }
667 }
668}
669
670#[derive(Clone, Debug)]
671pub(crate) struct CompiledOverride<State> {
672 id: OverrideId,
673 state: State,
674 pub(super) data: ProfileOverrideData,
675}
676
677impl<State> CompiledOverride<State> {
678 pub(crate) fn id(&self) -> &OverrideId {
679 &self.id
680 }
681}
682
683#[derive(Clone, Debug, Eq, Hash, PartialEq)]
684pub(crate) struct OverrideId {
685 pub(crate) profile_name: SmolStr,
686 index: usize,
687}
688
689#[derive(Clone, Debug)]
690pub(super) struct ProfileOverrideData {
691 host_spec: MaybeTargetSpec,
692 target_spec: MaybeTargetSpec,
693 filter: Option<FilterOrDefaultFilter>,
694 priority: Option<TestPriority>,
695 threads_required: Option<ThreadsRequired>,
696 run_extra_args: Option<Vec<String>>,
697 retries: Option<RetryPolicy>,
698 slow_timeout: Option<SlowTimeout>,
699 leak_timeout: Option<LeakTimeout>,
700 pub(super) test_group: Option<TestGroup>,
701 success_output: Option<TestOutputDisplay>,
702 failure_output: Option<TestOutputDisplay>,
703 junit: DeserializedJunitOutput,
704}
705
706impl CompiledOverride<PreBuildPlatform> {
707 fn new(
708 pcx: &ParseContext<'_>,
709 profile_name: &str,
710 index: usize,
711 source: &DeserializedOverride,
712 errors: &mut Vec<ConfigCompileError>,
713 ) -> Option<Self> {
714 if source.platform.host.is_none()
715 && source.platform.target.is_none()
716 && source.filter.is_none()
717 {
718 errors.push(ConfigCompileError {
719 profile_name: profile_name.to_owned(),
720 section: ConfigCompileSection::Override(index),
721 kind: ConfigCompileErrorKind::ConstraintsNotSpecified {
722 default_filter_specified: source.default_filter.is_some(),
723 },
724 });
725 return None;
726 }
727
728 let host_spec = MaybeTargetSpec::new(source.platform.host.as_deref());
729 let target_spec = MaybeTargetSpec::new(source.platform.target.as_deref());
730 let filter = source.filter.as_ref().map_or(Ok(None), |filter| {
731 Some(Filterset::parse(filter.clone(), pcx, FiltersetKind::Test)).transpose()
732 });
733 let default_filter = source.default_filter.as_ref().map_or(Ok(None), |filter| {
734 Some(Filterset::parse(
735 filter.clone(),
736 pcx,
737 FiltersetKind::DefaultFilter,
738 ))
739 .transpose()
740 });
741
742 match (host_spec, target_spec, filter, default_filter) {
743 (Ok(host_spec), Ok(target_spec), Ok(filter), Ok(default_filter)) => {
744 let filter = match (filter, default_filter) {
746 (Some(_), Some(_)) => {
747 errors.push(ConfigCompileError {
748 profile_name: profile_name.to_owned(),
749 section: ConfigCompileSection::Override(index),
750 kind: ConfigCompileErrorKind::FilterAndDefaultFilterSpecified,
751 });
752 return None;
753 }
754 (Some(filter), None) => Some(FilterOrDefaultFilter::Filter(filter)),
755 (None, Some(default_filter)) => {
756 let compiled = CompiledDefaultFilter {
757 expr: default_filter.compiled,
758 profile: profile_name.to_owned(),
759 section: CompiledDefaultFilterSection::Override(index),
760 };
761 Some(FilterOrDefaultFilter::DefaultFilter(compiled))
762 }
763 (None, None) => None,
764 };
765
766 Some(Self {
767 id: OverrideId {
768 profile_name: profile_name.into(),
769 index,
770 },
771 state: PreBuildPlatform {},
772 data: ProfileOverrideData {
773 host_spec,
774 target_spec,
775 filter,
776 priority: source.priority,
777 threads_required: source.threads_required,
778 run_extra_args: source.run_extra_args.clone(),
779 retries: source.retries,
780 slow_timeout: source.slow_timeout,
781 leak_timeout: source.leak_timeout,
782 test_group: source.test_group.clone(),
783 success_output: source.success_output,
784 failure_output: source.failure_output,
785 junit: source.junit,
786 },
787 })
788 }
789 (maybe_host_err, maybe_target_err, maybe_filter_err, maybe_default_filter_err) => {
790 let host_parse_error = maybe_host_err.err();
791 let target_parse_error = maybe_target_err.err();
792 let filter_parse_errors = maybe_filter_err
793 .err()
794 .into_iter()
795 .chain(maybe_default_filter_err.err())
796 .collect();
797
798 errors.push(ConfigCompileError {
799 profile_name: profile_name.to_owned(),
800 section: ConfigCompileSection::Override(index),
801 kind: ConfigCompileErrorKind::Parse {
802 host_parse_error,
803 target_parse_error,
804 filter_parse_errors,
805 },
806 });
807 None
808 }
809 }
810 }
811
812 pub(super) fn apply_build_platforms(
813 self,
814 build_platforms: &BuildPlatforms,
815 ) -> CompiledOverride<FinalConfig> {
816 let host_eval = self.data.host_spec.eval(&build_platforms.host.platform);
817 let host_test_eval = self.data.target_spec.eval(&build_platforms.host.platform);
818 let target_eval = build_platforms
819 .target
820 .as_ref()
821 .map_or(host_test_eval, |target| {
822 self.data.target_spec.eval(&target.triple.platform)
823 });
824
825 CompiledOverride {
826 id: self.id,
827 state: FinalConfig {
828 host_eval,
829 host_test_eval,
830 target_eval,
831 },
832 data: self.data,
833 }
834 }
835}
836
837impl CompiledOverride<FinalConfig> {
838 pub(crate) fn target_spec(&self) -> &MaybeTargetSpec {
840 &self.data.target_spec
841 }
842
843 pub(crate) fn filter(&self) -> Option<&Filterset> {
845 match self.data.filter.as_ref() {
846 Some(FilterOrDefaultFilter::Filter(filter)) => Some(filter),
847 _ => None,
848 }
849 }
850
851 pub(crate) fn default_filter_if_matches_platform(&self) -> Option<&CompiledDefaultFilter> {
853 match self.data.filter.as_ref() {
854 Some(FilterOrDefaultFilter::DefaultFilter(filter)) => {
855 (self.state.host_eval && self.state.target_eval).then_some(filter)
862 }
863 _ => None,
864 }
865 }
866}
867
868#[derive(Clone, Debug, Default)]
870pub(crate) enum MaybeTargetSpec {
871 Provided(TargetSpec),
872 #[default]
873 Any,
874}
875
876impl MaybeTargetSpec {
877 pub(super) fn new(platform_str: Option<&str>) -> Result<Self, target_spec::Error> {
878 Ok(match platform_str {
879 Some(platform_str) => {
880 MaybeTargetSpec::Provided(TargetSpec::new(platform_str.to_owned())?)
881 }
882 None => MaybeTargetSpec::Any,
883 })
884 }
885
886 pub(super) fn eval(&self, platform: &Platform) -> bool {
887 match self {
888 MaybeTargetSpec::Provided(spec) => spec
889 .eval(platform)
890 .unwrap_or(true),
891 MaybeTargetSpec::Any => true,
892 }
893 }
894}
895
896#[derive(Clone, Debug)]
900pub(crate) enum FilterOrDefaultFilter {
901 Filter(Filterset),
902 DefaultFilter(CompiledDefaultFilter),
903}
904
905#[derive(Clone, Debug, Deserialize)]
907#[serde(rename_all = "kebab-case")]
908pub(super) struct DeserializedOverride {
909 #[serde(default)]
911 platform: PlatformStrings,
912 #[serde(default)]
914 filter: Option<String>,
915 #[serde(default)]
918 priority: Option<TestPriority>,
919 #[serde(default)]
920 default_filter: Option<String>,
921 #[serde(default)]
922 threads_required: Option<ThreadsRequired>,
923 #[serde(default)]
924 run_extra_args: Option<Vec<String>>,
925 #[serde(default, deserialize_with = "super::deserialize_retry_policy")]
926 retries: Option<RetryPolicy>,
927 #[serde(default, deserialize_with = "super::deserialize_slow_timeout")]
928 slow_timeout: Option<SlowTimeout>,
929 #[serde(default, deserialize_with = "super::deserialize_leak_timeout")]
930 leak_timeout: Option<LeakTimeout>,
931 #[serde(default)]
932 test_group: Option<TestGroup>,
933 #[serde(default)]
934 success_output: Option<TestOutputDisplay>,
935 #[serde(default)]
936 failure_output: Option<TestOutputDisplay>,
937 #[serde(default)]
938 junit: DeserializedJunitOutput,
939}
940
941#[derive(Copy, Clone, Debug, Default, Deserialize)]
942#[serde(rename_all = "kebab-case")]
943pub(super) struct DeserializedJunitOutput {
944 store_success_output: Option<bool>,
945 store_failure_output: Option<bool>,
946}
947
948#[derive(Clone, Debug, Default)]
949pub(super) struct PlatformStrings {
950 pub(super) host: Option<String>,
951 pub(super) target: Option<String>,
952}
953
954impl<'de> Deserialize<'de> for PlatformStrings {
955 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
956 struct V;
957
958 impl<'de2> serde::de::Visitor<'de2> for V {
959 type Value = PlatformStrings;
960
961 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
962 formatter.write_str(
963 "a table ({ host = \"x86_64-apple-darwin\", \
964 target = \"cfg(windows)\" }) \
965 or a string (\"x86_64-unknown-gnu-linux\")",
966 )
967 }
968
969 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
970 where
971 E: serde::de::Error,
972 {
973 Ok(PlatformStrings {
974 host: None,
975 target: Some(v.to_owned()),
976 })
977 }
978
979 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
980 where
981 A: serde::de::MapAccess<'de2>,
982 {
983 #[derive(Deserialize)]
984 struct PlatformStringsInner {
985 #[serde(default)]
986 host: Option<String>,
987 #[serde(default)]
988 target: Option<String>,
989 }
990
991 let inner = PlatformStringsInner::deserialize(
992 serde::de::value::MapAccessDeserializer::new(map),
993 )?;
994 Ok(PlatformStrings {
995 host: inner.host,
996 target: inner.target,
997 })
998 }
999 }
1000
1001 deserializer.deserialize_any(V)
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use crate::config::{LeakTimeoutResult, NextestConfig, test_helpers::*};
1009 use camino_tempfile::tempdir;
1010 use indoc::indoc;
1011 use std::{num::NonZeroUsize, time::Duration};
1012 use test_case::test_case;
1013
1014 #[test]
1016 fn test_overrides_basic() {
1017 let config_contents = indoc! {r#"
1018 # Override 1
1019 [[profile.default.overrides]]
1020 platform = 'aarch64-apple-darwin' # this is the target platform
1021 filter = "test(test)"
1022 retries = { backoff = "exponential", count = 20, delay = "1s", max-delay = "20s" }
1023 slow-timeout = { period = "120s", terminate-after = 1, grace-period = "0s" }
1024 success-output = "immediate-final"
1025 junit = { store-success-output = true }
1026
1027 # Override 2
1028 [[profile.default.overrides]]
1029 filter = "test(test)"
1030 threads-required = 8
1031 retries = 3
1032 slow-timeout = "60s"
1033 leak-timeout = "300ms"
1034 test-group = "my-group"
1035 failure-output = "final"
1036 junit = { store-failure-output = false }
1037
1038 # Override 3
1039 [[profile.default.overrides]]
1040 platform = { host = "cfg(unix)" }
1041 filter = "test(override3)"
1042 retries = 5
1043
1044 # Override 4 -- host not matched
1045 [[profile.default.overrides]]
1046 platform = { host = 'aarch64-apple-darwin' }
1047 retries = 10
1048
1049 # Override 5 -- no filter provided, just platform
1050 [[profile.default.overrides]]
1051 platform = { host = 'cfg(target_os = "linux")', target = 'aarch64-apple-darwin' }
1052 filter = "test(override5)"
1053 retries = 8
1054
1055 [profile.default.junit]
1056 path = "my-path.xml"
1057
1058 [test-groups.my-group]
1059 max-threads = 20
1060 "#};
1061
1062 let workspace_dir = tempdir().unwrap();
1063
1064 let graph = temp_workspace(&workspace_dir, config_contents);
1065 let package_id = graph.workspace().iter().next().unwrap().id();
1066
1067 let pcx = ParseContext::new(&graph);
1068
1069 let nextest_config_result = NextestConfig::from_sources(
1070 graph.workspace().root(),
1071 &pcx,
1072 None,
1073 &[][..],
1074 &Default::default(),
1075 )
1076 .expect("config is valid");
1077 let profile = nextest_config_result
1078 .profile("default")
1079 .expect("valid profile name")
1080 .apply_build_platforms(&build_platforms());
1081
1082 let host_binary_query =
1084 binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
1085 let query = TestQuery {
1086 binary_query: host_binary_query.to_query(),
1087 test_name: "test",
1088 };
1089 let overrides = profile.settings_for(&query);
1090
1091 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1092 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(3));
1093 assert_eq!(
1094 overrides.slow_timeout(),
1095 SlowTimeout {
1096 period: Duration::from_secs(60),
1097 terminate_after: None,
1098 grace_period: Duration::from_secs(10),
1099 }
1100 );
1101 assert_eq!(
1102 overrides.leak_timeout(),
1103 LeakTimeout {
1104 period: Duration::from_millis(300),
1105 result: LeakTimeoutResult::Pass,
1106 }
1107 );
1108 assert_eq!(overrides.test_group(), &test_group("my-group"));
1109 assert_eq!(overrides.success_output(), TestOutputDisplay::Never);
1110 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1111 #[expect(clippy::bool_assert_comparison)]
1113 {
1114 assert_eq!(overrides.junit_store_success_output(), false);
1115 assert_eq!(overrides.junit_store_failure_output(), false);
1116 }
1117
1118 let target_binary_query = binary_query(
1120 &graph,
1121 package_id,
1122 "lib",
1123 "my-binary",
1124 BuildPlatform::Target,
1125 );
1126 let query = TestQuery {
1127 binary_query: target_binary_query.to_query(),
1128 test_name: "test",
1129 };
1130 let overrides = profile.settings_for(&query);
1131
1132 assert_eq!(overrides.threads_required(), ThreadsRequired::Count(8));
1133 assert_eq!(
1134 overrides.retries(),
1135 RetryPolicy::Exponential {
1136 count: 20,
1137 delay: Duration::from_secs(1),
1138 jitter: false,
1139 max_delay: Some(Duration::from_secs(20)),
1140 }
1141 );
1142 assert_eq!(
1143 overrides.slow_timeout(),
1144 SlowTimeout {
1145 period: Duration::from_secs(120),
1146 terminate_after: Some(NonZeroUsize::new(1).unwrap()),
1147 grace_period: Duration::ZERO,
1148 }
1149 );
1150 assert_eq!(
1151 overrides.leak_timeout(),
1152 LeakTimeout {
1153 period: Duration::from_millis(300),
1154 result: LeakTimeoutResult::Pass,
1155 }
1156 );
1157 assert_eq!(overrides.test_group(), &test_group("my-group"));
1158 assert_eq!(
1159 overrides.success_output(),
1160 TestOutputDisplay::ImmediateFinal
1161 );
1162 assert_eq!(overrides.failure_output(), TestOutputDisplay::Final);
1163 #[expect(clippy::bool_assert_comparison)]
1165 {
1166 assert_eq!(overrides.junit_store_success_output(), true);
1167 assert_eq!(overrides.junit_store_failure_output(), false);
1168 }
1169
1170 let query = TestQuery {
1172 binary_query: target_binary_query.to_query(),
1173 test_name: "override3",
1174 };
1175 let overrides = profile.settings_for(&query);
1176 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(5));
1177
1178 let query = TestQuery {
1180 binary_query: target_binary_query.to_query(),
1181 test_name: "override5",
1182 };
1183 let overrides = profile.settings_for(&query);
1184 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(8));
1185
1186 let query = TestQuery {
1188 binary_query: target_binary_query.to_query(),
1189 test_name: "no_match",
1190 };
1191 let overrides = profile.settings_for(&query);
1192 assert_eq!(overrides.retries(), RetryPolicy::new_without_delay(0));
1193 }
1194
1195 #[test_case(
1196 indoc! {r#"
1197 [[profile.default.overrides]]
1198 retries = 2
1199 "#},
1200 "default",
1201 &[MietteJsonReport {
1202 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1203 labels: vec![],
1204 }]
1205
1206 ; "neither platform nor filter specified"
1207 )]
1208 #[test_case(
1209 indoc! {r#"
1210 [[profile.default.overrides]]
1211 default-filter = "test(test1)"
1212 retries = 2
1213 "#},
1214 "default",
1215 &[MietteJsonReport {
1216 message: "for override with `default-filter`, `platform` must also be specified".to_owned(),
1217 labels: vec![],
1218 }]
1219
1220 ; "default-filter without platform"
1221 )]
1222 #[test_case(
1223 indoc! {r#"
1224 [[profile.default.overrides]]
1225 platform = 'cfg(unix)'
1226 default-filter = "not default()"
1227 retries = 2
1228 "#},
1229 "default",
1230 &[MietteJsonReport {
1231 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1232 labels: vec![
1233 MietteJsonLabel {
1234 label: "this predicate causes infinite recursion".to_owned(),
1235 span: MietteJsonSpan { offset: 4, length: 9 },
1236 },
1237 ],
1238 }]
1239
1240 ; "default filterset in default-filter"
1241 )]
1242 #[test_case(
1243 indoc! {r#"
1244 [[profile.default.overrides]]
1245 filter = 'test(test1)'
1246 default-filter = "test(test2)"
1247 retries = 2
1248 "#},
1249 "default",
1250 &[MietteJsonReport {
1251 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1252 labels: vec![],
1253 }]
1254
1255 ; "both filter and default-filter specified"
1256 )]
1257 #[test_case(
1258 indoc! {r#"
1259 [[profile.default.overrides]]
1260 filter = 'test(test1)'
1261 platform = 'cfg(unix)'
1262 default-filter = "test(test2)"
1263 retries = 2
1264 "#},
1265 "default",
1266 &[MietteJsonReport {
1267 message: "at most one of `filter` and `default-filter` must be specified".to_owned(),
1268 labels: vec![],
1269 }]
1270
1271 ; "both filter and default-filter specified with platform"
1272 )]
1273 #[test_case(
1274 indoc! {r#"
1275 [[profile.default.overrides]]
1276 platform = {}
1277 retries = 2
1278 "#},
1279 "default",
1280 &[MietteJsonReport {
1281 message: "at least one of `platform` and `filter` must be specified".to_owned(),
1282 labels: vec![],
1283 }]
1284
1285 ; "empty platform map"
1286 )]
1287 #[test_case(
1288 indoc! {r#"
1289 [[profile.ci.overrides]]
1290 platform = 'cfg(target_os = "macos)'
1291 retries = 2
1292 "#},
1293 "ci",
1294 &[MietteJsonReport {
1295 message: "error parsing cfg() expression".to_owned(),
1296 labels: vec![
1297 MietteJsonLabel { label: "unclosed quotes".to_owned(), span: MietteJsonSpan { offset: 16, length: 6 } }
1298 ]
1299 }]
1300
1301 ; "invalid platform expression"
1302 )]
1303 #[test_case(
1304 indoc! {r#"
1305 [[profile.ci.overrides]]
1306 filter = 'test(/foo)'
1307 retries = 2
1308 "#},
1309 "ci",
1310 &[MietteJsonReport {
1311 message: "expected close regex".to_owned(),
1312 labels: vec![
1313 MietteJsonLabel { label: "missing `/`".to_owned(), span: MietteJsonSpan { offset: 9, length: 0 } }
1314 ]
1315 }]
1316
1317 ; "invalid filterset"
1318 )]
1319 #[test_case(
1320 indoc! {r#"
1322 [profile.ci]
1323 default-filter = "test(foo) or default()"
1324 "#},
1325 "ci",
1326 &[MietteJsonReport {
1327 message: "predicate not allowed in `default-filter` expressions".to_owned(),
1328 labels: vec![
1329 MietteJsonLabel { label: "this predicate causes infinite recursion".to_owned(), span: MietteJsonSpan { offset: 13, length: 9 } }
1330 ]
1331 }]
1332
1333 ; "default-filter with default"
1334 )]
1335 fn parse_overrides_invalid(
1336 config_contents: &str,
1337 faulty_profile: &str,
1338 expected_reports: &[MietteJsonReport],
1339 ) {
1340 let workspace_dir = tempdir().unwrap();
1341
1342 let graph = temp_workspace(&workspace_dir, config_contents);
1343 let pcx = ParseContext::new(&graph);
1344
1345 let err = NextestConfig::from_sources(
1346 graph.workspace().root(),
1347 &pcx,
1348 None,
1349 [],
1350 &Default::default(),
1351 )
1352 .expect_err("config is invalid");
1353 match err.kind() {
1354 ConfigParseErrorKind::CompileErrors(compile_errors) => {
1355 assert_eq!(
1356 compile_errors.len(),
1357 1,
1358 "exactly one override error must be produced"
1359 );
1360 let error = compile_errors.first().unwrap();
1361 assert_eq!(
1362 error.profile_name, faulty_profile,
1363 "compile error profile matches"
1364 );
1365 let handler = miette::JSONReportHandler::new();
1366 let reports = error
1367 .kind
1368 .reports()
1369 .map(|report| {
1370 let mut out = String::new();
1371 handler.render_report(&mut out, report.as_ref()).unwrap();
1372
1373 let json_report: MietteJsonReport = serde_json::from_str(&out)
1374 .unwrap_or_else(|err| {
1375 panic!(
1376 "failed to deserialize JSON message produced by miette: {err}"
1377 )
1378 });
1379 json_report
1380 })
1381 .collect::<Vec<_>>();
1382 assert_eq!(&reports, expected_reports, "reports match");
1383 }
1384 other => {
1385 panic!(
1386 "for config error {other:?}, expected ConfigParseErrorKind::FiltersetOrCfgParseError"
1387 );
1388 }
1389 };
1390 }
1391
1392 #[test]
1396 fn cfg_unix_with_custom_platform() {
1397 let config_contents = indoc! {r#"
1398 [[profile.default.overrides]]
1399 platform = { host = "cfg(unix)" }
1400 filter = "test(test)"
1401 retries = 5
1402 "#};
1403
1404 let workspace_dir = tempdir().unwrap();
1405
1406 let graph = temp_workspace(&workspace_dir, config_contents);
1407 let package_id = graph.workspace().iter().next().unwrap().id();
1408 let pcx = ParseContext::new(&graph);
1409
1410 let nextest_config = NextestConfig::from_sources(
1411 graph.workspace().root(),
1412 &pcx,
1413 None,
1414 &[][..],
1415 &Default::default(),
1416 )
1417 .expect("config is valid");
1418
1419 let build_platforms = custom_build_platforms(workspace_dir.path());
1420
1421 let profile = nextest_config
1422 .profile("default")
1423 .expect("valid profile name")
1424 .apply_build_platforms(&build_platforms);
1425
1426 let target_binary_query = binary_query(
1428 &graph,
1429 package_id,
1430 "lib",
1431 "my-binary",
1432 BuildPlatform::Target,
1433 );
1434 let query = TestQuery {
1435 binary_query: target_binary_query.to_query(),
1436 test_name: "test",
1437 };
1438 let overrides = profile.settings_for(&query);
1439 assert_eq!(
1440 overrides.retries(),
1441 RetryPolicy::new_without_delay(5),
1442 "retries applied to custom platform"
1443 );
1444 }
1445}