Skip to main content

nextest_runner/config/elements/
inherits.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Inherit settings for profiles.
5#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
6pub struct Inherits(Option<String>);
7
8impl Inherits {
9    /// Creates a new `Inherits`.
10    pub fn new(inherits: Option<String>) -> Self {
11        Self(inherits)
12    }
13
14    /// Returns the profile that the custom profile inherits from.
15    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    /// Settings checked for inheritance below.
47    #[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            // prof_b's max-fail (4) should override prof_c's (10)
73            max_fail: Some(MaxFail::Count { max_fail: 4, terminate: TerminateMode::Wait }),
74            // prof_c's retries should be inherited through prof_b
75            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                // Spot check that inheritance works correctly.
226                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                        // Because inheritance errors are not in a deterministic
240                        // order in the Vec<InheritsError>, we use a HashSet
241                        // here to test whether the error seen by the expected
242                        // err.
243                        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                                    // SCC vectors do show the cycle, but
249                                    // we can't deterministically represent the cycle
250                                    // (i.e. A->B->C->A could be {A,B,C}, {C,A,B}, or
251                                    // {B,C,A})
252                                    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 that higher-priority files can inherit from lower-priority files.
281    #[test]
282    fn valid_downward_inheritance() {
283        let workspace_dir = tempdir().unwrap();
284
285        // Tool config 1 (higher priority): defines prof_a inheriting from prof_b
286        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        // Tool config 2 (lower priority): defines prof_b
298        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        // tool1 is first = higher priority, tool2 is second = lower priority
320        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        // prof_a should inherit retries=3 from prof_b, but override with retries=5
341        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        // prof_b should have retries=3
367        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 that lower-priority files cannot inherit from higher-priority files.
375    /// This is reported as an unknown profile error.
376    #[test]
377    fn invalid_upward_inheritance() {
378        let workspace_dir = tempdir().unwrap();
379
380        // Tool config 1 (higher priority): defines prof_a
381        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        // Tool config 2 (lower priority): tries to inherit from prof_a (not yet loaded)
392        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        // Error should be attributed to tool2 since that's where the invalid
430        // inheritance is defined.
431        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}