diff options
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/Cargo.toml | 2 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 243 | ||||
| -rw-r--r-- | xtask/src/main.rs | 227 |
4 files changed, 221 insertions, 253 deletions
@@ -717,6 +717,8 @@ dependencies = [ "fparkan-platform", "fparkan-platform-winit", "fparkan-render-vulkan", + "serde", + "serde_json", "winit", ] diff --git a/apps/fparkan-vulkan-smoke/Cargo.toml b/apps/fparkan-vulkan-smoke/Cargo.toml index 3744849..214c46a 100644 --- a/apps/fparkan-vulkan-smoke/Cargo.toml +++ b/apps/fparkan-vulkan-smoke/Cargo.toml @@ -9,6 +9,8 @@ repository.workspace = true fparkan-platform = { path = "../../crates/fparkan-platform" } fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" } fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" winit = "0.30" [lints] diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index 86052d5..ade568c 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -15,6 +15,7 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan}; use fparkan_render_vulkan::{ VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, }; +use serde::Serialize; use std::path::PathBuf; use std::process::Command; use winit::application::ApplicationHandler; @@ -178,7 +179,7 @@ impl SmokeApp { event_loop.exit(); return; } - let report = render_smoke_report_json( + let report = match render_smoke_report_json( &self.options, renderer, self.frames_presented, @@ -186,7 +187,14 @@ impl SmokeApp { validation.warning_count, validation.error_count, &validation.vuids, - ); + ) { + Ok(report) => report, + Err(err) => { + self.error = Some(err); + event_loop.exit(); + return; + } + }; if let Some(parent) = self.options.out.parent() { if let Err(err) = std::fs::create_dir_all(parent) { self.error = Some(format!("{}: {err}", parent.display())); @@ -340,6 +348,41 @@ impl ApplicationHandler for SmokeApp { } } +#[derive(Serialize)] +struct SmokeReport<'a> { + schema_version: &'static str, + commit_sha: String, + git_dirty: bool, + runner_identity: String, + rust_toolchain: String, + target_triple: String, + platform: &'static str, + status: &'static str, + frames: u32, + resize_count: u32, + swapchain_recreate_count: u32, + validation_warning_count: u32, + validation_error_count: u32, + validation_vuids: &'a [String], + requested_frames: u32, + shader_manifest_hash: &'a str, + vulkan_loader_status: &'static str, + vulkan_instance_status: &'static str, + window_status: &'static str, + vulkan_surface_status: &'static str, + vulkan_device_status: &'static str, + vulkan_device_name: &'a str, + vulkan_logical_device_status: &'static str, + vulkan_logical_device_graphics_queue_family: u32, + vulkan_logical_device_present_queue_family: u32, + vulkan_logical_device_enabled_extension_count: u32, + vulkan_swapchain_status: &'static str, + vulkan_swapchain_width: u32, + vulkan_swapchain_height: u32, + vulkan_swapchain_image_count: u32, + vulkan_portability_enumeration: bool, +} + fn render_smoke_report_json( options: &SmokeOptions, renderer: &VulkanSmokeRenderer, @@ -348,97 +391,44 @@ fn render_smoke_report_json( validation_warning_count: u32, validation_error_count: u32, validation_vuids: &[String], -) -> String { +) -> Result<String, String> { let report = renderer.report(); - let fields = vec![ - ("schema_version", json_string(SCHEMA_VERSION)), - ("commit_sha", json_string(¤t_git_commit_sha())), - ("git_dirty", bool_json(current_git_dirty())), - ("runner_identity", json_string(&measured_runner_identity())), - ("rust_toolchain", json_string(¤t_rustc_release())), - ("target_triple", json_string(¤t_rustc_host_triple())), - ("platform", json_string(actual_platform())), - ("status", json_string("passed")), - ("frames", frames_presented.to_string()), - ("resize_count", resize_count.to_string()), - ( - "swapchain_recreate_count", - renderer.swapchain_recreate_count().to_string(), - ), - ( - "validation_warning_count", - validation_warning_count.to_string(), - ), - ("validation_error_count", validation_error_count.to_string()), - ("validation_vuids", render_string_array(validation_vuids)), - ("requested_frames", options.frames.to_string()), - ( - "shader_manifest_hash", - json_string(&report.shader_manifest_hash), - ), - ("vulkan_loader_status", json_string("available")), - ("vulkan_instance_status", json_string("created")), - ("window_status", json_string("created")), - ("vulkan_surface_status", json_string("created")), - ("vulkan_device_status", json_string("selected")), - ("vulkan_device_name", json_string(&report.device_name)), - ("vulkan_logical_device_status", json_string("created")), - ( - "vulkan_logical_device_graphics_queue_family", - report.graphics_queue_family.to_string(), - ), - ( - "vulkan_logical_device_present_queue_family", - report.present_queue_family.to_string(), - ), - ( - "vulkan_logical_device_enabled_extension_count", - report.enabled_extension_count.to_string(), - ), - ("vulkan_swapchain_status", json_string("created")), - ( - "vulkan_swapchain_width", - report.swapchain_extent.0.to_string(), - ), - ( - "vulkan_swapchain_height", - report.swapchain_extent.1.to_string(), - ), - ( - "vulkan_swapchain_image_count", - report.swapchain_image_count.to_string(), - ), - ( - "vulkan_portability_enumeration", - bool_json(report.portability_enumeration), - ), - ]; - render_json_object(&fields) -} - -fn render_json_object(fields: &[(&str, String)]) -> String { - let mut out = String::from("{\n"); - for (index, (name, value)) in fields.iter().enumerate() { - out.push_str(" "); - out.push_str(&json_string(name)); - out.push_str(": "); - out.push_str(value); - if index + 1 < fields.len() { - out.push(','); - } - out.push('\n'); - } - out.push_str("}\n"); - out -} - -fn render_string_array(values: &[String]) -> String { - let items = values - .iter() - .map(|value| json_string(value)) - .collect::<Vec<_>>() - .join(", "); - format!("[{items}]") + let smoke_report = SmokeReport { + schema_version: SCHEMA_VERSION, + commit_sha: current_git_commit_sha(), + git_dirty: current_git_dirty(), + runner_identity: measured_runner_identity(), + rust_toolchain: current_rustc_release(), + target_triple: current_rustc_host_triple(), + platform: actual_platform(), + status: "passed", + frames: frames_presented, + resize_count, + swapchain_recreate_count: renderer.swapchain_recreate_count(), + validation_warning_count, + validation_error_count, + validation_vuids, + requested_frames: options.frames, + shader_manifest_hash: &report.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: &report.device_name, + vulkan_logical_device_status: "created", + vulkan_logical_device_graphics_queue_family: report.graphics_queue_family, + vulkan_logical_device_present_queue_family: report.present_queue_family, + vulkan_logical_device_enabled_extension_count: report.enabled_extension_count, + vulkan_swapchain_status: "created", + vulkan_swapchain_width: report.swapchain_extent.0, + vulkan_swapchain_height: report.swapchain_extent.1, + vulkan_swapchain_image_count: report.swapchain_image_count, + vulkan_portability_enumeration: report.portability_enumeration, + }; + serde_json::to_string_pretty(&smoke_report) + .map(|json| format!("{json}\n")) + .map_err(|err| format!("native smoke report serialization failed: {err}")) } fn actual_platform() -> &'static str { @@ -517,31 +507,6 @@ fn current_rustc_host_triple() -> String { .unwrap_or_else(|| "unknown".to_string()) } -fn json_string(value: &str) -> String { - let mut out = String::with_capacity(value.len() + 2); - out.push('"'); - for ch in value.chars() { - match ch { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if c.is_control() => { - use std::fmt::Write as _; - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} - -fn bool_json(value: bool) -> String { - if value { "true" } else { "false" }.to_string() -} - #[cfg(test)] mod tests { use super::*; @@ -596,10 +561,44 @@ mod tests { } #[test] - fn renders_string_array_json() { - assert_eq!( - render_string_array(&["VUID-A".to_string(), "VUID-B".to_string()]), - "[\"VUID-A\", \"VUID-B\"]" - ); + fn smoke_report_json_contains_expected_fields() { + let json = serde_json::to_string_pretty(&SmokeReport { + schema_version: SCHEMA_VERSION, + commit_sha: "0123456789abcdef0123456789abcdef01234567".to_string(), + git_dirty: false, + runner_identity: "github-actions/12345/stage0-macos".to_string(), + rust_toolchain: "1.87.0".to_string(), + target_triple: "aarch64-apple-darwin".to_string(), + platform: "macos", + status: "passed", + frames: 300, + resize_count: 1, + swapchain_recreate_count: 1, + validation_warning_count: 0, + validation_error_count: 0, + validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()], + requested_frames: 300, + shader_manifest_hash: "deadbeef", + 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: 2, + vulkan_swapchain_status: "created", + vulkan_swapchain_width: 960, + vulkan_swapchain_height: 540, + vulkan_swapchain_image_count: 3, + vulkan_portability_enumeration: true, + }) + .expect("smoke report should serialize"); + + assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\"")); + assert!(json.contains("\"validation_vuids\": [")); + assert!(json.contains("\"vulkan_device_name\": \"Apple GPU\"")); } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 816e56b..13aa656 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -23,10 +23,9 @@ use cargo_metadata::MetadataCommand; use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; -use std::fmt::Write as _; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -1858,8 +1857,8 @@ fn run_acceptance_audit(options: &AuditOptions) -> Result<(), String> { if let Some(parent) = options.out.parent() { fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; } - fs::write(&options.out, render_audit_json(&audit)) - .map_err(|err| format!("{}: {err}", options.out.display()))?; + let rendered = render_audit_json(&audit)?; + fs::write(&options.out, rendered).map_err(|err| format!("{}: {err}", options.out.display()))?; println!("{}", options.out.display()); let strict_failures = audit.strict_failures(); if options.strict && (!strict_failures.is_empty() || !audit.unknown_coverage.is_empty()) { @@ -2068,57 +2067,61 @@ fn build_acceptance_audit( } } -fn render_audit_json(audit: &AcceptanceAudit) -> String { +#[derive(Serialize)] +struct AcceptanceAuditJson<'a> { + schema_version: &'static str, + commit_sha: &'a str, + git_dirty: bool, + runner_identity: &'a str, + rust_toolchain: &'a str, + msrv: &'a str, + required_total: usize, + covered_total: usize, + partial_total: usize, + blocked_total: usize, + omitted_total: usize, + missing_total: usize, + unverified_total: usize, + unknown_coverage_total: usize, + by_stage: &'a BTreeMap<String, usize>, + covered: &'a [String], + partial: &'a [String], + blocked: &'a [String], + omitted: &'a [String], + missing: &'a [String], + unknown_coverage: &'a [String], + coverage_evidence: &'a BTreeMap<String, String>, +} + +fn render_audit_json(audit: &AcceptanceAudit) -> Result<String, String> { let unverified = audit.unverified(); - format!( - concat!( - "{{\n", - " \"schema_version\": \"fparkan-acceptance-coverage-v1\",\n", - " \"commit_sha\": \"{}\",\n", - " \"git_dirty\": {},\n", - " \"runner_identity\": \"{}\",\n", - " \"rust_toolchain\": \"{}\",\n", - " \"msrv\": \"{}\",\n", - " \"required_total\": {},\n", - " \"covered_total\": {},\n", - " \"partial_total\": {},\n", - " \"blocked_total\": {},\n", - " \"omitted_total\": {},\n", - " \"missing_total\": {},\n", - " \"unverified_total\": {},\n", - " \"unknown_coverage_total\": {},\n", - " \"by_stage\": {},\n", - " \"covered\": {},\n", - " \"partial\": {},\n", - " \"blocked\": {},\n", - " \"omitted\": {},\n", - " \"missing\": {},\n", - " \"unknown_coverage\": {},\n", - " \"coverage_evidence\": {}\n", - "}}\n" - ), - json_escape(&audit.commit_sha), - if audit.git_dirty { "true" } else { "false" }, - json_escape(&audit.runner_identity), - json_escape(&audit.rust_toolchain), - json_escape(&audit.msrv), - audit.required_total, - audit.covered.len(), - audit.partial.len(), - audit.blocked.len(), - audit.omitted.len(), - audit.missing.len(), - unverified.len(), - audit.unknown_coverage.len(), - render_string_usize_map(&audit.by_stage), - render_string_array(&audit.covered), - render_string_array(&audit.partial), - render_string_array(&audit.blocked), - render_string_array(&audit.omitted), - render_string_array(&audit.missing), - render_string_array(&audit.unknown_coverage), - render_string_string_map(&audit.coverage_evidence) - ) + let report = AcceptanceAuditJson { + schema_version: "fparkan-acceptance-coverage-v1", + commit_sha: &audit.commit_sha, + git_dirty: audit.git_dirty, + runner_identity: &audit.runner_identity, + rust_toolchain: &audit.rust_toolchain, + msrv: &audit.msrv, + required_total: audit.required_total, + covered_total: audit.covered.len(), + partial_total: audit.partial.len(), + blocked_total: audit.blocked.len(), + omitted_total: audit.omitted.len(), + missing_total: audit.missing.len(), + unverified_total: unverified.len(), + unknown_coverage_total: audit.unknown_coverage.len(), + by_stage: &audit.by_stage, + covered: &audit.covered, + partial: &audit.partial, + blocked: &audit.blocked, + omitted: &audit.omitted, + missing: &audit.missing, + unknown_coverage: &audit.unknown_coverage, + coverage_evidence: &audit.coverage_evidence, + }; + serde_json::to_string_pretty(&report) + .map(|json| format!("{json}\n")) + .map_err(|err| format!("acceptance audit serialization failed: {err}")) } fn current_git_commit_sha() -> String { @@ -2175,51 +2178,6 @@ fn measured_runner_identity() -> String { } } -fn render_string_usize_map(values: &BTreeMap<String, usize>) -> String { - let pairs = values - .iter() - .map(|(key, value)| format!("\"{}\": {}", json_escape(key), value)) - .collect::<Vec<_>>() - .join(", "); - format!("{{{pairs}}}") -} - -fn render_string_string_map(values: &BTreeMap<String, String>) -> String { - let pairs = values - .iter() - .map(|(key, value)| format!("\"{}\": \"{}\"", json_escape(key), json_escape(value))) - .collect::<Vec<_>>() - .join(", "); - format!("{{{pairs}}}") -} - -fn render_string_array(values: &[String]) -> String { - let items = values - .iter() - .map(|value| format!("\"{}\"", json_escape(value))) - .collect::<Vec<_>>() - .join(", "); - format!("[{items}]") -} - -fn json_escape(value: &str) -> String { - let mut out = String::new(); - for ch in value.chars() { - match ch { - '\\' => out.push_str("\\\\"), - '"' => out.push_str("\\\""), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - ch if ch.is_control() => { - let _ = write!(out, "\\u{:04x}", ch as u32); - } - ch => out.push(ch), - } - } - out -} - fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> { let roots = if options.suite == TestSuite::Licensed { Some(load_licensed_roots(options.manifest.as_deref())?) @@ -2231,42 +2189,48 @@ fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> { if let Some(parent) = options.out.parent() { fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; } - let report = render_acceptance_report(options); + let report = render_acceptance_report(options)?; fs::write(&options.out, report).map_err(|err| format!("{}: {err}", options.out.display()))?; println!("{}", options.out.display()); Ok(()) } -fn render_acceptance_report(options: &AcceptanceOptions) -> String { - let packages = stage_report_packages(options.stage) - .unwrap_or_default() - .into_iter() - .map(|package| format!(" \"{package}\"")) - .collect::<Vec<_>>() - .join(",\n"); - let corpus = if options.suite == TestSuite::Licensed { - "\n \"licensed_corpus\": {\n \"root\": \"redacted\",\n \"parts\": [\"IS\", \"IS2\"]\n }," - } else { - "" +#[derive(Serialize)] +struct AcceptanceLicensedCorpusReport<'a> { + root: &'a str, + parts: [&'a str; 2], +} + +#[derive(Serialize)] +struct AcceptanceReportJson { + schema_version: &'static str, + suite: String, + stage: String, + status: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + licensed_corpus: Option<AcceptanceLicensedCorpusReport<'static>>, + packages: Vec<String>, +} + +fn render_acceptance_report(options: &AcceptanceOptions) -> Result<String, String> { + let report = AcceptanceReportJson { + schema_version: "fparkan-acceptance-report-v1", + suite: options.suite.as_str().to_string(), + stage: options.stage.to_string(), + status: "passed", + licensed_corpus: if options.suite == TestSuite::Licensed { + Some(AcceptanceLicensedCorpusReport { + root: "redacted", + parts: ["IS", "IS2"], + }) + } else { + None + }, + packages: stage_report_packages(options.stage).unwrap_or_default(), }; - format!( - concat!( - "{{\n", - " \"schema_version\": \"fparkan-acceptance-report-v1\",\n", - " \"suite\": \"{}\",\n", - " \"stage\": \"{}\",\n", - " \"status\": \"passed\",", - "{}\n", - " \"packages\": [\n", - "{}\n", - " ]\n", - "}}\n" - ), - options.suite.as_str(), - options.stage, - corpus, - packages - ) + serde_json::to_string_pretty(&report) + .map(|json| format!("{json}\n")) + .map_err(|err| format!("acceptance report serialization failed: {err}")) } fn stage_report_packages(stage: Stage) -> Result<Vec<String>, String> { @@ -2488,7 +2452,8 @@ mod tests { manifest: Some(PathBuf::from("/private/corpora.toml")), out: PathBuf::from("target/report.json"), }; - let report = render_acceptance_report(&options); + let report = + render_acceptance_report(&options).expect("acceptance report should serialize"); assert!(report.contains("\"root\": \"redacted\"")); assert!(!report.contains("/private/game")); @@ -2581,7 +2546,7 @@ mod tests { .coverage_evidence .insert("S0-ARCH-001".to_string(), "quoted \"value\"".to_string()); - let json = render_audit_json(&audit); + let json = render_audit_json(&audit).expect("acceptance audit should serialize"); assert!(json.contains("quoted \\\"value\\\"")); assert!(json.contains("\"commit_sha\": \"0123456789abcdef0123456789abcdef01234567\"")); |
