ic-build 0.3.2

Cargo and Wasm builds, exact inputs and retained artifact caches
Documentation
use super::{ArtifactStagingDirectory, CapturedArtifact, OwnedDirectory};
use crate::artifacts::test_support::unique_temp_directory;
use std::{fs, io};

#[test]
fn owned_directory_cleans_up_on_unwind_and_preserves_transferred_custody() {
    let root = unique_temp_directory("owned-directory-unwind");
    let owned = root.join("owned");
    let retained = root.join("retained");
    fs::create_dir(&owned).unwrap();
    fs::create_dir(&retained).unwrap();
    fs::write(retained.join("evidence"), b"retained").unwrap();
    let result = std::panic::catch_unwind(|| {
        let _owned = OwnedDirectory::new(owned.clone());
        OwnedDirectory::new(retained.clone()).preserve();
        panic!("synthetic producer panic");
    });
    assert!(result.is_err());
    assert!(!owned.exists());
    assert_eq!(fs::read(retained.join("evidence")).unwrap(), b"retained");
    fs::remove_dir_all(root).unwrap();
}

#[cfg(unix)]
#[test]
fn cleanup_failure_keeps_build_retry_but_explicit_close_transfers_recovery() {
    use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};

    let root = unique_temp_directory("owned-directory-cleanup-failure");
    // Root bypasses permission refusal; release evidence runs as an ordinary user.
    if fs::metadata(&root).unwrap().uid() == 0 {
        fs::remove_dir_all(root).unwrap();
        return;
    }
    let owned = root.join("build");
    fs::create_dir(&owned).unwrap();
    let mut build = OwnedDirectory::new(owned.clone());
    let staging = ArtifactStagingDirectory::new(&root).unwrap();
    let stage_path = staging.path().to_owned();
    let original_permissions = fs::metadata(&root).unwrap().permissions();
    fs::set_permissions(&root, fs::Permissions::from_mode(0o500)).unwrap();
    let build_error = build.try_cleanup().unwrap_err();
    let close_error = staging.close().unwrap_err();
    fs::set_permissions(&root, original_permissions).unwrap();

    assert_eq!(build_error.kind(), io::ErrorKind::PermissionDenied);
    assert_eq!(close_error.kind(), io::ErrorKind::PermissionDenied);
    assert!(build.is_armed());
    drop(build);
    assert!(!owned.exists(), "failed build cleanup retries on Drop");
    assert!(
        stage_path.exists(),
        "explicit close leaves recovery to the caller"
    );
    fs::remove_dir_all(root).unwrap();
}

// Moved from Canic's capture tests; producer paths and public publication remain distinct.
#[test]
fn captured_cargo_input_survives_replacement_and_cleans_up() {
    let root = unique_temp_directory("captured-cargo-output");
    let source = root.join("cargo.wasm");
    let destination = root.join("published.wasm");
    fs::write(&source, b"compiled first role").unwrap();
    fs::write(&destination, b"previous artifact").unwrap();
    let captured = CapturedArtifact::capture(&source, &root, 1024).unwrap();
    let staging = captured.path().parent().unwrap().to_owned();

    fs::write(&source, b"later compilation").unwrap();
    assert_eq!(fs::read(captured.path()).unwrap(), b"compiled first role");
    assert_eq!(fs::read(&destination).unwrap(), b"previous artifact");
    fs::remove_file(&source).unwrap();
    assert_eq!(fs::read(captured.path()).unwrap(), b"compiled first role");

    drop(captured);
    assert!(!staging.exists());
    assert_eq!(fs::read(&destination).unwrap(), b"previous artifact");
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn failed_cargo_input_capture_leaves_no_stage_or_published_changes() {
    let root = unique_temp_directory("failed-cargo-capture");
    let destination = root.join("published.wasm");
    fs::write(&destination, b"previous artifact").unwrap();
    let error = CapturedArtifact::capture(&root.join("missing.wasm"), &root, 1024).unwrap_err();
    assert_eq!(error.kind(), io::ErrorKind::NotFound);
    assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
    assert_eq!(fs::read(&destination).unwrap(), b"previous artifact");
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn capture_honors_the_selected_byte_budget_and_preserves_its_typed_error() {
    let root = unique_temp_directory("bounded-capture");
    let source = root.join("input");
    fs::write(&source, b"four").unwrap();
    let error = CapturedArtifact::capture(&source, &root, 3).unwrap_err();
    assert!(matches!(
        error
            .get_ref()
            .unwrap()
            .downcast_ref::<ic_host_artifacts::artifact::ArtifactError>(),
        Some(ic_host_artifacts::artifact::ArtifactError::LimitExceeded { limit: 3 })
    ));
    assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
    let captured = CapturedArtifact::capture(&source, &root, 4).unwrap();
    assert_eq!(fs::read(captured.path()).unwrap(), b"four");
    let staging = captured.path().parent().unwrap().to_owned();
    captured.close().unwrap();
    assert!(!staging.exists());
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn simultaneous_captures_retain_distinct_versions_without_blocking() {
    let root = unique_temp_directory("distinct-captures");
    let source = root.join("input");
    fs::write(&source, b"first").unwrap();
    let first = CapturedArtifact::capture(&source, &root, 1024).unwrap();
    let duplicate = CapturedArtifact::capture(&source, &root, 1024).unwrap();
    fs::write(&source, b"second").unwrap();
    let second = CapturedArtifact::capture(&source, &root, 1024).unwrap();
    assert_ne!(first.path(), duplicate.path());
    assert_eq!(fs::read(first.path()).unwrap(), b"first");
    assert_eq!(fs::read(second.path()).unwrap(), b"second");
    first.close().unwrap();
    assert_eq!(fs::read(duplicate.path()).unwrap(), b"first");
    duplicate.close().unwrap();
    second.close().unwrap();
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn staging_preserves_unowned_children_and_requires_an_existing_parent() {
    let root = unique_temp_directory("staging-custody");
    let foreign = root.join("foreign");
    fs::create_dir(&foreign).unwrap();
    fs::write(foreign.join("evidence"), b"preserve").unwrap();
    let missing = root.join("missing");
    assert_eq!(
        ArtifactStagingDirectory::new(&missing).unwrap_err().kind(),
        io::ErrorKind::NotFound
    );
    assert!(!missing.exists());
    let staging = ArtifactStagingDirectory::new(&root).unwrap();
    fs::write(staging.path().join("candidate"), b"owned").unwrap();
    let staged_path = staging.path().to_owned();
    staging.close().unwrap();
    assert!(!staged_path.exists());
    assert_eq!(fs::read(foreign.join("evidence")).unwrap(), b"preserve");
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn retained_capture_survives_owner_and_source_without_retaining_other_captures() {
    let root = unique_temp_directory("retained-capture");
    let source = root.join("producer.wasm");
    fs::write(&source, b"exact evidence").unwrap();
    let retained = CapturedArtifact::capture(&source, &root, 32).unwrap();
    let temporary = CapturedArtifact::capture(&source, &root, 32).unwrap();
    let temporary_path = temporary.path().to_owned();
    let path = retained.retain();
    fs::remove_file(&source).unwrap();
    drop(temporary);
    assert!(!temporary_path.exists());
    assert_eq!(fs::read(&path).unwrap(), b"exact evidence");
    fs::remove_dir_all(path.parent().unwrap()).unwrap();
    assert!(!path.exists());
    fs::remove_dir_all(root).unwrap();
}