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