use super::{
ArchiveConfig, CompiledByProfile, CompiledData, CompiledDefaultFilter, ConfigExperimental,
CustomTestGroup, DeserializedOverride, DeserializedProfileScriptConfig,
NextestVersionDeserialize, RetryPolicy, ScriptConfig, ScriptId, SettingSource, SetupScripts,
SlowTimeout, TestGroup, TestGroupConfig, TestSettings, TestThreads, ThreadsRequired,
ToolConfigFile,
};
use crate::{
errors::{
provided_by_tool, ConfigParseError, ConfigParseErrorKind, ProfileNotFound,
UnknownConfigScriptError, UnknownTestGroupError,
},
list::TestList,
platform::BuildPlatforms,
reporter::{FinalStatusLevel, StatusLevel, TestOutputDisplay},
};
use camino::{Utf8Path, Utf8PathBuf};
use config::{builder::DefaultState, Config, ConfigBuilder, File, FileFormat, FileSourceFile};
use guppy::graph::PackageGraph;
use indexmap::IndexMap;
use nextest_filtering::{EvalContext, TestQuery};
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::{
collections::{hash_map, BTreeMap, BTreeSet, HashMap},
time::Duration,
};
use tracing::warn;
#[inline]
pub fn get_num_cpus() -> usize {
static NUM_CPUS: Lazy<usize> = Lazy::new(|| match std::thread::available_parallelism() {
Ok(count) => count.into(),
Err(err) => {
warn!("unable to determine num-cpus ({err}), assuming 1 logical CPU");
1
}
});
*NUM_CPUS
}
#[derive(Clone, Debug)]
pub struct NextestConfig {
workspace_root: Utf8PathBuf,
inner: NextestConfigImpl,
compiled: CompiledByProfile,
}
impl NextestConfig {
pub const CONFIG_PATH: &'static str = ".config/nextest.toml";
pub const DEFAULT_CONFIG: &'static str = include_str!("../../default-config.toml");
pub const ENVIRONMENT_PREFIX: &'static str = "NEXTEST";
pub const DEFAULT_PROFILE: &'static str = "default";
pub const DEFAULT_MIRI_PROFILE: &'static str = "default-miri";
pub const DEFAULT_PROFILES: &'static [&'static str] =
&[Self::DEFAULT_PROFILE, Self::DEFAULT_MIRI_PROFILE];
pub fn from_sources<'a, I>(
workspace_root: impl Into<Utf8PathBuf>,
graph: &PackageGraph,
config_file: Option<&Utf8Path>,
tool_config_files: impl IntoIterator<IntoIter = I>,
experimental: &BTreeSet<ConfigExperimental>,
) -> Result<Self, ConfigParseError>
where
I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
{
Self::from_sources_impl(
workspace_root,
graph,
config_file,
tool_config_files,
experimental,
|config_file, tool, unknown| {
let mut unknown_str = String::new();
if unknown.len() == 1 {
unknown_str.push(' ');
unknown_str.push_str(unknown.iter().next().unwrap());
} else {
for ignored_key in unknown {
unknown_str.push('\n');
unknown_str.push_str(" - ");
unknown_str.push_str(ignored_key);
}
}
warn!(
"ignoring unknown configuration keys in config file {config_file}{}:{unknown_str}",
provided_by_tool(tool),
)
},
)
}
fn from_sources_impl<'a, I>(
workspace_root: impl Into<Utf8PathBuf>,
graph: &PackageGraph,
config_file: Option<&Utf8Path>,
tool_config_files: impl IntoIterator<IntoIter = I>,
experimental: &BTreeSet<ConfigExperimental>,
mut unknown_callback: impl FnMut(&Utf8Path, Option<&str>, &BTreeSet<String>),
) -> Result<Self, ConfigParseError>
where
I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
{
let workspace_root = workspace_root.into();
let tool_config_files_rev = tool_config_files.into_iter().rev();
let (inner, compiled) = Self::read_from_sources(
graph,
&workspace_root,
config_file,
tool_config_files_rev,
experimental,
&mut unknown_callback,
)?;
Ok(Self {
workspace_root,
inner,
compiled,
})
}
#[cfg(test)]
pub(crate) fn default_config(workspace_root: impl Into<Utf8PathBuf>) -> Self {
use itertools::Itertools;
let config = Self::make_default_config()
.build()
.expect("default config is always valid");
let mut unknown = BTreeSet::new();
let deserialized: NextestConfigDeserialize =
serde_ignored::deserialize(config, |path: serde_ignored::Path| {
unknown.insert(path.to_string());
})
.expect("default config is always valid");
if !unknown.is_empty() {
panic!(
"found unknown keys in default config: {}",
unknown.iter().join(", ")
);
}
Self {
workspace_root: workspace_root.into(),
inner: deserialized.into_config_impl(),
compiled: CompiledByProfile::for_default_config(),
}
}
pub fn profile(&self, name: impl AsRef<str>) -> Result<EarlyProfile<'_>, ProfileNotFound> {
self.make_profile(name.as_ref())
}
fn read_from_sources<'a>(
graph: &PackageGraph,
workspace_root: &Utf8Path,
file: Option<&Utf8Path>,
tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
experimental: &BTreeSet<ConfigExperimental>,
unknown_callback: &mut impl FnMut(&Utf8Path, Option<&str>, &BTreeSet<String>),
) -> Result<(NextestConfigImpl, CompiledByProfile), ConfigParseError> {
let mut composite_builder = Self::make_default_config();
let mut compiled = CompiledByProfile::for_default_config();
let mut known_groups = BTreeSet::new();
let mut known_scripts = BTreeSet::new();
for ToolConfigFile { config_file, tool } in tool_config_files_rev {
let source = File::new(config_file.as_str(), FileFormat::Toml);
Self::deserialize_individual_config(
graph,
workspace_root,
config_file,
Some(tool),
source.clone(),
&mut compiled,
experimental,
unknown_callback,
&mut known_groups,
&mut known_scripts,
)?;
composite_builder = composite_builder.add_source(source);
}
let (config_file, source) = match file {
Some(file) => (file.to_owned(), File::new(file.as_str(), FileFormat::Toml)),
None => {
let config_file = workspace_root.join(Self::CONFIG_PATH);
let source = File::new(config_file.as_str(), FileFormat::Toml).required(false);
(config_file, source)
}
};
Self::deserialize_individual_config(
graph,
workspace_root,
&config_file,
None,
source.clone(),
&mut compiled,
experimental,
unknown_callback,
&mut known_groups,
&mut known_scripts,
)?;
composite_builder = composite_builder.add_source(source);
let (config, _unknown) = Self::build_and_deserialize_config(&composite_builder)
.map_err(|kind| ConfigParseError::new(config_file, None, kind))?;
compiled.default.reverse();
for data in compiled.other.values_mut() {
data.reverse();
}
Ok((config.into_config_impl(), compiled))
}
#[allow(clippy::too_many_arguments)]
fn deserialize_individual_config(
graph: &PackageGraph,
workspace_root: &Utf8Path,
config_file: &Utf8Path,
tool: Option<&str>,
source: File<FileSourceFile, FileFormat>,
compiled_out: &mut CompiledByProfile,
experimental: &BTreeSet<ConfigExperimental>,
unknown_callback: &mut impl FnMut(&Utf8Path, Option<&str>, &BTreeSet<String>),
known_groups: &mut BTreeSet<CustomTestGroup>,
known_scripts: &mut BTreeSet<ScriptId>,
) -> Result<(), ConfigParseError> {
let default_builder = Self::make_default_config();
let this_builder = default_builder.add_source(source);
let (this_config, unknown) = Self::build_and_deserialize_config(&this_builder)
.map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
if !unknown.is_empty() {
unknown_callback(config_file, tool, &unknown);
}
let (valid_groups, invalid_groups): (BTreeSet<_>, _) =
this_config.test_groups.keys().cloned().partition(|group| {
if let Some(tool) = tool {
group
.as_identifier()
.tool_components()
.map_or(false, |(tool_name, _)| tool_name == tool)
} else {
!group.as_identifier().is_tool_identifier()
}
});
if !invalid_groups.is_empty() {
let kind = if tool.is_some() {
ConfigParseErrorKind::InvalidTestGroupsDefinedByTool(invalid_groups)
} else {
ConfigParseErrorKind::InvalidTestGroupsDefined(invalid_groups)
};
return Err(ConfigParseError::new(config_file, tool, kind));
}
known_groups.extend(valid_groups);
if !this_config.scripts.is_empty()
&& !experimental.contains(&ConfigExperimental::SetupScripts)
{
return Err(ConfigParseError::new(
config_file,
tool,
ConfigParseErrorKind::ExperimentalFeatureNotEnabled {
feature: ConfigExperimental::SetupScripts,
},
));
}
let (valid_scripts, invalid_scripts): (BTreeSet<_>, _) =
this_config.scripts.keys().cloned().partition(|script| {
if let Some(tool) = tool {
script
.as_identifier()
.tool_components()
.map_or(false, |(tool_name, _)| tool_name == tool)
} else {
!script.as_identifier().is_tool_identifier()
}
});
if !invalid_scripts.is_empty() {
let kind = if tool.is_some() {
ConfigParseErrorKind::InvalidConfigScriptsDefinedByTool(invalid_scripts)
} else {
ConfigParseErrorKind::InvalidConfigScriptsDefined(invalid_scripts)
};
return Err(ConfigParseError::new(config_file, tool, kind));
}
known_scripts.extend(valid_scripts);
let this_config = this_config.into_config_impl();
let unknown_default_profiles: Vec<_> = this_config
.all_profiles()
.filter(|p| p.starts_with("default-") && !NextestConfig::DEFAULT_PROFILES.contains(p))
.collect();
if !unknown_default_profiles.is_empty() {
warn!(
"unknown profiles in the reserved `default-` namespace in config file {}{}:",
config_file
.strip_prefix(workspace_root)
.unwrap_or(config_file),
provided_by_tool(tool),
);
for profile in unknown_default_profiles {
warn!(" {profile}");
}
}
let this_compiled = CompiledByProfile::new(graph, &this_config)
.map_err(|kind| ConfigParseError::new(config_file, tool, kind))?;
let mut unknown_group_errors = Vec::new();
let mut check_test_group = |profile_name: &str, test_group: Option<&TestGroup>| {
if let Some(TestGroup::Custom(group)) = test_group {
if !known_groups.contains(group) {
unknown_group_errors.push(UnknownTestGroupError {
profile_name: profile_name.to_owned(),
name: TestGroup::Custom(group.clone()),
});
}
}
};
this_compiled
.default
.overrides
.iter()
.for_each(|override_| {
check_test_group("default", override_.data.test_group.as_ref());
});
this_compiled.other.iter().for_each(|(profile_name, data)| {
data.overrides.iter().for_each(|override_| {
check_test_group(profile_name, override_.data.test_group.as_ref());
});
});
if !unknown_group_errors.is_empty() {
let known_groups = TestGroup::make_all_groups(known_groups.iter().cloned()).collect();
return Err(ConfigParseError::new(
config_file,
tool,
ConfigParseErrorKind::UnknownTestGroups {
errors: unknown_group_errors,
known_groups,
},
));
}
let mut unknown_script_errors = Vec::new();
let mut check_script_ids = |profile_name: &str, scripts: &[ScriptId]| {
if !scripts.is_empty() && !experimental.contains(&ConfigExperimental::SetupScripts) {
return Err(ConfigParseError::new(
config_file,
tool,
ConfigParseErrorKind::ExperimentalFeatureNotEnabled {
feature: ConfigExperimental::SetupScripts,
},
));
}
for script in scripts {
if !known_scripts.contains(script) {
unknown_script_errors.push(UnknownConfigScriptError {
profile_name: profile_name.to_owned(),
name: script.clone(),
});
}
}
Ok(())
};
this_compiled
.default
.scripts
.iter()
.try_for_each(|scripts| check_script_ids("default", &scripts.setup))?;
this_compiled
.other
.iter()
.try_for_each(|(profile_name, data)| {
data.scripts
.iter()
.try_for_each(|scripts| check_script_ids(profile_name, &scripts.setup))
})?;
if !unknown_script_errors.is_empty() {
let known_scripts = known_scripts.iter().cloned().collect();
return Err(ConfigParseError::new(
config_file,
tool,
ConfigParseErrorKind::UnknownConfigScripts {
errors: unknown_script_errors,
known_scripts,
},
));
}
compiled_out.default.extend_reverse(this_compiled.default);
for (name, mut data) in this_compiled.other {
match compiled_out.other.entry(name) {
hash_map::Entry::Vacant(entry) => {
data.reverse();
entry.insert(data);
}
hash_map::Entry::Occupied(mut entry) => {
entry.get_mut().extend_reverse(data);
}
}
}
Ok(())
}
fn make_default_config() -> ConfigBuilder<DefaultState> {
Config::builder().add_source(File::from_str(Self::DEFAULT_CONFIG, FileFormat::Toml))
}
fn make_profile(&self, name: &str) -> Result<EarlyProfile<'_>, ProfileNotFound> {
let custom_profile = self.inner.get_profile(name)?;
let mut store_dir = self.workspace_root.join(&self.inner.store.dir);
store_dir.push(name);
let compiled_data = match self.compiled.other.get(name) {
Some(data) => data.clone().chain(self.compiled.default.clone()),
None => self.compiled.default.clone(),
};
Ok(EarlyProfile {
name: name.to_owned(),
store_dir,
default_profile: &self.inner.default_profile,
custom_profile,
test_groups: &self.inner.test_groups,
scripts: &self.inner.scripts,
compiled_data,
})
}
fn build_and_deserialize_config(
builder: &ConfigBuilder<DefaultState>,
) -> Result<(NextestConfigDeserialize, BTreeSet<String>), ConfigParseErrorKind> {
let config = builder
.build_cloned()
.map_err(|error| ConfigParseErrorKind::BuildError(Box::new(error)))?;
let mut ignored = BTreeSet::new();
let mut cb = |path: serde_ignored::Path| {
ignored.insert(path.to_string());
};
let ignored_de = serde_ignored::Deserializer::new(config, &mut cb);
let config: NextestConfigDeserialize = serde_path_to_error::deserialize(ignored_de)
.map_err(|error| ConfigParseErrorKind::DeserializeError(Box::new(error)))?;
Ok((config, ignored))
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct PreBuildPlatform {}
#[derive(Clone, Debug)]
pub(crate) struct FinalConfig {
pub(super) host_eval: bool,
pub(super) host_test_eval: bool,
pub(super) target_eval: bool,
}
pub struct EarlyProfile<'cfg> {
name: String,
store_dir: Utf8PathBuf,
default_profile: &'cfg DefaultProfileImpl,
custom_profile: Option<&'cfg CustomProfileImpl>,
test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
scripts: &'cfg IndexMap<ScriptId, ScriptConfig>,
pub(super) compiled_data: CompiledData<PreBuildPlatform>,
}
impl<'cfg> EarlyProfile<'cfg> {
pub fn store_dir(&self) -> &Utf8Path {
&self.store_dir
}
pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
self.test_groups
}
pub fn apply_build_platforms(
self,
build_platforms: &BuildPlatforms,
) -> EvaluatableProfile<'cfg> {
let compiled_data = self.compiled_data.apply_build_platforms(build_platforms);
let resolved_default_filter = {
let found_filter = compiled_data
.overrides
.iter()
.find_map(|override_data| override_data.default_filter_if_matches_platform());
found_filter.unwrap_or_else(|| {
compiled_data
.profile_default_filter
.as_ref()
.expect("compiled data always has default set")
})
}
.clone();
EvaluatableProfile {
name: self.name,
store_dir: self.store_dir,
default_profile: self.default_profile,
custom_profile: self.custom_profile,
scripts: self.scripts,
test_groups: self.test_groups,
compiled_data,
resolved_default_filter,
}
}
}
#[derive(Clone, Debug)]
pub struct EvaluatableProfile<'cfg> {
name: String,
store_dir: Utf8PathBuf,
default_profile: &'cfg DefaultProfileImpl,
custom_profile: Option<&'cfg CustomProfileImpl>,
test_groups: &'cfg BTreeMap<CustomTestGroup, TestGroupConfig>,
scripts: &'cfg IndexMap<ScriptId, ScriptConfig>,
pub(super) compiled_data: CompiledData<FinalConfig>,
resolved_default_filter: CompiledDefaultFilter,
}
impl<'cfg> EvaluatableProfile<'cfg> {
pub fn name(&self) -> &str {
&self.name
}
pub fn store_dir(&self) -> &Utf8Path {
&self.store_dir
}
pub fn filterset_ecx(&self) -> EvalContext<'_> {
EvalContext {
default_filter: &self.default_filter().expr,
}
}
pub fn default_filter(&self) -> &CompiledDefaultFilter {
&self.resolved_default_filter
}
pub fn test_group_config(&self) -> &'cfg BTreeMap<CustomTestGroup, TestGroupConfig> {
self.test_groups
}
pub fn script_config(&self) -> &'cfg IndexMap<ScriptId, ScriptConfig> {
self.scripts
}
pub fn retries(&self) -> RetryPolicy {
self.custom_profile
.and_then(|profile| profile.retries)
.unwrap_or(self.default_profile.retries)
}
pub fn test_threads(&self) -> TestThreads {
self.custom_profile
.and_then(|profile| profile.test_threads)
.unwrap_or(self.default_profile.test_threads)
}
pub fn threads_required(&self) -> ThreadsRequired {
self.custom_profile
.and_then(|profile| profile.threads_required)
.unwrap_or(self.default_profile.threads_required)
}
pub fn slow_timeout(&self) -> SlowTimeout {
self.custom_profile
.and_then(|profile| profile.slow_timeout)
.unwrap_or(self.default_profile.slow_timeout)
}
pub fn leak_timeout(&self) -> Duration {
self.custom_profile
.and_then(|profile| profile.leak_timeout)
.unwrap_or(self.default_profile.leak_timeout)
}
pub fn status_level(&self) -> StatusLevel {
self.custom_profile
.and_then(|profile| profile.status_level)
.unwrap_or(self.default_profile.status_level)
}
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)
}
pub fn failure_output(&self) -> TestOutputDisplay {
self.custom_profile
.and_then(|profile| profile.failure_output)
.unwrap_or(self.default_profile.failure_output)
}
pub fn success_output(&self) -> TestOutputDisplay {
self.custom_profile
.and_then(|profile| profile.success_output)
.unwrap_or(self.default_profile.success_output)
}
pub fn fail_fast(&self) -> bool {
self.custom_profile
.and_then(|profile| profile.fail_fast)
.unwrap_or(self.default_profile.fail_fast)
}
pub fn archive_config(&self) -> &'cfg ArchiveConfig {
self.custom_profile
.and_then(|profile| profile.archive.as_ref())
.unwrap_or(&self.default_profile.archive)
}
pub fn setup_scripts(&self, test_list: &TestList<'_>) -> SetupScripts<'_> {
SetupScripts::new(self, test_list)
}
pub fn settings_for(&self, query: &TestQuery<'_>) -> TestSettings {
TestSettings::new(self, query)
}
pub(crate) fn settings_with_source_for(
&self,
query: &TestQuery<'_>,
) -> TestSettings<SettingSource<'_>> {
TestSettings::new(self, query)
}
pub fn junit(&self) -> Option<NextestJunitConfig<'cfg>> {
let path = self
.custom_profile
.map(|profile| &profile.junit.path)
.unwrap_or(&self.default_profile.junit.path)
.as_deref();
path.map(|path| {
let path = self.store_dir.join(path);
let report_name = self
.custom_profile
.and_then(|profile| profile.junit.report_name.as_deref())
.unwrap_or(&self.default_profile.junit.report_name);
let store_success_output = self
.custom_profile
.and_then(|profile| profile.junit.store_success_output)
.unwrap_or(self.default_profile.junit.store_success_output);
let store_failure_output = self
.custom_profile
.and_then(|profile| profile.junit.store_failure_output)
.unwrap_or(self.default_profile.junit.store_failure_output);
NextestJunitConfig {
path,
report_name,
store_success_output,
store_failure_output,
}
})
}
#[allow(dead_code)]
pub(super) fn custom_profile(&self) -> Option<&'cfg CustomProfileImpl> {
self.custom_profile
}
}
#[derive(Clone, Debug)]
pub struct NextestJunitConfig<'cfg> {
path: Utf8PathBuf,
report_name: &'cfg str,
store_success_output: bool,
store_failure_output: bool,
}
impl<'cfg> NextestJunitConfig<'cfg> {
pub fn path(&self) -> &Utf8Path {
&self.path
}
pub fn report_name(&self) -> &'cfg str {
self.report_name
}
pub fn store_success_output(&self) -> bool {
self.store_success_output
}
pub fn store_failure_output(&self) -> bool {
self.store_failure_output
}
}
#[derive(Clone, Debug)]
pub(super) struct NextestConfigImpl {
store: StoreConfigImpl,
test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
scripts: IndexMap<ScriptId, ScriptConfig>,
default_profile: DefaultProfileImpl,
other_profiles: HashMap<String, CustomProfileImpl>,
}
impl NextestConfigImpl {
fn get_profile(&self, profile: &str) -> Result<Option<&CustomProfileImpl>, ProfileNotFound> {
let custom_profile = match profile {
NextestConfig::DEFAULT_PROFILE => None,
other => Some(
self.other_profiles
.get(other)
.ok_or_else(|| ProfileNotFound::new(profile, self.all_profiles()))?,
),
};
Ok(custom_profile)
}
fn all_profiles(&self) -> impl Iterator<Item = &str> {
self.other_profiles
.keys()
.map(|key| key.as_str())
.chain(std::iter::once(NextestConfig::DEFAULT_PROFILE))
}
pub(super) fn default_profile(&self) -> &DefaultProfileImpl {
&self.default_profile
}
pub(super) fn other_profiles(&self) -> impl Iterator<Item = (&str, &CustomProfileImpl)> {
self.other_profiles
.iter()
.map(|(key, value)| (key.as_str(), value))
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct NextestConfigDeserialize {
store: StoreConfigImpl,
#[allow(unused)]
#[serde(default)]
nextest_version: Option<NextestVersionDeserialize>,
#[allow(unused)]
#[serde(default)]
experimental: BTreeSet<String>,
#[serde(default)]
test_groups: BTreeMap<CustomTestGroup, TestGroupConfig>,
#[serde(default, rename = "script")]
scripts: IndexMap<ScriptId, ScriptConfig>,
#[serde(rename = "profile")]
profiles: HashMap<String, CustomProfileImpl>,
}
impl NextestConfigDeserialize {
fn into_config_impl(mut self) -> NextestConfigImpl {
let p = self
.profiles
.remove("default")
.expect("default profile should exist");
let default_profile = DefaultProfileImpl::new(p);
NextestConfigImpl {
store: self.store,
default_profile,
test_groups: self.test_groups,
scripts: self.scripts,
other_profiles: self.profiles,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct StoreConfigImpl {
dir: Utf8PathBuf,
}
#[derive(Clone, Debug)]
pub(super) struct DefaultProfileImpl {
default_filter: String,
test_threads: TestThreads,
threads_required: ThreadsRequired,
retries: RetryPolicy,
status_level: StatusLevel,
final_status_level: FinalStatusLevel,
failure_output: TestOutputDisplay,
success_output: TestOutputDisplay,
fail_fast: bool,
slow_timeout: SlowTimeout,
leak_timeout: Duration,
overrides: Vec<DeserializedOverride>,
scripts: Vec<DeserializedProfileScriptConfig>,
junit: DefaultJunitImpl,
archive: ArchiveConfig,
}
impl DefaultProfileImpl {
fn new(p: CustomProfileImpl) -> Self {
Self {
default_filter: p
.default_filter
.expect("default-filter present in default profile"),
test_threads: p
.test_threads
.expect("test-threads present in default profile"),
threads_required: p
.threads_required
.expect("threads-required present in default profile"),
retries: p.retries.expect("retries present in default profile"),
status_level: p
.status_level
.expect("status-level present in default profile"),
final_status_level: p
.final_status_level
.expect("final-status-level present in default profile"),
failure_output: p
.failure_output
.expect("failure-output present in default profile"),
success_output: p
.success_output
.expect("success-output present in default profile"),
fail_fast: p.fail_fast.expect("fail-fast present in default profile"),
slow_timeout: p
.slow_timeout
.expect("slow-timeout present in default profile"),
leak_timeout: p
.leak_timeout
.expect("leak-timeout present in default profile"),
overrides: p.overrides,
scripts: p.scripts,
junit: DefaultJunitImpl {
path: p.junit.path,
report_name: p
.junit
.report_name
.expect("junit.report present in default profile"),
store_success_output: p
.junit
.store_success_output
.expect("junit.store-success-output present in default profile"),
store_failure_output: p
.junit
.store_failure_output
.expect("junit.store-failure-output present in default profile"),
},
archive: p.archive.expect("archive present in default profile"),
}
}
pub(super) fn default_filter(&self) -> &str {
&self.default_filter
}
pub(super) fn overrides(&self) -> &[DeserializedOverride] {
&self.overrides
}
pub(super) fn setup_scripts(&self) -> &[DeserializedProfileScriptConfig] {
&self.scripts
}
}
#[derive(Clone, Debug)]
struct DefaultJunitImpl {
path: Option<Utf8PathBuf>,
report_name: String,
store_success_output: bool,
store_failure_output: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(super) struct CustomProfileImpl {
#[serde(default)]
default_filter: Option<String>,
#[serde(default, deserialize_with = "super::deserialize_retry_policy")]
retries: Option<RetryPolicy>,
#[serde(default)]
test_threads: Option<TestThreads>,
#[serde(default)]
threads_required: Option<ThreadsRequired>,
#[serde(default)]
status_level: Option<StatusLevel>,
#[serde(default)]
final_status_level: Option<FinalStatusLevel>,
#[serde(default)]
failure_output: Option<TestOutputDisplay>,
#[serde(default)]
success_output: Option<TestOutputDisplay>,
#[serde(default)]
fail_fast: Option<bool>,
#[serde(default, deserialize_with = "super::deserialize_slow_timeout")]
slow_timeout: Option<SlowTimeout>,
#[serde(default, with = "humantime_serde::option")]
leak_timeout: Option<Duration>,
#[serde(default)]
overrides: Vec<DeserializedOverride>,
#[serde(default)]
scripts: Vec<DeserializedProfileScriptConfig>,
#[serde(default)]
junit: JunitImpl,
#[serde(default)]
archive: Option<ArchiveConfig>,
}
#[allow(dead_code)]
impl CustomProfileImpl {
pub(super) fn test_threads(&self) -> Option<TestThreads> {
self.test_threads
}
pub(super) fn default_filter(&self) -> Option<&str> {
self.default_filter.as_deref()
}
pub(super) fn overrides(&self) -> &[DeserializedOverride] {
&self.overrides
}
pub(super) fn scripts(&self) -> &[DeserializedProfileScriptConfig] {
&self.scripts
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct JunitImpl {
#[serde(default)]
path: Option<Utf8PathBuf>,
#[serde(default)]
report_name: Option<String>,
#[serde(default)]
store_success_output: Option<bool>,
#[serde(default)]
store_failure_output: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::test_helpers::*;
use camino_tempfile::tempdir;
#[test]
fn default_config_is_valid() {
let default_config = NextestConfig::default_config("foo");
default_config
.profile(NextestConfig::DEFAULT_PROFILE)
.expect("default profile should exist");
}
#[test]
fn ignored_keys() {
let config_contents = r#"
ignored1 = "test"
[profile.default]
retries = 3
ignored2 = "hi"
[[profile.default.overrides]]
filter = 'test(test_foo)'
retries = 20
ignored3 = 42
"#;
let tool_config_contents = r#"
[store]
ignored4 = 20
[profile.default]
retries = 4
ignored5 = false
[profile.tool]
retries = 12
[[profile.tool.overrides]]
filter = 'test(test_baz)'
retries = 22
ignored6 = 6.5
"#;
let workspace_dir = tempdir().unwrap();
let graph = temp_workspace(workspace_dir.path(), config_contents);
let workspace_root = graph.workspace().root();
let tool_path = workspace_root.join(".config/tool.toml");
std::fs::write(&tool_path, tool_config_contents).unwrap();
let mut unknown_keys = HashMap::new();
let _ = NextestConfig::from_sources_impl(
workspace_root,
&graph,
None,
&[ToolConfigFile {
tool: "my-tool".to_owned(),
config_file: tool_path,
}][..],
&Default::default(),
|_path, tool, ignored| {
unknown_keys.insert(tool.map(|s| s.to_owned()), ignored.clone());
},
)
.expect("config is valid");
assert_eq!(
unknown_keys.len(),
2,
"there are two files with unknown keys"
);
let keys = unknown_keys
.remove(&None)
.expect("unknown keys for .config/nextest.toml");
assert_eq!(
keys,
maplit::btreeset! {
"ignored1".to_owned(),
"profile.default.ignored2".to_owned(),
"profile.default.overrides.0.ignored3".to_owned(),
}
);
let keys = unknown_keys
.remove(&Some("my-tool".to_owned()))
.expect("unknown keys for my-tool");
assert_eq!(
keys,
maplit::btreeset! {
"store.ignored4".to_owned(),
"profile.default.ignored5".to_owned(),
"profile.tool.overrides.0.ignored6".to_owned(),
}
);
}
}