use super::*;
use crate::test_support::temp_dir;
use std::io::Write as _;
const REUSE_CONFIG: &str = r#"[app]
name = "reuse"
[roles.root]
kind = "root"
[roles.app]
kind = "canister"
package = "."
[component_specs.app]
component_role = "app"
maximum_instances = 1
"#;
fn run_with_private_cargo_target(test: fn()) {
const CHILD_ENV: &str = "CANIC_TEST_PRIVATE_REUSE_TARGET";
let thread = std::thread::current();
let test_name = thread.name().expect("libtest names each test thread");
if env::var(CHILD_ENV).as_deref() == Ok(test_name) {
test();
return;
}
let scratch = temp_dir("reuse-cargo-target");
fs::create_dir_all(&scratch).unwrap();
let output = Command::new(env::current_exe().unwrap())
.args(["--exact", test_name])
.env(CHILD_ENV, test_name)
.env("CARGO_TARGET_DIR", scratch.join("target"))
.env("CARGO_BUILD_BUILD_DIR", scratch.join("build"))
.output()
.unwrap();
fs::remove_dir_all(scratch).unwrap();
assert!(
output.status.success(),
"isolated reuse test failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn infrastructure_build_fixture() -> (PathBuf, WorkspaceBuildContext) {
let root = temp_dir("reuse-first-infrastructure");
let app = root.join("app");
let canic = root.join("upstream/canic");
let control = root.join("upstream/canic-control-plane");
for directory in [&app, &canic, &control] {
fs::create_dir_all(directory.join("src")).unwrap();
}
fs::write(
app.join("Cargo.toml"),
r#"
[workspace]
[package]
name = "reuse-app"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
canic = { path = "../upstream/canic", default-features = false }
[profile.fast]
inherits = "release"
"#,
)
.unwrap();
fs::write(
canic.join("Cargo.toml"),
r#"
[package]
name = "canic"
version = "0.1.0"
edition = "2024"
[features]
fleet = ["dep:canic-control-plane"]
[dependencies]
canic-control-plane = { path = "../canic-control-plane", optional = true }
"#,
)
.unwrap();
fs::write(
control.join("Cargo.toml"),
"[package]\nname = \"canic-control-plane\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::write(
app.join("src/lib.rs"),
"pub fn value() -> u8 { canic::value() }\n",
)
.unwrap();
fs::write(canic.join("src/lib.rs"), "pub fn value() -> u8 { 1 }\n#[cfg(feature = \"fleet\")] pub use canic_control_plane::CONTROL;\n").unwrap();
let control_source = control.join("src/lib.rs");
fs::write(&control_source, "pub const CONTROL: u8 = 1;\n").unwrap();
fs::write(app.join("canic.toml"), REUSE_CONFIG).unwrap();
let lock = crate::cargo_command()
.args(["generate-lockfile", "--offline", "--manifest-path"])
.arg(app.join("Cargo.toml"))
.output()
.unwrap();
assert!(
lock.status.success(),
"{}",
String::from_utf8_lossy(&lock.stderr)
);
let context = WorkspaceBuildContext {
role: "root".into(),
profile: crate::canister_build::CanisterBuildProfile::Fast,
environment: "local".into(),
build_network: canic_core::ids::BuildNetwork::Local,
workspace_root: app.clone(),
icp_root: app.clone(),
config_path: app.join("canic.toml"),
local_replica: None,
refresh_canonical_infrastructure_did: false,
release_build_id: None,
};
(root, context)
}
fn compile_infrastructure_fixture(context: &WorkspaceBuildContext) {
compile_infrastructure_fixture_at(
context,
&crate::canister_build::cache::canister_build_target_root(&context.workspace_root),
);
}
fn compile_infrastructure_fixture_at(context: &WorkspaceBuildContext, target: &Path) {
let output = crate::cargo_command()
.args(["build", "--locked", "--offline", "--manifest-path"])
.arg(context.workspace_root.join("Cargo.toml"))
.args([
"--target",
"wasm32-unknown-unknown",
"--profile",
context.profile.target_dir_name(),
"--features",
"canic/fleet",
])
.env("CARGO_TARGET_DIR", target)
.env("RUSTC_WRAPPER", "")
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn first_release_build_admits_cargo_discovery_of_absent_package_prefixed_build_script() {
run_with_private_cargo_target(first_release_build_with_absent_cargo_input);
}
fn first_release_build_with_absent_cargo_input() {
let (root, mut context) = infrastructure_build_fixture();
context.profile = crate::canister_build::CanisterBuildProfile::Release;
let manifest = context.workspace_root.join("Cargo.toml");
let contents = fs::read_to_string(&manifest).unwrap();
fs::write(
&manifest,
contents.replace(
"edition = \"2024\"",
"edition = \"2024\"\nbuild = \"src/build.rs\"",
),
)
.unwrap();
let source = context.workspace_root.join("src/build.rs");
fs::write(
&source,
"fn main() { println!(\"cargo::rerun-if-changed=app/src/build.rs\"); }\n",
)
.unwrap();
let missing = context.workspace_root.join("app/src/build.rs");
let before = input_snapshot(&context, &[]).unwrap();
assert!(!before.files.contains_key(missing.to_str().unwrap()));
for target in [
crate::canister_build::cache::canister_build_target_root(&context.workspace_root),
crate::canister_build::cache::declaration_target_root(&context.workspace_root),
] {
let record = target.join("wasm32-unknown-unknown/release/reuse_app.d");
assert!(!record.exists());
compile_infrastructure_fixture_at(&context, &target);
assert!(
fs::read_to_string(record)
.unwrap()
.contains(missing.to_str().unwrap())
);
}
assert!(!missing.exists());
let after = input_snapshot(&context, &[]).unwrap();
assert_eq!(after.files[missing.to_str().unwrap()], "absent");
before.validate_after(&after).unwrap();
assert_ne!(before.digest(), after.digest());
compile_infrastructure_fixture(&context);
let replay = input_snapshot(&context, &[]).unwrap();
after.validate_after(&replay).unwrap();
assert_eq!(after.digest(), replay.digest());
fs::write(&source, b"fn main() {}\n").unwrap();
assert!(
matches!(after.validate_after(&input_snapshot(&context, &[]).unwrap()), Err(BuildReuseError::ChangedInput(path)) if path == source)
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn first_infrastructure_build_and_replaced_cargo_records_preserve_source_authority() {
run_with_private_cargo_target(first_infrastructure_build_fixture_preserves_source_authority);
}
fn first_infrastructure_build_fixture_preserves_source_authority() {
let (root, context) = infrastructure_build_fixture();
let control_source = root.join("upstream/canic-control-plane/src/lib.rs");
let metadata =
cargo_metadata_catalog_for_manifest(&context.workspace_root.join("Cargo.toml"), true, true)
.unwrap();
assert!(
!metadata
.packages
.iter()
.any(|package| package.name == "canic-control-plane")
);
let before = input_snapshot(&context, &[]).unwrap();
assert!(before.files.contains_key(control_source.to_str().unwrap()));
let target = crate::canister_build::cache::canister_build_target_root(&context.workspace_root);
compile_infrastructure_fixture(&context);
let after = input_snapshot(&context, &[]).unwrap();
before.validate_after(&after).unwrap();
assert_eq!(before.digest(), after.digest());
let old = root.join("previous.rs");
fs::write(&old, "pub const PREVIOUS: u8 = 1;\n").unwrap();
let record = target.join("wasm32-unknown-unknown/fast/reuse_app.d");
assert!(record.is_file());
fs::write(&record, format!("/unused.wasm: {}\n", old.display())).unwrap();
let stale = input_snapshot(&context, &[]).unwrap();
fs::remove_file(&record).unwrap();
compile_infrastructure_fixture(&context);
let refreshed = input_snapshot(&context, &[]).unwrap();
stale.validate_after(&refreshed).unwrap();
assert_ne!(stale.digest(), refreshed.digest());
assert_eq!(after.digest(), refreshed.digest());
fs::write(&control_source, "pub const CONTROL: u8 = 2;\n").unwrap();
assert!(
matches!(refreshed.validate_after(&input_snapshot(&context, &[]).unwrap()), Err(BuildReuseError::ChangedInput(path)) if path == control_source)
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn governed_inputs_invalidate_reuse_even_when_file_lengths_are_unchanged() {
run_with_private_cargo_target(governed_fixture_inputs_invalidate_reuse);
}
fn governed_fixture_inputs_invalidate_reuse() {
let root = temp_dir("build-reuse-inputs");
fs::create_dir_all(root.join("src")).unwrap();
fs::write(
root.join("Cargo.toml"),
"[workspace]\n[package]\nname = \"reuse-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
).unwrap();
fs::write(
root.join("Cargo.lock"),
"version = 4\n[[package]]\nname = \"reuse-fixture\"\nversion = \"0.1.0\"\n",
)
.unwrap();
fs::write(root.join("src/lib.rs"), "pub const VALUE: u8 = 1;\n").unwrap();
fs::write(root.join("canic.toml"), REUSE_CONFIG).unwrap();
fs::write(root.join("tool"), "tool payload 1\n").unwrap();
let context = WorkspaceBuildContext {
role: "root".into(),
profile: crate::canister_build::CanisterBuildProfile::Fast,
environment: "local".into(),
build_network: canic_core::ids::BuildNetwork::Local,
workspace_root: root.clone(),
icp_root: root.clone(),
config_path: root.join("canic.toml"),
local_replica: None,
refresh_canonical_infrastructure_did: false,
release_build_id: None,
};
let tools = [root.join("tool")];
let original_snapshot = input_snapshot(&context, &tools).unwrap();
let original = original_snapshot.digest();
let changed_config = format!("# changed configuration input\n{REUSE_CONFIG}");
for (path, replacement) in [
("src/lib.rs", "pub const VALUE: u8 = 2;\n"),
("canic.toml", changed_config.as_str()),
("tool", "tool payload 2\n"),
(
"Cargo.lock",
"# changed lock authority\nversion = 4\n[[package]]\nname = \"reuse-fixture\"\nversion = \"0.1.0\"\n",
),
] {
let path = root.join(path);
let bytes = fs::read(&path).unwrap();
fs::write(&path, replacement).unwrap();
assert_ne!(original, input_digest(&context, &tools).unwrap());
fs::write(&path, bytes).unwrap();
}
let mut changed = context.clone();
changed.build_network = canic_core::ids::BuildNetwork::Ic;
assert_ne!(original, input_digest(&changed, &tools).unwrap());
changed = context.clone();
changed.profile = crate::canister_build::CanisterBuildProfile::Release;
assert_ne!(original, input_digest(&changed, &tools).unwrap());
let restored = input_snapshot(&context, &tools).unwrap();
original_snapshot
.validate_after(&restored)
.expect("restored fixture inputs retain their original authority");
assert_eq!(original, restored.digest());
let external = temp_dir("build-reuse-external");
fs::create_dir_all(&external).unwrap();
let included = external.join("shared.rs");
fs::write(&included, "pub const SHARED: u8 = 1;\n").unwrap();
let build_script = external.join("build.rs");
fs::write(&build_script, "fn main() {}\n").unwrap();
fs::OpenOptions::new()
.append(true)
.open(root.join("Cargo.toml"))
.unwrap()
.write_all(format!("build = {build_script:?}\n").as_bytes())
.unwrap();
let declared = input_digest(&context, &tools).unwrap();
fs::write(
&build_script,
"fn main() { println!(\"cargo:rustc-cfg=changed\"); }\n",
)
.unwrap();
assert_ne!(declared, input_digest(&context, &tools).unwrap());
fs::OpenOptions::new().append(true).open(root.join("Cargo.toml")).unwrap()
.write_all(b"\n[[example]]\nname = \"omitted-from-published-package\"\npath = \"examples/omitted.rs\"\n").unwrap();
input_digest(&context, &tools).unwrap();
let target = crate::canister_build::cache::canister_build_target_root(&root)
.join("wasm32-unknown-unknown/fast");
fs::create_dir_all(&target).unwrap();
fs::write(
target.join("reuse_fixture.d"),
format!("/target/reuse_fixture.wasm: {}\n", included.display()),
)
.unwrap();
let observed = input_digest(&context, &tools).unwrap();
assert_ne!(original, observed);
fs::write(&included, "pub const SHARED: u8 = 2;\n").unwrap();
assert_ne!(observed, input_digest(&context, &tools).unwrap());
fs::remove_dir_all(external).unwrap();
fs::remove_dir_all(root).unwrap();
}
#[test]
fn source_bytes_additions_deletions_and_renames_change_the_input_set() {
let root = temp_dir("build-reuse-source");
fs::create_dir_all(root.join("src")).unwrap();
let source = root.join("src/lib.rs");
fs::write(&source, b"one").unwrap();
let collect = || {
let mut files = BTreeMap::new();
collect_files(&root, &root, &mut files, true).unwrap();
files
};
let original = collect();
fs::write(&source, b"two").unwrap();
assert_ne!(original, collect());
fs::write(&source, b"one").unwrap();
assert_eq!(original, collect());
fs::rename(&source, root.join("src/other.rs")).unwrap();
assert_ne!(original, collect());
fs::rename(root.join("src/other.rs"), &source).unwrap();
fs::write(root.join("src/include.txt"), b"new input").unwrap();
assert_ne!(original, collect());
fs::remove_file(root.join("src/include.txt")).unwrap();
fs::create_dir_all(root.join("target")).unwrap();
fs::write(root.join("target/build-output"), b"generated").unwrap();
assert_eq!(original, collect());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn output_snapshot_detects_corruption_missing_and_extra_files() {
let root = temp_dir("build-reuse-output");
fs::create_dir_all(&root).unwrap();
let wasm = root.join("role.wasm");
fs::write(&wasm, b"original").unwrap();
let original = output_files(&root).unwrap();
fs::write(&wasm, b"corrupt!").unwrap();
assert_ne!(original, output_files(&root).unwrap());
fs::write(&wasm, b"original").unwrap();
assert_eq!(original, output_files(&root).unwrap());
fs::write(root.join("extra.did"), b"extra").unwrap();
assert_ne!(original, output_files(&root).unwrap());
fs::remove_file(&wasm).unwrap();
assert_ne!(original, output_files(&root).unwrap());
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn cache_never_follows_a_replaced_output_symlink() {
let root = temp_dir("build-reuse-symlink");
fs::create_dir_all(&root).unwrap();
let target = root.join("target.wasm");
fs::write(&target, b"wasm").unwrap();
std::os::unix::fs::symlink(&target, root.join("role.wasm")).unwrap();
assert!(matches!(
output_files(&root),
Err(BuildReuseError::Unsupported(_))
));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn executable_launchers_do_not_claim_their_delegated_tool_bytes() {
let root = temp_dir("build-reuse-tool");
fs::create_dir_all(&root).unwrap();
let tool = root.join("tool");
fs::write(&tool, b"#!/bin/sh\nexec unknown-tool \"$@\"\n").unwrap();
assert!(matches!(
require_native_tool(&tool),
Err(BuildReuseError::UnboundTool(_))
));
require_native_tool(&env::current_exe().unwrap()).unwrap();
fs::remove_dir_all(root).unwrap();
}
#[test]
fn declared_fixture_payloads_participate_in_cache_inputs_inside_excluded_directories() {
let root = temp_dir("reuse-declared-fixture");
fs::create_dir_all(root.join(".canic/source")).unwrap();
fs::write(root.join("canic.toml"), REUSE_CONFIG).unwrap();
fs::write(root.join("Cargo.toml"), "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[package.metadata.canic]\nfixture = \".canic/source/fixture.json\"\n").unwrap();
fs::write(root.join(".canic/source/fixture.json"), serde_json::to_vec(&serde_json::json!({
"format_hash": "01".repeat(32), "completion_summary": "02".repeat(32), "chunk_paths": ["rows.bin"]
})).unwrap()).unwrap();
let rows = root.join(".canic/source/rows.bin");
fs::write(&rows, [3; 64]).unwrap();
let context = WorkspaceBuildContext {
role: "root".into(),
profile: crate::canister_build::CanisterBuildProfile::Fast,
environment: "local".into(),
build_network: canic_core::ids::BuildNetwork::Local,
workspace_root: root.clone(),
icp_root: root.clone(),
config_path: root.join("canic.toml"),
local_replica: None,
refresh_canonical_infrastructure_did: false,
release_build_id: None,
};
let mut files = BTreeMap::new();
collect_files(&root, &root, &mut files, true).unwrap();
assert!(!files.contains_key(rows.to_str().unwrap()));
append_fixture_inputs(&context, &mut files).unwrap();
assert!(files.contains_key(rows.to_str().unwrap()));
let before = BuildInputSnapshot {
identity: "fixture".into(),
files,
};
fs::write(&rows, [4; 64]).unwrap();
let mut files = BTreeMap::new();
collect_files(&root, &root, &mut files, true).unwrap();
append_fixture_inputs(&context, &mut files).unwrap();
let after = BuildInputSnapshot {
identity: "fixture".into(),
files,
};
assert_ne!(before.digest(), after.digest());
assert!(
matches!(before.validate_after(&after), Err(BuildReuseError::ChangedInput(path)) if path == rows)
);
fs::remove_dir_all(root).unwrap();
}