diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index ed8401cb294..9b9e6d54c5e 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -24,15 +24,15 @@ use nextest_runner::{ errors::{TargetTripleError, WriteTestListError}, input::InputHandlerKind, list::{ - BinaryList, OutputFormat, RustTestArtifact, SerializableFormat, TestExecuteContext, - TestList, + BinaryList, OutputFormat, RustBuildMeta, RustTestArtifact, SerializableFormat, + TestExecuteContext, TestList, TestListState, }, partition::PartitionerBuilder, platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform}, redact::Redactor, reporter::{ events::{FinalRunStats, RunStatsFailureKind}, - highlight_end, structured, FinalStatusLevel, ReporterBuilder, StatusLevel, + highlight_end, structured, EventAggregator, FinalStatusLevel, ReporterBuilder, StatusLevel, TestOutputDisplay, TestOutputErrorSlice, }, reuse_build::{archive_to_file, ArchiveReporter, PathMapper, ReuseBuildInfo}, @@ -611,20 +611,14 @@ impl TestBuildFilter { test_filter_builder: TestFilterBuilder, env: EnvironmentMap, ecx: &EvalContext<'_>, - reuse_build: &ReuseBuildInfo, + rust_build_meta: RustBuildMeta, + path_mapper: &PathMapper, ) -> Result> { - let path_mapper = make_path_mapper( - reuse_build, - graph, - &binary_list.rust_build_meta.target_directory, - )?; - - let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper); let test_artifacts = RustTestArtifact::from_binary_list( graph, binary_list, &rust_build_meta, - &path_mapper, + path_mapper, self.platform_filter.into(), )?; TestList::new( @@ -1420,12 +1414,11 @@ impl BaseApp { let binary_list = self.build_binary_list()?; let path_mapper = PathMapper::noop(); - let build_platforms = binary_list.rust_build_meta.build_platforms.clone(); let pcx = ParseContext::new(self.graph()); let (_, config) = self.load_config(&pcx)?; let profile = self .load_profile(&config)? - .apply_build_platforms(&build_platforms); + .into_evaluatable(&binary_list.rust_build_meta.build_platforms); let redactor = if should_redact() { Redactor::build_active(&binary_list.rust_build_meta) @@ -1502,11 +1495,6 @@ impl BaseApp { let profile = config .profile(profile_name) .map_err(ExpectedError::profile_not_found)?; - let store_dir = profile.store_dir(); - std::fs::create_dir_all(store_dir).map_err(|err| ExpectedError::StoreDirCreateError { - store_dir: store_dir.to_owned(), - err, - })?; Ok(profile) } } @@ -1569,6 +1557,8 @@ impl App { binary_list: Arc, test_filter_builder: TestFilterBuilder, ecx: &EvalContext<'_>, + rust_build_meta: RustBuildMeta, + path_mapper: &PathMapper, ) -> Result { let env = EnvironmentMap::new(&self.base.cargo_configs); self.build_filter.compute_test_list( @@ -1579,7 +1569,8 @@ impl App { test_filter_builder, env, ecx, - &self.base.reuse_build, + rust_build_meta, + path_mapper, ) } @@ -1617,7 +1608,7 @@ impl App { .base .load_runner(&binary_list.rust_build_meta.build_platforms); let profile = - profile.apply_build_platforms(&binary_list.rust_build_meta.build_platforms); + profile.into_evaluatable(&binary_list.rust_build_meta.build_platforms); let ctx = TestExecuteContext { profile_name: profile.name(), double_spawn, @@ -1625,8 +1616,21 @@ impl App { }; let ecx = profile.filterset_ecx(); - let test_list = - self.build_test_list(&ctx, binary_list, test_filter_builder, &ecx)?; + let path_mapper = make_path_mapper( + &self.base.reuse_build, + self.base.graph(), + &binary_list.rust_build_meta.target_directory, + )?; + let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper); + + let test_list = self.build_test_list( + &ctx, + binary_list, + test_filter_builder, + &ecx, + rust_build_meta, + &path_mapper, + )?; let mut writer = output_writer.stdout_writer(); test_list.write( @@ -1673,7 +1677,7 @@ impl App { let double_spawn = self.base.load_double_spawn(); let target_runner = self.base.load_runner(&build_platforms); - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let ctx = TestExecuteContext { profile_name: profile.name(), double_spawn, @@ -1681,7 +1685,21 @@ impl App { }; let ecx = profile.filterset_ecx(); - let test_list = self.build_test_list(&ctx, binary_list, test_filter_builder, &ecx)?; + let path_mapper = make_path_mapper( + &self.base.reuse_build, + self.base.graph(), + &binary_list.rust_build_meta.target_directory, + )?; + let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper); + + let test_list = self.build_test_list( + &ctx, + binary_list, + test_filter_builder, + &ecx, + rust_build_meta, + &path_mapper, + )?; let mut writer = output_writer.stdout_writer(); @@ -1712,7 +1730,8 @@ impl App { let (version_only_config, config) = self.base.load_config(&pcx)?; let profile = self.base.load_profile(&config)?; - // Construct this here so that errors are reported before the build step. + // Construct this here so that errors are reported before the build + // step. let mut structured_reporter = structured::StructuredReporter::new(); match reporter_opts.message_format { MessageFormat::Human => {} @@ -1752,11 +1771,25 @@ impl App { let test_filter_builder = self.build_filter.make_test_filter_builder(filter_exprs)?; let binary_list = self.base.build_binary_list()?; - let build_platforms = &binary_list.rust_build_meta.build_platforms.clone(); + let path_mapper = make_path_mapper( + &self.base.reuse_build, + self.base.graph(), + &binary_list.rust_build_meta.target_directory, + )?; + let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper); + + let profile = profile.into_evaluatable(&binary_list.rust_build_meta.build_platforms); + + // This is the earliest point where we can create the aggregator, since + // we need the remapped target directory which is only available after + // the test list is built. + let aggregator = EventAggregator::new(&profile, &rust_build_meta.target_directory)?; + let double_spawn = self.base.load_double_spawn(); - let target_runner = self.base.load_runner(build_platforms); + let target_runner = self + .base + .load_runner(&binary_list.rust_build_meta.build_platforms); - let profile = profile.apply_build_platforms(build_platforms); let ctx = TestExecuteContext { profile_name: profile.name(), double_spawn, @@ -1764,7 +1797,14 @@ impl App { }; let ecx = profile.filterset_ecx(); - let test_list = self.build_test_list(&ctx, binary_list, test_filter_builder, &ecx)?; + let test_list = self.build_test_list( + &ctx, + binary_list, + test_filter_builder, + &ecx, + rust_build_meta, + &path_mapper, + )?; let output = output_writer.reporter_output(); let should_colorize = self @@ -1805,7 +1845,13 @@ impl App { let mut reporter = reporter_opts .to_builder(no_capture, should_colorize) .set_verbose(self.base.output.verbose) - .build(&test_list, &profile, output, structured_reporter); + .build( + &test_list, + &profile, + output, + aggregator, + structured_reporter, + ); configure_handle_inheritance(no_capture)?; let run_stats = runner.try_execute(|event| { diff --git a/cargo-nextest/src/errors.rs b/cargo-nextest/src/errors.rs index 2e39c3b9d1a..6c0eee4c5c8 100644 --- a/cargo-nextest/src/errors.rs +++ b/cargo-nextest/src/errors.rs @@ -69,11 +69,10 @@ pub enum ExpectedError { #[from] err: ProfileNotFound, }, - #[error("failed to create store directory")] - StoreDirCreateError { - store_dir: Utf8PathBuf, - #[source] - err: std::io::Error, + #[error("junit setup error")] + JunitSetupError { + #[from] + err: JunitSetupError, }, #[error("cargo config error")] CargoConfigError { @@ -395,7 +394,7 @@ impl ExpectedError { | Self::SetCurrentDirFailed { .. } | Self::GetCurrentExeFailed { .. } | Self::ProfileNotFound { .. } - | Self::StoreDirCreateError { .. } + | Self::JunitSetupError { .. } | Self::RootManifestNotFound { .. } | Self::CargoConfigError { .. } | Self::TestFilterBuilderError { .. } @@ -521,12 +520,9 @@ impl ExpectedError { ); None } - Self::StoreDirCreateError { store_dir, err } => { - error!( - "failed to create store dir at `{}`", - store_dir.style(styles.bold) - ); - Some(err as &dyn Error) + Self::JunitSetupError { err } => { + error!("{}", err); + err.source() } Self::CargoConfigError { err } => { error!("{}", err); diff --git a/nextest-runner/src/config/archive.rs b/nextest-runner/src/config/archive.rs index 3a65373b493..d57c7042d9c 100644 --- a/nextest-runner/src/config/archive.rs +++ b/nextest-runner/src/config/archive.rs @@ -307,7 +307,7 @@ mod tests { config .profile("default") .expect("default profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .archive_config(), &default_config, "default matches" @@ -317,7 +317,7 @@ mod tests { config .profile("profile1") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .archive_config(), &ArchiveConfig { include: vec![ArchiveInclude { @@ -334,7 +334,7 @@ mod tests { config .profile("profile2") .expect("default profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .archive_config(), &ArchiveConfig { include: vec![] }, "profile2 matches" @@ -344,7 +344,7 @@ mod tests { config .profile("profile3") .expect("default profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .archive_config(), &default_config, "profile3 matches" diff --git a/nextest-runner/src/config/config_impl.rs b/nextest-runner/src/config/config_impl.rs index 19f2a2cd646..fc17c8f7865 100644 --- a/nextest-runner/src/config/config_impl.rs +++ b/nextest-runner/src/config/config_impl.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use super::{ - ArchiveConfig, CompiledByProfile, CompiledData, CompiledDefaultFilter, ConfigExperimental, - CustomTestGroup, DefaultJunitImpl, DeserializedOverride, DeserializedProfileScriptConfig, - JunitConfig, JunitImpl, MaxFail, NextestVersionDeserialize, RetryPolicy, ScriptConfig, - ScriptId, SettingSource, SetupScripts, SlowTimeout, TestGroup, TestGroupConfig, TestSettings, - TestThreads, ThreadsRequired, ToolConfigFile, + store::StoreConfigImpl, ArchiveConfig, CompiledByProfile, CompiledData, CompiledDefaultFilter, + ConfigExperimental, CustomTestGroup, DefaultJunitImpl, DeserializedOverride, + DeserializedProfileScriptConfig, JunitConfig, JunitImpl, MaxFail, NextestVersionDeserialize, + RetryPolicy, ScriptConfig, ScriptId, SettingSource, SetupScripts, SlowTimeout, TestGroup, + TestGroupConfig, TestSettings, TestThreads, ThreadsRequired, ToolConfigFile, }; use crate::{ errors::{ @@ -512,11 +512,7 @@ impl NextestConfig { fn make_profile(&self, name: &str) -> Result, ProfileNotFound> { let custom_profile = self.inner.get_profile(name)?; - // The profile was found: construct it. - let mut store_dir = self.workspace_root.join(&self.inner.store.dir); - store_dir.push(name); - - // Grab the compiled data as well. + // Grab the compiled data. let compiled_data = match self.compiled.other.get(name) { Some(data) => data.clone().chain(self.compiled.default.clone()), None => self.compiled.default.clone(), @@ -524,7 +520,8 @@ impl NextestConfig { Ok(EarlyProfile { name: name.to_owned(), - store_dir, + workspace_root: self.workspace_root.clone(), + store: self.inner.store.clone(), default_profile: &self.inner.default_profile, custom_profile, test_groups: &self.inner.test_groups, @@ -589,7 +586,8 @@ pub(crate) struct FinalConfig { /// Returned by [`NextestConfig::profile`]. pub struct EarlyProfile<'cfg> { name: String, - store_dir: Utf8PathBuf, + workspace_root: Utf8PathBuf, + store: StoreConfigImpl, default_profile: &'cfg DefaultProfileImpl, custom_profile: Option<&'cfg CustomProfileImpl>, test_groups: &'cfg BTreeMap, @@ -600,11 +598,6 @@ pub struct EarlyProfile<'cfg> { } impl<'cfg> EarlyProfile<'cfg> { - /// Returns the absolute profile-specific store directory. - pub fn store_dir(&self) -> &Utf8Path { - &self.store_dir - } - /// Returns the global test group configuration. pub fn test_group_config(&self) -> &'cfg BTreeMap { self.test_groups @@ -614,10 +607,7 @@ impl<'cfg> EarlyProfile<'cfg> { /// /// This is a separate step from parsing the config and reading a profile so that cargo-nextest /// can tell users about configuration parsing errors before building the binary list. - pub fn apply_build_platforms( - self, - build_platforms: &BuildPlatforms, - ) -> EvaluatableProfile<'cfg> { + pub fn into_evaluatable(self, build_platforms: &BuildPlatforms) -> EvaluatableProfile<'cfg> { let compiled_data = self.compiled_data.apply_build_platforms(build_platforms); let resolved_default_filter = { @@ -639,7 +629,8 @@ impl<'cfg> EarlyProfile<'cfg> { EvaluatableProfile { name: self.name, - store_dir: self.store_dir, + workspace_root: self.workspace_root, + store: self.store, default_profile: self.default_profile, custom_profile: self.custom_profile, scripts: self.scripts, @@ -652,11 +643,12 @@ impl<'cfg> EarlyProfile<'cfg> { /// A configuration profile for nextest. Contains most configuration used by the nextest runner. /// -/// Returned by [`EarlyProfile::apply_build_platforms`]. +/// Returned by [`EarlyProfile::into_evaluatable`]. #[derive(Clone, Debug)] pub struct EvaluatableProfile<'cfg> { name: String, - store_dir: Utf8PathBuf, + workspace_root: Utf8PathBuf, + store: StoreConfigImpl, default_profile: &'cfg DefaultProfileImpl, custom_profile: Option<&'cfg CustomProfileImpl>, test_groups: &'cfg BTreeMap, @@ -676,8 +668,17 @@ impl<'cfg> EvaluatableProfile<'cfg> { } /// Returns the absolute profile-specific store directory. - pub fn store_dir(&self) -> &Utf8Path { - &self.store_dir + /// + /// The target directory must be provided. + /// + /// The store directory might not exist yet. The caller is responsible for + /// creating it. + pub fn store_dir(&self, target_dir: &Utf8Path) -> Utf8PathBuf { + let mut store_dir = self + .store + .resolve_store_dir(&self.workspace_root, target_dir); + store_dir.push(&self.name); + store_dir } /// Returns the context in which to evaluate filtersets. @@ -808,7 +809,6 @@ impl<'cfg> EvaluatableProfile<'cfg> { /// Returns the JUnit configuration for this profile. pub fn junit(&self) -> Option> { JunitConfig::new( - self.store_dir(), self.custom_profile.map(|p| &p.junit), &self.default_profile.junit, ) @@ -901,12 +901,6 @@ impl NextestConfigDeserialize { } } -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -struct StoreConfigImpl { - dir: Utf8PathBuf, -} - #[derive(Clone, Debug)] pub(super) struct DefaultProfileImpl { default_filter: String, diff --git a/nextest-runner/src/config/junit.rs b/nextest-runner/src/config/junit.rs index 20d13bfa742..adbe665d982 100644 --- a/nextest-runner/src/config/junit.rs +++ b/nextest-runner/src/config/junit.rs @@ -17,7 +17,6 @@ pub struct JunitConfig<'cfg> { impl<'cfg> JunitConfig<'cfg> { pub(super) fn new( - store_dir: &Utf8Path, custom_data: Option<&'cfg JunitImpl>, default_data: &'cfg DefaultJunitImpl, ) -> Option { @@ -27,7 +26,6 @@ impl<'cfg> JunitConfig<'cfg> { .as_deref(); path.map(|path| { - let path = store_dir.join(path); let report_name = custom_data .and_then(|custom| custom.report_name.as_deref()) .unwrap_or(&default_data.report_name); @@ -38,7 +36,7 @@ impl<'cfg> JunitConfig<'cfg> { .and_then(|custom| custom.store_failure_output) .unwrap_or(default_data.store_failure_output); Self { - path, + path: path.to_owned(), report_name, store_success_output, store_failure_output, @@ -47,8 +45,8 @@ impl<'cfg> JunitConfig<'cfg> { } /// Returns the absolute path to the JUnit report. - pub fn path(&self) -> &Utf8Path { - &self.path + pub fn path(&self, store_dir: &Utf8Path) -> Utf8PathBuf { + store_dir.join(&self.path) } /// Returns the name of the JUnit report. diff --git a/nextest-runner/src/config/max_fail.rs b/nextest-runner/src/config/max_fail.rs index 0210773aac8..2db6baf4cf9 100644 --- a/nextest-runner/src/config/max_fail.rs +++ b/nextest-runner/src/config/max_fail.rs @@ -258,7 +258,7 @@ mod tests { let profile = config .profile("custom") .unwrap() - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); assert_eq!(profile.max_fail(), expected); } diff --git a/nextest-runner/src/config/mod.rs b/nextest-runner/src/config/mod.rs index 63578fdef50..7cb77bea25e 100644 --- a/nextest-runner/src/config/mod.rs +++ b/nextest-runner/src/config/mod.rs @@ -31,6 +31,7 @@ mod priority; mod retry_policy; mod scripts; mod slow_timeout; +mod store; mod test_group; mod test_threads; mod threads_required; diff --git a/nextest-runner/src/config/overrides.rs b/nextest-runner/src/config/overrides.rs index 4d5a39102c9..bea358b48fa 100644 --- a/nextest-runner/src/config/overrides.rs +++ b/nextest-runner/src/config/overrides.rs @@ -957,7 +957,7 @@ mod tests { let profile = nextest_config_result .profile("default") .expect("valid profile name") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); // This query matches override 2. let host_binary_query = @@ -1268,7 +1268,7 @@ mod tests { let profile = nextest_config .profile("default") .expect("valid profile name") - .apply_build_platforms(&build_platforms); + .into_evaluatable(&build_platforms); // Check that the override is correctly applied. let target_binary_query = binary_query( diff --git a/nextest-runner/src/config/retry_policy.rs b/nextest-runner/src/config/retry_policy.rs index 27703293a28..cf9cff26339 100644 --- a/nextest-runner/src/config/retry_policy.rs +++ b/nextest-runner/src/config/retry_policy.rs @@ -219,7 +219,7 @@ mod tests { config .profile("default") .expect("default profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::Fixed { count: 3, @@ -233,7 +233,7 @@ mod tests { config .profile("no-retries") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::new_without_delay(0), "no-retries retries matches" @@ -243,7 +243,7 @@ mod tests { config .profile("fixed-with-delay") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::Fixed { count: 3, @@ -257,7 +257,7 @@ mod tests { config .profile("exp") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::Exponential { count: 4, @@ -272,7 +272,7 @@ mod tests { config .profile("exp-with-max-delay") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::Exponential { count: 5, @@ -287,7 +287,7 @@ mod tests { config .profile("exp-with-max-delay-and-jitter") .expect("profile exists") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .retries(), RetryPolicy::Exponential { count: 6, @@ -637,7 +637,7 @@ mod tests { let profile = config .profile("ci") .expect("ci profile is defined") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); let settings_for = profile.settings_for(&query); assert_eq!( settings_for.retries(), diff --git a/nextest-runner/src/config/scripts.rs b/nextest-runner/src/config/scripts.rs index c09bd46d98e..157910c1905 100644 --- a/nextest-runner/src/config/scripts.rs +++ b/nextest-runner/src/config/scripts.rs @@ -683,7 +683,7 @@ mod tests { let profile = nextest_config_result .profile("default") .expect("valid profile name") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); // This query matches the foo and bar scripts. let host_binary_query = diff --git a/nextest-runner/src/config/slow_timeout.rs b/nextest-runner/src/config/slow_timeout.rs index c1ad6a67caa..5b29c857f2c 100644 --- a/nextest-runner/src/config/slow_timeout.rs +++ b/nextest-runner/src/config/slow_timeout.rs @@ -203,7 +203,7 @@ mod tests { nextest_config .profile("default") .expect("default profile should exist") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .slow_timeout(), expected_default, ); @@ -213,7 +213,7 @@ mod tests { nextest_config .profile("ci") .expect("ci profile should exist") - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .slow_timeout(), expected_ci, ); diff --git a/nextest-runner/src/config/store.rs b/nextest-runner/src/config/store.rs new file mode 100644 index 00000000000..dccfc14c445 --- /dev/null +++ b/nextest-runner/src/config/store.rs @@ -0,0 +1,99 @@ +// Copyright (c) The nextest Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::config::helpers::deserialize_relative_path; +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, +}; +use std::fmt; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(super) struct StoreConfigImpl { + dir: StoreDir, +} + +impl StoreConfigImpl { + pub(crate) fn resolve_store_dir( + &self, + workspace_root: &Utf8Path, + target_dir: &Utf8Path, + ) -> Utf8PathBuf { + match &self.dir { + StoreDir::Path(path) => workspace_root.join(path), + StoreDir::RelativeTo { dir, relative_to } => match relative_to { + StoreRelativeTo::WorkspaceRoot => workspace_root.join(dir), + StoreRelativeTo::TargetDir => target_dir.join(dir), + }, + } + } +} + +#[derive(Clone, Debug)] +enum StoreDir { + Path(Utf8PathBuf), + RelativeTo { + dir: Utf8PathBuf, + relative_to: StoreRelativeTo, + }, +} + +impl<'de> Deserialize<'de> for StoreDir { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct V; + + impl<'de2> Visitor<'de2> for V { + type Value = StoreDir; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a path relative to the workspace root, \ + or a map: { path = \"nextest\", relative-to = \"target\" }", + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(StoreDir::Path(v.into())) + } + + fn visit_map(self, map: A) -> Result + where + A: de::MapAccess<'de2>, + { + let de = de::value::MapAccessDeserializer::new(map); + let map = StoreDirMap::deserialize(de)?; + Ok(StoreDir::RelativeTo { + dir: map.path, + relative_to: map.relative_to, + }) + } + } + + deserializer.deserialize_any(V) + } +} + +/// A deserializer for `{ path = "nextest", relative-to = "target" }`. +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct StoreDirMap { + #[serde(deserialize_with = "deserialize_relative_path")] + path: Utf8PathBuf, + relative_to: StoreRelativeTo, +} + +/// A deserializer for store.dir.relative-to. +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +enum StoreRelativeTo { + WorkspaceRoot, + TargetDir, +} diff --git a/nextest-runner/src/config/test_group.rs b/nextest-runner/src/config/test_group.rs index 4aaccca307c..ada42e85422 100644 --- a/nextest-runner/src/config/test_group.rs +++ b/nextest-runner/src/config/test_group.rs @@ -218,7 +218,7 @@ mod tests { Ok(expected_groups) => { let config = config_res.expect("config is valid"); let profile = config.profile("default").expect("default profile is known"); - let profile = profile.apply_build_platforms(&build_platforms()); + let profile = profile.into_evaluatable(&build_platforms()); assert_eq!( profile .test_group_config() @@ -307,7 +307,7 @@ mod tests { Ok(expected_groups) => { let config = config_res.expect("config is valid"); let profile = config.profile("default").expect("default profile is known"); - let profile = profile.apply_build_platforms(&build_platforms()); + let profile = profile.into_evaluatable(&build_platforms()); assert_eq!( profile .test_group_config() diff --git a/nextest-runner/src/config/test_threads.rs b/nextest-runner/src/config/test_threads.rs index c4a99388ecd..c7637f34760 100644 --- a/nextest-runner/src/config/test_threads.rs +++ b/nextest-runner/src/config/test_threads.rs @@ -171,7 +171,7 @@ mod tests { .unwrap() .profile("custom") .unwrap() - .apply_build_platforms(&build_platforms()) + .into_evaluatable(&build_platforms()) .custom_profile() .unwrap() .test_threads() diff --git a/nextest-runner/src/config/threads_required.rs b/nextest-runner/src/config/threads_required.rs index fdae641e7e3..4298adeb431 100644 --- a/nextest-runner/src/config/threads_required.rs +++ b/nextest-runner/src/config/threads_required.rs @@ -183,7 +183,7 @@ mod tests { let profile = config .profile("custom") .unwrap() - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); let test_threads = profile.test_threads().compute(); let threads_required = profile.threads_required().compute(test_threads); diff --git a/nextest-runner/src/config/tool_config.rs b/nextest-runner/src/config/tool_config.rs index 20c9e8f851e..c46d7ddcd3c 100644 --- a/nextest-runner/src/config/tool_config.rs +++ b/nextest-runner/src/config/tool_config.rs @@ -221,7 +221,7 @@ mod tests { let default_profile = config .profile(NextestConfig::DEFAULT_PROFILE) .expect("default profile is present") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); // This is present in .config/nextest.toml and is the highest priority assert_eq!(default_profile.retries(), RetryPolicy::new_without_delay(3)); @@ -285,7 +285,7 @@ mod tests { let tool_profile = config .profile("tool") .expect("tool profile is present") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); assert_eq!(tool_profile.retries(), RetryPolicy::new_without_delay(12)); assert_eq!( tool_profile.settings_for(&test_foo_query).retries(), @@ -306,7 +306,7 @@ mod tests { let tool2_profile = config .profile("tool2") .expect("tool2 profile is present") - .apply_build_platforms(&build_platforms()); + .into_evaluatable(&build_platforms()); assert_eq!(tool2_profile.retries(), RetryPolicy::new_without_delay(18)); assert_eq!( tool2_profile.settings_for(&test_foo_query).retries(), diff --git a/nextest-runner/src/errors.rs b/nextest-runner/src/errors.rs index 11529af789a..8018380cb59 100644 --- a/nextest-runner/src/errors.rs +++ b/nextest-runner/src/errors.rs @@ -1328,6 +1328,22 @@ pub enum ArchiveExtractError { ReporterIo(std::io::Error), } +/// An error occurred while setting up the JUnit reporter. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum JunitSetupError { + /// An error occurred while creating the store directory. + #[error("error creating store directory `{path}`")] + CreateStoreDir { + /// The path that we couldn't create. + path: Utf8PathBuf, + + /// The error that occurred. + #[source] + error: std::io::Error, + }, +} + /// An error that occurs while writing an event. #[derive(Debug, Error)] #[non_exhaustive] diff --git a/nextest-runner/src/reporter/aggregator/imp.rs b/nextest-runner/src/reporter/aggregator/imp.rs index 5c3e37ef062..a16965bc06b 100644 --- a/nextest-runner/src/reporter/aggregator/imp.rs +++ b/nextest-runner/src/reporter/aggregator/imp.rs @@ -2,24 +2,37 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use super::junit::MetadataJunit; -use crate::{config::EvaluatableProfile, errors::WriteEventError, reporter::events::TestEvent}; -use camino::Utf8PathBuf; +use crate::{ + config::EvaluatableProfile, + errors::{JunitSetupError, WriteEventError}, + reporter::events::TestEvent, +}; +use camino::Utf8Path; +/// Aggregator for test events. +/// +/// Currently, this aggregator supports JUnit XML output. #[derive(Clone, Debug)] -#[expect(dead_code)] -pub(crate) struct EventAggregator<'cfg> { - store_dir: Utf8PathBuf, +pub struct EventAggregator<'cfg> { // TODO: log information in a JSONable report (converting that to XML later) instead of directly // writing it to XML junit: Option>, } impl<'cfg> EventAggregator<'cfg> { - pub(crate) fn new(profile: &EvaluatableProfile<'cfg>) -> Self { - Self { - store_dir: profile.store_dir().to_owned(), - junit: profile.junit().map(MetadataJunit::new), - } + /// Creates a new `EventAggregator`. + pub fn new( + profile: &EvaluatableProfile<'cfg>, + target_dir: &Utf8Path, + ) -> Result { + let junit = profile + .junit() + .map(|config| { + let store_dir = profile.store_dir(target_dir).to_owned(); + MetadataJunit::new(store_dir, config) + }) + .transpose()?; + Ok(Self { junit }) } pub(crate) fn write_event(&mut self, event: TestEvent<'cfg>) -> Result<(), WriteEventError> { diff --git a/nextest-runner/src/reporter/aggregator/junit.rs b/nextest-runner/src/reporter/aggregator/junit.rs index 1cc2eadc54d..d562dfef283 100644 --- a/nextest-runner/src/reporter/aggregator/junit.rs +++ b/nextest-runner/src/reporter/aggregator/junit.rs @@ -5,7 +5,7 @@ use crate::{ config::{JunitConfig, ScriptId}, - errors::{DisplayErrorChain, WriteEventError}, + errors::{DisplayErrorChain, JunitSetupError, WriteEventError}, list::TestInstanceId, reporter::{ events::{ExecutionDescription, ExecutionResult, TestEvent, TestEventKind, UnitKind}, @@ -13,6 +13,7 @@ use crate::{ }, test_output::{ChildExecutionOutput, ChildOutput}, }; +use camino::Utf8PathBuf; use debug_ignore::DebugIgnore; use indexmap::IndexMap; use nextest_metadata::RustBinaryId; @@ -28,16 +29,28 @@ static PROCESS_FAILED_TO_START: &str = "(process failed to start)"; #[derive(Clone, Debug)] pub(super) struct MetadataJunit<'cfg> { + junit_path: Utf8PathBuf, config: JunitConfig<'cfg>, test_suites: DebugIgnore, TestSuite>>, } impl<'cfg> MetadataJunit<'cfg> { - pub(super) fn new(config: JunitConfig<'cfg>) -> Self { - Self { + pub(super) fn new( + store_dir: Utf8PathBuf, + config: JunitConfig<'cfg>, + ) -> Result { + let junit_path = config.path(&store_dir); + let junit_dir = junit_path.parent().expect("junit path must have a parent"); + std::fs::create_dir_all(junit_dir).map_err(|error| JunitSetupError::CreateStoreDir { + path: junit_dir.to_path_buf(), + error, + })?; + + Ok(Self { + junit_path, config, test_suites: DebugIgnore(IndexMap::new()), - } + }) } pub(super) fn write_event(&mut self, event: TestEvent<'cfg>) -> Result<(), WriteEventError> { @@ -207,21 +220,14 @@ impl<'cfg> MetadataJunit<'cfg> { .set_time(elapsed) .add_test_suites(self.test_suites.drain(..).map(|(_, testsuite)| testsuite)); - let junit_path = self.config.path(); - let junit_dir = junit_path.parent().expect("junit path must have a parent"); - std::fs::create_dir_all(junit_dir).map_err(|error| WriteEventError::Fs { - file: junit_dir.to_path_buf(), - error, - })?; - - let f = File::create(junit_path).map_err(|error| WriteEventError::Fs { - file: junit_path.to_path_buf(), + let f = File::create(&self.junit_path).map_err(|error| WriteEventError::Fs { + file: self.junit_path.clone(), error, })?; report .serialize(f) .map_err(|error| WriteEventError::Junit { - file: junit_path.to_path_buf(), + file: self.junit_path.clone(), error, })?; } diff --git a/nextest-runner/src/reporter/aggregator/mod.rs b/nextest-runner/src/reporter/aggregator/mod.rs index 1c18920728a..a9d65cd3d7e 100644 --- a/nextest-runner/src/reporter/aggregator/mod.rs +++ b/nextest-runner/src/reporter/aggregator/mod.rs @@ -6,4 +6,4 @@ mod imp; mod junit; -pub(crate) use imp::*; +pub use imp::*; diff --git a/nextest-runner/src/reporter/imp.rs b/nextest-runner/src/reporter/imp.rs index 0bedbe305e3..d2f13eaf216 100644 --- a/nextest-runner/src/reporter/imp.rs +++ b/nextest-runner/src/reporter/imp.rs @@ -104,10 +104,9 @@ impl ReporterBuilder { test_list: &TestList, profile: &EvaluatableProfile<'a>, output: ReporterStderr<'a>, + aggregator: EventAggregator<'a>, structured_reporter: StructuredReporter<'a>, ) -> Reporter<'a> { - let aggregator = EventAggregator::new(profile); - let status_level = self.status_level.unwrap_or_else(|| profile.status_level()); let final_status_level = self .final_status_level diff --git a/nextest-runner/src/reporter/mod.rs b/nextest-runner/src/reporter/mod.rs index 106e5f7d551..224f1c5c5bc 100644 --- a/nextest-runner/src/reporter/mod.rs +++ b/nextest-runner/src/reporter/mod.rs @@ -13,6 +13,7 @@ mod helpers; mod imp; pub mod structured; +pub use aggregator::EventAggregator; pub use displayer::{FinalStatusLevel, StatusLevel, TestOutputDisplay}; pub use error_description::*; pub use helpers::highlight_end; diff --git a/nextest-runner/src/runner/imp.rs b/nextest-runner/src/runner/imp.rs index df1e7e40558..1514275ee88 100644 --- a/nextest-runner/src/runner/imp.rs +++ b/nextest-runner/src/runner/imp.rs @@ -494,7 +494,7 @@ mod tests { let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); let signal_handler = SignalHandlerKind::Noop; let input_handler = InputHandlerKind::Noop; - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let runner = builder .build( &test_list, diff --git a/nextest-runner/tests/integration/basic.rs b/nextest-runner/tests/integration/basic.rs index 2ea15d7595f..2d7992fbf91 100644 --- a/nextest-runner/tests/integration/basic.rs +++ b/nextest-runner/tests/integration/basic.rs @@ -125,7 +125,7 @@ fn test_run() -> Result<()> { .profile(NextestConfig::DEFAULT_PROFILE) .expect("default config is valid"); let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let runner = TestRunnerBuilder::default() .build( @@ -263,7 +263,7 @@ fn test_run_ignored() -> Result<()> { .profile(NextestConfig::DEFAULT_PROFILE) .expect("default config is valid"); let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let runner = TestRunnerBuilder::default() .build( @@ -507,7 +507,7 @@ fn test_retries(retries: Option) -> Result<()> { .profile("with-retries") .expect("with-retries config is valid"); let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let profile_retries = profile.retries(); assert_eq!( @@ -673,7 +673,7 @@ fn test_termination() -> Result<()> { .profile("with-termination") .expect("with-termination config is valid"); let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); - let profile = profile.apply_build_platforms(&build_platforms); + let profile = profile.into_evaluatable(&build_platforms); let runner = TestRunnerBuilder::default() .build( diff --git a/nextest-runner/tests/integration/target_runner.rs b/nextest-runner/tests/integration/target_runner.rs index f7a79530b89..40e874ab388 100644 --- a/nextest-runner/tests/integration/target_runner.rs +++ b/nextest-runner/tests/integration/target_runner.rs @@ -242,7 +242,7 @@ fn test_run_with_target_runner() -> Result<()> { let profile = config .profile(NextestConfig::DEFAULT_PROFILE) .expect("default config is valid") - .apply_build_platforms(&build_platforms); + .into_evaluatable(&build_platforms); let runner = TestRunnerBuilder::default(); let runner = runner