pray-cli 1.12.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
#[path = "install_support.rs"]
mod support;

use std::fs;
#[cfg(unix)]
use std::os::unix::fs::symlink;

use support::{run_pray, temporary_directory};

fn write_lockfile(repo: &std::path::Path, package_path: &str) {
    fs::write(
        repo.join("Prayfile.lock"),
        format!(
            r#"prayfile_lock = "1"
spec = "0.1"
generated_by = "pray test"
manifest_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
source = []
target = []
managed_span = []
provisioned = []

[[package]]
name = "sample/base"
version = "1.4.3"
path = "{package_path}"
tree_hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
artifact_hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
artifact = "path:{package_path}"
exports = []
dependencies = []
"#
        ),
    )
    .expect("lockfile");
}

#[test]
fn clean_unused_retains_only_locked_registry_entries() {
    let repo = temporary_directory("pray-clean-unused");
    let locked = repo.join(".pray/cache/registry/sample/base/1.4.3/source");
    let stale_version = repo.join(".pray/cache/registry/sample/base/1.4.2/source");
    let stale_source = repo.join(".pray/cache/registry/sample/base/1.4.3/other");
    let legacy = repo.join(".pray/cache/registry/legacy/sample/base/1.4.3");
    let staging = repo.join(".pray/cache/registry/sample/base/1.4.3/source.staging");
    for path in [&locked, &stale_version, &stale_source, &legacy, &staging] {
        fs::create_dir_all(path).expect("cache path");
        fs::write(path.join("entry"), "cached").expect("cache entry");
    }
    fs::create_dir_all(repo.join(".pray/cache/git/repository")).expect("git cache");
    fs::create_dir_all(repo.join(".pray/vendor/sample-base")).expect("vendor");
    fs::write(repo.join(".pray/state.json"), "{}").expect("state");
    write_lockfile(&repo, "./.pray/cache/registry/sample/base/1.4.3/source");

    let clean = run_pray(&repo, &["clean", "--unused"]);
    assert!(
        clean.status.success(),
        "clean failed: {}",
        String::from_utf8_lossy(&clean.stderr)
    );
    assert!(locked.exists());
    assert!(!stale_version.exists());
    assert!(!stale_source.exists());
    assert!(!legacy.exists());
    assert!(!staging.exists());
    assert!(repo.join(".pray/cache/git/repository").exists());
    assert!(repo.join(".pray/vendor/sample-base").exists());
    assert!(repo.join(".pray/state.json").exists());
}

#[test]
fn clean_unused_requires_a_readable_parseable_lockfile() {
    for contents in [None, Some("not valid = [")] {
        let repo = temporary_directory("pray-clean-unused-lock");
        let cache = repo.join(".pray/cache/registry/sample/base/1.0.0/source");
        fs::create_dir_all(&cache).expect("cache");
        fs::write(cache.join("entry"), "cached").expect("cache entry");
        if let Some(contents) = contents {
            fs::write(repo.join("Prayfile.lock"), contents).expect("lockfile");
        }

        assert!(!run_pray(&repo, &["clean", "--unused"]).status.success());
        assert!(cache.exists());
    }
}

#[test]
fn clean_unused_rejects_incomplete_lockfile_before_deleting() {
    let repo = temporary_directory("pray-clean-unused-incomplete-lock");
    let cache = repo.join(".pray/cache/registry/sample/base/1.0.0/source");
    fs::create_dir_all(&cache).expect("cache");
    fs::write(cache.join("entry"), "cached").expect("cache entry");
    write_lockfile(&repo, "./.pray/cache/registry/sample/base/1.0.0/source");
    let lockfile_path = repo.join("Prayfile.lock");
    let lockfile = fs::read_to_string(&lockfile_path)
        .expect("lockfile")
        .replace(
            "sha256:0000000000000000000000000000000000000000000000000000000000000000",
            "sha256:incomplete",
        );
    fs::write(lockfile_path, lockfile).expect("incomplete lockfile");

    assert!(!run_pray(&repo, &["clean", "--unused"]).status.success());
    assert!(cache.exists());
}

#[test]
fn clean_uses_the_selected_project_root() {
    let repo = temporary_directory("pray-clean-project-root");
    let nested = repo.join("nested");
    fs::create_dir_all(&nested).expect("nested directory");
    for relative in [".pray/cache", ".pray/vendor"] {
        fs::create_dir_all(repo.join(relative)).expect("local state");
    }
    fs::write(repo.join(".pray/state.json"), "{}").expect("state");

    let clean = run_pray(
        &nested,
        &["--path", repo.to_str().expect("utf-8 path"), "clean"],
    );
    assert!(
        clean.status.success(),
        "clean failed: {}",
        String::from_utf8_lossy(&clean.stderr)
    );
    assert!(!repo.join(".pray/cache").exists());
    assert!(!repo.join(".pray/vendor").exists());
    assert!(!repo.join(".pray/state.json").exists());
}

#[cfg(unix)]
#[test]
fn clean_unused_does_not_follow_registry_symlinks() {
    let repo = temporary_directory("pray-clean-unused-symlink");
    let outside = temporary_directory("pray-clean-unused-outside");
    fs::write(outside.join("keep"), "outside").expect("outside file");
    fs::create_dir_all(repo.join(".pray/cache/registry")).expect("registry");
    symlink(&outside, repo.join(".pray/cache/registry/stale")).expect("symlink");
    write_lockfile(&repo, "./packages/base");

    assert!(run_pray(&repo, &["clean", "--unused"]).status.success());
    assert!(!repo.join(".pray/cache/registry/stale").exists());
    assert!(outside.join("keep").exists());
}

#[test]
fn clean_rejects_unrelated_arguments() {
    let repo = temporary_directory("pray-clean-arguments");
    assert!(!run_pray(&repo, &["clean", "--other"]).status.success());
    assert!(!run_pray(&repo, &["clean", "unused"]).status.success());
}