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