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: BTreeSet<_> = 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::Script(_) => {
87                        panic!("show-test-groups is not set via script section");
88                    }
89                    SettingSource::Profile | SettingSource::Default => {
90                        if let Some(non_overrides) = non_overrides.as_mut() {
91                            if settings.mode.matches_group(&TestGroup::Global) {
92                                non_overrides.insert(&suite.binary_id, test_name);
93                            }
94                        }
95                    }
96                }
97            }
98        }
99
100        Self {
101            test_list,
102            indexed_overrides,
103            test_group_config: profile.test_group_config(),
104            non_overrides,
105        }
106    }
107
108    fn should_show_group(&self, group: &TestGroup) -> bool {
109        // So this is a bit tricky. We want to show a group if it matches the filter.
110        //
111        //     group     filter    show-default   |   show?
112        //    -------   --------   -------------  |  -------
113        //    @global    matches       true       |   always
114        //    @global    matches      false       |   only if any overrides set @global
115        //    @global   no match         *        |   false  [1]
116        //     custom    matches         *        |   always
117        //     custom   no match         *        |   false  [1]
118        //
119        // [1]: filtered out by the constructor above, so not handled below
120
121        match (group, self.non_overrides.is_some()) {
122            (TestGroup::Global, true) => true,
123            (TestGroup::Global, false) => self
124                .indexed_overrides
125                .get(group)
126                .map(|override_map| !override_map.values().all(|data| data.is_empty()))
127                .unwrap_or(false),
128            _ => true,
129        }
130    }
131
132    /// Writes the test groups to the given writer in a human-friendly format.
133    pub fn write_human(&self, mut writer: &mut dyn WriteStr, colorize: bool) -> io::Result<()> {
134        static INDENT: &str = "      ";
135
136        let mut styles = Styles::default();
137        if colorize {
138            styles.colorize();
139        }
140
141        for (test_group, override_map) in &self.indexed_overrides {
142            if !self.should_show_group(test_group) {
143                continue;
144            }
145
146            write!(writer, "group: {}", test_group.style(styles.group))?;
147            if let TestGroup::Custom(group) = test_group {
148                write!(
149                    writer,
150                    " (max threads = {})",
151                    self.test_group_config[group]
152                        .max_threads
153                        .style(styles.max_threads)
154                )?;
155            }
156            writeln!(writer)?;
157
158            let mut any_printed = false;
159
160            for (override_id, data) in override_map {
161                any_printed = true;
162                write!(
163                    writer,
164                    "  * override for {} profile",
165                    override_id.profile_name.style(styles.profile),
166                )?;
167
168                if let Some(expr) = data.override_.filter() {
169                    write!(
170                        writer,
171                        " with filter {}",
172                        QuotedDisplay(&expr.parsed).style(styles.filter)
173                    )?;
174                }
175                if let MaybeTargetSpec::Provided(target_spec) = data.override_.target_spec() {
176                    write!(
177                        writer,
178                        " on platform {}",
179                        QuotedDisplay(target_spec).style(styles.platform)
180                    )?;
181                }
182
183                writeln!(writer, ":")?;
184
185                let mut inner_writer = indented(writer).with_str(INDENT);
186                self.test_list.write_human_with_filter(
187                    &data.matching_tests,
188                    &mut inner_writer,
189                    false,
190                    colorize,
191                )?;
192                inner_writer.write_str_flush()?;
193                writer = inner_writer.into_inner();
194            }
195
196            // Also show tests that don't match an override if they match the global config below.
197            if test_group == &TestGroup::Global {
198                if let Some(non_overrides) = &self.non_overrides {
199                    any_printed = true;
200                    writeln!(writer, "  * from default settings:")?;
201                    let mut inner_writer = indented(writer).with_str(INDENT);
202                    self.test_list.write_human_with_filter(
203                        non_overrides,
204                        &mut inner_writer,
205                        false,
206                        colorize,
207                    )?;
208                    inner_writer.write_str_flush()?;
209                    writer = inner_writer.into_inner();
210                }
211            }
212
213            if !any_printed {
214                writeln!(writer, "    (no matches)")?;
215            }
216        }
217
218        Ok(())
219    }
220}
221
222/// Settings for showing test groups.
223#[derive(Clone, Debug)]
224pub struct ShowTestGroupSettings {
225    /// Whether to show tests that have default settings and don't match any overrides.
226    pub show_default: bool,
227
228    /// Which groups of tests to show.
229    pub mode: ShowTestGroupsMode,
230}
231
232/// Which groups of tests to show.
233#[derive(Clone, Debug)]
234pub enum ShowTestGroupsMode {
235    /// Show all groups.
236    All,
237    /// Show only the named groups.
238    Only(ValidatedTestGroups),
239}
240
241impl ShowTestGroupsMode {
242    fn matches_group(&self, group: &TestGroup) -> bool {
243        match self {
244            Self::All => true,
245            Self::Only(groups) => groups.0.contains(group),
246        }
247    }
248}
249
250/// Validated test groups, part of [`ShowTestGroupsMode`].
251#[derive(Clone, Debug)]
252pub struct ValidatedTestGroups(BTreeSet<TestGroup>);
253
254impl ValidatedTestGroups {
255    /// Returns the set of test groups.
256    pub fn into_inner(self) -> BTreeSet<TestGroup> {
257        self.0
258    }
259}
260
261#[derive(Debug)]
262struct ShowTestGroupsData<'a> {
263    override_: &'a CompiledOverride<FinalConfig>,
264    matching_tests: TestListDisplayFilter<'a>,
265}
266
267impl<'a> ShowTestGroupsData<'a> {
268    fn new(override_: &'a CompiledOverride<FinalConfig>) -> Self {
269        Self {
270            override_,
271            matching_tests: TestListDisplayFilter::new(),
272        }
273    }
274
275    fn is_empty(&self) -> bool {
276        self.matching_tests.test_count() == 0
277    }
278}
279
280#[derive(Clone, Debug, Default)]
281struct Styles {
282    group: Style,
283    max_threads: Style,
284    profile: Style,
285    filter: Style,
286    platform: Style,
287}
288
289impl Styles {
290    fn colorize(&mut self) {
291        self.group = Style::new().bold().underline();
292        self.max_threads = Style::new().bold();
293        self.profile = Style::new().bold();
294        self.filter = Style::new().yellow();
295        self.platform = Style::new().yellow();
296    }
297}