service_build_qualification.rs (26147B)
1 use std::{fmt, fs, io::Read as _, path::Path}; 2 3 use serde::Deserialize; 4 use sha2::{Digest as _, Sha256}; 5 6 use crate::service_source_lock::{ 7 LOCK_FILENAME, NixMaterialState, PREDECESSOR_LOCK_FILENAME, ServiceSourceLockV2, 8 }; 9 10 const CONTRACT_RELATIVE: &str = 11 "contracts/architecture/decisions/services_hardening_build_qualification.v3.json"; 12 const FIXTURE_RELATIVE: &str = "tools/xtask/fixtures/service-build-qualification"; 13 const MAX_CONTRACT_BYTES: usize = 32_768; 14 const MAX_FIXTURE_FILE_BYTES: usize = 1_048_576; 15 const SOURCE_LOCK_V3_SHA256: &str = 16 "3bc32c8ca2cecb06c8f8239ab1fe1fcfba93fe3ef0d60e9b078390347d08f817"; 17 const ARTIFACT_CONTRACT_V3_SHA256: &str = 18 "11d8fa7bc96191919a3d20189061aadcee105eefe546290032c2f2bab91b2f77"; 19 const NIX_SYSTEMS_RELATIVE: &str = "build/nix/service/systems.nix"; 20 const NIX_SYSTEMS_BYTES: &[u8] = b"[\n \"aarch64-darwin\"\n \"x86_64-linux\"\n]\n"; 21 22 const SUPPORTED_RUST_TARGETS: [&str; 2] = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]; 23 const SUPPORTED_NIX_SYSTEMS: [&str; 2] = ["aarch64-darwin", "x86_64-linux"]; 24 const EXCLUDED_NIX_SYSTEMS: [&str; 2] = ["x86_64-darwin", "aarch64-linux"]; 25 const REQUIRED_XTASK_COMMANDS: [&str; 5] = [ 26 "cargo test --locked -p xtask service_source_lock::tests", 27 "cargo test --locked -p xtask service_release_artifacts::tests", 28 "cargo test --locked -p xtask service_build_qualification::tests", 29 "cargo run --locked -q -p xtask -- contract validate", 30 "cargo run --locked -q -p xtask -- release preflight", 31 ]; 32 const REQUIRED_NATIVE_COMMANDS: [&str; 6] = [ 33 "cargo build --locked --release", 34 "cargo fmt --all --check", 35 "cargo check --workspace --all-targets --locked", 36 "cargo test --workspace --all-targets --locked", 37 "cargo clippy --workspace --all-targets --locked -- -D warnings", 38 "RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps --locked", 39 ]; 40 const REQUIRED_EVIDENCE: [&str; 11] = [ 41 "cargo_lock", 42 "source_lock", 43 "package_metadata", 44 "release_metadata", 45 "binary_archive", 46 "oci_source_artifact", 47 "cyclonedx_sbom", 48 "notices", 49 "artifact_manifest", 50 "unsigned_provenance_input", 51 "checksums", 52 ]; 53 const DEFERRED_OUTPUTS: [&str; 6] = [ 54 "nix_packages", 55 "nix_apps", 56 "nix_checks", 57 "nix_development_shells", 58 "nixos_modules", 59 "nix_produced_oci", 60 ]; 61 62 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 63 enum BuildQualificationError { 64 InvalidContract, 65 InvalidFixture, 66 } 67 68 impl fmt::Display for BuildQualificationError { 69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 70 formatter.write_str(match self { 71 Self::InvalidContract => "service build qualification contract is invalid", 72 Self::InvalidFixture => "service build qualification fixture is invalid", 73 }) 74 } 75 } 76 77 impl std::error::Error for BuildQualificationError {} 78 79 #[derive(Debug, Deserialize)] 80 #[serde(deny_unknown_fields)] 81 struct BuildQualificationDecision { 82 schema: String, 83 contract_version: u32, 84 decision_state: String, 85 predecessor: PredecessorDecision, 86 qualification_scope: String, 87 fixture_root: String, 88 supported_rust_targets: Vec<String>, 89 supported_nix_systems: Vec<String>, 90 excluded_nix_systems: Vec<String>, 91 required_native_commands: Vec<String>, 92 required_xtask_commands: Vec<String>, 93 required_evidence: Vec<String>, 94 fixture_source_lock: String, 95 fixture_contract: String, 96 release_artifact_command: String, 97 source_lock_command: String, 98 source_lock_v3_definition: String, 99 artifact_contract_v3: String, 100 nix_output_implementation_owner_step: u32, 101 signing_authority: String, 102 deferred_outputs: Vec<String>, 103 } 104 105 #[derive(Debug, Deserialize)] 106 #[serde(deny_unknown_fields)] 107 struct PredecessorDecision { 108 schema: String, 109 filename: String, 110 transition: String, 111 } 112 113 pub(crate) fn validate_contract(workspace_root: &Path) -> Result<(), String> { 114 validate_contract_inner(workspace_root).map_err(|error| error.to_string()) 115 } 116 117 fn validate_contract_inner(workspace_root: &Path) -> Result<(), BuildQualificationError> { 118 let bytes = read_bounded( 119 &workspace_root.join(CONTRACT_RELATIVE), 120 MAX_CONTRACT_BYTES, 121 BuildQualificationError::InvalidContract, 122 )?; 123 let decision = serde_json::from_slice::<BuildQualificationDecision>(&bytes) 124 .map_err(|_| BuildQualificationError::InvalidContract)?; 125 validate_decision(&decision)?; 126 validate_bound_contract( 127 workspace_root, 128 &decision.source_lock_v3_definition, 129 SOURCE_LOCK_V3_SHA256, 130 )?; 131 validate_bound_contract( 132 workspace_root, 133 &decision.artifact_contract_v3, 134 ARTIFACT_CONTRACT_V3_SHA256, 135 )?; 136 if read_bounded( 137 &workspace_root.join(NIX_SYSTEMS_RELATIVE), 138 1_024, 139 BuildQualificationError::InvalidContract, 140 )? != NIX_SYSTEMS_BYTES 141 { 142 return Err(BuildQualificationError::InvalidContract); 143 } 144 validate_fixture(workspace_root) 145 } 146 147 fn validate_bound_contract( 148 workspace_root: &Path, 149 relative: &str, 150 expected_sha256: &str, 151 ) -> Result<(), BuildQualificationError> { 152 let bytes = read_bounded( 153 &workspace_root.join(relative), 154 MAX_CONTRACT_BYTES, 155 BuildQualificationError::InvalidContract, 156 )?; 157 let _: serde_json::Value = 158 serde_json::from_slice(&bytes).map_err(|_| BuildQualificationError::InvalidContract)?; 159 if digest(&bytes) == expected_sha256 { 160 Ok(()) 161 } else { 162 Err(BuildQualificationError::InvalidContract) 163 } 164 } 165 166 fn validate_decision(decision: &BuildQualificationDecision) -> Result<(), BuildQualificationError> { 167 let exact = decision.schema == "radroots.services-hardening.build-qualification-decisions.v3" 168 && decision.contract_version == 3 169 && decision.decision_state == "active" 170 && decision.predecessor.schema 171 == "radroots.services-hardening.build-qualification-decisions.v2" 172 && decision.predecessor.filename == "services_hardening_build_qualification.v2.json" 173 && decision.predecessor.transition == "forward_only_replace" 174 && decision.qualification_scope == "native_and_nix_release_foundation" 175 && decision.fixture_root == FIXTURE_RELATIVE 176 && decision.supported_rust_targets == SUPPORTED_RUST_TARGETS 177 && decision.supported_nix_systems == SUPPORTED_NIX_SYSTEMS 178 && decision.excluded_nix_systems == EXCLUDED_NIX_SYSTEMS 179 && decision.required_native_commands == REQUIRED_NATIVE_COMMANDS 180 && decision.required_xtask_commands == REQUIRED_XTASK_COMMANDS 181 && decision.required_evidence == REQUIRED_EVIDENCE 182 && decision.fixture_source_lock 183 == "tools/xtask/fixtures/service-build-qualification/radroots.service.source-lock.v2.toml" 184 && decision.fixture_contract 185 == "historical_v2_fixture_until_v3_service_instances_are_owned" 186 && decision.release_artifact_command == "cargo xtask service-release-artifacts" 187 && decision.source_lock_command == "cargo xtask service-source-lock" 188 && decision.source_lock_v3_definition 189 == "contracts/architecture/decisions/services_hardening_source_lock.v3.json" 190 && decision.artifact_contract_v3 == "contracts/release/lib-artifact-contract.v3.json" 191 && decision.nix_output_implementation_owner_step == 299 192 && decision.signing_authority == "external_only" 193 && decision.deferred_outputs == DEFERRED_OUTPUTS; 194 if exact { 195 Ok(()) 196 } else { 197 Err(BuildQualificationError::InvalidContract) 198 } 199 } 200 201 fn validate_fixture(workspace_root: &Path) -> Result<(), BuildQualificationError> { 202 let fixture = workspace_root.join(FIXTURE_RELATIVE); 203 let lock_bytes = read_bounded( 204 &fixture.join(LOCK_FILENAME), 205 4_096, 206 BuildQualificationError::InvalidFixture, 207 )?; 208 let lock = ServiceSourceLockV2::from_canonical_bytes(&lock_bytes) 209 .map_err(|_| BuildQualificationError::InvalidFixture)?; 210 let cargo_lock = read_bounded( 211 &fixture.join("Cargo.lock"), 212 MAX_FIXTURE_FILE_BYTES, 213 BuildQualificationError::InvalidFixture, 214 )?; 215 let manifest = read_bounded( 216 &fixture.join("Cargo.toml"), 217 MAX_FIXTURE_FILE_BYTES, 218 BuildQualificationError::InvalidFixture, 219 )?; 220 let manifest = 221 std::str::from_utf8(&manifest).map_err(|_| BuildQualificationError::InvalidFixture)?; 222 let manifest = toml::from_str::<toml::Value>(manifest) 223 .map_err(|_| BuildQualificationError::InvalidFixture)?; 224 let source_metadata = manifest 225 .get("workspace") 226 .and_then(|value| value.get("metadata")) 227 .and_then(|value| value.get("radroots")) 228 .and_then(|value| value.get("service_source_lock")) 229 .and_then(toml::Value::as_table) 230 .ok_or(BuildQualificationError::InvalidFixture)?; 231 let release_metadata = manifest 232 .get("workspace") 233 .and_then(|value| value.get("metadata")) 234 .and_then(|value| value.get("radroots")) 235 .and_then(|value| value.get("service_release")) 236 .and_then(toml::Value::as_table) 237 .ok_or(BuildQualificationError::InvalidFixture)?; 238 let versions = lock.contract_versions(); 239 let exact = lock.service() == "fixture_service" 240 && lock.revision() == "2222222222222222222222222222222222222222" 241 && lock.cargo_lock_sha256() == digest(&cargo_lock) 242 && lock.nix_material_state() == NixMaterialState::Absent 243 && lock.nix_lib_revision().is_none() 244 && lock.flake_lock_sha256().is_none() 245 && ["flake.nix", "flake.lock", PREDECESSOR_LOCK_FILENAME] 246 .into_iter() 247 .all(|name| { 248 fs::symlink_metadata(fixture.join(name)) 249 .is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound) 250 }) 251 && versions.config() == 1 252 && versions.state() == 2 253 && versions.admin() == 3 254 && versions.status() == 4 255 && versions.provider() == 5 256 && manifest 257 .get("package") 258 .and_then(|value| value.get("version")) 259 .and_then(toml::Value::as_str) 260 == Some("0.1.0-alpha") 261 && source_metadata.len() == 8 262 && source_metadata.get("service").and_then(toml::Value::as_str) == Some("fixture_service") 263 && source_metadata 264 .get("host_feature_profile") 265 .and_then(toml::Value::as_str) 266 == Some("service-host") 267 && source_metadata 268 .get("nix_material") 269 .and_then(toml::Value::as_str) 270 == Some("absent") 271 && source_metadata 272 .get("config_contract_version") 273 .and_then(toml::Value::as_integer) 274 == Some(i64::from(versions.config())) 275 && source_metadata 276 .get("state_contract_version") 277 .and_then(toml::Value::as_integer) 278 == Some(i64::from(versions.state())) 279 && source_metadata 280 .get("admin_contract_version") 281 .and_then(toml::Value::as_integer) 282 == Some(i64::from(versions.admin())) 283 && source_metadata 284 .get("status_contract_version") 285 .and_then(toml::Value::as_integer) 286 == Some(i64::from(versions.status())) 287 && source_metadata 288 .get("provider_contract_version") 289 .and_then(toml::Value::as_integer) 290 == Some(i64::from(versions.provider())) 291 && release_metadata.len() == 4 292 && release_metadata 293 .get("service") 294 .and_then(toml::Value::as_str) 295 == Some("fixture_service") 296 && release_metadata 297 .get("service_package") 298 .and_then(toml::Value::as_str) 299 == Some("fixture-service") 300 && release_metadata 301 .get("binary_name") 302 .and_then(toml::Value::as_str) 303 == Some("fixture-service") 304 && release_metadata 305 .get("version") 306 .and_then(toml::Value::as_str) 307 == Some("0.1.0-alpha"); 308 if exact { 309 Ok(()) 310 } else { 311 Err(BuildQualificationError::InvalidFixture) 312 } 313 } 314 315 fn read_bounded( 316 path: &Path, 317 maximum: usize, 318 error: BuildQualificationError, 319 ) -> Result<Vec<u8>, BuildQualificationError> { 320 let metadata = fs::symlink_metadata(path).map_err(|_| error)?; 321 if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.len() > maximum as u64 { 322 return Err(error); 323 } 324 let mut bytes = Vec::with_capacity(metadata.len() as usize); 325 fs::File::open(path) 326 .map_err(|_| error)? 327 .take(maximum as u64 + 1) 328 .read_to_end(&mut bytes) 329 .map_err(|_| error)?; 330 if bytes.len() > maximum { 331 Err(error) 332 } else { 333 Ok(bytes) 334 } 335 } 336 337 fn digest(bytes: &[u8]) -> String { 338 hex::encode(Sha256::digest(bytes)) 339 } 340 341 #[cfg(test)] 342 mod tests { 343 use std::error::Error as _; 344 345 use tempfile::TempDir; 346 347 use super::*; 348 349 #[test] 350 fn checked_in_contract_and_fixture_are_exact() { 351 let root = workspace_root(); 352 validate_contract_inner(root).expect("build qualification"); 353 } 354 355 #[test] 356 fn contract_rejects_every_independent_governed_field_drift() { 357 let bytes = fs::read(workspace_root().join(CONTRACT_RELATIVE)).expect("decision"); 358 let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("decision json"); 359 for (pointer, replacement) in [ 360 ("/schema", serde_json::json!("other")), 361 ("/contract_version", serde_json::json!(1)), 362 ("/decision_state", serde_json::json!("draft")), 363 ("/predecessor/schema", serde_json::json!("other")), 364 ("/predecessor/filename", serde_json::json!("other")), 365 ("/predecessor/transition", serde_json::json!("other")), 366 ("/qualification_scope", serde_json::json!("other")), 367 ("/fixture_root", serde_json::json!("other")), 368 ("/supported_rust_targets", serde_json::json!([])), 369 ("/supported_nix_systems", serde_json::json!([])), 370 ("/excluded_nix_systems", serde_json::json!([])), 371 ("/required_native_commands", serde_json::json!([])), 372 ("/required_xtask_commands", serde_json::json!([])), 373 ("/required_evidence", serde_json::json!([])), 374 ("/fixture_source_lock", serde_json::json!("other")), 375 ("/fixture_contract", serde_json::json!("other")), 376 ("/release_artifact_command", serde_json::json!("other")), 377 ("/source_lock_command", serde_json::json!("other")), 378 ("/source_lock_v3_definition", serde_json::json!("other")), 379 ("/artifact_contract_v3", serde_json::json!("other")), 380 ( 381 "/nix_output_implementation_owner_step", 382 serde_json::json!(1), 383 ), 384 ("/signing_authority", serde_json::json!("internal")), 385 ("/deferred_outputs", serde_json::json!([])), 386 ] { 387 let mut drifted = canonical.clone(); 388 *drifted.pointer_mut(pointer).expect("governed field") = replacement; 389 let decision = serde_json::from_value::<BuildQualificationDecision>(drifted) 390 .expect("structurally valid drift"); 391 assert_eq!( 392 validate_decision(&decision), 393 Err(BuildQualificationError::InvalidContract), 394 "accepted drift at {pointer}" 395 ); 396 } 397 } 398 399 #[test] 400 fn fixture_rejects_every_identity_and_lockfile_drift() { 401 for (name, from, to) in [ 402 ( 403 "Cargo.toml", 404 "version = \"0.1.0-alpha\"", 405 "version = \"0.1.1\"", 406 ), 407 ( 408 "Cargo.toml", 409 "config_contract_version = 1", 410 "config_contract_version = 9", 411 ), 412 ( 413 "Cargo.toml", 414 "service_package = \"fixture-service\"", 415 "service_package = \"other-service\"", 416 ), 417 ( 418 "radroots.service.source-lock.v2.toml", 419 "revision = \"2222222222222222222222222222222222222222\"", 420 "revision = \"3333333333333333333333333333333333333333\"", 421 ), 422 ("Cargo.lock", "version = 4", "version = 3"), 423 ] { 424 let root = copied_fixture(); 425 let path = root.path().join(FIXTURE_RELATIVE).join(name); 426 let current = fs::read_to_string(&path).expect("fixture text"); 427 assert!(current.contains(from), "missing mutation anchor {from}"); 428 fs::write(&path, current.replacen(from, to, 1)).expect("mutated fixture"); 429 assert_eq!( 430 validate_fixture(root.path()), 431 Err(BuildQualificationError::InvalidFixture) 432 ); 433 } 434 435 let root = copied_fixture(); 436 let manifest = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml"); 437 let mut current = fs::read_to_string(&manifest).expect("fixture manifest"); 438 current.push_str("\n[workspace.metadata.radroots.service_release.extra]\nvalue = 1\n"); 439 fs::write(manifest, current).expect("extra fixture metadata"); 440 assert_eq!( 441 validate_fixture(root.path()), 442 Err(BuildQualificationError::InvalidFixture) 443 ); 444 445 for name in ["flake.nix", "flake.lock", PREDECESSOR_LOCK_FILENAME] { 446 let root = copied_fixture(); 447 fs::write(root.path().join(FIXTURE_RELATIVE).join(name), b"unexpected") 448 .expect("unexpected Nix material"); 449 assert_eq!( 450 validate_fixture(root.path()), 451 Err(BuildQualificationError::InvalidFixture), 452 "accepted absent-state fixture with {name}" 453 ); 454 } 455 } 456 457 #[test] 458 fn canonical_fixture_locks_bind_service_and_each_positive_contract_version() { 459 for (from, to) in [ 460 ( 461 "service = \"fixture_service\"", 462 "service = \"other_service\"", 463 ), 464 ("config = 1", "config = 9"), 465 ("state = 2", "state = 9"), 466 ("admin = 3", "admin = 9"), 467 ("status = 4", "status = 9"), 468 ("provider = 5", "provider = 9"), 469 ( 470 "material = \"absent\"", 471 "material = \"deferred\"\nlib_revision = \"1111111111111111111111111111111111111111\"\nflake_lock_sha256 = \"1111111111111111111111111111111111111111111111111111111111111111\"", 472 ), 473 ] { 474 let root = copied_fixture(); 475 validate_fixture(root.path()).unwrap(); 476 let path = root.path().join(FIXTURE_RELATIVE).join(LOCK_FILENAME); 477 let original = fs::read_to_string(&path).unwrap(); 478 assert!(original.contains(from)); 479 let changed = original.replacen(from, to, 1); 480 ServiceSourceLockV2::from_canonical_bytes(changed.as_bytes()) 481 .expect("valid canonical lock with substituted identity"); 482 fs::write(&path, changed).unwrap(); 483 assert_eq!( 484 validate_fixture(root.path()), 485 Err(BuildQualificationError::InvalidFixture), 486 "{from}" 487 ); 488 } 489 let root = copied_fixture(); 490 let path = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml"); 491 let original = fs::read_to_string(&path).unwrap(); 492 let changed = original.replace("nix_material = \"absent\"", "nix_material = \"deferred\""); 493 assert_ne!(changed, original); 494 fs::write(path, changed).unwrap(); 495 assert_eq!( 496 validate_fixture(root.path()), 497 Err(BuildQualificationError::InvalidFixture) 498 ); 499 } 500 501 #[test] 502 fn bound_contracts_reject_digest_substitution_and_unsafe_file_inputs() { 503 let root = TempDir::new().unwrap(); 504 let path = root.path().join("input"); 505 fs::write(&path, b"{}").unwrap(); 506 validate_bound_contract(root.path(), "input", &digest(b"{}")).unwrap(); 507 assert!(validate_bound_contract(root.path(), "input", &"1".repeat(64)).is_err()); 508 assert!(read_bounded(&path, 1, BuildQualificationError::InvalidContract).is_err()); 509 assert!(read_bounded(root.path(), 10, BuildQualificationError::InvalidContract).is_err()); 510 #[cfg(unix)] 511 { 512 let link = root.path().join("link"); 513 std::os::unix::fs::symlink(&path, &link).unwrap(); 514 assert!(read_bounded(&link, 10, BuildQualificationError::InvalidContract).is_err()); 515 } 516 } 517 518 #[test] 519 fn fixture_rejects_every_independent_metadata_drift() { 520 for (section, field, replacement) in [ 521 ( 522 "service_source_lock", 523 "service", 524 toml::Value::String("other".into()), 525 ), 526 ( 527 "service_source_lock", 528 "host_feature_profile", 529 toml::Value::String("other".into()), 530 ), 531 ( 532 "service_source_lock", 533 "config_contract_version", 534 toml::Value::Integer(9), 535 ), 536 ( 537 "service_source_lock", 538 "state_contract_version", 539 toml::Value::Integer(9), 540 ), 541 ( 542 "service_source_lock", 543 "admin_contract_version", 544 toml::Value::Integer(9), 545 ), 546 ( 547 "service_source_lock", 548 "status_contract_version", 549 toml::Value::Integer(9), 550 ), 551 ( 552 "service_source_lock", 553 "provider_contract_version", 554 toml::Value::Integer(9), 555 ), 556 ( 557 "service_release", 558 "service", 559 toml::Value::String("other".into()), 560 ), 561 ( 562 "service_release", 563 "service_package", 564 toml::Value::String("other".into()), 565 ), 566 ( 567 "service_release", 568 "binary_name", 569 toml::Value::String("other".into()), 570 ), 571 ( 572 "service_release", 573 "version", 574 toml::Value::String("0.2.0".into()), 575 ), 576 ] { 577 let root = copied_fixture(); 578 let path = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml"); 579 let current = fs::read_to_string(&path).expect("fixture manifest"); 580 let mut manifest = toml::from_str::<toml::Value>(¤t).expect("fixture toml"); 581 manifest["workspace"]["metadata"]["radroots"][section][field] = replacement; 582 fs::write(&path, toml::to_string(&manifest).expect("render fixture")) 583 .expect("mutated fixture"); 584 assert_eq!( 585 validate_fixture(root.path()), 586 Err(BuildQualificationError::InvalidFixture), 587 "accepted {section}.{field} drift" 588 ); 589 } 590 591 for section in ["service_source_lock", "service_release"] { 592 let root = copied_fixture(); 593 let path = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml"); 594 let current = fs::read_to_string(&path).expect("fixture manifest"); 595 let mut manifest = toml::from_str::<toml::Value>(¤t).expect("fixture toml"); 596 manifest["workspace"]["metadata"]["radroots"][section] 597 .as_table_mut() 598 .expect("metadata section") 599 .insert("extra".into(), toml::Value::Integer(1)); 600 fs::write(&path, toml::to_string(&manifest).expect("render fixture")) 601 .expect("mutated fixture"); 602 assert_eq!( 603 validate_fixture(root.path()), 604 Err(BuildQualificationError::InvalidFixture), 605 "accepted {section} field-count drift" 606 ); 607 } 608 609 let root = copied_fixture(); 610 let path = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml"); 611 let current = fs::read_to_string(&path).expect("fixture manifest"); 612 let mut manifest = toml::from_str::<toml::Value>(¤t).expect("fixture toml"); 613 manifest["package"]["version"] = toml::Value::String("0.2.0".into()); 614 fs::write(&path, toml::to_string(&manifest).expect("render fixture")) 615 .expect("mutated fixture"); 616 assert_eq!( 617 validate_fixture(root.path()), 618 Err(BuildQualificationError::InvalidFixture) 619 ); 620 } 621 622 #[test] 623 fn contract_inventory_is_literal_and_complete() { 624 assert_eq!(SUPPORTED_RUST_TARGETS.len(), 2); 625 assert_eq!(SUPPORTED_NIX_SYSTEMS.len(), 2); 626 assert_eq!(EXCLUDED_NIX_SYSTEMS.len(), 2); 627 assert_eq!(REQUIRED_NATIVE_COMMANDS.len(), 6); 628 assert_eq!(REQUIRED_XTASK_COMMANDS.len(), 5); 629 assert_eq!(REQUIRED_EVIDENCE.len(), 11); 630 assert_eq!(DEFERRED_OUTPUTS.len(), 6); 631 } 632 633 #[test] 634 fn errors_are_fixed_and_source_free() { 635 for error in [ 636 BuildQualificationError::InvalidContract, 637 BuildQualificationError::InvalidFixture, 638 ] { 639 assert!(!error.to_string().contains("fixture_service")); 640 assert!(error.source().is_none()); 641 } 642 } 643 644 fn workspace_root() -> &'static Path { 645 Path::new(env!("CARGO_MANIFEST_DIR")) 646 .parent() 647 .and_then(Path::parent) 648 .expect("workspace root") 649 } 650 651 fn copied_fixture() -> TempDir { 652 let root = TempDir::new().expect("fixture root"); 653 let destination = root.path().join(FIXTURE_RELATIVE); 654 fs::create_dir_all(&destination).expect("fixture directory"); 655 let source = workspace_root().join(FIXTURE_RELATIVE); 656 for name in ["Cargo.toml", "Cargo.lock", LOCK_FILENAME] { 657 fs::copy(source.join(name), destination.join(name)).expect("fixture file"); 658 } 659 root 660 } 661 }