pray-cli 1.16.0

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

use std::fs;
use support::UpdateFixture;

#[test]
fn latest_collision_preserves_recipe_and_reports_all_paths() {
    let fixture = UpdateFixture::new("~> 1.0");
    fixture.omit_ledger();
    let manifest = fs::read(fixture.consumer.join("Prayfile")).unwrap();
    let lockfile = fs::read(fixture.consumer.join("Prayfile.lock")).unwrap();
    let compose = fs::read(fixture.consumer.join("INSTRUCTIONS.md")).unwrap();
    fixture.publish("2.0.0");
    fs::write(fixture.consumer.join("rules/rules.md"), "new rules\n").unwrap();

    for flags in [
        vec!["update", "--latest"],
        vec!["update", "--latest", "--json"],
    ] {
        let output = fixture.run(&flags);
        assert_eq!(output.status.code(), Some(5));
        assert_eq!(
            fs::read(fixture.consumer.join("Prayfile")).unwrap(),
            manifest
        );
        assert_eq!(
            fs::read(fixture.consumer.join("Prayfile.lock")).unwrap(),
            lockfile
        );
        assert_eq!(
            fs::read(fixture.consumer.join("INSTRUCTIONS.md")).unwrap(),
            compose
        );
        let error = String::from_utf8_lossy(&output.stderr);
        for expected in ["a.md", "b.md", "sample/files", "move"] {
            assert!(error.contains(expected), "missing {expected}: {error}");
        }
    }
    fixture.success(&["install"]);
    fixture.success(&["update", "--latest"]);
    let updated =
        pray_core::lockfile::read_lockfile(&fixture.consumer.join("Prayfile.lock")).unwrap();
    assert_eq!(
        updated
            .package
            .iter()
            .find(|package| package.name == "sample/files")
            .unwrap()
            .version,
        "2.0.0"
    );
}

#[test]
fn latest_dry_run_checks_candidate_destinations_without_writes() {
    let fixture = UpdateFixture::new("~> 1.0");
    fixture.omit_ledger();
    let manifest = fs::read(fixture.consumer.join("Prayfile")).unwrap();
    let lockfile = fs::read(fixture.consumer.join("Prayfile.lock")).unwrap();
    fixture.publish("2.0.0");
    let output = fixture.run(&["update", "--latest", "--dry-run"]);
    assert_eq!(output.status.code(), Some(5));
    let error = String::from_utf8_lossy(&output.stderr);
    assert!(error.contains("a.md") && error.contains("b.md"), "{error}");
    assert_eq!(
        fs::read(fixture.consumer.join("Prayfile")).unwrap(),
        manifest
    );
    assert_eq!(
        fs::read(fixture.consumer.join("Prayfile.lock")).unwrap(),
        lockfile
    );
}

#[test]
fn latest_json_updates_versions_already_allowed_by_the_constraint() {
    let fixture = UpdateFixture::new(">= 1.0");
    fixture.publish("2.0.0");
    let output = fixture.success(&["update", "--latest", "--json"]);
    let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(report["status"], "updated");
    let lockfile =
        pray_core::lockfile::read_lockfile(&fixture.consumer.join("Prayfile.lock")).unwrap();
    assert_eq!(
        lockfile
            .package
            .iter()
            .find(|package| package.name == "sample/files")
            .unwrap()
            .version,
        "2.0.0"
    );
}

#[test]
fn verify_gives_recovery_that_preserves_an_edited_destination() {
    let fixture = UpdateFixture::new("~> 1.0");
    let lockfile =
        pray_core::lockfile::read_lockfile(&fixture.consumer.join("Prayfile.lock")).unwrap();
    let destination = fixture.consumer.join(&lockfile.provisioned[0].path);
    fs::write(&destination, "operator changes").unwrap();
    let output = fixture.run(&["verify"]);
    assert_eq!(output.status.code(), Some(6));
    let error = String::from_utf8_lossy(&output.stderr);
    assert!(error.contains("move"), "{error}");
    fs::rename(&destination, destination.with_extension("saved")).unwrap();
    fixture.success(&["install"]);
    fixture.success(&["verify"]);
    assert_eq!(
        fs::read_to_string(destination.with_extension("saved")).unwrap(),
        "operator changes"
    );
}

#[test]
fn oversized_destination_is_rejected_before_output_changes() {
    let fixture = UpdateFixture::new("~> 1.0");
    let lockfile_path = fixture.consumer.join("Prayfile.lock");
    let lockfile = pray_core::lockfile::read_lockfile(&lockfile_path).unwrap();
    let destination = fixture.consumer.join(&lockfile.provisioned[0].path);
    fs::OpenOptions::new()
        .write(true)
        .open(&destination)
        .unwrap()
        .set_len(32 * 1024 * 1024 + 1)
        .unwrap();
    let previous_lock = fs::read(&lockfile_path).unwrap();
    let previous_compose = fs::read(fixture.consumer.join("INSTRUCTIONS.md")).unwrap();
    fs::write(fixture.consumer.join("rules/rules.md"), "new rules\n").unwrap();
    let output = fixture.run(&["install"]);
    assert_eq!(output.status.code(), Some(5));
    assert!(String::from_utf8_lossy(&output.stderr).contains("32 MiB"));
    assert_eq!(
        fs::metadata(destination).unwrap().len(),
        32 * 1024 * 1024 + 1
    );
    assert_eq!(fs::read(lockfile_path).unwrap(), previous_lock);
    assert_eq!(
        fs::read(fixture.consumer.join("INSTRUCTIONS.md")).unwrap(),
        previous_compose
    );
}

#[cfg(unix)]
#[test]
fn late_lock_failure_restores_manifest_outputs_and_pruned_files() {
    let fixture = UpdateFixture::new("~> 1.0");
    let lock_path = fixture.consumer.join("Prayfile.lock");
    let mut lockfile = pray_core::lockfile::read_lockfile(&lock_path).unwrap();
    fs::write(fixture.consumer.join("dropped.txt"), "old export").unwrap();
    lockfile
        .provisioned
        .push(pray_core::lockfile::ProvisionedFileRecord {
            path: "dropped.txt".into(),
            content_hash: pray_core::hashing::sha256_prefixed(b"old export"),
            package: "sample/files".into(),
            export: "files".into(),
        });
    pray_core::lockfile::write_lockfile(&lock_path, &lockfile).unwrap();
    let mut paths = vec![
        "Prayfile".to_owned(),
        "Prayfile.lock".to_owned(),
        "INSTRUCTIONS.md".to_owned(),
    ];
    paths.extend(lockfile.provisioned.iter().map(|file| file.path.clone()));
    let original: Vec<_> = paths
        .iter()
        .map(|path| fs::read(fixture.consumer.join(path)).unwrap())
        .collect();
    fs::rename(&lock_path, fixture.consumer.join("original.lock")).unwrap();
    std::os::unix::fs::symlink("original.lock", &lock_path).unwrap();
    fixture.publish("2.0.0");
    fs::write(fixture.consumer.join("rules/rules.md"), "new rules\n").unwrap();
    let output = fixture.run(&["update", "--latest"]);
    assert_eq!(
        output.status.code(),
        Some(5),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(String::from_utf8_lossy(&output.stderr).contains("symbolic link"));
    for (path, bytes) in paths.iter().zip(original) {
        assert_eq!(
            fs::read(fixture.consumer.join(path)).unwrap(),
            bytes,
            "{path}"
        );
    }
}