hpsvm-cli 0.1.6

Command-line fixture tools for hpsvm
#![allow(missing_docs)]

use std::{path::PathBuf, process::Command};

use hpsvm::HPSVM;
use hpsvm_fixture::{
    AccountSnapshot, CaptureBuilder, Compare, ExecutionSnapshot, FixtureFormat,
    RuntimeFixtureConfig,
};
use solana_address::Address;
use solana_keypair::Keypair;
use solana_message::Message;
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::versioned::VersionedTransaction;

struct FixtureDir(PathBuf);

impl FixtureDir {
    fn new(stem: &str) -> Self {
        let unique = Address::new_unique();
        let process = std::process::id();
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system time must be after unix epoch")
            .as_nanos();
        Self(std::env::temp_dir().join(format!("{stem}-{process}-{nanos}-{unique}")))
    }

    fn path(&self) -> &std::path::Path {
        &self.0
    }
}

impl Drop for FixtureDir {
    fn drop(&mut self) {
        std::fs::remove_dir_all(&self.0).ok();
    }
}

fn snapshot_account(svm: &HPSVM, address: Address) -> AccountSnapshot {
    let account = svm.get_account(&address).expect("account must exist");
    AccountSnapshot::from_readable(address, &account)
}

fn fixture_path(stem: &str) -> PathBuf {
    let unique = Address::new_unique();
    std::env::temp_dir().join(format!("{stem}-{unique}.json"))
}

fn write_fixture(name: &str, blockhash_check: bool) -> PathBuf {
    let path = fixture_path("hpsvm-cli-run");
    write_fixture_to_path(name, blockhash_check, path.clone(), FixtureFormat::Json);
    path
}

fn write_fixture_to_path(name: &str, blockhash_check: bool, path: PathBuf, format: FixtureFormat) {
    let mut svm = HPSVM::new();
    let payer = Keypair::new();
    let recipient = Address::new_unique();

    svm.airdrop(&payer.pubkey(), 10_000).expect("payer airdrop must succeed");
    svm.airdrop(&recipient, 1).expect("recipient airdrop must succeed");
    let tx = VersionedTransaction::from(solana_transaction::Transaction::new(
        &[&payer],
        Message::new(&[transfer(&payer.pubkey(), &recipient, 64)], Some(&payer.pubkey())),
        svm.latest_blockhash(),
    ));
    let baseline = ExecutionSnapshot::from_outcome(&svm.transact(tx.clone()));
    let fixture = CaptureBuilder::new(name)
        .runtime(RuntimeFixtureConfig::new(svm.block_env().slot, None, true, blockhash_check))
        .pre_accounts(vec![
            snapshot_account(&svm, payer.pubkey()),
            snapshot_account(&svm, recipient),
        ])
        .baseline(baseline)
        .compares(Compare::everything())
        .capture_transaction(&tx)
        .expect("fixture capture must succeed");

    fixture.save(&path, format).expect("fixture save must succeed");
}

#[test]
fn fixture_run_reports_pass_for_matching_fixture() {
    let path = write_fixture("cli-run", false);

    let output = Command::new(env!("CARGO_BIN_EXE_hpsvm"))
        .args(["fixture", "run", path.to_str().expect("temp path must be valid utf-8")])
        .output()
        .expect("run command must execute");

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains("PASS:"));

    std::fs::remove_file(path).ok();
}

#[test]
fn fixture_run_reports_pass_for_fixture_directory() {
    let dir = FixtureDir::new("hpsvm-cli-run-dir");
    std::fs::create_dir(dir.path()).expect("fixture directory must be created");
    write_fixture_to_path(
        "cli-run-dir-second",
        false,
        dir.path().join("b.json"),
        FixtureFormat::Json,
    );
    write_fixture_to_path(
        "cli-run-dir-first",
        false,
        dir.path().join("a.bin"),
        FixtureFormat::Binary,
    );
    std::fs::write(dir.path().join("notes.txt"), "ignore me").expect("notes file must be written");

    let output = Command::new(env!("CARGO_BIN_EXE_hpsvm"))
        .args(["fixture", "run", dir.path().to_str().expect("temp path must be valid utf-8")])
        .output()
        .expect("run command must execute");

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    let first = stdout.find("PASS: cli-run-dir-first").expect("first fixture should pass");
    let second = stdout.find("PASS: cli-run-dir-second").expect("second fixture should pass");
    assert!(first < second, "fixtures should run in sorted path order: {stdout}");
}

#[test]
fn fixture_run_rejects_empty_fixture_directory() {
    let dir = FixtureDir::new("hpsvm-cli-run-empty-dir");
    std::fs::create_dir(dir.path()).expect("fixture directory must be created");
    std::fs::write(dir.path().join("notes.txt"), "ignore me").expect("notes file must be written");

    let output = Command::new(env!("CARGO_BIN_EXE_hpsvm"))
        .args(["fixture", "run", dir.path().to_str().expect("temp path must be valid utf-8")])
        .output()
        .expect("run command must execute");

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("no fixture files found"));
}

#[test]
fn fixture_run_rejects_blockhash_checked_fixture() {
    let path = write_fixture("cli-run-blockhash", true);

    let output = Command::new(env!("CARGO_BIN_EXE_hpsvm"))
        .args(["fixture", "run", path.to_str().expect("temp path must be valid utf-8")])
        .output()
        .expect("run command must execute");

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("blockhash_check"));

    std::fs::remove_file(path).ok();
}

#[test]
fn fixture_run_rejects_invalid_program_mapping() {
    let path = write_fixture("cli-run-programs", false);

    let output = Command::new(env!("CARGO_BIN_EXE_hpsvm"))
        .args([
            "fixture",
            "run",
            path.to_str().expect("temp path must be valid utf-8"),
            "--program",
            "not-a-mapping",
        ])
        .output()
        .expect("run command must execute");

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("expected <program-id>=<path>"));

    std::fs::remove_file(path).ok();
}