nextest_runner/config/elements/
inherits.rs1#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
6pub struct Inherits(Option<String>);
7
8impl Inherits {
9 pub fn new(inherits: Option<String>) -> Self {
11 Self(inherits)
12 }
13
14 pub fn inherits_from(&self) -> Option<&str> {
16 self.0.as_deref()
17 }
18}
19
20#[cfg(test)]
21mod tests {
22 use crate::{
23 config::{
24 core::{NextestConfig, ToolConfigFile, ToolName},
25 elements::{MaxFail, RetryPolicy, TerminateMode},
26 utils::test_helpers::*,
27 },
28 errors::{
29 ConfigParseErrorKind,
30 InheritsError::{self, *},
31 },
32 run_mode::NextestRunMode,
33 };
34 use camino_tempfile::tempdir;
35 use guppy::graph::cargo::BuildPlatform;
36 use indoc::indoc;
37 use nextest_filtering::{ParseContext, TestQuery};
38 use nextest_metadata::TestCaseName;
39 use std::{collections::HashSet, fs};
40 use test_case::test_case;
41
42 fn tool_name(s: &str) -> ToolName {
43 ToolName::new(s.into()).unwrap()
44 }
45
46 #[derive(Default)]
48 pub struct InheritSettings {
49 name: String,
50 inherits: Option<String>,
51 max_fail: Option<MaxFail>,
52 retries: Option<RetryPolicy>,
53 }
54
55 #[test_case(
56 indoc! {r#"
57 [profile.prof_a]
58 inherits = "prof_b"
59
60 [profile.prof_b]
61 inherits = "prof_c"
62 fail-fast = { max-fail = 4 }
63
64 [profile.prof_c]
65 inherits = "default"
66 fail-fast = { max-fail = 10 }
67 retries = 3
68 "#},
69 Ok(InheritSettings {
70 name: "prof_a".to_string(),
71 inherits: Some("prof_b".to_string()),
72 max_fail: Some(MaxFail::Count { max_fail: 4, terminate: TerminateMode::Wait }),
74 retries: Some(RetryPolicy::new_without_delay(3)),
76 })
77 ; "three-level inheritance"
78 )]
79 #[test_case(
80 indoc! {r#"
81 [profile.prof_a]
82 inherits = "prof_b"
83
84 [profile.prof_b]
85 inherits = "prof_c"
86
87 [profile.prof_c]
88 inherits = "prof_c"
89 "#},
90 Err(
91 vec![
92 InheritsError::SelfReferentialInheritance("prof_c".to_string()),
93 ]
94 ) ; "self referential error not inheritance cycle"
95 )]
96 #[test_case(
97 indoc! {r#"
98 [profile.prof_a]
99 inherits = "prof_b"
100
101 [profile.prof_b]
102 inherits = "prof_c"
103
104 [profile.prof_c]
105 inherits = "prof_d"
106
107 [profile.prof_d]
108 inherits = "prof_e"
109
110 [profile.prof_e]
111 inherits = "prof_c"
112 "#},
113 Err(
114 vec![
115 InheritsError::InheritanceCycle(
116 vec![vec!["prof_c".to_string(),"prof_d".to_string(), "prof_e".to_string()]],
117 ),
118 ]
119 ) ; "C to D to E SCC cycle"
120 )]
121 #[test_case(
122 indoc! {r#"
123 [profile.default]
124 inherits = "prof_a"
125
126 [profile.default-miri]
127 inherits = "prof_c"
128
129 [profile.prof_a]
130 inherits = "prof_b"
131
132 [profile.prof_b]
133 inherits = "prof_c"
134
135 [profile.prof_c]
136 inherits = "prof_a"
137
138 [profile.prof_d]
139 inherits = "prof_d"
140
141 [profile.prof_e]
142 inherits = "nonexistent_profile"
143 "#},
144 Err(
145 vec![
146 InheritsError::DefaultProfileInheritance("default".to_string()),
147 InheritsError::DefaultProfileInheritance("default-miri".to_string()),
148 InheritsError::SelfReferentialInheritance("prof_d".to_string()),
149 InheritsError::UnknownInheritance(
150 "prof_e".to_string(),
151 "nonexistent_profile".to_string(),
152 ),
153 InheritsError::InheritanceCycle(
154 vec![
155 vec!["prof_a".to_string(),"prof_b".to_string(), "prof_c".to_string()],
156 ]
157 ),
158 ]
159 )
160 ; "inheritance errors detected"
161 )]
162 #[test_case(
163 indoc! {r#"
164 [profile.my-profile]
165 inherits = "default-nonexistent"
166 retries = 5
167 "#},
168 Err(
169 vec![
170 InheritsError::UnknownInheritance(
171 "my-profile".to_string(),
172 "default-nonexistent".to_string(),
173 ),
174 ]
175 )
176 ; "inherit from nonexistent default profile"
177 )]
178 #[test_case(
179 indoc! {r#"
180 [profile.default-custom]
181 retries = 3
182
183 [profile.my-profile]
184 inherits = "default-custom"
185 fail-fast = { max-fail = 5 }
186 "#},
187 Ok(InheritSettings {
188 name: "my-profile".to_string(),
189 inherits: Some("default-custom".to_string()),
190 max_fail: Some(MaxFail::Count { max_fail: 5, terminate: TerminateMode::Wait }),
191 retries: Some(RetryPolicy::new_without_delay(3)),
192 })
193 ; "inherit from defined default profile"
194 )]
195 fn profile_inheritance(
196 config_contents: &str,
197 expected: Result<InheritSettings, Vec<InheritsError>>,
198 ) {
199 let workspace_dir = tempdir().unwrap();
200 let graph = temp_workspace(&workspace_dir, config_contents);
201 let pcx = ParseContext::new(&graph);
202
203 let config_res = NextestConfig::from_sources(
204 graph.workspace().root(),
205 &pcx,
206 None,
207 [],
208 &Default::default(),
209 );
210
211 match expected {
212 Ok(custom_profile) => {
213 let config = config_res.expect("config is valid");
214 let default_profile = config
215 .profile("default")
216 .unwrap_or_else(|_| panic!("default profile is known"));
217 let default_profile = default_profile.apply_build_platforms(&build_platforms());
218 let profile = config
219 .profile(&custom_profile.name)
220 .unwrap_or_else(|_| panic!("{} profile is known", custom_profile.name));
221 let profile = profile.apply_build_platforms(&build_platforms());
222 assert_eq!(default_profile.inherits(), None);
223 assert_eq!(profile.inherits(), custom_profile.inherits.as_deref());
224
225 assert_eq!(
227 profile.max_fail(),
228 custom_profile.max_fail.expect("max fail should exist")
229 );
230 if let Some(expected_retries) = custom_profile.retries {
231 assert_eq!(profile.retries(), expected_retries);
232 }
233 }
234 Err(expected_inherits_err) => {
235 let error = config_res.expect_err("config is invalid");
236 assert_eq!(error.tool(), None);
237 match error.kind() {
238 ConfigParseErrorKind::InheritanceErrors(inherits_err) => {
239 let expected_err: HashSet<&InheritsError> =
244 expected_inherits_err.iter().collect();
245 for actual_err in inherits_err.iter() {
246 match actual_err {
247 InheritanceCycle(sccs) => {
248 let mut sccs = sccs.clone();
253 for scc in sccs.iter_mut() {
254 scc.sort()
255 }
256 assert!(
257 expected_err.contains(&InheritanceCycle(sccs)),
258 "unexpected inherit error {:?}",
259 actual_err
260 )
261 }
262 _ => {
263 assert!(
264 expected_err.contains(&actual_err),
265 "unexpected inherit error {:?}",
266 actual_err
267 )
268 }
269 }
270 }
271 }
272 other => {
273 panic!("expected ConfigParseErrorKind::InheritanceErrors, got {other}")
274 }
275 }
276 }
277 }
278 }
279
280 #[test]
282 fn valid_downward_inheritance() {
283 let workspace_dir = tempdir().unwrap();
284
285 let tool1_config = workspace_dir.path().join("tool1.toml");
287 fs::write(
288 &tool1_config,
289 indoc! {r#"
290 [profile.prof_a]
291 inherits = "prof_b"
292 retries = 5
293 "#},
294 )
295 .unwrap();
296
297 let tool2_config = workspace_dir.path().join("tool2.toml");
299 fs::write(
300 &tool2_config,
301 indoc! {r#"
302 [profile.prof_b]
303 retries = 3
304
305 [[profile.prof_b.overrides]]
306 filter = "test(overridden)"
307 retries = 7
308 "#},
309 )
310 .unwrap();
311
312 let workspace_config = indoc! {r#"
313 [profile.default]
314 "#};
315
316 let graph = temp_workspace(&workspace_dir, workspace_config);
317 let pcx = ParseContext::new(&graph);
318
319 let tool_configs = [
321 ToolConfigFile {
322 tool: tool_name("tool1"),
323 config_file: tool1_config,
324 },
325 ToolConfigFile {
326 tool: tool_name("tool2"),
327 config_file: tool2_config,
328 },
329 ];
330
331 let config = NextestConfig::from_sources(
332 graph.workspace().root(),
333 &pcx,
334 None,
335 &tool_configs,
336 &Default::default(),
337 )
338 .expect("config should be valid");
339
340 let profile = config
342 .profile("prof_a")
343 .unwrap()
344 .apply_build_platforms(&build_platforms());
345 assert_eq!(profile.retries(), RetryPolicy::new_without_delay(5));
346
347 let package_id = graph.workspace().iter().next().unwrap().id();
348 let binary = binary_query(
349 &graph,
350 package_id,
351 "lib",
352 "test-package",
353 BuildPlatform::Target,
354 );
355 let test_name = TestCaseName::new("test_overridden");
356 let query = TestQuery {
357 binary_query: binary.to_query(),
358 test_name: &test_name,
359 };
360 assert_eq!(
361 profile.settings_for(NextestRunMode::Test, &query).retries(),
362 RetryPolicy::new_without_delay(7),
363 "prof_a applies the override inherited from prof_b in a lower-priority file"
364 );
365
366 let profile = config
368 .profile("prof_b")
369 .unwrap()
370 .apply_build_platforms(&build_platforms());
371 assert_eq!(profile.retries(), RetryPolicy::new_without_delay(3));
372 }
373
374 #[test]
377 fn invalid_upward_inheritance() {
378 let workspace_dir = tempdir().unwrap();
379
380 let tool1_config = workspace_dir.path().join("tool1.toml");
382 fs::write(
383 &tool1_config,
384 indoc! {r#"
385 [profile.prof_a]
386 retries = 5
387 "#},
388 )
389 .unwrap();
390
391 let tool2_config = workspace_dir.path().join("tool2.toml");
393 fs::write(
394 &tool2_config,
395 indoc! {r#"
396 [profile.prof_b]
397 inherits = "prof_a"
398 "#},
399 )
400 .unwrap();
401
402 let workspace_config = indoc! {r#"
403 [profile.default]
404 "#};
405
406 let graph = temp_workspace(&workspace_dir, workspace_config);
407 let pcx = ParseContext::new(&graph);
408
409 let tool_configs = [
410 ToolConfigFile {
411 tool: tool_name("tool1"),
412 config_file: tool1_config,
413 },
414 ToolConfigFile {
415 tool: tool_name("tool2"),
416 config_file: tool2_config,
417 },
418 ];
419
420 let error = NextestConfig::from_sources(
421 graph.workspace().root(),
422 &pcx,
423 None,
424 &tool_configs,
425 &Default::default(),
426 )
427 .expect_err("config should fail: upward inheritance not allowed");
428
429 assert_eq!(error.tool(), Some(&tool_name("tool2")));
432
433 match error.kind() {
434 ConfigParseErrorKind::InheritanceErrors(errors) => {
435 assert_eq!(errors.len(), 1);
436 assert!(
437 matches!(
438 &errors[0],
439 InheritsError::UnknownInheritance(from, to)
440 if from == "prof_b" && to == "prof_a"
441 ),
442 "expected UnknownInheritance(prof_b, prof_a), got {:?}",
443 errors[0]
444 );
445 }
446 other => {
447 panic!("expected ConfigParseErrorKind::InheritanceErrors, got {other}")
448 }
449 }
450 }
451}