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