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