use super::*;
use crate::test_support::temp_dir;
use std::{collections::BTreeMap, env, fs};
#[test]
fn isolated_invocations_report_a_synthetic_environment_change_and_repeat_exactly() {
const CHILD_ROOT: &str = "CANIC_TEST_DIAGNOSTIC_CHILD_ROOT";
const INPUT_KEY: &str = "CANIC_TEST_SYNTHETIC_BUILD_INPUT";
if let Some(root) = env::var_os(CHILD_ROOT) {
let root = PathBuf::from(root);
InputDiagnostics::prepare(&root);
let values = crate::build_environment::inputs();
let value_comparison = environment::EnvironmentComparison::capture(&root, &values);
let (environment, environment_keys) = environment_evidence(values);
InputDiagnostics {
schema_version: 1,
environment,
environment_keys,
value_comparison,
source: "fixed-source".into(),
configuration: "fixed-configuration".into(),
}
.retain(&root);
return;
}
let root = temp_dir("reuse-isolated-environment");
let thread = std::thread::current();
let invoke = |value: Option<&str>| {
let mut command = std::process::Command::new(env::current_exe().unwrap());
command
.args(["--exact", thread.name().unwrap()])
.env(CHILD_ROOT, &root)
.env_remove(INPUT_KEY);
if let Some(value) = value {
command.env(INPUT_KEY, value);
}
let output = command.output().unwrap();
assert!(output.status.success());
let bytes = fs::read(root.join("last-input-diagnostics.json")).unwrap();
assert!(!String::from_utf8_lossy(&bytes).contains("synthetic-build-setting"));
serde_json::from_slice::<InputDiagnostics>(&bytes).unwrap()
};
let baseline = invoke(None);
let candidate = invoke(Some("synthetic-build-setting"));
let reason = candidate.compare(&baseline);
assert!(reason.contains(INPUT_KEY));
assert!(reason.contains("environment changed"));
assert!(!reason.contains("synthetic-build-setting"));
let repeat = invoke(Some("synthetic-build-setting"));
assert_eq!(candidate.environment, repeat.environment);
assert_eq!(candidate.environment_keys, repeat.environment_keys);
let changed_value = invoke(Some("changed-synthetic-build-setting"));
assert_eq!(
changed_value.changed_environment_keys(&candidate),
Ok(vec![INPUT_KEY.into()])
);
fs::remove_dir_all(root).unwrap();
}
fn evidence(environment: &[(&str, &str)]) -> InputDiagnostics {
let (environment, environment_keys) = environment_evidence(
environment
.iter()
.map(|(key, value)| (OsString::from(key), OsString::from(value)))
.collect(),
);
InputDiagnostics {
schema_version: 1,
environment,
environment_keys,
value_comparison: Err(environment::CaptureFailure::LocalKeyUnavailable),
source: "source".into(),
configuration: "configuration".into(),
}
}
#[test]
fn retained_capture_failures_identify_the_unavailable_build() {
let root = temp_dir("reuse-diagnostics-capture-failure");
InputDiagnostics::prepare(&root);
let mut current = evidence(&[("SAFE_KEY", "new")]);
current.value_comparison =
environment::EnvironmentComparison::capture(&root, &[("SAFE_KEY".into(), "new".into())]);
for cause in [
environment::CaptureFailure::LocalKeyUnavailable,
environment::CaptureFailure::InputLimitExceeded,
] {
let mut previous = evidence(&[("SAFE_KEY", "old")]);
previous.value_comparison = Err(cause);
previous.retain(&root);
let retained: InputDiagnostics =
serde_json::from_slice(&fs::read(root.join("last-input-diagnostics.json")).unwrap())
.unwrap();
assert_eq!(
current.changed_environment_keys(&retained),
Err(environment::ComparisonFailure::PreviousCapture(cause))
);
assert_eq!(
retained.changed_environment_keys(¤t),
Err(environment::ComparisonFailure::CurrentCapture(cause))
);
}
fs::remove_dir_all(root).unwrap();
}
#[test]
fn environment_comparison_names_added_keys_without_storing_values() {
let root = temp_dir("reuse-diagnostics-environment");
let baseline = evidence(&[("PATH", "synthetic-path")]);
baseline.retain(&root);
let changed = evidence(&[
("PATH", "synthetic-path"),
("CANIC_TEST_BUILD_INPUT", "synthetic-value"),
]);
let reason = changed.explain_miss(&root);
assert!(reason.contains("environment changed"));
assert!(reason.contains("CANIC_TEST_BUILD_INPUT"));
assert!(!reason.contains("synthetic-value"));
let bytes = serde_json::to_string(&changed).unwrap();
assert!(!bytes.contains("synthetic-value"));
assert!(!bytes.contains("synthetic-path"));
let value_changed = evidence(&[("PATH", "another-path")]);
assert_eq!(
value_changed.changed_environment_keys(&baseline),
Err(environment::ComparisonFailure::CurrentCapture(
environment::CaptureFailure::LocalKeyUnavailable
))
);
assert_ne!(value_changed.environment, baseline.environment);
assert_eq!(
evidence(&[("PATH", "synthetic-path")]).environment,
baseline.environment
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn missing_corrupt_and_unsafe_diagnostics_remain_bounded_and_optional() {
let root = temp_dir("reuse-diagnostics-invalid");
let current = evidence(&[("SAFE_KEY", "value")]);
assert_eq!(
current.explain_miss(&root),
"no comparable prior input evidence"
);
write_bytes(&root.join("last-input-diagnostics.json"), b"invalid").unwrap();
assert_eq!(
current.explain_miss(&root),
"no comparable prior input evidence"
);
let mut previous = evidence(&[]);
previous.environment_keys.insert("BAD\nKEY".into());
previous.environment_keys.insert("x".repeat(1000));
previous
.environment_keys
.extend((0..100).map(|i| format!("KEY_{i}")));
let reason = current.compare(&previous);
assert!(!reason.contains('\n'));
assert!(reason.len() < 1000);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn source_and_configuration_changes_are_separately_attributed() {
let root = PathBuf::from("/synthetic-reuse");
let context = WorkspaceBuildContext {
role: "root".into(),
profile: crate::canister_build::CanisterBuildProfile::Fast,
environment: "local".into(),
build_network: canic_contracts::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 inputs = BuildInputSnapshot {
identity: "unchanged-authority".into(),
files: BTreeMap::from([
("/synthetic-reuse/src/lib.rs".into(), "old-source".into()),
("/synthetic-reuse/canic.toml".into(), "old-config".into()),
]),
};
let previous = InputDiagnostics::capture(&context, &[], &inputs);
inputs
.files
.insert("/synthetic-reuse/src/lib.rs".into(), "new-source".into());
let source = InputDiagnostics::capture(&context, &[], &inputs);
assert!(
source
.compare(&previous)
.contains("source/dependency inputs changed")
);
assert!(
!source
.compare(&previous)
.contains("toolchain/configuration")
);
inputs
.files
.insert("/synthetic-reuse/canic.toml".into(), "new-config".into());
let configuration = InputDiagnostics::capture(&context, &[], &inputs);
assert!(
configuration
.compare(&source)
.contains("toolchain/configuration inputs changed")
);
assert!(!configuration.compare(&source).contains("source/dependency"));
}