canic 0.100.2

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use std::{
    collections::BTreeSet,
    fs,
    path::{Path, PathBuf},
};

const MANAGED_START_MARKERS: &[&str] = &[
    "canic::start!()",
    "canic::start!(",
    "canic::start_local!()",
    "canic::start_local!(",
    "canic::start_wasm_store!()",
    "canic::start_wasm_store!(",
];

const RAW_ENDPOINT_MARKERS: &[&str] = &[
    "#[ic_cdk::query",
    "#[ic_cdk::update",
    "#[::ic_cdk::query",
    "#[::ic_cdk::update",
    "#[query]",
    "#[query(",
    "#[update]",
    "#[update(",
];

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 collect_files(root: &Path, filename: Option<&str>, extension: Option<&str>) -> Vec<PathBuf> {
    let mut pending = vec![root.to_path_buf()];
    let mut files = Vec::new();
    while let Some(directory) = pending.pop() {
        let mut entries = fs::read_dir(&directory)
            .unwrap_or_else(|error| panic!("read {}: {error}", directory.display()))
            .collect::<Result<Vec<_>, _>>()
            .unwrap_or_else(|error| panic!("read entry under {}: {error}", directory.display()));
        entries.sort_by_key(std::fs::DirEntry::path);
        for entry in entries {
            let path = entry.path();
            let file_type = entry
                .file_type()
                .unwrap_or_else(|error| panic!("inspect {}: {error}", path.display()));
            if file_type.is_dir() {
                pending.push(path);
            } else if file_type.is_file()
                && filename.is_none_or(|expected| entry.file_name() == expected)
                && extension
                    .is_none_or(|expected| path.extension().is_some_and(|ext| ext == expected))
            {
                files.push(path);
            }
        }
    }
    files.sort();
    files
}

#[test]
fn managed_canisters_export_endpoints_only_through_canic_macros() {
    let workspace = workspace_root();
    let mut managed_sources = BTreeSet::new();

    for source_root in ["apps", "canisters"] {
        for manifest in collect_files(&workspace.join(source_root), Some("Cargo.toml"), None) {
            let package_root = manifest.parent().expect("package manifest parent");
            let sources = collect_files(&package_root.join("src"), None, Some("rs"));
            let managed = sources.iter().any(|path| {
                let source = fs::read_to_string(path)
                    .unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
                MANAGED_START_MARKERS
                    .iter()
                    .any(|marker| source.contains(marker))
            });
            if managed {
                managed_sources.extend(sources);
            }
        }
    }

    let mut violations = Vec::new();
    for path in managed_sources {
        let source = fs::read_to_string(&path)
            .unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
        for marker in RAW_ENDPOINT_MARKERS {
            if source.contains(marker) {
                violations.push(format!(
                    "{} contains raw managed endpoint marker {marker}",
                    path.strip_prefix(&workspace).unwrap_or(&path).display()
                ));
            }
        }
    }

    assert!(
        violations.is_empty(),
        "managed Canister endpoints bypass the Canic activation dispatcher: {violations:#?}"
    );
}

