diff --git a/src/commands/runtime/build.rs b/src/commands/runtime/build.rs index 24ffc320..918d8ef3 100644 --- a/src/commands/runtime/build.rs +++ b/src/commands/runtime/build.rs @@ -1152,9 +1152,6 @@ impl RuntimeBuildCommand { // modes, so it would otherwise vanish exactly where builds usually // run. TUI queues it above the task region; JSON mode writes it to // stderr without corrupting the NDJSON stream. - let project_dir = std::path::Path::new(&self.config_path) - .parent() - .unwrap_or(std::path::Path::new(".")); if let Err(e) = self .generate_tuf_staging( config, @@ -1165,7 +1162,6 @@ impl RuntimeBuildCommand { repo_release, container_helper, runs_on_context, - project_dir, ) .await { @@ -1265,7 +1261,6 @@ echo -n '}}' repo_release: Option<&String>, container_helper: &SdkContainer, runs_on_context: Option<&RunsOnContext>, - project_dir: &std::path::Path, ) -> Result<()> { // Resolve signing keys. At Level 0 (Connect, no local key), skip TUF staging entirely. let signing_key_name = config.get_runtime_signing_key_name(&self.runtime_name); @@ -1307,7 +1302,6 @@ echo -n '}}' repo_release, container_helper, runs_on_context, - project_dir, &content_signer, ) .await; @@ -1388,37 +1382,9 @@ echo -n '}}' )?; // Phase 3: Write signed files into the build volume via a container run. - // We write them to a temp dir under the project directory (accessible inside - // the container as /opt/src/.tuf-staging-tmp/), then copy into the volume. - let tmp_dir = project_dir.join(".tuf-staging-tmp"); - let tmp_delegations = tmp_dir.join("delegations"); - std::fs::create_dir_all(&tmp_delegations) - .context("Failed to create TUF staging temp directory")?; - - std::fs::write(tmp_dir.join("targets.json"), &repo_metadata.targets_json) - .context("Failed to write targets.json to temp dir")?; - std::fs::write( - tmp_delegations.join(format!("runtime-{}.json", collection.runtime_uuid)), - &repo_metadata.delegated_targets_json, - ) - .context("Failed to write delegated targets to temp dir")?; - - let runtime_name = &self.runtime_name; - let runtime_uuid = &collection.runtime_uuid; - let copy_script = format!( - r#"set -euo pipefail -DEST="$AVOCADO_PREFIX/runtimes/{runtime_name}/var-staging/lib/avocado/tuf-staging" -mkdir -p "$DEST/delegations" -cp /opt/src/.tuf-staging-tmp/targets.json "$DEST/targets.json" -cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ - "$DEST/delegations/runtime-{runtime_uuid}.json" -"# - ); - let copy_run_config = RunConfig { container_image: container_image.to_string(), target: target_arch.to_string(), - command: copy_script, verbose: false, source_environment: true, interactive: false, @@ -1430,10 +1396,25 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ env_vars: self.runtime_env_vars(), ..Default::default() }; - run_container_command(container_helper, copy_run_config, runs_on_context).await?; - - // Clean up host temp files. - let _ = std::fs::remove_dir_all(&tmp_dir); + stage_into_volume( + container_helper, + &self.runtime_name, + &collection.runtime_uuid, + &repo_metadata.targets_json, + &repo_metadata.delegated_targets_json, + "delegation", + |command| { + run_container_command( + container_helper, + RunConfig { + command, + ..copy_run_config + }, + runs_on_context, + ) + }, + ) + .await?; print_info( &format!( @@ -1459,7 +1440,6 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ repo_release: Option<&String>, container_helper: &SdkContainer, runs_on_context: Option<&RunsOnContext>, - project_dir: &std::path::Path, content_signer: &crate::utils::update_signing::TufSigner, ) -> Result<()> { // Phase 1: Collect artifact hashes from inside the build volume. @@ -1507,35 +1487,9 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ )?; // Phase 3: Write files into the build volume. - let tmp_dir = project_dir.join(".tuf-staging-tmp"); - let tmp_delegations = tmp_dir.join("delegations"); - std::fs::create_dir_all(&tmp_delegations) - .context("Failed to create TUF staging temp directory")?; - - std::fs::write(tmp_dir.join("targets.json"), &metadata.targets_json) - .context("Failed to write targets.json to temp dir")?; - std::fs::write( - tmp_delegations.join(format!("runtime-{}.json", collection.runtime_uuid)), - &metadata.delegated_targets_json, - ) - .context("Failed to write delegated targets to temp dir")?; - - let runtime_name = &self.runtime_name; - let runtime_uuid = &collection.runtime_uuid; - let copy_script = format!( - r#"set -euo pipefail -DEST="$AVOCADO_PREFIX/runtimes/{runtime_name}/var-staging/lib/avocado/tuf-staging" -mkdir -p "$DEST/delegations" -cp /opt/src/.tuf-staging-tmp/targets.json "$DEST/targets.json" -cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ - "$DEST/delegations/runtime-{runtime_uuid}.json" -"# - ); - let copy_run_config = RunConfig { container_image: container_image.to_string(), target: target_arch.to_string(), - command: copy_script, verbose: false, source_environment: true, interactive: false, @@ -1547,9 +1501,25 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \ env_vars: self.runtime_env_vars(), ..Default::default() }; - run_container_command(container_helper, copy_run_config, runs_on_context).await?; - - let _ = std::fs::remove_dir_all(&tmp_dir); + stage_into_volume( + container_helper, + &self.runtime_name, + &collection.runtime_uuid, + &metadata.targets_json, + &metadata.delegated_targets_json, + "content delegation", + |command| { + run_container_command( + container_helper, + RunConfig { + command, + ..copy_run_config + }, + runs_on_context, + ) + }, + ) + .await?; print_info( &format!( @@ -3157,6 +3127,78 @@ fn collect_runtime_compile_entries(merged_runtime: &serde_yaml::Value) -> Vec Result<(tempfile::TempDir, String)> { + let staging = tempfile::Builder::new() + .prefix(".tuf-staging-") + .tempdir_in(root) + .context("Failed to create TUF staging temp directory")?; + let name = staging + .path() + .file_name() + .and_then(|n| n.to_str()) + .context("TUF staging temp directory has no name")? + .to_string(); + Ok((staging, name)) +} + +/// Write the signed `targets.json` and delegated targets under the host +/// directory the container mounts at `/opt/src`, then hand `copy` the script +/// that moves them into the build volume. `copy` returns whether the +/// container run succeeded; a failed copy is an error naming the runtime. +async fn stage_into_volume( + container_helper: &SdkContainer, + runtime_name: &str, + runtime_uuid: &str, + targets_json: &str, + delegated_targets_json: &str, + kind: &str, + copy: F, +) -> Result<()> +where + F: FnOnce(String) -> Fut, + Fut: std::future::Future>, +{ + let (staging, staging_dir) = tuf_staging_dir(container_helper.mounted_src_dir())?; + let tmp_dir = staging.path(); + let tmp_delegations = tmp_dir.join("delegations"); + std::fs::create_dir_all(&tmp_delegations) + .context("Failed to create TUF staging temp directory")?; + + std::fs::write(tmp_dir.join("targets.json"), targets_json) + .context("Failed to write targets.json to temp dir")?; + std::fs::write( + tmp_delegations.join(format!("runtime-{runtime_uuid}.json")), + delegated_targets_json, + ) + .context("Failed to write delegated targets to temp dir")?; + + let copy_script = format!( + r#"set -euo pipefail +DEST="$AVOCADO_PREFIX/runtimes/{runtime_name}/var-staging/lib/avocado/tuf-staging" +mkdir -p "$DEST/delegations" +cp "/opt/src/{staging_dir}/targets.json" "$DEST/targets.json" +cp "/opt/src/{staging_dir}/delegations/runtime-{runtime_uuid}.json" \ + "$DEST/delegations/runtime-{runtime_uuid}.json" +"# + ); + + let copied = copy(copy_script).await?; + + // Clean up host temp files. + drop(staging); + + if !copied { + return Err(anyhow::anyhow!( + "failed to copy the {kind} staging files into the build volume for runtime '{runtime_name}'" + )); + } + Ok(()) +} + /// Helper function to run a container command, using shared context if available async fn run_container_command( container_helper: &SdkContainer, @@ -3317,6 +3359,127 @@ fi #[cfg(test)] mod tests { + use super::{Config, SdkContainer}; + + /// Each staging call gets its own directory under the project root, so + /// two builds sharing a `src_dir` never write or delete each other's + /// files. The name is what the copy script appends to `/opt/src`. + #[test] + fn tuf_staging_dir_is_unique_per_call_and_removed_on_drop() { + let root = tempfile::TempDir::new().unwrap(); + let (a, a_name) = super::tuf_staging_dir(root.path()).unwrap(); + let (b, b_name) = super::tuf_staging_dir(root.path()).unwrap(); + assert_ne!(a_name, b_name); + assert!(a_name.starts_with(".tuf-staging-")); + assert_eq!(a.path(), root.path().join(&a_name)); + assert!(a.path().is_dir() && b.path().is_dir()); + let a_path = a.path().to_path_buf(); + drop(a); + assert!( + !a_path.exists(), + "dropping the handle removes the directory" + ); + assert!(b.path().is_dir(), "the other call's directory is untouched"); + } + + /// A project whose config sits in `/project/avocado/` with + /// `src_dir: ..`. The container mounts `/project` at `/opt/src`. + fn src_dir_above_config_dir() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) { + let root = tempfile::TempDir::new().unwrap(); + let project = root.path().canonicalize().unwrap().join("project"); + let config_dir = project.join("avocado"); + std::fs::create_dir_all(&config_dir).unwrap(); + let config_path = config_dir.join("avocado.yaml"); + (root, project, config_path) + } + + fn staging_dirs(dir: &std::path::Path) -> Vec { + std::fs::read_dir(dir) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with(".tuf-staging-")) + }) + .collect() + } + + /// The copy script reads `/opt/src//`, so the files must be on + /// the host under the directory the container mounts there: `src_dir` + /// when set, else the working directory. Never the config file's + /// directory unless one of those is it. + #[tokio::test] + async fn stage_into_volume_writes_under_the_mounted_src_dir() { + let (_root, project, config_path) = src_dir_above_config_dir(); + let config_str = config_path.to_str().unwrap(); + let config = Config::load_from_yaml_str("src_dir: ..\nextensions: {}\n").unwrap(); + let with_src_dir = SdkContainer::from_config(config_str, &config).unwrap(); + + // No `src_dir`, run from the parent with `-C project/avocado/avocado.yaml`. + let config = Config::load_from_yaml_str("extensions: {}\n").unwrap(); + let mut from_parent = SdkContainer::from_config(config_str, &config).unwrap(); + from_parent.cwd = project.clone(); + + for container in [with_src_dir, from_parent] { + super::stage_into_volume( + &container, + "dev", + "uuid-1", + "{\"targets\":1}", + "{\"delegated\":1}", + "delegation", + |script| { + let staged = staging_dirs(&project); + assert_eq!( + staged.len(), + 1, + "one staging dir under {}", + project.display() + ); + let name = staged[0].file_name().unwrap().to_str().unwrap(); + assert!(script.contains(&format!("/opt/src/{name}/targets.json"))); + assert_eq!( + std::fs::read_to_string(staged[0].join("targets.json")).unwrap(), + "{\"targets\":1}" + ); + assert!(staged[0].join("delegations/runtime-uuid-1.json").is_file()); + assert!(staging_dirs(config_path.parent().unwrap()).is_empty()); + async { Ok(true) } + }, + ) + .await + .unwrap(); + assert!(staging_dirs(&project).is_empty(), "staging is removed"); + } + } + + /// A failed copy leaves the runtime without `tuf-staging/`, so it must + /// be an error naming the runtime and not a success line. + #[tokio::test] + async fn stage_into_volume_fails_when_the_copy_fails() { + let (_root, project, config_path) = src_dir_above_config_dir(); + let config_str = config_path.to_str().unwrap(); + let config = Config::load_from_yaml_str("src_dir: ..\nextensions: {}\n").unwrap(); + let container = SdkContainer::from_config(config_str, &config).unwrap(); + + let err = super::stage_into_volume( + &container, + "dev", + "uuid-1", + "{}", + "{}", + "content delegation", + |_| async { Ok(false) }, + ) + .await + .unwrap_err() + .to_string(); + assert!(err.contains("content delegation staging files"), "{err}"); + assert!(err.contains("runtime 'dev'"), "{err}"); + assert!(staging_dirs(&project).is_empty(), "staging is removed"); + } + #[test] fn fit_assembly_script_is_gated_and_handles_signing_both_ways() { let s = super::generate_fit_assembly_script(); diff --git a/src/utils/config.rs b/src/utils/config.rs index 0baaf676..857c97b2 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -2382,14 +2382,15 @@ impl Config { return Ok(RemoteExtensionMerge::default()); } - // Get src_dir for loading volume state + // `src_dir` is the base for `path:` sources. The volume state is keyed + // on the working directory, not on `src_dir`. let config_path_str = config_path.to_string_lossy(); let src_dir = temp_config .get_resolved_src_dir::<&str>(config_path_str.as_ref()) .unwrap_or_else(|| config_file_dir(config_path).to_path_buf()); // Try to load volume state for container-based config reading - let volume_state = crate::utils::volume::VolumeState::load_from_dir(&src_dir) + let volume_state = crate::utils::volume::VolumeState::load_from_current_dir() .ok() .flatten(); @@ -5505,8 +5506,9 @@ impl Config { .to_path_buf() }); - // Try to load volume state and get the mountpoint - if let Ok(Some(volume_state)) = crate::utils::volume::VolumeState::load_from_dir(&src_dir) { + // Try to load volume state and get the mountpoint. The state file is + // keyed on the working directory, not on `src_dir`. + if let Ok(Some(volume_state)) = crate::utils::volume::VolumeState::load_from_current_dir() { // Use synchronous Docker inspect to get the mountpoint if let Ok(mountpoint) = crate::utils::ext_source_reader::volume_mountpoint(&volume_state) @@ -6895,6 +6897,95 @@ mod tests { ); } + /// A `src_dir: ..` project run from its config directory: the config and + /// the `.avocado-state` that `get_or_create_volume` wrote sit in + /// `/project/avocado`, and `src_dir` is `/project`. The state + /// names a fake container tool whose `volume inspect` prints + /// `/mount`, which holds a fetched `remote-ext`. Returns the root, + /// the config dir and the config path. + #[cfg(unix)] + fn state_in_config_dir_below_src_dir( + target: &str, + ) -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) { + use std::os::unix::fs::PermissionsExt; + let root = tempfile::TempDir::new().unwrap(); + let base = root.path().canonicalize().unwrap(); + let config_dir = base.join("project").join("avocado"); + std::fs::create_dir_all(&config_dir).unwrap(); + let config_path = config_dir.join("avocado.yaml"); + + let mount = base.join("mount"); + let ext_dir = mount.join(target).join("includes").join("remote-ext"); + std::fs::create_dir_all(&ext_dir).unwrap(); + std::fs::write( + ext_dir.join("avocado.yaml"), + "extensions:\n remote-ext:\n types: [sysext]\n", + ) + .unwrap(); + + let tool = base.join("fake-docker"); + std::fs::write(&tool, format!("#!/bin/sh\necho {}\n", mount.display())).unwrap(); + std::fs::set_permissions(&tool, std::fs::Permissions::from_mode(0o755)).unwrap(); + crate::utils::volume::VolumeState::new( + config_dir.clone(), + tool.to_string_lossy().to_string(), + ) + .save_to_dir(&config_dir) + .unwrap(); + + (root, config_dir, config_path) + } + + /// The extensions dir comes from the volume named in the working + /// directory's state file, not the `src_dir` fallback. + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn get_extensions_dir_reads_the_volume_state_from_the_working_directory() { + let target = "qemux86-64"; + let (root, config_dir, config_path) = state_in_config_dir_below_src_dir(target); + let config = super::Config::load_from_yaml_str("src_dir: ..\nextensions: {}\n").unwrap(); + + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(&config_dir).unwrap(); + let dir = config.get_extensions_dir(config_path.to_str().unwrap(), target); + std::env::set_current_dir(prev).unwrap(); + + let base = root.path().canonicalize().unwrap(); + assert_eq!(dir, base.join("mount").join(target).join("includes")); + } + + /// A fetched remote extension is read out of the volume named in the + /// working directory's state file, so install does not stop with "has + /// not been merged". + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn remote_extensions_merge_from_the_volume_in_the_working_directory() { + let target = "qemux86-64"; + let (_root, config_dir, config_path) = state_in_config_dir_below_src_dir(target); + let mut main_config: serde_yaml::Value = serde_yaml::from_str( + "src_dir: ..\nextensions:\n remote-ext:\n source: { type: package, version: \"1.0.0\" }\n", + ) + .unwrap(); + + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(&config_dir).unwrap(); + let merged = super::Config::merge_installed_remote_extensions( + &mut main_config, + &config_path, + Some(target), + ); + std::env::set_current_dir(prev).unwrap(); + + let merged = merged.unwrap(); + assert!( + merged.extension_sources.contains_key("remote-ext"), + "remote-ext was not read from the volume: {:?}", + merged.extension_sources + ); + } + /// Recording the feed set must be a no-op when there is nothing to record. /// /// Two ways to get this wrong, and the first shipped: comparing the record's diff --git a/src/utils/container.rs b/src/utils/container.rs index f35b7e1f..f3be03a9 100644 --- a/src/utils/container.rs +++ b/src/utils/container.rs @@ -1347,6 +1347,12 @@ impl SdkContainer { self } + /// The host directory the container mounts at `/opt/src`: `src_dir` when + /// set, else the working directory. + pub fn mounted_src_dir(&self) -> &Path { + self.src_dir.as_deref().unwrap_or(&self.cwd) + } + /// Create a new SdkContainer with configuration from config file pub fn from_config(config_path: &str, config: &crate::utils::config::Config) -> Result { let src_dir = config.get_resolved_src_dir(config_path); @@ -1517,7 +1523,7 @@ impl SdkContainer { let volume_manager = VolumeManager::new(self.container_tool.clone(), self.verbose); let volume_state = volume_manager.get_or_create_volume(&self.cwd).await?; - let src_dir = self.src_dir.as_ref().unwrap_or(&self.cwd); + let src_dir = self.mounted_src_dir(); print_info( &format!("Setting up remote execution on {runs_on}..."), @@ -1739,7 +1745,7 @@ impl SdkContainer { let volume_manager = VolumeManager::new(self.container_tool.clone(), self.verbose); let volume_state = volume_manager.get_or_create_volume(&self.cwd).await?; - let src_dir = self.src_dir.as_ref().unwrap_or(&self.cwd); + let src_dir = self.mounted_src_dir(); print_info( &format!("Setting up remote execution on {runs_on}..."), @@ -2027,7 +2033,7 @@ impl SdkContainer { // Volume mounts: docker volume for persistent state, bind mount for source // Source is mounted to /mnt/src, then bindfs remounts it to /opt/src with permission translation container_cmd.push("-v".to_string()); - let src_path = self.src_dir.as_ref().unwrap_or(&self.cwd); + let src_path = self.mounted_src_dir(); // Every host-path bind goes through translate_bind_for_vm: when VM // routing is active it rewrites the host path to its in-VM // /run/workspace/ equivalent; otherwise it's a pass-through. @@ -3108,7 +3114,7 @@ impl SdkContainer { // Volume mounts: docker volume for persistent state, bind mount for source. // Host path is translated to /run/workspace/ when VM routing is on. container_cmd.push("-v".to_string()); - let src_path = self.src_dir.as_ref().unwrap_or(&self.cwd); + let src_path = self.mounted_src_dir(); container_cmd.push(format!( "{}:/mnt/src:rw", translate_bind_for_vm(src_path).display() @@ -5258,4 +5264,29 @@ extensions: "every os-release read must pipe through `tail -n 1`" ); } + + /// `/opt/src` is `src_dir` when the config sets one, resolved against the + /// config file's directory, and the working directory otherwise. + #[test] + #[serial_test::serial] + fn mounted_src_dir_is_src_dir_or_the_working_directory() { + let root = tempfile::TempDir::new().unwrap(); + let project = root.path().canonicalize().unwrap().join("project"); + std::fs::create_dir_all(project.join("avocado")).unwrap(); + let config_path = project.join("avocado").join("avocado.yaml"); + let config_str = config_path.to_str().unwrap(); + + let config = + crate::utils::config::Config::load_from_yaml_str("src_dir: ..\nextensions: {}\n") + .unwrap(); + let container = SdkContainer::from_config(config_str, &config).unwrap(); + assert_eq!(container.mounted_src_dir(), project); + + let config = crate::utils::config::Config::load_from_yaml_str("extensions: {}\n").unwrap(); + let container = SdkContainer::from_config(config_str, &config).unwrap(); + assert_eq!( + container.mounted_src_dir(), + env::current_dir().unwrap().as_path() + ); + } } diff --git a/src/utils/volume.rs b/src/utils/volume.rs index 37bfaa6a..03780745 100644 --- a/src/utils/volume.rs +++ b/src/utils/volume.rs @@ -59,6 +59,18 @@ impl VolumeState { Ok(Some(state)) } + /// Load the volume state from the process working directory. + /// + /// `SdkContainer` keys the volume on its `cwd`, so install, build and the + /// stamps code find `.avocado-state` in the working directory, not in + /// `src_dir`. Not every reader agrees: runtime provision keys its state + /// file copy on `src_dir`, and `profiles`, `save` and `load` read from the + /// config file's directory. + pub fn load_from_current_dir() -> Result> { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + Self::load_from_dir(&cwd) + } + /// Save volume state to .avocado-state file in the given directory pub fn save_to_dir(&self, dir_path: &Path) -> Result<()> { let state_file = dir_path.join(".avocado-state"); @@ -661,4 +673,32 @@ mod tests { "Cannot connect to the Docker daemon at unix:///var/run/docker.sock\n" )); } + + /// The state file lives in the working directory, which is where + /// `get_or_create_volume` writes it. A copy in `src_dir` is not consulted, + /// so a `src_dir: ..` project run from its config directory finds the + /// volume the container layer created there. + #[test] + #[serial_test::serial] + fn load_from_current_dir_reads_the_working_directory_only() { + let temp_dir = TempDir::new().unwrap(); + let src_dir = temp_dir.path().canonicalize().unwrap(); + let config_dir = src_dir.join("avocado"); + fs::create_dir_all(&config_dir).unwrap(); + VolumeState::new(src_dir.clone(), "docker".to_string()) + .save_to_dir(&src_dir) + .unwrap(); + + // Restore the cwd before asserting, so a failure here does not leave + // later tests running from a deleted directory. + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(&config_dir).unwrap(); + let from_config_dir = VolumeState::load_from_current_dir(); + std::env::set_current_dir(&src_dir).unwrap(); + let from_src_dir = VolumeState::load_from_current_dir(); + std::env::set_current_dir(prev).unwrap(); + + assert!(from_config_dir.unwrap().is_none()); + assert!(from_src_dir.unwrap().is_some()); + } } diff --git a/tests/ext_version_source.rs b/tests/ext_version_source.rs index ef213fbf..be8bd7f3 100644 --- a/tests/ext_version_source.rs +++ b/tests/ext_version_source.rs @@ -166,7 +166,10 @@ fn path_sourced_extension_resolves_a_bare_version_file() { /// /// The dev-fallback root (`/.avocado//includes/`) stands /// in for the installed payload; it is the same directory layout DNF produces. +/// The fallback is taken only when the working directory has no +/// `.avocado-state`, so the loads run from an empty directory of their own. #[test] +#[serial_test::serial] fn package_sourced_payload_resolves_to_the_same_version_as_a_path_source() { let dir = TestDir::new("package_parity"); write_cargo_extension(&dir, "my-ext", "avocado-ext-prog", "3.1.4"); @@ -189,6 +192,9 @@ fn package_sourced_payload_resolves_to_the_same_version_as_a_path_source() { type: path\n \ path: ../my-ext\n", ); + let cwd = tempfile::TempDir::new().unwrap(); + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(cwd.path()).unwrap(); let path_version = composed_ext_version(&via_path, "avocado-ext-prog"); let via_package = dir.write( @@ -202,20 +208,22 @@ fn package_sourced_payload_resolves_to_the_same_version_as_a_path_source() { ); let package_version = composed_ext_version(&via_package, "avocado-ext-prog"); - assert_eq!(path_version.as_deref(), Some("3.1.4")); - assert_eq!( - path_version, package_version, - "switching source kind changed the composed version" - ); - // Prove the package leg really read the installed payload rather than // happening to agree with the sibling checkout: bump only the payload. dir.write( "project/.avocado/qemux86-64/includes/avocado-ext-prog/Cargo.toml", "[package]\nname = \"prog\"\nversion = \"3.2.0\"\nedition = \"2021\"\n", ); + let bumped_version = composed_ext_version(&via_package, "avocado-ext-prog"); + std::env::set_current_dir(prev).unwrap(); + + assert_eq!(path_version.as_deref(), Some("3.1.4")); + assert_eq!( + path_version, package_version, + "switching source kind changed the composed version" + ); assert_eq!( - composed_ext_version(&via_package, "avocado-ext-prog").as_deref(), + bumped_version.as_deref(), Some("3.2.0"), "package source did not resolve from the installed payload" );