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