nextest_runner/config/
tool_config.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::errors::ToolConfigFileParseError;
5use camino::{Utf8Path, Utf8PathBuf};
6use std::str::FromStr;
7
8/// A tool-specific config file.
9///
10/// Tool-specific config files are lower priority than repository configs, but higher priority than
11/// the default config shipped with nextest.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct ToolConfigFile {
14    /// The name of the tool.
15    pub tool: String,
16
17    /// The path to the config file.
18    pub config_file: Utf8PathBuf,
19}
20
21impl FromStr for ToolConfigFile {
22    type Err = ToolConfigFileParseError;
23
24    fn from_str(input: &str) -> Result<Self, Self::Err> {
25        match input.split_once(':') {
26            Some((tool, config_file)) => {
27                if tool.is_empty() {
28                    Err(ToolConfigFileParseError::EmptyToolName {
29                        input: input.to_owned(),
30                    })
31                } else if config_file.is_empty() {
32                    Err(ToolConfigFileParseError::EmptyConfigFile {
33                        input: input.to_owned(),
34                    })
35                } else {
36                    let config_file = Utf8Path::new(config_file);
37                    if config_file.is_absolute() {
38                        Ok(Self {
39                            tool: tool.to_owned(),
40                            config_file: Utf8PathBuf::from(config_file),
41                        })
42                    } else {
43                        Err(ToolConfigFileParseError::ConfigFileNotAbsolute {
44                            config_file: config_file.to_owned(),
45                        })
46                    }
47                }
48            }
49            None => Err(ToolConfigFileParseError::InvalidFormat {
50                input: input.to_owned(),
51            }),
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::config::{
60        NextestConfig, NextestVersionConfig, NextestVersionReq, RetryPolicy, TestGroup,
61        VersionOnlyConfig, test_helpers::*,
62    };
63    use camino_tempfile::tempdir;
64    use camino_tempfile_ext::prelude::*;
65    use guppy::graph::cargo::BuildPlatform;
66    use nextest_filtering::{ParseContext, TestQuery};
67
68    #[test]
69    fn parse_tool_config_file() {
70        cfg_if::cfg_if! {
71            if #[cfg(windows)] {
72                let valid = ["tool:C:\\foo\\bar", "tool:\\\\?\\C:\\foo\\bar"];
73                let invalid = ["C:\\foo\\bar", "tool:\\foo\\bar", "tool:", ":/foo/bar"];
74            } else {
75                let valid = ["tool:/foo/bar"];
76                let invalid = ["/foo/bar", "tool:", ":/foo/bar", "tool:foo/bar"];
77            }
78        }
79
80        for valid_input in valid {
81            valid_input.parse::<ToolConfigFile>().unwrap_or_else(|err| {
82                panic!("valid input {valid_input} should parse correctly: {err}")
83            });
84        }
85
86        for invalid_input in invalid {
87            invalid_input
88                .parse::<ToolConfigFile>()
89                .expect_err(&format!("invalid input {invalid_input} should error out"));
90        }
91    }
92
93    #[test]
94    fn tool_config_basic() {
95        let config_contents = r#"
96        nextest-version = "0.9.50"
97
98        [profile.default]
99        retries = 3
100
101        [[profile.default.overrides]]
102        filter = 'test(test_foo)'
103        retries = 20
104        test-group = 'foo'
105
106        [[profile.default.overrides]]
107        filter = 'test(test_quux)'
108        test-group = '@tool:tool1:group1'
109
110        [test-groups.foo]
111        max-threads = 2
112        "#;
113
114        let tool1_config_contents = r#"
115        nextest-version = { required = "0.9.51", recommended = "0.9.52" }
116
117        [profile.default]
118        retries = 4
119
120        [[profile.default.overrides]]
121        filter = 'test(test_bar)'
122        retries = 21
123
124        [profile.tool]
125        retries = 12
126
127        [[profile.tool.overrides]]
128        filter = 'test(test_baz)'
129        retries = 22
130        test-group = '@tool:tool1:group1'
131
132        [[profile.tool.overrides]]
133        filter = 'test(test_quux)'
134        retries = 22
135        test-group = '@tool:tool2:group2'
136
137        [test-groups.'@tool:tool1:group1']
138        max-threads = 2
139        "#;
140
141        let tool2_config_contents = r#"
142        nextest-version = { recommended = "0.9.49" }
143
144        [profile.default]
145        retries = 5
146
147        [[profile.default.overrides]]
148        filter = 'test(test_)'
149        retries = 23
150
151        [profile.tool]
152        retries = 16
153
154        [[profile.tool.overrides]]
155        filter = 'test(test_ba)'
156        retries = 24
157        test-group = '@tool:tool2:group2'
158
159        [[profile.tool.overrides]]
160        filter = 'test(test_)'
161        retries = 25
162        test-group = '@global'
163
164        [profile.tool2]
165        retries = 18
166
167        [[profile.tool2.overrides]]
168        filter = 'all()'
169        retries = 26
170
171        [test-groups.'@tool:tool2:group2']
172        max-threads = 4
173        "#;
174
175        let workspace_dir = tempdir().unwrap();
176
177        let graph = temp_workspace(&workspace_dir, config_contents);
178        let tool1_path = workspace_dir.child(".config/tool1.toml");
179        let tool2_path = workspace_dir.child(".config/tool2.toml");
180        tool1_path.write_str(tool1_config_contents).unwrap();
181        tool2_path.write_str(tool2_config_contents).unwrap();
182
183        let workspace_root = graph.workspace().root();
184
185        let tool_config_files = [
186            ToolConfigFile {
187                tool: "tool1".to_owned(),
188                config_file: tool1_path.to_path_buf(),
189            },
190            ToolConfigFile {
191                tool: "tool2".to_owned(),
192                config_file: tool2_path.to_path_buf(),
193            },
194        ];
195
196        let version_only_config =
197            VersionOnlyConfig::from_sources(workspace_root, None, &tool_config_files).unwrap();
198        let nextest_version = version_only_config.nextest_version();
199        assert_eq!(
200            nextest_version,
201            &NextestVersionConfig {
202                required: NextestVersionReq::Version {
203                    version: "0.9.51".parse().unwrap(),
204                    tool: Some("tool1".to_owned())
205                },
206                recommended: NextestVersionReq::Version {
207                    version: "0.9.52".parse().unwrap(),
208                    tool: Some("tool1".to_owned())
209                }
210            },
211        );
212
213        let pcx = ParseContext::new(&graph);
214        let config = NextestConfig::from_sources(
215            workspace_root,
216            &pcx,
217            None,
218            &tool_config_files,
219            &Default::default(),
220        )
221        .expect("config is valid");
222
223        let default_profile = config
224            .profile(NextestConfig::DEFAULT_PROFILE)
225            .expect("default profile is present")
226            .apply_build_platforms(&build_platforms());
227        // This is present in .config/nextest.toml and is the highest priority
228        assert_eq!(default_profile.retries(), RetryPolicy::new_without_delay(3));
229
230        let package_id = graph.workspace().iter().next().unwrap().id();
231
232        let binary_query = binary_query(
233            &graph,
234            package_id,
235            "lib",
236            "my-binary",
237            BuildPlatform::Target,
238        );
239        let test_foo_query = TestQuery {
240            binary_query: binary_query.to_query(),
241            test_name: "test_foo",
242        };
243        let test_bar_query = TestQuery {
244            binary_query: binary_query.to_query(),
245            test_name: "test_bar",
246        };
247        let test_baz_query = TestQuery {
248            binary_query: binary_query.to_query(),
249            test_name: "test_baz",
250        };
251        let test_quux_query = TestQuery {
252            binary_query: binary_query.to_query(),
253            test_name: "test_quux",
254        };
255
256        assert_eq!(
257            default_profile.settings_for(&test_foo_query).retries(),
258            RetryPolicy::new_without_delay(20),
259            "retries for test_foo/default profile"
260        );
261        assert_eq!(
262            default_profile.settings_for(&test_foo_query).test_group(),
263            &test_group("foo"),
264            "test_group for test_foo/default profile"
265        );
266        assert_eq!(
267            default_profile.settings_for(&test_bar_query).retries(),
268            RetryPolicy::new_without_delay(21),
269            "retries for test_bar/default profile"
270        );
271        assert_eq!(
272            default_profile.settings_for(&test_bar_query).test_group(),
273            &TestGroup::Global,
274            "test_group for test_bar/default profile"
275        );
276        assert_eq!(
277            default_profile.settings_for(&test_baz_query).retries(),
278            RetryPolicy::new_without_delay(23),
279            "retries for test_baz/default profile"
280        );
281        assert_eq!(
282            default_profile.settings_for(&test_quux_query).test_group(),
283            &test_group("@tool:tool1:group1"),
284            "test group for test_quux/default profile"
285        );
286
287        let tool_profile = config
288            .profile("tool")
289            .expect("tool profile is present")
290            .apply_build_platforms(&build_platforms());
291        assert_eq!(tool_profile.retries(), RetryPolicy::new_without_delay(12));
292        assert_eq!(
293            tool_profile.settings_for(&test_foo_query).retries(),
294            RetryPolicy::new_without_delay(25),
295            "retries for test_foo/default profile"
296        );
297        assert_eq!(
298            tool_profile.settings_for(&test_bar_query).retries(),
299            RetryPolicy::new_without_delay(24),
300            "retries for test_bar/default profile"
301        );
302        assert_eq!(
303            tool_profile.settings_for(&test_baz_query).retries(),
304            RetryPolicy::new_without_delay(22),
305            "retries for test_baz/default profile"
306        );
307
308        let tool2_profile = config
309            .profile("tool2")
310            .expect("tool2 profile is present")
311            .apply_build_platforms(&build_platforms());
312        assert_eq!(tool2_profile.retries(), RetryPolicy::new_without_delay(18));
313        assert_eq!(
314            tool2_profile.settings_for(&test_foo_query).retries(),
315            RetryPolicy::new_without_delay(26),
316            "retries for test_foo/default profile"
317        );
318        assert_eq!(
319            tool2_profile.settings_for(&test_bar_query).retries(),
320            RetryPolicy::new_without_delay(26),
321            "retries for test_bar/default profile"
322        );
323        assert_eq!(
324            tool2_profile.settings_for(&test_baz_query).retries(),
325            RetryPolicy::new_without_delay(26),
326            "retries for test_baz/default profile"
327        );
328    }
329}