Skip to content
Merged
11 changes: 11 additions & 0 deletions crates/pixi/tests/integration_rust/common/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,17 @@ pub struct LockBuilder {
pub args: lock::Args,
}

impl LockBuilder {
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.args.dry_run = dry_run;
self
}
pub fn with_check(mut self, check: bool) -> Self {
self.args.check = check;
self
}
}

impl IntoFuture for LockBuilder {
type Output = miette::Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;
Expand Down
1 change: 1 addition & 0 deletions crates/pixi/tests/integration_rust/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ impl PixiControl {
no_install_config: NoInstallConfig { no_install: false },
check: false,
json: false,
dry_run: false,
},
}
}
Expand Down
147 changes: 147 additions & 0 deletions crates/pixi/tests/integration_rust/lock_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use crate::common::{LockFileExt, PixiControl};
use pixi_test_utils::{MockRepoData, Package};
use rattler_conda_types::Platform;
use tempfile::TempDir;

/// Test that `pixi lock --dry-run` doesn't modify the lock file on disk
#[tokio::test]
async fn test_lock_dry_run_doesnt_modify_lockfile() {
// Create a mock package database
let mut package_database = MockRepoData::default();

// Add mock packages
package_database.add_package(
Package::build("python", "3.11.0")
.with_subdir(Platform::current())
.finish(),
);
package_database.add_package(
Package::build("numpy", "1.24.0")
.with_subdir(Platform::current())
.finish(),
);

// Write the repodata to disk
let channel_dir = TempDir::new().unwrap();
package_database
.write_repodata(channel_dir.path())
.await
.unwrap();

// Create a new pixi project using our local channel
let pixi = PixiControl::new().unwrap();
pixi.init()
.with_local_channel(channel_dir.path())
.await
.unwrap();

// Add a dependency to create an initial lock file
pixi.add("python").await.unwrap();

// Get the original lock file
let original_lock_file = pixi.lock_file().await.unwrap();
let platform = Platform::current();

// Verify python is in the original lock file
assert!(
original_lock_file.contains_conda_package("default", platform, "python"),
"python should be in the initial lock file"
);

// Add another dependency to the manifest without updating the lock file
let manifest_content = pixi.manifest_contents().unwrap();
let updated_manifest =
manifest_content.replace("[dependencies]", "[dependencies]\nnumpy = \"*\"");
pixi.update_manifest(&updated_manifest).unwrap();

// Run `pixi lock --dry-run`
pixi.lock().with_dry_run(true).await.unwrap();

// Verify the lock file was NOT modified
let lock_after_dry_run = pixi.lock_file().await.unwrap();

assert!(
lock_after_dry_run.contains_conda_package("default", platform, "python"),
"python should still be in lock file after --dry-run"
);

assert!(
!lock_after_dry_run.contains_conda_package("default", platform, "numpy"),
"numpy should NOT be in lock file after --dry-run"
);

// Now run without --dry-run to actually update the lock file
pixi.lock().await.unwrap();

// Verify the lock file WAS modified this time
let lock_after_normal = pixi.lock_file().await.unwrap();

assert!(
lock_after_normal.contains_conda_package("default", platform, "python"),
"python should still be in lock file"
);

assert!(
lock_after_normal.contains_conda_package("default", platform, "numpy"),
"numpy should NOW be in lock file after normal lock"
);
}

