diff --git a/Cargo.lock b/Cargo.lock index 0491e87c854..7497d4ca75f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,8 +1340,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -2110,6 +2108,7 @@ dependencies = [ "nix", "owo-colors", "pathdiff", + "petgraph", "pin-project-lite", "pretty_assertions", "proptest", @@ -2178,6 +2177,7 @@ dependencies = [ "miniz_oxide", "mio", "num-traits", + "petgraph", "ppv-lite86", "proc-macro2", "quote", @@ -2412,13 +2412,14 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", "indexmap", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3dc60f0480f..c6d3c9bc475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ nix = { version = "0.30.1", default-features = false, features = ["signal"] } num_threads = "0.1.7" owo-colors = "4.2.3" pathdiff = { version = "0.2.3", features = ["camino"] } +petgraph = "0.8.3" pin-project-lite = "0.2.16" pretty_assertions = "1.4.1" proptest = "1.9.0" diff --git a/nextest-runner/Cargo.toml b/nextest-runner/Cargo.toml index 02bbe7ab985..f2aa76fe1d6 100644 --- a/nextest-runner/Cargo.toml +++ b/nextest-runner/Cargo.toml @@ -49,6 +49,7 @@ nextest-metadata.workspace = true nextest-workspace-hack.workspace = true owo-colors.workspace = true pin-project-lite.workspace = true +petgraph.workspace = true quick-junit.workspace = true rand.workspace = true regex.workspace = true diff --git a/nextest-runner/src/config/core/imp.rs b/nextest-runner/src/config/core/imp.rs index f2a3500d215..916c52ef085 100644 --- a/nextest-runner/src/config/core/imp.rs +++ b/nextest-runner/src/config/core/imp.rs @@ -6,7 +6,7 @@ use crate::{ config::{ core::ConfigExperimental, elements::{ - ArchiveConfig, CustomTestGroup, DefaultJunitImpl, GlobalTimeout, JunitConfig, + ArchiveConfig, CustomTestGroup, DefaultJunitImpl, GlobalTimeout, Inherits, JunitConfig, JunitImpl, LeakTimeout, MaxFail, RetryPolicy, SlowTimeout, TestGroup, TestGroupConfig, TestThreads, ThreadsRequired, deserialize_fail_fast, deserialize_leak_timeout, deserialize_retry_policy, deserialize_slow_timeout, @@ -21,9 +21,10 @@ use crate::{ }, }, errors::{ - ConfigParseError, ConfigParseErrorKind, ProfileListScriptUsesRunFiltersError, - ProfileNotFound, ProfileScriptErrors, ProfileUnknownScriptError, - ProfileWrongConfigScriptTypeError, UnknownTestGroupError, provided_by_tool, + ConfigParseError, ConfigParseErrorKind, InheritsError, + ProfileListScriptUsesRunFiltersError, ProfileNotFound, ProfileScriptErrors, + ProfileUnknownScriptError, ProfileWrongConfigScriptTypeError, UnknownTestGroupError, + provided_by_tool, }, helpers::plural, list::TestList, @@ -37,6 +38,7 @@ use config::{ use iddqd::IdOrdMap; use indexmap::IndexMap; use nextest_filtering::{BinaryQuery, EvalContext, Filterset, ParseContext, TestQuery}; +use petgraph::{Directed, Graph, algo::scc::kosaraju_scc, graph::NodeIndex}; use serde::Deserialize; use std::{ collections::{BTreeMap, BTreeSet, HashMap, hash_map}, @@ -575,6 +577,9 @@ impl NextestConfig { ); } + // Checks that the profiles correctly use the inherits setting. + this_config.sanitize_profile_inherits()?; + // Compile the overrides for this file. let this_compiled = CompiledByProfile::new(pcx, &this_config) .map_err(|kind| ConfigParseError::new(config_file, tool, kind))?; @@ -794,6 +799,13 @@ impl NextestConfig { fn make_profile(&self, name: &str) -> Result, ProfileNotFound> { let custom_profile = self.inner.get_profile(name)?; + // Resolves the inherited profile into a profile chain + let inheritance_chain = if custom_profile.is_some() { + self.inner.resolve_profile_chain(name)? + } else { + Vec::new() + }; + // The profile was found: construct it. let mut store_dir = self.workspace_root.join(&self.inner.store.dir); store_dir.push(name); @@ -809,6 +821,7 @@ impl NextestConfig { store_dir, default_profile: &self.inner.default_profile, custom_profile, + inheritance_chain, test_groups: &self.inner.test_groups, scripts: &self.inner.scripts, compiled_data, @@ -874,6 +887,7 @@ pub struct EarlyProfile<'cfg> { store_dir: Utf8PathBuf, default_profile: &'cfg DefaultProfileImpl, custom_profile: Option<&'cfg CustomProfileImpl>, + inheritance_chain: Vec<&'cfg CustomProfileImpl>, test_groups: &'cfg BTreeMap, // This is ordered because the scripts are used in the order they're defined. scripts: &'cfg ScriptConfig, @@ -924,6 +938,7 @@ impl<'cfg> EarlyProfile<'cfg> { store_dir: self.store_dir, default_profile: self.default_profile, custom_profile: self.custom_profile, + inheritance_chain: self.inheritance_chain, scripts: self.scripts, test_groups: self.test_groups, compiled_data, @@ -941,6 +956,7 @@ pub struct EvaluatableProfile<'cfg> { store_dir: Utf8PathBuf, default_profile: &'cfg DefaultProfileImpl, custom_profile: Option<&'cfg CustomProfileImpl>, + inheritance_chain: Vec<&'cfg CustomProfileImpl>, test_groups: &'cfg BTreeMap, // This is ordered because the scripts are used in the order they're defined. scripts: &'cfg ScriptConfig, @@ -951,6 +967,30 @@ pub struct EvaluatableProfile<'cfg> { resolved_default_filter: CompiledDefaultFilter, } +/// Returns a specific config field from an EvaluatableProfile +/// given priority in the following order: +/// it's current custom profile, the first custom profile +/// in the inheritance chain that contains the field, +/// or the default profile +macro_rules! profile_field { + ($eval_prof:ident.$field:ident) => { + $eval_prof + .custom_profile + .iter() + .chain($eval_prof.inheritance_chain.iter()) + .find_map(|inherit_profile| inherit_profile.$field) + .unwrap_or($eval_prof.default_profile.$field) + }; + ($eval_prof:ident.$field:ident.$ref_func:ident()) => { + $eval_prof + .custom_profile + .iter() + .chain($eval_prof.inheritance_chain.iter()) + .find_map(|inherit_profile| inherit_profile.$field.$ref_func()) + .unwrap_or(&$eval_prof.default_profile.$field) + }; +} + impl<'cfg> EvaluatableProfile<'cfg> { /// Returns the name of the profile. pub fn name(&self) -> &str { @@ -986,94 +1026,68 @@ impl<'cfg> EvaluatableProfile<'cfg> { /// Returns the retry count for this profile. pub fn retries(&self) -> RetryPolicy { - self.custom_profile - .and_then(|profile| profile.retries) - .unwrap_or(self.default_profile.retries) + profile_field!(self.retries) } /// Returns the number of threads to run against for this profile. pub fn test_threads(&self) -> TestThreads { - self.custom_profile - .and_then(|profile| profile.test_threads) - .unwrap_or(self.default_profile.test_threads) + profile_field!(self.test_threads) } /// Returns the number of threads required for each test. pub fn threads_required(&self) -> ThreadsRequired { - self.custom_profile - .and_then(|profile| profile.threads_required) - .unwrap_or(self.default_profile.threads_required) + profile_field!(self.threads_required) } /// Returns extra arguments to be passed to the test binary at runtime. pub fn run_extra_args(&self) -> &'cfg [String] { - self.custom_profile - .and_then(|profile| profile.run_extra_args.as_deref()) - .unwrap_or(&self.default_profile.run_extra_args) + profile_field!(self.run_extra_args.as_deref()) } /// Returns the time after which tests are treated as slow for this profile. pub fn slow_timeout(&self) -> SlowTimeout { - self.custom_profile - .and_then(|profile| profile.slow_timeout) - .unwrap_or(self.default_profile.slow_timeout) + profile_field!(self.slow_timeout) } /// Returns the time after which we should stop running tests. pub fn global_timeout(&self) -> GlobalTimeout { - self.custom_profile - .and_then(|profile| profile.global_timeout) - .unwrap_or(self.default_profile.global_timeout) + profile_field!(self.global_timeout) } /// Returns the time after which a child process that hasn't closed its handles is marked as /// leaky. pub fn leak_timeout(&self) -> LeakTimeout { - self.custom_profile - .and_then(|profile| profile.leak_timeout) - .unwrap_or(self.default_profile.leak_timeout) + profile_field!(self.leak_timeout) } /// Returns the test status level. pub fn status_level(&self) -> StatusLevel { - self.custom_profile - .and_then(|profile| profile.status_level) - .unwrap_or(self.default_profile.status_level) + profile_field!(self.status_level) } /// Returns the test status level at the end of the run. pub fn final_status_level(&self) -> FinalStatusLevel { - self.custom_profile - .and_then(|profile| profile.final_status_level) - .unwrap_or(self.default_profile.final_status_level) + profile_field!(self.final_status_level) } /// Returns the failure output config for this profile. pub fn failure_output(&self) -> TestOutputDisplay { - self.custom_profile - .and_then(|profile| profile.failure_output) - .unwrap_or(self.default_profile.failure_output) + profile_field!(self.failure_output) } /// Returns the failure output config for this profile. pub fn success_output(&self) -> TestOutputDisplay { - self.custom_profile - .and_then(|profile| profile.success_output) - .unwrap_or(self.default_profile.success_output) + profile_field!(self.success_output) } /// Returns the max-fail config for this profile. pub fn max_fail(&self) -> MaxFail { - self.custom_profile - .and_then(|profile| profile.max_fail) - .unwrap_or(self.default_profile.max_fail) + profile_field!(self.max_fail) } /// Returns the archive configuration for this profile. pub fn archive_config(&self) -> &'cfg ArchiveConfig { - self.custom_profile - .and_then(|profile| profile.archive.as_ref()) - .unwrap_or(&self.default_profile.archive) + profile_field!(self.archive.as_ref()) } /// Returns the list of setup scripts. @@ -1108,6 +1122,14 @@ impl<'cfg> EvaluatableProfile<'cfg> { ) } + /// Returns the profile that this profile inherits from. + pub fn inherits(&self) -> Option<&str> { + if let Some(custom_profile) = self.custom_profile { + return custom_profile.inherits(); + } + None + } + #[cfg(test)] pub(in crate::config) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> { self.custom_profile @@ -1154,6 +1176,151 @@ impl NextestConfigImpl { .iter() .map(|(key, value)| (key.as_str(), value)) } + + /// Resolves a profile with an inheritance chain recursively + /// + /// This function does not check for cycles. Use `check_inheritance_cycles()` + /// to observe for cycles in an inheritance chain. + fn resolve_profile_chain( + &self, + profile_name: &str, + ) -> Result, ProfileNotFound> { + let mut chain = Vec::new(); + + self.resolve_profile_chain_recursive(profile_name, &mut chain)?; + Ok(chain) + } + + /// Helper function for resolving an inheritance chain + fn resolve_profile_chain_recursive<'cfg>( + &'cfg self, + profile_name: &str, + chain: &mut Vec<&'cfg CustomProfileImpl>, + ) -> Result<(), ProfileNotFound> { + let profile = self.get_profile(profile_name)?; + if let Some(profile) = profile { + if let Some(parent_name) = &profile.inherits { + self.resolve_profile_chain_recursive(parent_name, chain)?; + } + chain.push(profile); + } + + Ok(()) + } + + /// Sanitize inherits settings on default and custom profiles + fn sanitize_profile_inherits(&self) -> Result<(), ConfigParseError> { + let mut inherit_err_collector = Vec::new(); + + self.default_profile_inheritance(&mut inherit_err_collector); + self.check_inheritance_cycles(&mut inherit_err_collector); + + if !inherit_err_collector.is_empty() { + return Err(ConfigParseError::new( + "inheritance error(s) detected", + None, + ConfigParseErrorKind::InheritanceErrors(inherit_err_collector), + )); + } + + Ok(()) + } + + /// Check that default profiles do not attempt to inherit from other + /// profiles + fn default_profile_inheritance(&self, inherit_err_collector: &mut Vec) { + for default_profile in NextestConfig::DEFAULT_PROFILES { + // covers for "default" profile + if *default_profile == NextestConfig::DEFAULT_PROFILE { + if self.default_profile().inherits().is_some() { + inherit_err_collector.push(InheritsError::DefaultProfileInheritance( + default_profile.to_string(), + )); + } + } + // covers for other and any future default profiles reserved by Nextest + // (i.e. "default-miri") + else if let Ok(ok_default) = self.get_profile(default_profile) + && let Some(other_default) = ok_default + && other_default.inherits().is_some() + { + inherit_err_collector.push(InheritsError::DefaultProfileInheritance( + default_profile.to_string(), + )); + } + } + } + + /// Checks for the following: invalid inheritance, self referential inheritance, + /// and inheritance chain cycle + fn check_inheritance_cycles(&self, inherit_err_collector: &mut Vec) { + let mut profile_graph = Graph::<&str, (), Directed>::new(); + let mut profile_map = HashMap::new(); + + // Iterates through all custom profiles within the config file and constructs + // a reduced graph of the inheritance chain(s) after handling nonexistent + // inheritance and self referential inheritance + for (name, custom_profile) in self.other_profiles() { + // Certain reserved default profile are in other_profiles (i.e. "default-miri") + // which should be ignored. Note that DEFAULT_PROFILES contains 2 strings, so this + // check is pretty much constant time, but if more reserved default profile were to + // be added, we should consider having a Hashmap impl to check if current profile name + // is a default profile + let profile_type = NextestConfig::DEFAULT_PROFILES.contains(&name); + if !profile_type && let Some(inherits_name) = custom_profile.inherits() { + if inherits_name == name { + inherit_err_collector + .push(InheritsError::SelfReferentialInheritance(name.to_string())) + } else if self.get_profile(inherits_name).is_ok() { + // inherited profile exists, create the edge in the graph + let from_node = match profile_map.get(name) { + None => { + let profile_node = profile_graph.add_node(name); + profile_map.insert(name, profile_node); + profile_node + } + Some(node_idx) => *node_idx, + }; + let to_node = match profile_map.get(inherits_name) { + None => { + let profile_node = profile_graph.add_node(inherits_name); + profile_map.insert(inherits_name, profile_node); + profile_node + } + Some(node_idx) => *node_idx, + }; + profile_graph.add_edge(from_node, to_node, ()); + } else { + inherit_err_collector.push(InheritsError::UnknownInheritance( + name.to_string(), + inherits_name.to_string(), + )) + } + } + } + + // Detects all *cyclic* strongly connected components (SCCs) within the graph + let profile_sccs: Vec> = kosaraju_scc(&profile_graph); + let profile_sccs: Vec> = profile_sccs + .into_iter() + .filter(|scc| scc.len() >= 2) + .collect(); + + if !profile_sccs.is_empty() { + inherit_err_collector.push(InheritsError::InheritanceCycle( + profile_sccs + .iter() + .map(|node_idxs| { + let profile_names: Vec = node_idxs + .iter() + .map(|node_idx| profile_graph[*node_idx].to_string()) + .collect(); + profile_names + }) + .collect(), + )); + } + } } // This is the form of `NextestConfig` that gets deserialized. @@ -1235,6 +1402,7 @@ pub(in crate::config) struct DefaultProfileImpl { scripts: Vec, junit: DefaultJunitImpl, archive: ArchiveConfig, + inherits: Inherits, } impl DefaultProfileImpl { @@ -1279,6 +1447,7 @@ impl DefaultProfileImpl { scripts: p.scripts, junit: DefaultJunitImpl::for_default_profile(p.junit), archive: p.archive.expect("archive present in default profile"), + inherits: Inherits::new(p.inherits), } } @@ -1286,6 +1455,10 @@ impl DefaultProfileImpl { &self.default_filter } + pub(in crate::config) fn inherits(&self) -> Option<&str> { + self.inherits.inherits_from() + } + pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] { &self.overrides } @@ -1337,6 +1510,8 @@ pub(in crate::config) struct CustomProfileImpl { junit: JunitImpl, #[serde(default)] archive: Option, + #[serde(default)] + inherits: Option, } impl CustomProfileImpl { @@ -1349,6 +1524,10 @@ impl CustomProfileImpl { self.default_filter.as_deref() } + pub(in crate::config) fn inherits(&self) -> Option<&str> { + self.inherits.as_deref() + } + pub(in crate::config) fn overrides(&self) -> &[DeserializedOverride] { &self.overrides } diff --git a/nextest-runner/src/config/elements/inherits.rs b/nextest-runner/src/config/elements/inherits.rs new file mode 100644 index 00000000000..7cecf75ae2e --- /dev/null +++ b/nextest-runner/src/config/elements/inherits.rs @@ -0,0 +1,238 @@ +// Copyright (c) The nextest Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/// Inherit settings for profiles +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct Inherits(Option); + +impl Inherits { + /// Creates a new `Inherits`. + pub fn new(inherits: Option) -> Self { + Self(inherits) + } + + /// Returns the profile that the custom profile inherits from + pub fn inherits_from(&self) -> Option<&str> { + self.0.as_deref() + } +} + +// TODO: Need to write test cases for this +#[cfg(test)] +mod tests { + use super::super::*; + use crate::{ + config::{ + core::NextestConfig, overrides::DeserializedOverride, + scripts::DeserializedProfileScriptConfig, utils::test_helpers::*, + }, + errors::{ + ConfigParseErrorKind, + InheritsError::{self, *}, + }, + reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay}, + }; + use camino_tempfile::tempdir; + use indoc::indoc; + use nextest_filtering::ParseContext; + use std::collections::HashSet; + use test_case::test_case; + + #[derive(Default)] + #[allow(dead_code)] + pub struct CustomProfileTest { + /// The default set of tests run by `cargo nextest run`. + name: String, + default_filter: Option, + retries: Option, + test_threads: Option, + threads_required: Option, + run_extra_args: Option>, + status_level: Option, + final_status_level: Option, + failure_output: Option, + success_output: Option, + max_fail: Option, + slow_timeout: Option, + global_timeout: Option, + leak_timeout: Option, + overrides: Vec, + scripts: Vec, + junit: JunitImpl, + archive: Option, + inherits: Option, + } + + #[test_case( + indoc! {r#" + [profile.prof_a] + inherits = "prof_b" + + [profile.prof_b] + inherits = "default" + fail-fast = { max-fail = 4 } + "#}, + Ok(CustomProfileTest { + name: "prof_a".to_string(), + inherits: Some("prof_b".to_string()), + max_fail: Some(MaxFail::Count { max_fail: 4, terminate: TerminateMode::Wait }), + ..Default::default() + }) + ; "custom profile a inherits from another custom profile b" + )] + #[test_case( + indoc! {r#" + [profile.prof_a] + inherits = "prof_b" + + [profile.prof_b] + inherits = "prof_c" + + [profile.prof_c] + inherits = "prof_c" + "#}, + Err( + vec![ + InheritsError::SelfReferentialInheritance("prof_c".to_string()), + ] + ) ; "self referential error not inheritance cycle" + )] + #[test_case( + indoc! {r#" + [profile.prof_a] + inherits = "prof_b" + + [profile.prof_b] + inherits = "prof_c" + + [profile.prof_c] + inherits = "prof_d" + + [profile.prof_d] + inherits = "prof_e" + + [profile.prof_e] + inherits = "prof_c" + "#}, + Err( + vec![ + InheritsError::InheritanceCycle(vec![vec!["prof_c".to_string(),"prof_d".to_string(), "prof_e".to_string()]]), + ] + ) ; "C to D to E SCC cycle" + )] + #[test_case( + indoc! {r#" + [profile.default] + inherits = "prof_a" + + [profile.default-miri] + inherits = "prof_c" + + [profile.prof_a] + inherits = "prof_b" + + [profile.prof_b] + inherits = "prof_c" + + [profile.prof_c] + inherits = "prof_a" + + [profile.prof_d] + inherits = "prof_d" + + [profile.prof_e] + inherits = "nonexistent_profile" + "#}, + Err( + vec![ + InheritsError::DefaultProfileInheritance("default".to_string()), + InheritsError::DefaultProfileInheritance("default-miri".to_string()), + InheritsError::SelfReferentialInheritance("prof_d".to_string()), + InheritsError::UnknownInheritance("prof_e".to_string(), "nonexistent_profile".to_string()), + InheritsError::InheritanceCycle(vec![vec!["prof_a".to_string(),"prof_b".to_string(), "prof_c".to_string()]]), + ] + ) + ; "inheritance errors detected" + )] + fn profile_inheritance( + config_contents: &str, + expected: Result>, + ) { + let workspace_dir = tempdir().unwrap(); + let graph = temp_workspace(&workspace_dir, config_contents); + let pcx = ParseContext::new(&graph); + + let config_res = NextestConfig::from_sources( + graph.workspace().root(), + &pcx, + None, + [], + &Default::default(), + ); + + match expected { + Ok(custom_profile) => { + let config = config_res.expect("config is valid"); + let default_profile = config + .profile("default") + .unwrap_or_else(|_| panic!("default profile is known")); + let default_profile = default_profile.apply_build_platforms(&build_platforms()); + let profile = config + .profile(&custom_profile.name) + .unwrap_or_else(|_| panic!("{} profile is known", &custom_profile.name)); + let profile = profile.apply_build_platforms(&build_platforms()); + assert_eq!(default_profile.inherits(), None); + assert_eq!(profile.inherits(), custom_profile.inherits.as_deref()); + // This is a confirmation on a custom profile inheriting another custom + // profile's configs properly; however, this should cross check the all expected + // field with the inherited custom profile fields fully + assert_eq!( + profile.max_fail(), + custom_profile.max_fail.expect("max fail should exist") + ); + } + Err(expected_inherits_err) => { + let error = config_res.expect_err("config is invalid"); + assert_eq!(error.tool(), None); + match error.kind() { + ConfigParseErrorKind::InheritanceErrors(inherits_err) => { + // because inheritance errors are not in a deterministic order in the Vec + // we use a Hashset here to test whether the error seen by the expected err + let expected_err: HashSet<&InheritsError> = + expected_inherits_err.iter().collect(); + for actual_err in inherits_err.iter() { + match actual_err { + InheritanceCycle(sccs) => { + // to check if the sccs exists in our expected errors, + // we must sort the SCC (since these SCC chain are also + // in a non-deterministic order). this runs under the + // assumption that our expected_err contains the SCC cycle + // in alphabetical sorting order as well + let mut sccs = sccs.clone(); + for scc in sccs.iter_mut() { + scc.sort() + } + assert!( + expected_err.contains(&InheritanceCycle(sccs)), + "unexpected inherit error {:?}", + actual_err + ) + } + _ => { + assert!( + expected_err.contains(&actual_err), + "unexpected inherit error {:?}", + actual_err + ) + } + } + } + } + other => { + panic!("expected ConfigParseErrorKind::InheritanceErrors, got {other}") + } + } + } + } + } +} diff --git a/nextest-runner/src/config/elements/mod.rs b/nextest-runner/src/config/elements/mod.rs index d6a5345355e..a6d88eb9d87 100644 --- a/nextest-runner/src/config/elements/mod.rs +++ b/nextest-runner/src/config/elements/mod.rs @@ -5,6 +5,7 @@ mod archive; mod global_timeout; +mod inherits; mod junit; mod leak_timeout; mod max_fail; @@ -17,6 +18,7 @@ mod threads_required; pub use archive::*; pub use global_timeout::*; +pub use inherits::*; pub use junit::*; pub use leak_timeout::*; pub use max_fail::*; diff --git a/nextest-runner/src/errors.rs b/nextest-runner/src/errors.rs index a496d6c9295..7d2c3cfa572 100644 --- a/nextest-runner/src/errors.rs +++ b/nextest-runner/src/errors.rs @@ -178,6 +178,9 @@ pub enum ConfigParseErrorKind { /// The features that were not enabled. missing_features: BTreeSet, }, + /// An inheritance cycle was detected in the profile configuration. + #[error("inheritance error(s) detected: {}", .0.iter().join(", "))] + InheritanceErrors(Vec), } /// An error that occurred while compiling overrides or scripts specified in @@ -1809,6 +1812,25 @@ pub enum ShowTestGroupsError { }, } +/// An error occurred while processing profile's inherits setting +#[derive(Debug, Error, PartialEq, Eq, Hash)] +pub enum InheritsError { + /// The default profile should not be able to inherit from other profiles + #[error("the {} profile should not inherit from other profiles", .0)] + DefaultProfileInheritance(String), + /// An unknown/unfound profile was detected to inherit from in profile configuration + #[error("profile {} inherits from an unknown profile {}", .0, .1)] + UnknownInheritance(String, String), + /// A self referential inheritance is detected from this profile + #[error("a self referential inheritance is detected from profile: {}", .0)] + SelfReferentialInheritance(String), + /// An inheritance cycle was detected in the profile configuration. + #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| { + format!("[{}]", scc.iter().join(", ")) + }).join(", "))] + InheritanceCycle(Vec>), +} + #[cfg(feature = "self-update")] mod self_update_errors { use super::*; diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index cae0810cb7c..5d49b0ada28 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -30,6 +30,7 @@ log = { version = "0.4.28", default-features = false, features = ["std"] } memchr = { version = "2.7.5" } miette = { version = "7.6.0", features = ["fancy"] } num-traits = { version = "0.2.19", default-features = false, features = ["std"] } +petgraph = { version = "0.8.3" } ppv-lite86 = { version = "0.2.21", default-features = false, features = ["simd", "std"] } rand = { version = "0.9.2" } rand_chacha = { version = "0.9.0", default-features = false, features = ["std"] }