Add tests for puffin sync (#161)

Closes #158.
This commit is contained in:
Charlie Marsh 2023-10-21 23:25:00 -04:00 committed by GitHub
parent 3072c3265e
commit b665f1489a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 638 additions and 35 deletions

View file

@ -15,6 +15,7 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
PYTHON_VERSION: "3.12"
jobs:
cargo-fmt:
@ -47,6 +48,10 @@ jobs:
name: "cargo test | ${{ matrix.os }}"
steps:
- uses: actions/checkout@v4
- name: "Install Python"
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo insta"

2
Cargo.lock generated
View file

@ -1308,6 +1308,7 @@ dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"regex",
"serde",
"similar",
"yaml-rust",
@ -2060,6 +2061,7 @@ dependencies = [
"futures",
"gourgeist",
"indicatif",
"insta",
"insta-cmd",
"install-wheel-rs",
"itertools",

View file

@ -48,4 +48,11 @@ which = { workspace = true }
assert_cmd = { version = "2.0.8" }
assert_fs = { version = "1.0.13" }
insta-cmd = { version = "0.4.0" }
insta = { version = "1.34.0", features = ["filters"] }
predicates = { version = "3.0.4" }
[features]
# Introduces a dependency on a local Python installation.
python = []
# Introduces a dependency on PyPI.
pypi = []

View file

@ -32,6 +32,10 @@ struct Cli {
/// Avoid reading from or writing to the cache.
#[arg(global = true, long, short)]
no_cache: bool,
/// Path to the cache directory.
#[arg(global = true, long, env = "PUFFIN_CACHE_DIR")]
cache_dir: Option<PathBuf>,
}
#[derive(Subcommand)]
@ -133,9 +137,17 @@ async fn main() -> ExitCode {
printer::Printer::Default
};
let project_dirs = ProjectDirs::from("", "", "puffin");
let cache_dir = (!cli.no_cache)
.then(|| {
cli.cache_dir
.as_deref()
.or_else(|| project_dirs.as_ref().map(ProjectDirs::cache_dir))
})
.flatten();
let result = match cli.command {
Commands::PipCompile(args) => {
let dirs = ProjectDirs::from("", "", "puffin");
let requirements = args
.src_file
.into_iter()
@ -151,59 +163,30 @@ async fn main() -> ExitCode {
&constraints,
args.output_file.as_deref(),
args.resolution.unwrap_or_default(),
dirs.as_ref()
.map(ProjectDirs::cache_dir)
.filter(|_| !cli.no_cache),
cache_dir,
printer,
)
.await
}
Commands::PipSync(args) => {
let dirs = ProjectDirs::from("", "", "puffin");
let sources = args
.src_file
.into_iter()
.map(RequirementsSource::from)
.collect::<Vec<_>>();
commands::pip_sync(
&sources,
dirs.as_ref()
.map(ProjectDirs::cache_dir)
.filter(|_| !cli.no_cache),
printer,
)
.await
commands::pip_sync(&sources, cache_dir, printer).await
}
Commands::PipUninstall(args) => {
let dirs = ProjectDirs::from("", "", "puffin");
let sources = args
.package
.into_iter()
.map(RequirementsSource::from)
.chain(args.requirement.into_iter().map(RequirementsSource::from))
.collect::<Vec<_>>();
commands::pip_uninstall(
&sources,
dirs.as_ref()
.map(ProjectDirs::cache_dir)
.filter(|_| !cli.no_cache),
printer,
)
.await
}
Commands::Clean => {
let dirs = ProjectDirs::from("", "", "puffin");
commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await
}
Commands::Freeze => {
let dirs = ProjectDirs::from("", "", "puffin");
commands::freeze(
dirs.as_ref()
.map(ProjectDirs::cache_dir)
.filter(|_| !cli.no_cache),
printer,
)
commands::pip_uninstall(&sources, cache_dir, printer).await
}
Commands::Clean => commands::clean(cache_dir, printer).await,
Commands::Freeze => commands::freeze(cache_dir, printer),
Commands::Venv(args) => commands::venv(&args.name, args.python.as_deref(), printer).await,
Commands::Add(args) => commands::add(&args.name, printer),
Commands::Remove(args) => commands::remove(&args.name, printer),

View file

@ -0,0 +1,387 @@
#![cfg(all(feature = "python", feature = "pypi"))]
use std::process::Command;
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use insta_cmd::_macro_support::insta;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "puffin";
#[test]
fn missing_requirements_txt() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let requirements_txt = temp_dir.child("requirements.txt");
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir));
requirements_txt.assert(predicates::path::missing());
Ok(())
}
#[test]
fn missing_venv() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
venv.assert(predicates::path::missing());
Ok(())
}
/// Install a package into a virtual environment.
#[test]
fn install() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3")?;
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe")
.current_dir(&temp_dir)
.assert()
.success();
Ok(())
}
/// Install multiple packages into a virtual environment.
#[test]
fn install_many() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe; import tomli")
.current_dir(&temp_dir)
.assert()
.success();
Ok(())
}
/// Attempt to install an already-installed package into a virtual environment.
#[test]
fn noop() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3")?;
Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir)
.assert()
.success();
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe")
.current_dir(&temp_dir)
.assert()
.success();
Ok(())
}
/// Install a package into a virtual environment, then install the same package into a different
/// virtual environment.
#[test]
fn link() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3")?;
Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir)
.assert()
.success();
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe")
.current_dir(&temp_dir)
.assert()
.success();
Ok(())
}
/// Install a package into a virtual environment, then sync the virtual environment with a
/// different requirements file.
#[test]
fn add_remove() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3")?;
Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir)
.assert()
.success();
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("tomli==2.0.1")?;
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import tomli")
.current_dir(&temp_dir)
.assert()
.success();
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe")
.current_dir(&temp_dir)
.assert()
.failure();
Ok(())
}
/// Install a package into a virtual environment, then install a second package into the same
/// virtual environment.
#[test]
fn install_sequential() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = temp_dir.child(".venv");
Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.current_dir(&temp_dir)
.assert()
.success();
venv.assert(predicates::path::is_dir());
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3")?;
Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir)
.assert()
.success();
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
insta::with_settings!({
filters => vec![
(r"\d+ms", "[TIME]"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.txt")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Command::new(venv.join("bin").join("python"))
.arg("-c")
.arg("import markupsafe; import tomli")
.current_dir(&temp_dir)
.assert()
.success();
Ok(())
}

View file

@ -0,0 +1,25 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpdWoKpL
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpVhsDyT/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Unzipped 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- markupsafe@2.1.3
+ tomli@2.0.1

View file

@ -0,0 +1,20 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--no-cache"
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Unzipped 1 package in [TIME]
Installed 1 package in [TIME]
+ markupsafe@2.1.3

View file

@ -0,0 +1,24 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpoqh2hP
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpZCdve2/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Unzipped 2 packages in [TIME]
Installed 2 packages in [TIME]
+ markupsafe@2.1.3
+ tomli@2.0.1

View file

@ -0,0 +1,23 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpWxh1hG
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpUUUwuX/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Unzipped 1 package in [TIME]
Installed 1 package in [TIME]
+ tomli@2.0.1

View file

@ -0,0 +1,20 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmprConY4
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpxukjZu/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 1 package in [TIME]
+ markupsafe@2.1.3

View file

@ -0,0 +1,16 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
---
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to open file `requirements.txt`
Caused by: No such file or directory (os error 2)

View file

@ -0,0 +1,18 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpvJQh45/.venv
---
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to open file `requirements.txt`
Caused by: No such file or directory (os error 2)

View file

@ -0,0 +1,19 @@
---
source: crates/puffin-cli/tests/pip_sync.rs
info:
program: puffin
args:
- pip-sync
- requirements.txt
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpEvtFoB
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpYZqcOB/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Audited 1 package in [TIME]

View file

@ -0,0 +1,16 @@
---
source: crates/puffin-cli/tests/venv.rs
info:
program: puffin
args:
- venv
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpn0fxWx/.venv
---
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python interpreter: /usr/bin/python3
Creating virtual environment at: /home/ferris/project/.venv

View file

@ -0,0 +1,32 @@
#![cfg(feature = "python")]
use std::process::Command;
use anyhow::Result;
use assert_fs::prelude::*;
use insta_cmd::_macro_support::insta;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "puffin";
#[test]
fn create_venv() -> Result<()> {
let tempdir = assert_fs::TempDir::new()?;
let venv = tempdir.child(".venv");
insta::with_settings!({
filters => vec![
(r"Using Python interpreter: .+", "Using Python interpreter: /usr/bin/python3"),
(tempdir.to_str().unwrap(), "/home/ferris/project"),
]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("venv")
.arg(venv.as_os_str())
.current_dir(&tempdir));
});
venv.assert(predicates::path::is_dir());
Ok(())
}

View file

@ -36,3 +36,7 @@ waitmap = { workspace = true }
[dev-dependencies]
once_cell = { version = "1.18.0" }
insta = { version = "1.34.0" }
[features]
# Introduces a dependency on PyPI.
pypi = []

View file

@ -1,3 +1,5 @@
#![cfg(feature = "pypi")]
//! Integration tests for the resolver. These tests rely on a live network connection, and hit
//! `PyPI` directly.