diff options
Diffstat (limited to 'xtask/src/main.rs')
| -rw-r--r-- | xtask/src/main.rs | 214 |
1 files changed, 208 insertions, 6 deletions
diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 735ec94..249a7bb 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -133,7 +133,7 @@ fn run(args: &[String]) -> Result<(), String> { Ok(()) } _ => Err( - "usage: cargo xtask ci | policy | shader-provenance | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | native-smoke audit --dir <path> | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path>" + "usage: cargo xtask ci | policy | shader-provenance | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | native-smoke audit --dir <path> [--expected-commit <sha>] [--expected-shader-manifest-hash <sha256>] | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path>" .to_string(), ), } @@ -226,6 +226,12 @@ fn load_shader_manifest(path: &Path) -> Result<ShaderManifestJson, String> { path.display() )); } + if manifest.manifest_hash.trim().is_empty() { + return Err(format!( + "{}: shader manifest must include a non-empty manifest_hash", + path.display() + )); + } Ok(manifest) } @@ -1655,6 +1661,8 @@ struct AuditOptions { #[derive(Clone, Debug, Eq, PartialEq)] struct NativeSmokeAuditOptions { dir: PathBuf, + expected_commit: String, + expected_shader_manifest_hash: String, } #[derive(Debug, Deserialize)] @@ -1663,6 +1671,7 @@ struct ShaderManifestJson { compiler: ShaderToolManifestJson, validator: ShaderToolManifestJson, modules: Vec<ShaderModuleManifestJson>, + manifest_hash: String, } #[derive(Debug, Deserialize)] @@ -1821,6 +1830,8 @@ fn parse_audit_options(args: &[String]) -> Result<AuditOptions, String> { fn parse_native_smoke_audit_options(args: &[String]) -> Result<NativeSmokeAuditOptions, String> { let mut dir = None; + let mut expected_commit = expected_native_smoke_commit()?; + let mut expected_shader_manifest_hash = current_shader_manifest_hash()?; let mut iter = args.iter(); while let Some(arg) = iter.next() { match arg.as_str() { @@ -1830,17 +1841,43 @@ fn parse_native_smoke_audit_options(args: &[String]) -> Result<NativeSmokeAuditO .ok_or_else(|| "--dir requires a path".to_string())?; dir = Some(PathBuf::from(value)); } + "--expected-commit" => { + let value = iter + .next() + .ok_or_else(|| "--expected-commit requires a value".to_string())?; + if !is_commit_sha(value) { + return Err(format!( + "--expected-commit must be a 40-character lowercase or uppercase hex string, found {value:?}" + )); + } + expected_commit = value.to_string(); + } + "--expected-shader-manifest-hash" => { + let value = iter.next().ok_or_else(|| { + "--expected-shader-manifest-hash requires a value".to_string() + })?; + if value.trim().is_empty() { + return Err("--expected-shader-manifest-hash must be non-empty".to_string()); + } + expected_shader_manifest_hash = value.to_string(); + } _ => return Err(format!("unknown native-smoke audit option: {arg}")), } } Ok(NativeSmokeAuditOptions { dir: dir.ok_or_else(|| "native-smoke audit requires --dir".to_string())?, + expected_commit, + expected_shader_manifest_hash, }) } fn run_native_smoke_audit(options: &NativeSmokeAuditOptions) -> Result<(), String> { let reports = read_native_smoke_reports(&options.dir)?; - let failures = audit_native_smoke_reports(&reports); + let failures = audit_native_smoke_reports( + &reports, + &options.expected_commit, + &options.expected_shader_manifest_hash, + ); if failures.is_empty() { println!("native smoke artifacts passed: {}", options.dir.display()); Ok(()) @@ -1877,7 +1914,11 @@ fn read_native_smoke_reports(dir: &Path) -> Result<BTreeMap<String, serde_json:: Ok(reports) } -fn audit_native_smoke_reports(reports: &BTreeMap<String, serde_json::Value>) -> Vec<String> { +fn audit_native_smoke_reports( + reports: &BTreeMap<String, serde_json::Value>, + expected_commit: &str, + expected_shader_manifest_hash: &str, +) -> Vec<String> { let mut failures = Vec::new(); let mut commit_shas = BTreeSet::new(); let mut rust_toolchains = BTreeSet::new(); @@ -1887,6 +1928,20 @@ fn audit_native_smoke_reports(reports: &BTreeMap<String, serde_json::Value>) -> continue; }; validate_native_smoke_report(platform, report, &mut failures); + expect_string_field( + platform, + report, + "commit_sha", + expected_commit, + &mut failures, + ); + expect_string_field( + platform, + report, + "shader_manifest_hash", + expected_shader_manifest_hash, + &mut failures, + ); if let Ok(commit_sha) = json_string_field(report, "commit_sha") { if commit_sha == "unknown" { failures.push(format!("{platform}: commit_sha must not be \"unknown\"")); @@ -1918,6 +1973,32 @@ fn audit_native_smoke_reports(reports: &BTreeMap<String, serde_json::Value>) -> failures } +fn expected_native_smoke_commit() -> Result<String, String> { + if let Ok(commit_sha) = std::env::var("GITHUB_SHA") { + if is_commit_sha(&commit_sha) { + return Ok(commit_sha); + } + return Err(format!( + "GITHUB_SHA must be a 40-character lowercase or uppercase hex string when set, found {commit_sha:?}" + )); + } + let commit_sha = current_git_commit_sha(); + if is_commit_sha(&commit_sha) { + Ok(commit_sha) + } else { + Err( + "native-smoke audit could not resolve expected commit from GITHUB_SHA or git rev-parse HEAD" + .to_string(), + ) + } +} + +fn current_shader_manifest_hash() -> Result<String, String> { + let manifest_path = workspace_relative_path(SHADER_MANIFEST_REPORT); + let manifest = load_shader_manifest(&manifest_path)?; + Ok(manifest.manifest_hash) +} + fn validate_native_smoke_report( platform: &str, report: &serde_json::Value, @@ -2514,6 +2595,10 @@ fn current_git_commit_sha() -> String { .unwrap_or_else(|| "unknown".to_string()) } +fn is_commit_sha(value: &str) -> bool { + value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit()) +} + fn current_git_dirty() -> bool { Command::new("git") .args(["status", "--short"]) @@ -2936,6 +3021,9 @@ mod tests { #[test] fn native_smoke_audit_accepts_complete_required_platform_pass() { + let expected_commit = "0123456789abcdef0123456789abcdef01234567"; + let expected_shader_manifest_hash = + "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; let reports = ["macos"] .into_iter() .map(|platform| { @@ -2962,7 +3050,7 @@ mod tests { "swapchain_recreate_count": 1, "validation_warning_count": 0, "validation_error_count": 0, - "shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c", + "shader_manifest_hash": expected_shader_manifest_hash, "vulkan_loader_status": "available", "vulkan_instance_status": "created", "window_status": "created", @@ -2984,11 +3072,17 @@ mod tests { }) .collect::<BTreeMap<_, _>>(); - assert_eq!(audit_native_smoke_reports(&reports), Vec::<String>::new()); + assert_eq!( + audit_native_smoke_reports(&reports, expected_commit, expected_shader_manifest_hash,), + Vec::<String>::new() + ); } #[test] fn native_smoke_audit_rejects_blocked_or_incomplete_reports() { + let expected_commit = "0123456789abcdef0123456789abcdef01234567"; + let expected_shader_manifest_hash = + "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; let reports = [( "macos".to_string(), serde_json::json!({ @@ -3018,7 +3112,8 @@ mod tests { .into_iter() .collect::<BTreeMap<_, _>>(); - let failures = audit_native_smoke_reports(&reports); + let failures = + audit_native_smoke_reports(&reports, expected_commit, expected_shader_manifest_hash); assert!( failures.contains(&"macos: status expected \"passed\", found \"blocked\"".to_string()) @@ -3042,6 +3137,113 @@ mod tests { } #[test] + fn native_smoke_audit_rejects_stale_commit_sha() { + let expected_shader_manifest_hash = + "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; + let reports = [( + "macos".to_string(), + serde_json::json!({ + "schema_version": "fparkan-native-smoke-v1", + "commit_sha": "fedcba98765432100123456789abcdef01234567", + "git_dirty": false, + "runner_identity": "github-actions/12345/stage0-macos", + "runner_architecture": "aarch64", + "rust_toolchain": measured_rust_toolchain_version(), + "target_triple": "aarch64-apple-darwin", + "platform": "macos", + "status": "passed", + "frames": 300, + "resize_count": 1, + "swapchain_recreate_count": 1, + "validation_warning_count": 0, + "validation_error_count": 0, + "shader_manifest_hash": expected_shader_manifest_hash, + "vulkan_loader_status": "available", + "vulkan_instance_status": "created", + "window_status": "created", + "vulkan_surface_status": "created", + "vulkan_device_status": "selected", + "vulkan_device_name": "Apple GPU", + "vulkan_logical_device_status": "created", + "vulkan_logical_device_graphics_queue_family": 0, + "vulkan_logical_device_present_queue_family": 0, + "vulkan_logical_device_enabled_extension_count": 1, + "vulkan_swapchain_status": "created", + "vulkan_swapchain_width": 1280, + "vulkan_swapchain_height": 720, + "vulkan_swapchain_image_count": 3, + "vulkan_portability_enumeration": true, + "vulkan_portability_subset_enabled": true + }), + )] + .into_iter() + .collect::<BTreeMap<_, _>>(); + + let failures = audit_native_smoke_reports( + &reports, + "0123456789abcdef0123456789abcdef01234567", + expected_shader_manifest_hash, + ); + + assert!(failures.contains( + &"macos: commit_sha expected \"0123456789abcdef0123456789abcdef01234567\", found \"fedcba98765432100123456789abcdef01234567\"".to_string() + )); + } + + #[test] + fn native_smoke_audit_rejects_stale_shader_manifest_hash() { + let expected_commit = "0123456789abcdef0123456789abcdef01234567"; + let reports = [( + "macos".to_string(), + serde_json::json!({ + "schema_version": "fparkan-native-smoke-v1", + "commit_sha": expected_commit, + "git_dirty": false, + "runner_identity": "github-actions/12345/stage0-macos", + "runner_architecture": "aarch64", + "rust_toolchain": measured_rust_toolchain_version(), + "target_triple": "aarch64-apple-darwin", + "platform": "macos", + "status": "passed", + "frames": 300, + "resize_count": 1, + "swapchain_recreate_count": 1, + "validation_warning_count": 0, + "validation_error_count": 0, + "shader_manifest_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vulkan_loader_status": "available", + "vulkan_instance_status": "created", + "window_status": "created", + "vulkan_surface_status": "created", + "vulkan_device_status": "selected", + "vulkan_device_name": "Apple GPU", + "vulkan_logical_device_status": "created", + "vulkan_logical_device_graphics_queue_family": 0, + "vulkan_logical_device_present_queue_family": 0, + "vulkan_logical_device_enabled_extension_count": 1, + "vulkan_swapchain_status": "created", + "vulkan_swapchain_width": 1280, + "vulkan_swapchain_height": 720, + "vulkan_swapchain_image_count": 3, + "vulkan_portability_enumeration": true, + "vulkan_portability_subset_enabled": true + }), + )] + .into_iter() + .collect::<BTreeMap<_, _>>(); + + let failures = audit_native_smoke_reports( + &reports, + expected_commit, + "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c", + ); + + assert!(failures.contains( + &"macos: shader_manifest_hash expected \"dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c\", found \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"".to_string() + )); + } + + #[test] fn defaults_to_all_stage_and_testdata_root() { let args = Vec::new(); let parsed = parse_test_options(&args, PathBuf::from("testdata")); |
