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