Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ jobs:
components: rustfmt
- run: cargo fmt --all -- --check

coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: stable
components: llvm-tools-preview
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.event_name == 'push' }}
- uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2.85.11
with:
tool: cargo-llvm-cov
# Informational only: continue-on-error means neither a low-coverage
# number nor cargo llvm-cov itself exiting nonzero (e.g. a test fails
# during this instrumented run) can fail the build. The `test` job
# already runs the full hermetic suite and gates on real test failures;
# this job exists purely to publish the coverage summary.
- name: Run coverage
continue-on-error: true
run: |
{
echo "### Coverage summary"
echo '```'
cargo llvm-cov --workspace --locked --summary-only
echo '```'
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
} >> "$GITHUB_STEP_SUMMARY"

roadmap-drift:
name: Roadmap drift
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ desktop.ini
# Python
__pycache__/
*.py[cod]

# Coverage
*.profraw
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ toml = "0.8"

# HTTP + Downloads
reqwest = { version = "0.12", features = ["blocking"] }
url = "2"

# Archive extraction
tar = "0.4"
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,7 @@ mod tests {

#[test]
fn doctor_fix_auto_creates_layout_and_inits() {
let _numan_root_guard = crate::util::test_paths::NumanRootRestoreGuard::new();
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir_all(root).unwrap();
Expand Down Expand Up @@ -1889,6 +1890,7 @@ mod tests {

#[test]
fn doctor_fix_adds_official_registry_when_initialized_without_registries() {
let _numan_root_guard = crate::util::test_paths::NumanRootRestoreGuard::new();
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("nu_state")).unwrap();
Expand Down
119 changes: 115 additions & 4 deletions src/cmd/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ use crate::nu::paths::NuPaths;
use crate::nupm_compat::schema::NUPM_IMPORT_ORIGIN;
use crate::state::lockfile::Lockfile;
use anyhow::Result;
use std::io::Write;
use std::path::Path;

pub fn execute(root: &Path) -> Result<()> {
let mut stdout = std::io::stdout();
execute_to(root, &mut stdout)
}

fn execute_to(root: &Path, out: &mut dyn Write) -> Result<()> {
let lockfile = Lockfile::load(root)?;
let nu_paths = NuPaths::load(root).ok();

if lockfile.is_empty() {
println!("No packages installed.");
writeln!(out, "No packages installed.")?;
return Ok(());
}

println!("Installed packages ({}):\n", lockfile.packages.len());
writeln!(out, "Installed packages ({}):\n", lockfile.packages.len())?;

for (id, entry) in &lockfile.packages {
let status = match &nu_paths {
Expand All @@ -33,11 +39,116 @@ pub fn execute(root: &Path) -> Result<()> {
} else {
""
};
println!(
writeln!(
out,
" {} v{} [{}] {}{}",
id, entry.version, entry.package_type, status, origin_tag
);
)?;
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::state::lockfile::{LockfileEntry, PluginActivation};

fn base_entry(version: &str, package_type: &str) -> LockfileEntry {
LockfileEntry {
version: version.to_string(),
package_type: package_type.to_string(),
source: "binary".to_string(),
target: None,
artifact_url: None,
artifact_sha256: None,
executable_path: None,
archive_root: None,
include: None,
entry: None,
installed_at: "0".to_string(),
nu_version_at_install: None,
activation: None,
registry_url: None,
registry_revision: None,
index_sha256: None,
signing_key_fingerprint: None,
git_url: None,
git_rev: None,
cargo_name: None,
cargo_lock_sha256: None,
built_sha256: None,
payload_path: String::new(),
revision_id: None,
payload_sha256: None,
executable_sha256: None,
selection_reason: None,
origin: None,
module_activation: None,
module_import_mode: None,
locked_dependencies: Default::default(),
}
}

#[test]
fn execute_empty_lockfile() {
let dir = tempfile::tempdir().unwrap();
Lockfile::empty().save(dir.path()).unwrap();
let mut out = Vec::new();
execute_to(dir.path(), &mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "No packages installed.\n");
}

#[test]
fn execute_one_package() {
let dir = tempfile::tempdir().unwrap();
let mut lock = Lockfile::empty();
lock.packages
.insert("owner/pkg".to_string(), base_entry("1.0.0", "plugin"));
lock.save(dir.path()).unwrap();
let mut out = Vec::new();
execute_to(dir.path(), &mut out).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("Installed packages (1):"));
assert!(s.contains("owner/pkg v1.0.0 [plugin] installed"));
}

#[test]
fn execute_multiple_packages_with_one_active() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();

let mut lock = Lockfile::empty();
let mut active = base_entry("1.0.0", "plugin");
active.activation = Some(PluginActivation {
plugin_registry_path: "/path/to/plugins.msgpackz".to_string(),
nu_executable_sha256: "abc123".to_string(),
nu_version: "0.113.1".to_string(),
activated_at: "0".to_string(),
});
lock.packages.insert("owner/active".to_string(), active);
lock.packages
.insert("owner/inactive".to_string(), base_entry("2.0.0", "module"));
lock.save(root).unwrap();

std::fs::create_dir_all(root.join("nu_state")).unwrap();
let nu_paths = NuPaths {
nu_executable: "/usr/bin/nu".to_string(),
nu_version: "0.113.1".to_string(),
plugin_registry_path: "/path/to/plugins.msgpackz".to_string(),
nu_executable_hash: "abc123".to_string(),
platform: "x86_64-unknown-linux-gnu".to_string(),
data_dir: None,
vendor_autoload_dirs: vec![],
vendor_autoload_dir: None,
};
nu_paths.save(root).unwrap();

let mut out = Vec::new();
execute_to(root, &mut out).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("Installed packages (2):"));
assert!(s.contains("owner/active v1.0.0 [plugin] activated"));
assert!(s.contains("owner/inactive v2.0.0 [module] installed"));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
85 changes: 85 additions & 0 deletions src/cmd/nu_pin_offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,88 @@ pub fn is_nu_mismatch(diagnosis: &PackageIncompatibility) -> bool {
| Incompatibility::NuUnsatisfied { .. }
)
}

#[cfg(test)]
mod tests {
use super::*;

fn diagnosis_with_pin(pin: &str) -> PackageIncompatibility {
PackageIncompatibility {
issue: Incompatibility::NuTooOld {
constraint: ">=0.113.0".to_string(),
},
suggested_pin: Some(pin.to_string()),
available_versions: vec![],
}
}

#[test]
fn accept_proceeds_to_install_and_fails_hermetically_on_bad_pin() {
// A malformed pin fails local version normalization before any
// network call, so this exercises the accept branch deterministically.
let dir = tempfile::tempdir().unwrap();
let diagnosis = diagnosis_with_pin("not-a-version");
let err =
offer_managed_nu_pin_with_interaction(dir.path(), "0.112.0", &diagnosis, true, || {
Ok("y\n".to_string())
})
.unwrap_err();
assert!(
err.to_string().contains("Failed to install managed Nu"),
"expected install failure context, got: {err}"
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let chain: String = err
.chain()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(" / ");
assert!(
chain.contains("Failed to normalize requested version 'not-a-version'"),
"expected version-normalization failure in the error chain, got: {chain}"
);
}

#[test]
fn decline_returns_false_without_installing() {
let dir = tempfile::tempdir().unwrap();
let diagnosis = diagnosis_with_pin("0.113.1");
let result =
offer_managed_nu_pin_with_interaction(dir.path(), "0.112.0", &diagnosis, true, || {
Ok("n\n".to_string())
})
.unwrap();
assert!(!result);
assert!(
std::fs::read_dir(dir.path()).unwrap().next().is_none(),
"declining must not install anything under root"
);
}

#[test]
fn invalid_input_is_treated_as_decline() {
let dir = tempfile::tempdir().unwrap();
let diagnosis = diagnosis_with_pin("0.113.1");
let result =
offer_managed_nu_pin_with_interaction(dir.path(), "0.112.0", &diagnosis, true, || {
Ok("maybe\n".to_string())
})
.unwrap();
assert!(!result);
assert!(
std::fs::read_dir(dir.path()).unwrap().next().is_none(),
"invalid input must not install anything under root"
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[test]
fn non_interactive_short_circuits_without_reading_input() {
let dir = tempfile::tempdir().unwrap();
let diagnosis = diagnosis_with_pin("0.113.1");
let result =
offer_managed_nu_pin_with_interaction(dir.path(), "0.112.0", &diagnosis, false, || {
panic!("read_line must not be called when non-interactive")
})
.unwrap();
assert!(!result);
}
}
Loading
Loading