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 guppy::graph::cargo::BuildPlatform;
65    use nextest_filtering::{ParseContext, TestQuery};
66
67    #[test]
68    fn parse_tool_config_file() {
69        cfg_if::cfg_if! {
70            if #[cfg(windows)] {
71                let valid = ["tool:C:\\foo\\bar", "tool:\\\\?\\C:\\foo\\bar"];
72                let invalid = ["C:\\foo\\bar", "tool:\\foo\\bar", "tool:", ":/foo/bar"];
73            } else {
74                let valid = ["tool:/foo/bar"];
75                let invalid = ["/foo/bar", "tool:", ":/foo/bar", "tool:foo/bar"];
76            }
77        }
78
79        for valid_input in valid {
80            valid_input.parse::<ToolConfigFile>().unwrap_or_else(|err| {
81                panic!("valid input {valid_input} should parse correctly: {err}")
82            });
83        }
84
85        for invalid_input in invalid {
86            invalid_input
87                .parse::<ToolConfigFile>()
88                .expect_err(&format!("invalid input {invalid_input} should error out"));
89        }
90    }
91
92    #[test]
93    fn tool_config_basic() {
94        let config_contents = r#"
95        nextest-version = "0.9.50"
96
97        [profile.default]
98        retries = 3
99
100        [[profile.default.overrides]]
101        filter = 'test(test_foo)'
102        retries = 20
103        test-group = 'foo'
104
105        [[profile.default.overrides]]
106        filter = 'test(test_quux)'
107        test-group = '@tool:tool1:group1'
108
109        [test-groups.foo]
110        max-threads = 2
111        "#;
112
113        let tool1_config_contents = r#"
114        nextest-version = { required = "0.9.51", recommended = "0.9.52" }
115
116        [profile.default]
117        retries = 4
118
119        [[profile.default.overrides]]
120        filter = 'test(test_bar)'
121        retries = 21
122
123        [profile.tool]
124        retries = 12
125
126        [[profile.tool.overrides]]
127        filter = 'test(test_baz)'
128        retries = 22
129        test-group = '@tool:tool1:group1'
130
131        [[profile.tool.overrides]]
132        filter = 'test(test_quux)'
133        retries = 22
134        test-group = '@tool:tool2:group2'
135
136        [test-groups.'@tool:tool1:group1']
137        max-threads = 2
138        "#;
139
140        let tool2_config_contents = r#"
141        nextest-version = { recommended = "0.9.49" }
142
143        [profile.default]
144        retries = 5
145
146        [[profile.default.overrides]]
147        filter = 'test(test_)'
148        retries = 23
149
150        [profile.tool]
151        retries = 16
152
153        [[profile.tool.overrides]]
154        filter = 'test(test_ba)'
155        retries = 24
156        test-group = '@tool:tool2:group2'
157
158        [[profile.tool.overrides]]
159        filter = 'test(test_)'
160        retries = 25
161        test-group = '@global'
162
163        [profile.tool2]
164        retries = 18
165
166        [[profile.tool2.overrides]]
167        filter = 'all()'
168        retries = 26
169
170        [test-groups.'@tool:tool2:group2']
171        max-threads = 4
172        "#;
173
174        let workspace_dir = tempdir().unwrap();
175
176        let graph = temp_workspace(workspace_dir.path(), config_contents);
177        let workspace_root = graph.workspace().root();
178        let tool1_path = workspace_root.join(".config/tool1.toml");
179        let tool2_path = workspace_root.join(".config/tool2.toml");
180        std::fs::write(&tool1_path, tool1_config_contents).unwrap();
181        std::fs::write(&tool2_path, tool2_config_contents).unwrap();
182
183        let tool_config_files = [
184            ToolConfigFile {
185                tool: "tool1".to_owned(),
186                config_file: tool1_path,
187            },
188            ToolConfigFile {
189                tool: "tool2".to_owned(),
190                config_file: tool2_path,
191            },
192        ];
193
194        let version_only_config =
195            VersionOnlyConfig::from_sources(workspace_root, None, &tool_config_files).unwrap();
196        let nextest_version = version_only_config.nextest_version();
197        assert_eq!(
198            nextest_version,
199            &NextestVersionConfig {
200                required: NextestVersionReq::Version {
201                    version: "0.9.51".parse().unwrap(),
202                    tool: Some("tool1".to_owned())
203                },
204                recommended: NextestVersionReq::Version {
205                    version: "0.9.52".parse().unwrap(),
206                    tool: Some("tool1".to_owned())
207                }
208            },
209        );
210
211        let pcx = ParseContext::new(&graph);
212        let config = NextestConfig::from_sources(
213            workspace_root,
214            &pcx,
215            None,
216            &tool_config_files,
217            &Default::default(),
218        )
219        .expect("config is valid");
220
221        let default_profile = config
222            .profile(NextestConfig::DEFAULT_PROFILE)
223            .expect("default profile is present")
224            .apply_build_platforms(&build_platforms());
225        // This is present in .config/nextest.toml and is the highest priority
226        assert_eq!(default_profile.retries(), RetryPolicy::new_without_delay(3));
227
228        let package_id = graph.workspace().iter().next().unwrap().id();
229
230        let binary_query = binary_query(
231            &graph,
232            package_id,
233            "lib",
234            "my-binary",
235            BuildPlatform::Target,
236        );
237        let test_foo_query = TestQuery {
238            binary_query: binary_query.to_query(),
239            test_name: "test_foo",
240        };
241        let test_bar_query = TestQuery {
242            binary_query: binary_query.to_query(),
243            test_name: "test_bar",
244        };
245        let test_baz_query = TestQuery {
246            binary_query: binary_query.to_query(),
247            test_name: "test_baz",
248        };
249        let test_quux_query = TestQuery {
250            binary_query: binary_query.to_query(),
251            test_name: "test_quux",
252        };
253
254        assert_eq!(
255            default_profile.settings_for(&test_foo_query).retries(),
256            RetryPolicy::new_without_delay(20),
257            "retries for test_foo/default profile"
258        );
259        assert_eq!(
260            default_profile.settings_for(&test_foo_query).test_group(),
261            &test_group("foo"),
262            "test_group for test_foo/default profile"
263        );
264        assert_eq!(
265            default_profile.settings_for(&test_bar_query).retries(),
266            RetryPolicy::new_without_delay(21),
267            "retries for test_bar/default profile"
268        );
269        assert_eq!(
270            default_profile.settings_for(&test_bar_query).test_group(),
271            &TestGroup::Global,
272            "test_group for test_bar/default profile"
273        );
274        assert_eq!(
275            default_profile.settings_for(&test_baz_query).retries(),
276            RetryPolicy::new_without_delay(23),
277            "retries for test_baz/default profile"
278        );
279        assert_eq!(
280            default_profile.settings_for(&test_quux_query).test_group(),
281            &test_group("@tool:tool1:group1"),
282            "test group for test_quux/default profile"
283        );
284
285        let tool_profile = config
286            .profile("tool")
287            .expect("tool profile is present")
288            .apply_build_platforms(&build_platforms());
289        assert_eq!(tool_profile.retries(), RetryPolicy::new_without_delay(12));
290        assert_eq!(
291            tool_profile.settings_for(&test_foo_query).retries(),
292            RetryPolicy::new_without_delay(25),
293            "retries for test_foo/default profile"
294        );
295        assert_eq!(
296            tool_profile.settings_for(&test_bar_query).retries(),
297            RetryPolicy::new_without_delay(24),
298            "retries for test_bar/default profile"
299        );
300        assert_eq!(
301            tool_profile.settings_for(&test_baz_query).retries(),
302            RetryPolicy::new_without_delay(22),
303            "retries for test_baz/default profile"
304        );
305
306        let tool2_profile = config
307            .profile("tool2")
308            .expect("tool2 profile is present")
309            .apply_build_platforms(&build_platforms());
310        assert_eq!(tool2_profile.retries(), RetryPolicy::new_without_delay(18));
311        assert_eq!(
312            tool2_profile.settings_for(&test_foo_query).retries(),
313            RetryPolicy::new_without_delay(26),
314            "retries for test_foo/default profile"
315        );
316        assert_eq!(
317            tool2_profile.settings_for(&test_bar_query).retries(),
318            RetryPolicy::new_without_delay(26),
319            "retries for test_bar/default profile"
320        );
321        assert_eq!(
322            tool2_profile.settings_for(&test_baz_query).retries(),
323            RetryPolicy::new_without_delay(26),
324            "retries for test_baz/default profile"
325        );
326    }
327}