ic-testkit 0.21.2

PocketIC-oriented test utilities for IC canister tests
Documentation
use super::test_support::{fake_wasm_build_spec, unique_temp_directory, write_executable_script};
use super::{ArtifactCachePreparation, ArtifactCacheSpec, prepare_artifact_cache};
use crate::{
    ic_host_artifacts::artifact::{ArtifactError, Sha256Digest},
    ic_host_process::tool::{
        AdmittedTool, ExecutionContext, OutputLimits, ToolError, ToolSpec, resolve_executable,
    },
};
use std::{ffi::OsString, fs, path::Path, time::Duration};

#[test]
fn admitted_shared_tool_output_is_published_and_invalidated_by_tool_bytes() {
    let root = unique_temp_directory("shared-admitted-tool")
        .canonicalize()
        .unwrap();
    let script = b"#!/bin/sh\nif [ \"$1\" = --version ]; then printf 'fixture-tool 1.0\\n'; else printf 'compiled output'; fi\n";
    let executable = root.join("producer");
    write_executable_script(&executable, script);
    let executable = resolve_executable(Path::new("./producer"), &root, &[]).unwrap();
    let context = ExecutionContext {
        current_dir: &root,
        environment: &[],
    };
    let limits = OutputLimits {
        stdout_bytes: 128,
        stderr_bytes: 128,
        timeout: Duration::from_secs(5),
    };
    let tool = AdmittedTool::admit(
        &ToolSpec {
            executable: &executable,
            sha256: Sha256Digest::compute(script),
            executable_bytes: script.len() as u64,
            version_arguments: &[OsString::from("--version")],
            version_identity: "fixture-tool 1.0",
        },
        &context,
        limits,
    )
    .unwrap();
    let destination = root.join("output");
    let spec = ArtifactCacheSpec::new(&root.join("cache"), "shared-tool", "compile/v1")
        .with_tool("producer", &executable)
        .with_output("compiled", &destination);
    let ArtifactCachePreparation::Build(transaction) = prepare_artifact_cache(&spec).unwrap()
    else {
        panic!("first acquisition must build");
    };
    let evidence = tool
        .run(&[OsString::from("compile")], &context, limits)
        .unwrap();
    fs::write(
        transaction.output_path("compiled").unwrap(),
        &evidence.stdout,
    )
    .unwrap();
    let record = transaction.commit().unwrap();
    assert_eq!(fs::read(&destination).unwrap(), b"compiled output");
    let ArtifactCachePreparation::Reused(reused) = prepare_artifact_cache(&spec).unwrap() else {
        panic!("same admitted producer must reuse its output");
    };
    write_executable_script(&executable, b"#!/bin/sh\nexit 1\n");
    assert!(matches!(
        tool.run(&[OsString::from("compile")], &context, limits),
        Err(ToolError::Artifact(ArtifactError::DigestMismatch { .. }))
    ));
    assert!(matches!(
        prepare_artifact_cache(&spec).unwrap(),
        ArtifactCachePreparation::Build(_)
    ));
    drop(reused);
    drop(record);
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn shim_wasm_and_post_link_caches_reuse_and_invalidate_independently() {
    use super::{
        ArtifactCacheOutcome, WasmBuildOutcome, build_wasm_canisters_cached,
        resolve_cargo_build_inputs,
    };
    use crate::ic_host_artifacts::artifact::hash_reader;

    let (root, spec) = fake_wasm_build_spec("post-link-composition");
    let root = root.canonicalize().unwrap();
    let compiler = root.join("selected-rustc");
    write_executable_script(&compiler, "#!/bin/sh\nprintf 'compiler-A\\n'\n");
    let spec = spec.with_rustc_program(compiler.as_os_str());
    let executable = root.join("optimizer");
    let script = b"#!/bin/sh\nif [ \"$1\" = --version ]; then printf 'optimizer 1\\n'; else /bin/cp \"$1\" \"$3\"; printf 'run\\n' >> \"$0.calls\"; fi\n";
    write_executable_script(&executable, script);
    let context = ExecutionContext {
        current_dir: &root,
        environment: &[],
    };
    let limits = OutputLimits {
        stdout_bytes: 128,
        stderr_bytes: 128,
        timeout: Duration::from_secs(5),
    };
    let admit = |bytes: &[u8]| {
        AdmittedTool::admit(
            &ToolSpec {
                executable: &executable,
                sha256: Sha256Digest::compute(bytes),
                executable_bytes: bytes.len() as u64,
                version_arguments: &[OsString::from("--version")],
                version_identity: "optimizer 1",
            },
            &context,
            limits,
        )
        .unwrap()
    };
    let acquire = |optimizer: &AdmittedTool| {
        let resolved = resolve_cargo_build_inputs(&spec).unwrap();
        let wasm = build_wasm_canisters_cached(&spec).unwrap();
        let input = &wasm.record().artifacts()[0];
        let post_spec = ArtifactCacheSpec::new(&root.join("post-cache"), "deploy", "optimizer/v1")
            .with_cargo_build_inputs("cargo", &spec, &resolved)
            .with_input("wasm", input)
            .with_tool("optimizer", optimizer.path())
            .with_arguments(["<input>", "-o", "<output>"])
            .with_environment(context.environment.iter().cloned())
            .with_identity_bytes(
                "optimizer-cwd",
                context.current_dir.as_os_str().as_encoded_bytes(),
            )
            .with_output("deploy", &root.join("deploy.wasm"));
        let deploy = match prepare_artifact_cache(&post_spec).unwrap() {
            ArtifactCachePreparation::Reused(record) => ArtifactCacheOutcome::Reused(record),
            ArtifactCachePreparation::Build(transaction) => {
                let output = transaction.output_path("deploy").unwrap();
                optimizer
                    .run(
                        &[input.into(), "-o".into(), output.clone().into()],
                        &context,
                        limits,
                    )
                    .unwrap();
                hash_reader(fs::File::open(&output).unwrap(), 1024).unwrap();
                transaction.commit().unwrap()
            }
        };
        assert_eq!(
            fs::read(deploy.record().artifacts()[0].path()).unwrap(),
            b"\0asm\x01\0\0\0"
        );
        (
            matches!(wasm, WasmBuildOutcome::Reused { .. }),
            matches!(deploy, ArtifactCacheOutcome::Reused(_)),
        )
    };
    let optimizer = admit(script);
    assert_eq!(acquire(&optimizer), (false, false));
    assert_eq!(acquire(&optimizer), (true, true));
    assert_eq!(
        fs::read_to_string(root.join("optimizer.calls")).unwrap(),
        "run\n"
    );
    write_executable_script(&compiler, "#!/bin/sh\nprintf 'compiler-B\\n'\n");
    assert_eq!(acquire(&optimizer), (false, false));
    let changed_script = [script.as_slice(), b"# changed optimizer bytes\n"].concat();
    write_executable_script(&executable, &changed_script);
    let optimizer = admit(&changed_script);
    assert_eq!(acquire(&optimizer), (true, false));
    assert_eq!(acquire(&optimizer), (true, true));
    assert_eq!(
        fs::read_to_string(root.join("optimizer.calls")).unwrap(),
        "run\nrun\nrun\n"
    );
    fs::remove_dir_all(root).unwrap();
}