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