#[test]
fn prepared_managed_init_defers_application_work_while_standalone_local_starts_it() {
    let macro_path = workspace_root().join("crates/canic/src/macros/start.rs");
    let source = fs::read_to_string(&macro_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", macro_path.display()));
    let managed = source
        .split("macro_rules! __canic_start_nonroot_lifecycle_core")
        .nth(1)
        .and_then(|rest| {
            rest.split("macro_rules! __canic_start_local_lifecycle_core")
                .next()
        })
        .expect("managed non-root lifecycle macro");
    let managed_init = managed
        .split("#[$crate::__internal::cdk::init]")
        .nth(1)
        .and_then(|rest| {
            rest.split("#[$crate::__internal::cdk::post_upgrade]")
                .next()
        })
        .expect("managed non-root init body");

    assert!(
        managed_init.contains("LifecycleApi::init_nonroot_canister_before_bootstrap"),
        "managed non-root init must enter the canonical Prepared lifecycle"
    );
    assert!(
        !managed_init.contains("schedule_init_nonroot_bootstrap")
            && !managed_init.contains("TimerApi::defer_lifecycle")
            && !managed_init.contains("canic_install("),
        "Prepared managed init must not schedule bootstrap, timers, or application hooks"
    );

    let local = source
        .split("macro_rules! __canic_start_local_lifecycle_core")
        .nth(1)
        .and_then(|rest| {
            rest.split("macro_rules! __canic_root_lifecycle_core")
                .next()
        })
        .expect("standalone-local lifecycle macro");
    let local_init = local
        .split("#[$crate::__internal::cdk::init]")
        .nth(1)
        .and_then(|rest| {
            rest.split("#[$crate::__internal::cdk::post_upgrade]")
                .next()
        })
        .expect("standalone-local init body");

    assert!(
        local_init.contains("LifecycleApi::init_local_nonroot_canister_before_bootstrap")
            && local_init.contains("schedule_init_nonroot_bootstrap")
            && local_init.contains("canic_install(args)"),
        "standalone-local init must retain its explicit local lifecycle and application startup"
    );
    assert!(
        !local.contains("CanisterInitPayload") && !local.contains("FleetBinding"),
        "standalone-local lifecycle must not fabricate managed Fleet identity"
    );
}

#[test]
fn prepared_activation_schedules_each_current_application_install_hook_once() {
    let workspace = workspace_root();
    let macro_path = workspace.join("crates/canic/src/macros/start.rs");
    let source = fs::read_to_string(&macro_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", macro_path.display()));
    let nonroot = source
        .split("macro_rules! __canic_start_nonroot_lifecycle_core")
        .nth(1)
        .and_then(|rest| {
            rest.split("macro_rules! __canic_start_local_lifecycle_core")
                .next()
        })
        .expect("managed non-root lifecycle macro");
    let root = source
        .split("macro_rules! __canic_root_lifecycle_core")
        .nth(1)
        .and_then(|rest| {
            rest.split("// Run the optional init block from a lifecycle timer")
                .next()
        })
        .expect("managed root lifecycle macro");

    assert!(
        nonroot.contains("fn __canic_schedule_prepared_activation_init(args: Option<Vec<u8>>)")
            && nonroot.contains("canic_install(args).await;"),
        "managed non-root activation must receive durable init bytes from its transition"
    );
    assert!(
        root.contains("canic_setup().await;") && root.contains("canic_install().await;"),
        "managed root activation must schedule its current application install hooks"
    );
    assert_eq!(
        source
            .matches("__CANIC_PREPARED_APPLICATION_INIT_SCHEDULED.replace(true)")
            .count(),
        2,
        "root and non-root activation adapters must each suppress duplicate hook scheduling"
    );

    let nonroot_path = workspace.join("crates/canic/src/macros/endpoints/nonroot.rs");
    let nonroot_endpoints = fs::read_to_string(&nonroot_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", nonroot_path.display()));
    assert!(
        nonroot_endpoints.contains(
            "__canic_schedule_prepared_activation_init(transition.application_init_args);"
        ),
        "managed non-root activation must hand durable init bytes to the lifecycle adapter"
    );

    let root_path = workspace.join("crates/canic/src/macros/endpoints/root.rs");
    let root_endpoints = fs::read_to_string(&root_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", root_path.display()));
    assert!(
        root_endpoints.contains("__canic_schedule_prepared_activation_init();"),
        "managed root activation must hand success to the lifecycle adapter"
    );
}

#[test]
fn standalone_local_bundle_excludes_managed_fleet_activation_updates() {
    let workspace = workspace_root();
    let start_path = workspace.join("crates/canic/src/macros/start.rs");
    let start = fs::read_to_string(&start_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", start_path.display()));
    let bundles_path = workspace.join("crates/canic/src/macros/endpoints/bundles.rs");
    let bundles = fs::read_to_string(&bundles_path)
        .unwrap_or_else(|error| panic!("read {}: {error}", bundles_path.display()));

    let local_start = start
        .split("macro_rules! start_local")
        .nth(1)
        .and_then(|rest| rest.split("macro_rules! start_wasm_store").next())
        .expect("standalone-local start macro");
    assert!(
        local_start.contains("canic_bundle_local_nonroot_only_endpoints!()")
            && !local_start.contains("canic_bundle_managed_nonroot_only_endpoints!()"),
        "standalone-local startup must select only its local endpoint bundle"
    );

    let local_bundle = bundles
        .split("macro_rules! canic_bundle_local_nonroot_only_endpoints")
        .nth(1)
        .and_then(|rest| {
            rest.split("macro_rules! canic_bundle_wasm_store_runtime_endpoints")
                .next()
        })
        .expect("standalone-local endpoint bundle");
    assert!(
        local_bundle.contains("canic_emit_nonroot_sync_topology_endpoints!()")
            && !local_bundle.contains("canic_emit_nonroot_fleet_activation_endpoints!()"),
        "standalone-local canisters must not export managed Fleet activation updates"
    );

    let managed_bundle = bundles
        .split("macro_rules! canic_bundle_managed_nonroot_only_endpoints")
        .nth(1)
        .and_then(|rest| {
            rest.split("macro_rules! canic_bundle_local_nonroot_only_endpoints")
                .next()
        })
        .expect("managed non-root endpoint bundle");
    assert!(
        managed_bundle.contains("canic_emit_nonroot_fleet_activation_endpoints!()"),
        "managed non-roots must retain the current Fleet activation updates"
    );
}