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 };
33 use camino_tempfile::tempdir;
34 use indoc::indoc;
35 use nextest_filtering::ParseContext;
36 use std::{collections::HashSet, fs};
37 use test_case::test_case;
38
39 fn tool_name(s: &str) -> ToolName {
40 ToolName::new(s.into()).unwrap()
41 }
42
43 #[derive(Default)]
45 #[allow(dead_code)]
46 pub struct InheritSettings {
47 name: String,
48 inherits: Option<String>,
49 max_fail: Option<MaxFail>,
50 retries: Option<RetryPolicy>,
51 }
52
53 #[test_case(
54 indoc! {r#"
55 [profile.prof_a]
56 inherits = "prof_b"
57
58 [profile.prof_b]
59 inherits = "prof_c"
60 fail-fast = { max-fail = 4 }
61
62 [profile.prof_c]
63 inherits = "default"
64 fail-fast = { max-fail = 10 }
65 retries = 3
66 "#},
67 Ok(InheritSettings {
68 name: "prof_a".to_string(),
69 inherits: Some("prof_b".to_string()),
70 max_fail: Some(MaxFail::Count { max_fail: 4, terminate: TerminateMode::Wait }),
72 retries: Some(RetryPolicy::new_without_delay(3)),
74 })
75 ; "three-level inheritance"
76 )]
77 #[test_case(
78 indoc! {r#"
79 [profile.prof_a]
80 inherits = "prof_b"
81
82 [profile.prof_b]
83 inherits = "prof_c"
84
85 [profile.prof_c]
86 inherits = "prof_c"
87 "#},
88 Err(
89 vec![
90 InheritsError::SelfReferentialInheritance("prof_c".to_string()),
91 ]
92 ) ; "self referential error not inheritance cycle"
93 )]
94 #[test_case(
95 indoc! {r#"
96 [profile.prof_a]
97 inherits = "prof_b"
98
99 [profile.prof_b]
100 inherits = "prof_c"
101
102 [profile.prof_c]
103 inherits = "prof_d"
104
105 [profile.prof_d]
106 inherits = "prof_e"
107
108 [profile.prof_e]
109 inherits = "prof_c"
110 "#},
111 Err(
112 vec![
113 InheritsError::InheritanceCycle(
114 vec![vec!["prof_c".to_string(),"prof_d".to_string(), "prof_e".to_string()]],
115 ),
116 ]
117 ) ; "C to D to E SCC cycle"
118 )]
119 #[test_case(
120 indoc! {r#"
121 [profile.default]
122 inherits = "prof_a"
123
124 [profile.default-miri]
125 inherits = "prof_c"
126
127 [profile.prof_a]
128 inherits = "prof_b"
129
130 [profile.prof_b]
131 inherits = "prof_c"
132
133 [profile.prof_c]
134 inherits = "prof_a"
135
136 [profile.prof_d]
137 inherits = "prof_d"
138
139 [profile.prof_e]
140 inherits = "nonexistent_profile"
141 "#},
142 Err(
143 vec![
144 InheritsError::DefaultProfileInheritance("default".to_string()),
145 InheritsError::DefaultProfileInheritance("default-miri".to_string()),
146 InheritsError::SelfReferentialInheritance("prof_d".to_string()),
147 InheritsError::UnknownInheritance(
148 "prof_e".to_string(),
149 "nonexistent_profile".to_string(),
150 ),
151 InheritsError::InheritanceCycle(
152 vec![
153 vec!["prof_a".to_string(),"prof_b".to_string(), "prof_c".to_string()],
154 ]
155 ),
156 ]
157 )
158 ; "inheritance errors detected"
159 )]
160 #[test_case(
161 indoc! {r#"
162 [profile.my-profile]
163 inherits = "default-nonexistent"
164 retries = 5
165 "#},
166 Err(
167 vec![
168 InheritsError::UnknownInheritance(
169 "my-profile".to_string(),
170 "default-nonexistent".to_string(),
171 ),
172 ]
173 )
174 ; "inherit from nonexistent default profile"
175 )]
176 #[test_case(
177 indoc! {r#"
178 [profile.default-custom]
179 retries = 3
180
181 [profile.my-profile]
182 inherits = "default-custom"
183 fail-fast = { max-fail = 5 }
184 "#},
185 Ok(InheritSettings {
186 name: "my-profile".to_string(),
187 inherits: Some("default-custom".to_string()),
188 max_fail: Some(MaxFail::Count { max_fail: 5, terminate: TerminateMode::Wait }),
189 retries: Some(RetryPolicy::new_without_delay(3)),
190 })
191 ; "inherit from defined default profile"
192 )]
193 fn profile_inheritance(
194 config_contents: &str,
195 expected: Result<InheritSettings, Vec<InheritsError>>,
196 ) {
197 let workspace_dir = tempdir().unwrap();
198 let graph = temp_workspace(&workspace_dir, config_contents);
199 let pcx = ParseContext::new(&graph);
200
201 let config_res = NextestConfig::from_sources(
202 graph.workspace().root(),
203 &pcx,
204 None,
205 [],
206 &Default::default(),
207 );
208
209 match expected {
210 Ok(custom_profile) => {
211 let config = config_res.expect("config is valid");
212 let default_profile = config
213 .profile("default")
214 .unwrap_or_else(|_| panic!("default profile is known"));
215 let default_profile = default_profile.apply_build_platforms(&build_platforms());
216 let profile = config
217 .profile(&custom_profile.name)
218 .unwrap_or_else(|_| panic!("{} profile is known", &custom_profile.name));
219 let profile = profile.apply_build_platforms(&build_platforms());
220 assert_eq!(default_profile.inherits(), None);
221 assert_eq!(profile.inherits(), custom_profile.inherits.as_deref());
222
223 assert_eq!(
225 profile.max_fail(),
226 custom_profile.max_fail.expect("max fail should exist")
227 );
228 if let Some(expected_retries) = custom_profile.retries {
229 assert_eq!(profile.retries(), expected_retries);
230 }
231 }
232 Err(expected_inherits_err) => {
233 let error = config_res.expect_err("config is invalid");
234 assert_eq!(error.tool(), None);
235 match error.kind() {
236 ConfigParseErrorKind::InheritanceErrors(inherits_err) => {
237 let expected_err: HashSet<&InheritsError> =
242 expected_inherits_err.iter().collect();
243 for actual_err in inherits_err.iter() {
244 match actual_err {
245 InheritanceCycle(sccs) => {
246 let mut sccs = sccs.clone();
251 for scc in sccs.iter_mut() {
252 scc.sort()
253 }
254 assert!(
255 expected_err.contains(&InheritanceCycle(sccs)),
256 "unexpected inherit error {:?}",
257 actual_err
258 )
259 }
260 _ => {
261 assert!(
262 expected_err.contains(&actual_err),
263 "unexpected inherit error {:?}",
264 actual_err
265 )
266 }
267 }
268 }
269 }
270 other => {
271 panic!("expected ConfigParseErrorKind::InheritanceErrors, got {other}")
272 }
273 }
274 }
275 }
276 }
277
278 #[test]
280 fn valid_downward_inheritance() {
281 let workspace_dir = tempdir().unwrap();
282
283 let tool1_config = workspace_dir.path().join("tool1.toml");
285 fs::write(
286 &tool1_config,
287 indoc! {r#"
288 [profile.prof_a]
289 inherits = "prof_b"
290 retries = 5
291 "#},
292 )
293 .unwrap();
294
295 let tool2_config = workspace_dir.path().join("tool2.toml");
297 fs::write(
298 &tool2_config,
299 indoc! {r#"
300 [profile.prof_b]
301 retries = 3
302 "#},
303 )
304 .unwrap();
305
306 let workspace_config = indoc! {r#"
307 [profile.default]
308 "#};
309
310 let graph = temp_workspace(&workspace_dir, workspace_config);
311 let pcx = ParseContext::new(&graph);
312
313 let tool_configs = [
315 ToolConfigFile {
316 tool: tool_name("tool1"),
317 config_file: tool1_config,
318 },
319 ToolConfigFile {
320 tool: tool_name("tool2"),
321 config_file: tool2_config,
322 },
323 ];
324
325 let config = NextestConfig::from_sources(
326 graph.workspace().root(),
327 &pcx,
328 None,
329 &tool_configs,
330 &Default::default(),
331 )
332 .expect("config should be valid");
333
334 let profile = config
336 .profile("prof_a")
337 .unwrap()
338 .apply_build_platforms(&build_platforms());
339 assert_eq!(profile.retries(), RetryPolicy::new_without_delay(5));
340
341 let profile = config
343 .profile("prof_b")
344 .unwrap()
345 .apply_build_platforms(&build_platforms());
346 assert_eq!(profile.retries(), RetryPolicy::new_without_delay(3));
347 }
348
349 #[test]
352 fn invalid_upward_inheritance() {
353 let workspace_dir = tempdir().unwrap();
354
355 let tool1_config = workspace_dir.path().join("tool1.toml");
357 fs::write(
358 &tool1_config,
359 indoc! {r#"
360 [profile.prof_a]
361 retries = 5
362 "#},
363 )
364 .unwrap();
365
366 let tool2_config = workspace_dir.path().join("tool2.toml");
368 fs::write(
369 &tool2_config,
370 indoc! {r#"
371 [profile.prof_b]
372 inherits = "prof_a"
373 "#},
374 )
375 .unwrap();
376
377 let workspace_config = indoc! {r#"
378 [profile.default]
379 "#};
380
381 let graph = temp_workspace(&workspace_dir, workspace_config);
382 let pcx = ParseContext::new(&graph);
383
384 let tool_configs = [
385 ToolConfigFile {
386 tool: tool_name("tool1"),
387 config_file: tool1_config,
388 },
389 ToolConfigFile {
390 tool: tool_name("tool2"),
391 config_file: tool2_config,
392 },
393 ];
394
395 let error = NextestConfig::from_sources(
396 graph.workspace().root(),
397 &pcx,
398 None,
399 &tool_configs,
400 &Default::default(),
401 )
402 .expect_err("config should fail: upward inheritance not allowed");
403
404 assert_eq!(error.tool(), Some(&tool_name("tool2")));
407
408 match error.kind() {
409 ConfigParseErrorKind::InheritanceErrors(errors) => {
410 assert_eq!(errors.len(), 1);
411 assert!(
412 matches!(
413 &errors[0],
414 InheritsError::UnknownInheritance(from, to)
415 if from == "prof_b" && to == "prof_a"
416 ),
417 "expected UnknownInheritance(prof_b, prof_a), got {:?}",
418 errors[0]
419 );
420 }
421 other => {
422 panic!("expected ConfigParseErrorKind::InheritanceErrors, got {other}")
423 }
424 }
425 }
426}