/// Test that `pixi lock --dry-run` implies `--no-install`
#[tokio::test]
async fn test_lock_dry_run_implies_no_install() {
// Create a mock package database
let mut package_database = MockRepoData::default();

// Add mock packages
package_database.add_package(
Package::build("python", "3.11.0")
.with_subdir(Platform::current())
.finish(),
);
package_database.add_package(
Package::build("numpy", "1.24.0")
.with_subdir(Platform::current())
.finish(),
);

// Write the repodata to disk
let channel_dir = TempDir::new().unwrap();
package_database
.write_repodata(channel_dir.path())
.await
.unwrap();

// Create a new pixi project using our local channel
let pixi = PixiControl::new().unwrap();
pixi.init()
.with_local_channel(channel_dir.path())
.await
.unwrap();

// Add a dependency
pixi.add("python").await.unwrap();

// Get the environment path
let env_path = pixi.default_env_path().unwrap();

// Remove the environment directory if it exists
if env_path.exists() {
fs_err::remove_dir_all(&env_path).unwrap();
}

// Add another dependency to manifest
let manifest_content = pixi.manifest_contents().unwrap();
let updated_manifest =
manifest_content.replace("[dependencies]", "[dependencies]\nnumpy = \"*\"");
pixi.update_manifest(&updated_manifest).unwrap();

// Run `pixi lock --dry-run`
pixi.lock().with_dry_run(true).await.unwrap();

// Environment should NOT have been created
assert!(
!env_path.exists(),
"Environment should not be created with --dry-run"
);
}
1 change: 1 addition & 0 deletions crates/pixi/tests/integration_rust/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod global_tests;
mod init_tests;
mod install_filter_tests;
mod install_tests;
mod lock_tests;
mod project_tests;
mod pypi_tests;
mod search_tests;
Expand Down
28 changes: 26 additions & 2 deletions crates/pixi_cli/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ pub struct Args {
/// If yes, exit with a non-zero code.
#[clap(long)]
pub check: bool,

///Compute the lock file without writing to disk.
/// Implies --no-install
#[clap(long)]
pub dry_run: bool,
}

pub async fn execute(args: Args) -> miette::Result<()> {
Expand All @@ -47,8 +52,12 @@ pub async fn execute(args: Args) -> miette::Result<()> {
let original_lock_file = workspace.load_lock_file().await?.into_lock_file_or_empty();
let (LockFileDerivedData { lock_file, .. }, lock_updated) = workspace
.update_lock_file(UpdateLockFileOptions {
lock_file_usage: LockFileUsage::Update,
no_install: args.no_install_config.no_install,
lock_file_usage: if args.dry_run {
LockFileUsage::DryRun
} else {
LockFileUsage::Update
},
no_install: args.no_install_config.no_install || args.dry_run,
max_concurrent_solves: workspace.config().max_concurrent_solves(),
})
.await?;
Expand All @@ -62,6 +71,21 @@ pub async fn execute(args: Args) -> miette::Result<()> {
let json_diff = LockFileJsonDiff::new(Some(workspace.named_environments()), diff);
let json = serde_json::to_string_pretty(&json_diff).expect("failed to convert to json");
println!("{json}");
} else if args.dry_run {
if !diff.is_empty() {
eprintln!(
"{}Dry-run: lock-file would be updated (not written to disk)",
console::style(console::Emoji("i ", "i ")).blue()
);
diff.print()
.into_diagnostic()
.context("failed to print lock-file diff")?;
} else {
eprintln!(
"{}Dry-run:lock file would not change",
console::style(console::Emoji("i ", "i ")).blue()
);
}
} else if lock_updated {
eprintln!(
"{}Updated lock-file",
Expand Down
4 changes: 3 additions & 1 deletion crates/pixi_core/src/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ pub enum LockFileUsage {
Locked,
/// Don't update the lock-file and don't check if it is out of date
Frozen,
/// Don't update the lock-file, but don't check if it is out of date
DryRun,
}

impl LockFileUsage {
Expand All @@ -472,7 +474,7 @@ impl LockFileUsage {
/// Returns true if the lock-file should be checked if it is out of date.
pub(crate) fn should_check_if_out_of_date(self) -> bool {
match self {
LockFileUsage::Update | LockFileUsage::Locked => true,
LockFileUsage::Update | LockFileUsage::Locked | LockFileUsage::DryRun => true,
LockFileUsage::Frozen => false,
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,10 @@ impl Workspace {
.await?;

// Write the lock-file to disk
lock_file_derived_data.write_to_disk()?;

if options.lock_file_usage != LockFileUsage::DryRun {
lock_file_derived_data.write_to_disk()?;
}

Ok((lock_file_derived_data, true))
}
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/cli/pixi/lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pixi lock [OPTIONS]
: Output the changes in JSON format
- <a id="arg---check" href="#arg---check">`--check`</a>
: Check if any changes have been made to the lock file. If yes, exit with a non-zero code
- <a id="arg---dry-run" href="#arg---dry-run">`--dry-run`</a>
: Compute the lock file without writing to disk. Implies --no-install

## Update Options
- <a id="arg---no-install" href="#arg---no-install">`--no-install`</a>
Expand Down
Loading