ic-testkit 0.8.2

PocketIC-oriented test utilities for IC canister tests
Documentation
use super::{ArtifactCacheBatchFailure, build_artifact_caches_batch};
use crate::artifacts::{
    ArtifactCacheOutcome, ArtifactCachePreparation, ArtifactCacheSpec, prepare_artifact_cache,
    test_support::unique_temp_directory,
};
use std::fs;

#[test]
fn independent_transactions_build_then_reuse_in_order() {
    let root = unique_temp_directory("artifact-cache-batch");
    let input = root.join("input");
    fs::write(&input, b"input").expect("write batch input");
    let specs = [
        ArtifactCacheSpec::new(&root.join("cache"), "first", "recipe/v1")
            .with_coordination_scope("shared-builder")
            .with_input("input", &input)
            .with_output("output", &root.join("first.out")),
        ArtifactCacheSpec::new(&root.join("cache"), "second", "recipe/v1")
            .with_coordination_scope("shared-builder")
            .with_input("input", &input)
            .with_output("output", &root.join("second.out")),
    ];
    let mut built_indices = Vec::new();
    let built = build_artifact_caches_batch(&specs, |index, transaction| {
        built_indices.push(index);
        fs::write(
            transaction
                .output_path("output")
                .expect("batch output path"),
            format!("output-{index}"),
        )
        .expect("write batch output");
        Ok::<(), &'static str>(())
    });

    assert!(built.is_success());
    assert_eq!(built_indices, [0, 1]);
    assert!(
        built
            .outcomes()
            .all(|(_index, outcome)| matches!(outcome, ArtifactCacheOutcome::Built(_)))
    );
    let built_metrics = built.metrics();
    assert_eq!(built_metrics.entries(), 2);
    assert_eq!(built_metrics.succeeded(), 2);
    assert_eq!(built_metrics.failed(), 0);
    assert_eq!(built_metrics.built(), 2);
    assert_eq!(built_metrics.reused(), 0);
    assert!(built_metrics.successful_timings().caller_build().is_some());

    let reused = build_artifact_caches_batch(&specs, |_index, _transaction| {
        Err::<(), _>("unexpected cache miss")
    });
    assert!(reused.is_success());
    assert!(
        reused
            .outcomes()
            .all(|(_index, outcome)| outcome.is_reused())
    );
    let reused_metrics = reused.metrics();
    assert_eq!(reused_metrics.built(), 0);
    assert_eq!(reused_metrics.reused(), 2);
    assert!(reused_metrics.successful_timings().caller_build().is_none());
    fs::remove_dir_all(root).expect("remove artifact batch fixture");
}

#[test]
fn builder_failure_is_retained_and_later_transaction_runs() {
    let root = unique_temp_directory("artifact-cache-batch-builder-failure");
    let input = root.join("input");
    fs::write(&input, b"input").expect("write batch input");
    let specs = [
        artifact_spec(&root, &input, "first", "first.out"),
        artifact_spec(&root, &input, "second", "second.out"),
        artifact_spec(&root, &input, "third", "third.out"),
    ];
    let report = build_artifact_caches_batch(&specs, |index, transaction| {
        if index == 1 {
            return Err("synthetic builder failure");
        }
        fs::write(
            transaction
                .output_path("output")
                .expect("batch output path"),
            format!("output-{index}"),
        )
        .expect("write successful batch output");
        Ok(())
    });

    assert!(!report.is_success());
    assert_eq!(report.results().len(), 3);
    assert_eq!(
        report
            .outcomes()
            .map(|(index, _outcome)| index)
            .collect::<Vec<_>>(),
        [0, 2]
    );
    let failures = report.failures().collect::<Vec<_>>();
    assert_eq!(failures.len(), 1);
    assert_eq!(failures[0].0, 1);
    assert!(matches!(
        failures[0].1,
        ArtifactCacheBatchFailure::Build {
            cleanup_error: None,
            ..
        }
    ));
    assert!(!root.join("second.out").exists());
    assert_eq!(fs::read(root.join("third.out")).unwrap(), b"output-2");
    let metrics = report.metrics();
    assert_eq!(metrics.succeeded(), 2);
    assert_eq!(metrics.failed(), 1);
    assert_eq!(metrics.built(), 2);

    let preparation = prepare_artifact_cache(&specs[1]).expect("prepare failed entry again");
    let ArtifactCachePreparation::Build(transaction) = preparation else {
        panic!("failed batch entry must not be published");
    };
    transaction.abort().expect("abort verification transaction");
    fs::remove_dir_all(root).expect("remove artifact batch failure fixture");
}

#[test]
fn cache_failure_is_indexed_and_does_not_stop_later_entries() {
    let root = unique_temp_directory("artifact-cache-batch-cache-failure");
    let input = root.join("input");
    fs::write(&input, b"input").expect("write batch input");
    let specs = [
        artifact_spec(&root, &input, "first", "first.out"),
        ArtifactCacheSpec::new(&root.join("cache"), "", "recipe/v1")
            .with_input("input", &input)
            .with_output("output", &root.join("invalid.out")),
        artifact_spec(&root, &input, "third", "third.out"),
    ];
    let report = build_artifact_caches_batch(&specs, |index, transaction| {
        fs::write(
            transaction
                .output_path("output")
                .expect("batch output path"),
            format!("output-{index}"),
        )
        .expect("write successful batch output");
        Ok::<(), &'static str>(())
    });

    assert_eq!(
        report
            .failures()
            .map(|(index, failure)| {
                assert!(matches!(failure, ArtifactCacheBatchFailure::Cache { .. }));
                index
            })
            .collect::<Vec<_>>(),
        [1]
    );
    assert_eq!(fs::read(root.join("third.out")).unwrap(), b"output-2");
    fs::remove_dir_all(root).expect("remove artifact cache failure fixture");
}

#[test]
fn commit_failure_is_indexed_and_does_not_stop_later_entries() {
    let root = unique_temp_directory("artifact-cache-batch-commit-failure");
    let input = root.join("input");
    fs::write(&input, b"input").expect("write batch input");
    let specs = [
        artifact_spec(&root, &input, "first", "first.out"),
        artifact_spec(&root, &input, "second", "second.out"),
        artifact_spec(&root, &input, "third", "third.out"),
    ];
    let report = build_artifact_caches_batch(&specs, |index, transaction| {
        if index != 1 {
            fs::write(
                transaction
                    .output_path("output")
                    .expect("batch output path"),
                format!("output-{index}"),
            )
            .expect("write successful batch output");
        }
        Ok::<(), &'static str>(())
    });

    assert_eq!(
        report
            .failures()
            .map(|(index, failure)| {
                assert!(matches!(failure, ArtifactCacheBatchFailure::Cache { .. }));
                index
            })
            .collect::<Vec<_>>(),
        [1]
    );
    assert!(!root.join("second.out").exists());
    assert_eq!(fs::read(root.join("third.out")).unwrap(), b"output-2");
    fs::remove_dir_all(root).expect("remove artifact cache commit failure fixture");
}

fn artifact_spec(
    root: &std::path::Path,
    input: &std::path::Path,
    namespace: &str,
    output: &str,
) -> ArtifactCacheSpec {
    ArtifactCacheSpec::new(&root.join("cache"), namespace, "recipe/v1")
        .with_input("input", input)
        .with_output("output", &root.join(output))
}