nextest_runner/show_config/
test_groups.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    config::{
6        core::{EarlyProfile, EvaluatableProfile, FinalConfig},
7        elements::{CustomTestGroup, TestGroup, TestGroupConfig},
8        overrides::{CompiledOverride, MaybeTargetSpec, OverrideId, SettingSource},
9    },
10    errors::ShowTestGroupsError,
11    helpers::QuotedDisplay,
12    indenter::indented,
13    list::{TestInstance, TestList, TestListDisplayFilter},
14    write_str::WriteStr,
15};
16use indexmap::IndexMap;
17use owo_colors::{OwoColorize, Style};
18use std::{
19    collections::{BTreeMap, BTreeSet},
20    io,
21};
22
23/// Shows sets of tests that are in various groups.
24#[derive(Debug)]
25pub struct ShowTestGroups<'a> {
26    test_list: &'a TestList<'a>,
27    indexed_overrides: BTreeMap<TestGroup, IndexMap<OverrideId, ShowTestGroupsData<'a>>>,
28    test_group_config: &'a BTreeMap<CustomTestGroup, TestGroupConfig>,
29    // This is Some iff settings.show_default is true.
30    non_overrides: Option<TestListDisplayFilter<'a>>,
31}
32
33impl<'a> ShowTestGroups<'a> {
34    /// Validates that the given groups are known to this profile.
35    pub fn validate_groups(
36        profile: &EarlyProfile<'_>,
37        groups: impl IntoIterator<Item = TestGroup>,
38    ) -> Result<ValidatedTestGroups, ShowTestGroupsError> {
39        let groups: BTreeSet<_> = groups.into_iter().collect();
40        let known_groups: BTreeSet<_> =
41            TestGroup::make_all_groups(profile.test_group_config().keys().cloned()).collect();
42        let unknown_groups = &groups - &known_groups;
43        if !unknown_groups.is_empty() {
44            return Err(ShowTestGroupsError::UnknownGroups {
45                unknown_groups,
46                known_groups,
47            });
48        }
49        Ok(ValidatedTestGroups(groups))
50    }
51
52    /// Creates a new `ShowTestGroups` from the given profile and test list.
53    pub fn new(
54        profile: &'a EvaluatableProfile<'a>,
55        test_list: &'a TestList<'a>,
56        settings: &ShowTestGroupSettings,
57    ) -> Self {
58        let mut indexed_overrides: BTreeMap<_, _> =
59            TestGroup::make_all_groups(profile.test_group_config().keys().cloned())
60                .filter_map(|group| {
61                    settings
62                        .mode
63                        .matches_group(&group)
64                        .then(|| (group, IndexMap::new()))
65                })
66                .collect();
67        let mut non_overrides = settings.show_default.then(TestListDisplayFilter::new);
68
69        for suite in test_list.iter() {
70            for (test_name, test_case) in suite.status.test_cases() {
71                let test_instance = TestInstance::new(test_name, suite, test_case);
72                let query = test_instance.to_test_query();
73                let test_settings = profile.settings_with_source_for(&query);
74                let (test_group, source) = test_settings.test_group_with_source();
75
76                match source {
77                    SettingSource::Override(source) => {
78                        let override_map = match indexed_overrides.get_mut(test_group) {
79                            Some(override_map) => override_map,
80                            None => continue,
81                        };
82                        let data = override_map
83                            .entry(source.id().clone())
84                            .or_insert_with(|| ShowTestGroupsData::new(source));
85                        data.matching_tests.insert(&suite.binary_id, test_name);
86                    }
87                    SettingSource::Script(_) => {
88                        panic!("show-test-groups is not set via script section");
89                    }
90                    SettingSource::Profile | SettingSource::Default => {
91                        if let Some(non_overrides) = non_overrides.as_mut() {
92                            if settings.mode.matches_group(&TestGroup::Global) {
93                                non_overrides.insert(&suite.binary_id, test_name);
94                            }
95                        }
96                    }
97                }
98            }
99        }
100
101        Self {
102            test_list,
103            indexed_overrides,
104            test_group_config: profile.test_group_config(),
105            non_overrides,
106        }
107    }
108
109    fn should_show_group(&self, group: &TestGroup) -> bool {
110        // So this is a bit tricky. We want to show a group if it matches the filter.
111        //
112        //     group     filter    show-default   |   show?
113        //    -------   --------   -------------  |  -------
114        //    @global    matches       true       |   always
115        //    @global    matches      false       |   only if any overrides set @global
116        //    @global   no match         *        |   false  [1]
117        //     custom    matches         *        |   always
118        //     custom   no match         *        |   false  [1]
119        //
120        // [1]: filtered out by the constructor above, so not handled below
121
122        match (group, self.non_overrides.is_some()) {
123            (TestGroup::Global, true) => true,
124            (TestGroup::Global, false) => self
125                .indexed_overrides
126                .get(group)
127                .map(|override_map| !override_map.values().all(|data| data.is_empty()))
128                .unwrap_or(false),
129            _ => true,
130        }
131    }
132
133    /// Writes the test groups to the given writer in a human-friendly format.
134    pub fn write_human(&self, mut writer: &mut dyn WriteStr, colorize: bool) -> io::Result<()> {
135        static INDENT: &str = "      ";
136
137        let mut styles = Styles::default();
138        if colorize {
139            styles.colorize();
140        }
141
142        for (test_group, override_map) in &self.indexed_overrides {
143            if !self.should_show_group(test_group) {
144                continue;
145            }
146
147            write!(writer, "group: {}", test_group.style(styles.group))?;
148            if let TestGroup::Custom(group) = test_group {
149                write!(
150                    writer,
151                    " (max threads = {})",
152                    self.test_group_config[group]
153                        .max_threads
154                        .style(styles.max_threads)
155                )?;
156            }
157            writeln!(writer)?;
158
159            let mut any_printed = false;
160
161            for (override_id, data) in override_map {
162                any_printed = true;
163                write!(
164                    writer,
165                    "  * override for {} profile",
166                    override_id.profile_name.style(styles.profile),
167                )?;
168
169                if let Some(expr) = data.override_.filter() {
170                    write!(
171                        writer,
172                        " with filter {}",
173                        QuotedDisplay(&expr.parsed).style(styles.filter)
174                    )?;
175                }
176                if let MaybeTargetSpec::Provided(target_spec) = data.override_.target_spec() {
177                    write!(
178                        writer,
179                        " on platform {}",
180                        QuotedDisplay(target_spec).style(styles.platform)
181                    )?;
182                }
183
184                writeln!(writer, ":")?;
185
186                let mut inner_writer = indented(writer).with_str(INDENT);
187                self.test_list.write_human_with_filter(
188                    &data.matching_tests,
189                    &mut inner_writer,
190                    false,
191                    colorize,
192                )?;
193                inner_writer.write_str_flush()?;
194                writer = inner_writer.into_inner();
195            }
196
197            // Also show tests that don't match an override if they match the global config below.
198            if test_group == &TestGroup::Global {
199                if let Some(non_overrides) = &self.non_overrides {
200                    any_printed = true;
201                    writeln!(writer, "  * from default settings:")?;
202                    let mut inner_writer = indented(writer).with_str(INDENT);
203                    self.test_list.write_human_with_filter(
204                        non_overrides,
205                        &mut inner_writer,
206                        false,
207                        colorize,
208                    )?;
209                    inner_writer.write_str_flush()?;
210                    writer = inner_writer.into_inner();
211                }
212            }
213
214            if !any_printed {
215                writeln!(writer, "    (no matches)")?;
216            }
217        }
218
219        Ok(())
220    }
221}
222
223/// Settings for showing test groups.
224#[derive(Clone, Debug)]
225pub struct ShowTestGroupSettings {
226    /// Whether to show tests that have default settings and don't match any overrides.
227    pub show_default: bool,
228
229    /// Which groups of tests to show.
230    pub mode: ShowTestGroupsMode,
231}
232
233/// Which groups of tests to show.
234#[derive(Clone, Debug)]
235pub enum ShowTestGroupsMode {
236    /// Show all groups.
237    All,
238    /// Show only the named groups.
239    Only(ValidatedTestGroups),
240}
241
242impl ShowTestGroupsMode {
243    fn matches_group(&self, group: &TestGroup) -> bool {
244        match self {
245            Self::All => true,
246            Self::Only(groups) => groups.0.contains(group),
247        }
248    }
249}
250
251/// Validated test groups, part of [`ShowTestGroupsMode`].
252#[derive(Clone, Debug)]
253pub struct ValidatedTestGroups(BTreeSet<TestGroup>);
254
255impl ValidatedTestGroups {
256    /// Returns the set of test groups.
257    pub fn into_inner(self) -> BTreeSet<TestGroup> {
258        self.0
259    }
260}
261
262#[derive(Debug)]
263struct ShowTestGroupsData<'a> {
264    override_: &'a CompiledOverride<FinalConfig>,
265    matching_tests: TestListDisplayFilter<'a>,
266}
267
268impl<'a> ShowTestGroupsData<'a> {
269    fn new(override_: &'a CompiledOverride<FinalConfig>) -> Self {
270        Self {
271            override_,
272            matching_tests: TestListDisplayFilter::new(),
273        }
274    }
275
276    fn is_empty(&self) -> bool {
277        self.matching_tests.test_count() == 0
278    }
279}
280
281#[derive(Clone, Debug, Default)]
282struct Styles {
283    group: Style,
284    max_threads: Style,
285    profile: Style,
286    filter: Style,
287    platform: Style,
288}
289
290impl Styles {
291    fn colorize(&mut self) {
292        self.group = Style::new().bold().underline();
293        self.max_threads = Style::new().bold();
294        self.profile = Style::new().bold();
295        self.filter = Style::new().yellow();
296        self.platform = Style::new().yellow();
297    }
298}