canic 0.83.21

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    time::{SystemTime, UNIX_EPOCH},
};

fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("crate directory should have a parent")
        .parent()
        .expect("workspace root should exist")
        .to_path_buf()
}

fn unique_temp_repo(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("time should be after epoch")
        .as_nanos();
    std::env::temp_dir().join(format!("canic-{name}-{}-{nanos}", std::process::id()))
}

fn run_git(root: &Path, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(root)
        .output()
        .expect("git should run");
    assert!(
        output.status.success(),
        "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn write_file(root: &Path, relative: &str, contents: &str) {
    let path = root.join(relative);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("parent directory should be created");
    }
    fs::write(&path, contents).unwrap_or_else(|err| panic!("failed to write {relative}: {err}"));
}

fn create_repo(name: &str) -> PathBuf {
    let root = unique_temp_repo(name);
    fs::create_dir_all(&root).expect("temp repo should be created");
    run_git(&root, &["init"]);
    write_file(&root, "Cargo.toml", "[workspace]\n");
    write_file(&root, "Cargo.lock", "# lock\n");
    write_file(&root, "README.md", "readme\n");
    write_file(&root, "crates/canic-host/README.md", "host readme\n");
    write_file(&root, "scripts/dev/install_dev.sh", "#!/usr/bin/env bash\n");
    write_file(
        &root,
        "scripts/ci/sync-release-surface-version.sh",
        "#!/usr/bin/env bash\n",
    );
    write_file(&root, "src/lib.rs", "pub fn marker() {}\n");
    run_git(&root, &["add", "."]);
    run_git(
        &root,
        &[
            "-c",
            "user.name=Canic Test",
            "-c",
            "user.email=canic@example.invalid",
            "commit",
            "-m",
            "initial",
        ],
    );
    root
}

fn run_guard(root: &Path) -> Output {
    let script = workspace_root().join("scripts/ci/check-release-index.sh");
    Command::new(script)
        .current_dir(root)
        .output()
        .expect("release index guard should run")
}

fn output_text(output: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

#[test]
fn release_index_guard_accepts_complete_release_files() {
    let root = create_repo("release-index-ok");
    write_file(&root, "Cargo.toml", "[workspace]\n# bumped\n");
    write_file(
        &root,
        "scripts/dev/install_dev.sh",
        "#!/usr/bin/env bash\n# bumped\n",
    );
    run_git(&root, &["add", "Cargo.toml", "scripts/dev/install_dev.sh"]);

    let output = run_guard(&root);

    assert!(
        output.status.success(),
        "guard should accept release-only index\n{}",
        output_text(&output)
    );
    let _ = fs::remove_dir_all(root);
}

#[test]
fn release_index_guard_rejects_empty_index() {
    let root = create_repo("release-index-empty");

    let output = run_guard(&root);
    let text = output_text(&output);

    assert!(
        !output.status.success(),
        "guard should reject an empty index"
    );
    assert!(text.contains("No staged release files"));
    let _ = fs::remove_dir_all(root);
}

#[test]
fn release_index_guard_rejects_staged_deletions() {
    let root = create_repo("release-index-deletion");
    write_file(&root, "Cargo.toml", "[workspace]\n# bumped\n");
    run_git(&root, &["add", "Cargo.toml"]);
    run_git(&root, &["rm", "README.md"]);

    let output = run_guard(&root);
    let text = output_text(&output);

    assert!(
        !output.status.success(),
        "guard should reject staged deletions"
    );
    assert!(text.contains("Release commit index contains staged deletions"));
    assert!(text.contains("README.md"));
    let _ = fs::remove_dir_all(root);
}

#[test]
fn release_index_guard_rejects_staged_non_release_files() {
    let root = create_repo("release-index-non-release");
    write_file(
        &root,
        "src/lib.rs",
        "pub fn marker() {}\npub fn changed() {}\n",
    );
    run_git(&root, &["add", "src/lib.rs"]);

    let output = run_guard(&root);
    let text = output_text(&output);

    assert!(!output.status.success(), "guard should reject source files");
    assert!(text.contains("Release commit index contains non-release files"));
    assert!(text.contains("src/lib.rs"));
    let _ = fs::remove_dir_all(root);
}

#[test]
fn release_index_guard_rejects_partially_staged_release_files() {
    let root = create_repo("release-index-partial");
    write_file(&root, "Cargo.toml", "[workspace]\n# staged\n");
    run_git(&root, &["add", "Cargo.toml"]);
    write_file(&root, "Cargo.toml", "[workspace]\n# staged\n# unstaged\n");

    let output = run_guard(&root);
    let text = output_text(&output);

    assert!(
        !output.status.success(),
        "guard should reject partially staged release files"
    );
    assert!(text.contains("Release files are staged with additional unstaged changes"));
    assert!(text.contains("Cargo.toml"));
    let _ = fs::remove_dir_all(root);
}