aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--apps/fparkan-vulkan-smoke/Cargo.toml2
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs243
-rw-r--r--xtask/src/main.rs227
4 files changed, 221 insertions, 253 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 061950c..f80a711 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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(&current_git_commit_sha())),
- ("git_dirty", bool_json(current_git_dirty())),
- ("runner_identity", json_string(&measured_runner_identity())),
- ("rust_toolchain", json_string(&current_rustc_release())),
- ("target_triple", json_string(&current_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\""));