pray-core 1.13.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use pray_core::{transaction, PrayError};
use std::{fs, process::Command};

fn root() -> std::path::PathBuf {
    let path = std::env::temp_dir().join(format!(
        "pray-transaction-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    fs::create_dir(&path).unwrap();
    path
}

#[test]
fn rolls_back_files_and_metadata_after_failure() {
    let root = root();
    fs::write(root.join("Prayfile"), "original").unwrap();
    let result = transaction::run(&root, || {
        transaction::write_file(&root.join("Prayfile"), b"intermediate")?;
        transaction::write_file(&root.join("Prayfile"), b"candidate")?;
        transaction::write_file(&root.join("output"), b"created")?;
        Err::<(), _>(PrayError::Render("late failure".into()))
    });
    assert!(result.is_err());
    assert_eq!(fs::read(root.join("Prayfile")).unwrap(), b"original");
    assert!(!root.join("output").exists());
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn recovers_after_process_exit_and_preserves_later_edits() {
    for edited in [false, true] {
        let root = root();
        fs::write(root.join("Prayfile"), "original").unwrap();
        let output = Command::new(std::env::current_exe().unwrap())
            .args(["--exact", "crash_child", "--nocapture"])
            .env("PRAY_TRANSACTION_TEST_ROOT", &root)
            .output()
            .unwrap();
        assert_eq!(
            output.status.code(),
            Some(91),
            "{} {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
        if edited {
            fs::write(root.join("Prayfile"), "operator edit").unwrap();
        }
        let result = transaction::run(&root, || Ok(()));
        if edited {
            assert!(result.is_err());
            assert_eq!(fs::read(root.join("Prayfile")).unwrap(), b"operator edit");
            fs::rename(root.join("Prayfile"), root.join("operator.saved")).unwrap();
            transaction::run(&root, || Ok(())).unwrap();
        } else {
            result.unwrap();
        }
        assert_eq!(fs::read(root.join("Prayfile")).unwrap(), b"original");
        assert!(!root.join("output").exists());
        fs::remove_dir_all(root).unwrap();
    }
}

#[test]
fn crash_child() {
    let Some(root) = std::env::var_os("PRAY_TRANSACTION_TEST_ROOT") else {
        return;
    };
    let root = std::path::PathBuf::from(root);
    if std::env::var("PRAY_TRANSACTION_TEST_ACTION").as_deref() == Ok("recover") {
        transaction::run(&root, || Ok(())).unwrap();
        return;
    }
    transaction::run(&root, || -> pray_core::PrayResult<()> {
        transaction::write_file(&root.join("Prayfile"), b"intermediate")?;
        transaction::write_file(&root.join("Prayfile"), b"candidate")?;
        transaction::write_file(&root.join("output"), b"created")?;
        std::process::exit(91);
    })
    .unwrap();
}

#[test]
fn excludes_a_second_writer_until_commit() {
    let root = root();
    transaction::run(&root, || {
        transaction::write_file(&root.join("output"), b"first")?;
        std::thread::scope(|scope| {
            let result = scope
                .spawn(|| {
                    transaction::run(&root, || {
                        transaction::write_file(&root.join("output"), b"second")
                    })
                })
                .join()
                .unwrap();
            assert!(result
                .unwrap_err()
                .to_string()
                .contains("another pray command"));
        });
        Ok(())
    })
    .unwrap();
    assert_eq!(fs::read(root.join("output")).unwrap(), b"first");
    fs::remove_dir_all(root).unwrap();
}