nextest_runner/config/core/
tool_config.rs1use super::ToolName;
5use crate::errors::ToolConfigFileParseError;
6use camino::{Utf8Path, Utf8PathBuf};
7use std::str::FromStr;
8
9#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct ToolConfigFile {
15 pub tool: ToolName,
17
18 pub config_file: Utf8PathBuf,
20}
21
22impl FromStr for ToolConfigFile {
23 type Err = ToolConfigFileParseError;
24
25 fn from_str(input: &str) -> Result<Self, Self::Err> {
26 match input.split_once(':') {
27 Some((tool, config_file)) => {
28 let tool = ToolName::new(tool.into()).map_err(|error| {
29 ToolConfigFileParseError::InvalidToolName {
30 input: input.to_owned(),
31 error,
32 }
33 })?;
34 if config_file.is_empty() {
35 Err(ToolConfigFileParseError::EmptyConfigFile {
36 input: input.to_owned(),
37 })
38 } else {
39 let config_file = Utf8Path::new(config_file);
40 if config_file.is_absolute() {
41 Ok(Self {
42 tool,
43 config_file: Utf8PathBuf::from(config_file),
44 })
45 } else {
46 Err(ToolConfigFileParseError::ConfigFileNotAbsolute {
47 config_file: config_file.to_owned(),
48 })
49 }
50 }
51 }
52 None => Err(ToolConfigFileParseError::InvalidFormat {
53 input: input.to_owned(),
54 }),
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::{
63 config::{
64 core::{NextestConfig, NextestVersionConfig, NextestVersionReq, VersionOnlyConfig},
65 elements::{RetryPolicy, TestGroup},
66 utils::test_helpers::*,
67 },
68 run_mode::NextestRunMode,
69 };
70 use camino_tempfile::tempdir;
71 use camino_tempfile_ext::prelude::*;
72 use guppy::graph::cargo::BuildPlatform;
73 use nextest_filtering::{ParseContext, TestQuery};
74 use nextest_metadata::TestCaseName;
75
76 fn tool_name(s: &str) -> ToolName {
77 ToolName::new(s.into()).unwrap()
78 }
79
80 #[test]
81 fn parse_tool_config_file() {
82 use crate::errors::{InvalidToolName, ToolConfigFileParseError};
83
84 cfg_if::cfg_if! {
85 if #[cfg(windows)] {
86 let valid = ["tool:C:\\foo\\bar", "tool:\\\\?\\C:\\foo\\bar"];
87 } else {
88 let valid = ["tool:/foo/bar"];
89 }
90 }
91
92 for valid_input in valid {
93 valid_input.parse::<ToolConfigFile>().unwrap_or_else(|err| {
94 panic!("valid input {valid_input} should parse correctly: {err}")
95 });
96 }
97
98 cfg_if::cfg_if! {
99 if #[cfg(windows)] {
100 let invalid: &[(&str, ToolConfigFileParseError)] = &[
101 ("no-colon-here", ToolConfigFileParseError::InvalidFormat {
102 input: "no-colon-here".to_owned(),
103 }),
104 ("tool:\\foo\\bar", ToolConfigFileParseError::ConfigFileNotAbsolute {
105 config_file: "\\foo\\bar".into(),
106 }),
107 ("tool:foo/bar", ToolConfigFileParseError::ConfigFileNotAbsolute {
108 config_file: "foo/bar".into(),
109 }),
110 ];
111 } else {
112 let invalid: &[(&str, ToolConfigFileParseError)] = &[
113 ("/foo/bar", ToolConfigFileParseError::InvalidFormat {
114 input: "/foo/bar".to_owned(),
115 }),
116 ("tool:foo/bar", ToolConfigFileParseError::ConfigFileNotAbsolute {
117 config_file: "foo/bar".into(),
118 }),
119 ("tool:./foo", ToolConfigFileParseError::ConfigFileNotAbsolute {
120 config_file: "./foo".into(),
121 }),
122 ];
123 }
124 }
125
126 let common_invalid: &[(&str, ToolConfigFileParseError)] = &[
128 (
129 ":/foo/bar",
130 ToolConfigFileParseError::InvalidToolName {
131 input: ":/foo/bar".to_owned(),
132 error: InvalidToolName::Empty,
133 },
134 ),
135 (
136 "_invalid:/foo/bar",
137 ToolConfigFileParseError::InvalidToolName {
138 input: "_invalid:/foo/bar".to_owned(),
139 error: InvalidToolName::InvalidXid("_invalid".into()),
140 },
141 ),
142 (
144 "@tool:/path",
145 ToolConfigFileParseError::InvalidToolName {
146 input: "@tool:/path".to_owned(),
147 error: InvalidToolName::StartsWithToolPrefix("@tool".into()),
148 },
149 ),
150 (
151 "tool:",
152 ToolConfigFileParseError::EmptyConfigFile {
153 input: "tool:".to_owned(),
154 },
155 ),
156 ];
157
158 for (input, expected) in invalid.iter().chain(common_invalid.iter()) {
159 let actual = input.parse::<ToolConfigFile>().unwrap_err();
160 assert_eq!(&actual, expected, "for input {input:?}");
161 }
162 }
163
164 #[test]
165 fn tool_config_basic() {
166 let config_contents = r#"
167 nextest-version = "0.9.50"
168
169 [profile.default]
170 retries = 3
171
172 [[profile.default.overrides]]
173 filter = 'test(test_foo)'
174 retries = 20
175 test-group = 'foo'
176
177 [[profile.default.overrides]]
178 filter = 'test(test_quux)'
179 test-group = '@tool:tool1:group1'
180
181 [test-groups.foo]
182 max-threads = 2
183 "#;
184
185 let tool1_config_contents = r#"
186 nextest-version = { required = "0.9.51", recommended = "0.9.52" }
187
188 [profile.default]
189 retries = 4
190
191 [[profile.default.overrides]]
192 filter = 'test(test_bar)'
193 retries = 21
194
195 [profile.tool]
196 retries = 12
197
198 [[profile.tool.overrides]]
199 filter = 'test(test_baz)'
200 retries = 22
201 test-group = '@tool:tool1:group1'
202
203 [[profile.tool.overrides]]
204 filter = 'test(test_quux)'
205 retries = 22
206 test-group = '@tool:tool2:group2'
207
208 [test-groups.'@tool:tool1:group1']
209 max-threads = 2
210 "#;
211
212 let tool2_config_contents = r#"
213 nextest-version = { recommended = "0.9.49" }
214
215 [profile.default]
216 retries = 5
217
218 [[profile.default.overrides]]
219 filter = 'test(test_)'
220 retries = 23
221
222 [profile.tool]
223 retries = 16
224
225 [[profile.tool.overrides]]
226 filter = 'test(test_ba)'
227 retries = 24
228 test-group = '@tool:tool2:group2'
229
230 [[profile.tool.overrides]]
231 filter = 'test(test_)'
232 retries = 25
233 test-group = '@global'
234
235 [profile.tool2]
236 retries = 18
237
238 [[profile.tool2.overrides]]
239 filter = 'all()'
240 retries = 26
241
242 [test-groups.'@tool:tool2:group2']
243 max-threads = 4
244 "#;
245
246 let workspace_dir = tempdir().unwrap();
247
248 let graph = temp_workspace(&workspace_dir, config_contents);
249 let tool1_path = workspace_dir.child(".config/tool1.toml");
250 let tool2_path = workspace_dir.child(".config/tool2.toml");
251 tool1_path.write_str(tool1_config_contents).unwrap();
252 tool2_path.write_str(tool2_config_contents).unwrap();
253
254 let workspace_root = graph.workspace().root();
255
256 let tool_config_files = [
257 ToolConfigFile {
258 tool: tool_name("tool1"),
259 config_file: tool1_path.to_path_buf(),
260 },
261 ToolConfigFile {
262 tool: tool_name("tool2"),
263 config_file: tool2_path.to_path_buf(),
264 },
265 ];
266
267 let version_only_config =
268 VersionOnlyConfig::from_sources(workspace_root, None, &tool_config_files).unwrap();
269 let nextest_version = version_only_config.nextest_version();
270 assert_eq!(
271 nextest_version,
272 &NextestVersionConfig {
273 required: NextestVersionReq::Version {
274 version: "0.9.51".parse().unwrap(),
275 tool: Some(tool_name("tool1"))
276 },
277 recommended: NextestVersionReq::Version {
278 version: "0.9.52".parse().unwrap(),
279 tool: Some(tool_name("tool1"))
280 }
281 },
282 );
283
284 let pcx = ParseContext::new(&graph);
285 let config = NextestConfig::from_sources(
286 workspace_root,
287 &pcx,
288 None,
289 &tool_config_files,
290 &Default::default(),
291 )
292 .expect("config is valid");
293
294 let default_profile = config
295 .profile(NextestConfig::DEFAULT_PROFILE)
296 .expect("default profile is present")
297 .apply_build_platforms(&build_platforms());
298 assert_eq!(default_profile.retries(), RetryPolicy::new_without_delay(3));
300
301 let package_id = graph.workspace().iter().next().unwrap().id();
302
303 let binary_query = binary_query(
304 &graph,
305 package_id,
306 "lib",
307 "my-binary",
308 BuildPlatform::Target,
309 );
310 let test_foo = TestCaseName::new("test_foo");
311 let test_foo_query = TestQuery {
312 binary_query: binary_query.to_query(),
313 test_name: &test_foo,
314 };
315 let test_bar = TestCaseName::new("test_bar");
316 let test_bar_query = TestQuery {
317 binary_query: binary_query.to_query(),
318 test_name: &test_bar,
319 };
320 let test_baz = TestCaseName::new("test_baz");
321 let test_baz_query = TestQuery {
322 binary_query: binary_query.to_query(),
323 test_name: &test_baz,
324 };
325 let test_quux = TestCaseName::new("test_quux");
326 let test_quux_query = TestQuery {
327 binary_query: binary_query.to_query(),
328 test_name: &test_quux,
329 };
330
331 assert_eq!(
332 default_profile
333 .settings_for(NextestRunMode::Test, &test_foo_query)
334 .retries(),
335 RetryPolicy::new_without_delay(20),
336 "retries for test_foo/default profile"
337 );
338 assert_eq!(
339 default_profile
340 .settings_for(NextestRunMode::Test, &test_foo_query)
341 .test_group(),
342 &test_group("foo"),
343 "test_group for test_foo/default profile"
344 );
345 assert_eq!(
346 default_profile
347 .settings_for(NextestRunMode::Test, &test_bar_query)
348 .retries(),
349 RetryPolicy::new_without_delay(21),
350 "retries for test_bar/default profile"
351 );
352 assert_eq!(
353 default_profile
354 .settings_for(NextestRunMode::Test, &test_bar_query)
355 .test_group(),
356 &TestGroup::Global,
357 "test_group for test_bar/default profile"
358 );
359 assert_eq!(
360 default_profile
361 .settings_for(NextestRunMode::Test, &test_baz_query)
362 .retries(),
363 RetryPolicy::new_without_delay(23),
364 "retries for test_baz/default profile"
365 );
366 assert_eq!(
367 default_profile
368 .settings_for(NextestRunMode::Test, &test_quux_query)
369 .test_group(),
370 &test_group("@tool:tool1:group1"),
371 "test group for test_quux/default profile"
372 );
373
374 let tool_profile = config
375 .profile("tool")
376 .expect("tool profile is present")
377 .apply_build_platforms(&build_platforms());
378 assert_eq!(tool_profile.retries(), RetryPolicy::new_without_delay(12));
379 assert_eq!(
380 tool_profile
381 .settings_for(NextestRunMode::Test, &test_foo_query)
382 .retries(),
383 RetryPolicy::new_without_delay(25),
384 "retries for test_foo/default profile"
385 );
386 assert_eq!(
387 tool_profile
388 .settings_for(NextestRunMode::Test, &test_bar_query)
389 .retries(),
390 RetryPolicy::new_without_delay(24),
391 "retries for test_bar/default profile"
392 );
393 assert_eq!(
394 tool_profile
395 .settings_for(NextestRunMode::Test, &test_baz_query)
396 .retries(),
397 RetryPolicy::new_without_delay(22),
398 "retries for test_baz/default profile"
399 );
400
401 let tool2_profile = config
402 .profile("tool2")
403 .expect("tool2 profile is present")
404 .apply_build_platforms(&build_platforms());
405 assert_eq!(tool2_profile.retries(), RetryPolicy::new_without_delay(18));
406 assert_eq!(
407 tool2_profile
408 .settings_for(NextestRunMode::Test, &test_foo_query)
409 .retries(),
410 RetryPolicy::new_without_delay(26),
411 "retries for test_foo/default profile"
412 );
413 assert_eq!(
414 tool2_profile
415 .settings_for(NextestRunMode::Test, &test_bar_query)
416 .retries(),
417 RetryPolicy::new_without_delay(26),
418 "retries for test_bar/default profile"
419 );
420 assert_eq!(
421 tool2_profile
422 .settings_for(NextestRunMode::Test, &test_baz_query)
423 .retries(),
424 RetryPolicy::new_without_delay(26),
425 "retries for test_baz/default profile"
426 );
427 }
428}