refactor(reporting): use typed serde report schemas
This commit is contained in:
Generated
+2
@@ -717,6 +717,8 @@ dependencies = [
|
|||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-platform-winit",
|
"fparkan-platform-winit",
|
||||||
"fparkan-render-vulkan",
|
"fparkan-render-vulkan",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ repository.workspace = true
|
|||||||
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
|
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
|
|||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
@@ -178,7 +179,7 @@ impl SmokeApp {
|
|||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let report = render_smoke_report_json(
|
let report = match render_smoke_report_json(
|
||||||
&self.options,
|
&self.options,
|
||||||
renderer,
|
renderer,
|
||||||
self.frames_presented,
|
self.frames_presented,
|
||||||
@@ -186,7 +187,14 @@ impl SmokeApp {
|
|||||||
validation.warning_count,
|
validation.warning_count,
|
||||||
validation.error_count,
|
validation.error_count,
|
||||||
&validation.vuids,
|
&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 Some(parent) = self.options.out.parent() {
|
||||||
if let Err(err) = std::fs::create_dir_all(parent) {
|
if let Err(err) = std::fs::create_dir_all(parent) {
|
||||||
self.error = Some(format!("{}: {err}", parent.display()));
|
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(
|
fn render_smoke_report_json(
|
||||||
options: &SmokeOptions,
|
options: &SmokeOptions,
|
||||||
renderer: &VulkanSmokeRenderer,
|
renderer: &VulkanSmokeRenderer,
|
||||||
@@ -348,97 +391,44 @@ fn render_smoke_report_json(
|
|||||||
validation_warning_count: u32,
|
validation_warning_count: u32,
|
||||||
validation_error_count: u32,
|
validation_error_count: u32,
|
||||||
validation_vuids: &[String],
|
validation_vuids: &[String],
|
||||||
) -> String {
|
) -> Result<String, String> {
|
||||||
let report = renderer.report();
|
let report = renderer.report();
|
||||||
let fields = vec![
|
let smoke_report = SmokeReport {
|
||||||
("schema_version", json_string(SCHEMA_VERSION)),
|
schema_version: SCHEMA_VERSION,
|
||||||
("commit_sha", json_string(¤t_git_commit_sha())),
|
commit_sha: current_git_commit_sha(),
|
||||||
("git_dirty", bool_json(current_git_dirty())),
|
git_dirty: current_git_dirty(),
|
||||||
("runner_identity", json_string(&measured_runner_identity())),
|
runner_identity: measured_runner_identity(),
|
||||||
("rust_toolchain", json_string(¤t_rustc_release())),
|
rust_toolchain: current_rustc_release(),
|
||||||
("target_triple", json_string(¤t_rustc_host_triple())),
|
target_triple: current_rustc_host_triple(),
|
||||||
("platform", json_string(actual_platform())),
|
platform: actual_platform(),
|
||||||
("status", json_string("passed")),
|
status: "passed",
|
||||||
("frames", frames_presented.to_string()),
|
frames: frames_presented,
|
||||||
("resize_count", resize_count.to_string()),
|
resize_count,
|
||||||
(
|
swapchain_recreate_count: renderer.swapchain_recreate_count(),
|
||||||
"swapchain_recreate_count",
|
validation_warning_count,
|
||||||
renderer.swapchain_recreate_count().to_string(),
|
validation_error_count,
|
||||||
),
|
validation_vuids,
|
||||||
(
|
requested_frames: options.frames,
|
||||||
"validation_warning_count",
|
shader_manifest_hash: &report.shader_manifest_hash,
|
||||||
validation_warning_count.to_string(),
|
vulkan_loader_status: "available",
|
||||||
),
|
vulkan_instance_status: "created",
|
||||||
("validation_error_count", validation_error_count.to_string()),
|
window_status: "created",
|
||||||
("validation_vuids", render_string_array(validation_vuids)),
|
vulkan_surface_status: "created",
|
||||||
("requested_frames", options.frames.to_string()),
|
vulkan_device_status: "selected",
|
||||||
(
|
vulkan_device_name: &report.device_name,
|
||||||
"shader_manifest_hash",
|
vulkan_logical_device_status: "created",
|
||||||
json_string(&report.shader_manifest_hash),
|
vulkan_logical_device_graphics_queue_family: report.graphics_queue_family,
|
||||||
),
|
vulkan_logical_device_present_queue_family: report.present_queue_family,
|
||||||
("vulkan_loader_status", json_string("available")),
|
vulkan_logical_device_enabled_extension_count: report.enabled_extension_count,
|
||||||
("vulkan_instance_status", json_string("created")),
|
vulkan_swapchain_status: "created",
|
||||||
("window_status", json_string("created")),
|
vulkan_swapchain_width: report.swapchain_extent.0,
|
||||||
("vulkan_surface_status", json_string("created")),
|
vulkan_swapchain_height: report.swapchain_extent.1,
|
||||||
("vulkan_device_status", json_string("selected")),
|
vulkan_swapchain_image_count: report.swapchain_image_count,
|
||||||
("vulkan_device_name", json_string(&report.device_name)),
|
vulkan_portability_enumeration: report.portability_enumeration,
|
||||||
("vulkan_logical_device_status", json_string("created")),
|
};
|
||||||
(
|
serde_json::to_string_pretty(&smoke_report)
|
||||||
"vulkan_logical_device_graphics_queue_family",
|
.map(|json| format!("{json}\n"))
|
||||||
report.graphics_queue_family.to_string(),
|
.map_err(|err| format!("native smoke report serialization failed: {err}"))
|
||||||
),
|
|
||||||
(
|
|
||||||
"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}]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn actual_platform() -> &'static str {
|
fn actual_platform() -> &'static str {
|
||||||
@@ -517,31 +507,6 @@ fn current_rustc_host_triple() -> String {
|
|||||||
.unwrap_or_else(|| "unknown".to_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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -596,10 +561,44 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renders_string_array_json() {
|
fn smoke_report_json_contains_expected_fields() {
|
||||||
assert_eq!(
|
let json = serde_json::to_string_pretty(&SmokeReport {
|
||||||
render_string_array(&["VUID-A".to_string(), "VUID-B".to_string()]),
|
schema_version: SCHEMA_VERSION,
|
||||||
"[\"VUID-A\", \"VUID-B\"]"
|
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\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-131
@@ -23,10 +23,9 @@
|
|||||||
|
|
||||||
use cargo_metadata::MetadataCommand;
|
use cargo_metadata::MetadataCommand;
|
||||||
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
|
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Write as _;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -1858,8 +1857,8 @@ fn run_acceptance_audit(options: &AuditOptions) -> Result<(), String> {
|
|||||||
if let Some(parent) = options.out.parent() {
|
if let Some(parent) = options.out.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
||||||
}
|
}
|
||||||
fs::write(&options.out, render_audit_json(&audit))
|
let rendered = render_audit_json(&audit)?;
|
||||||
.map_err(|err| format!("{}: {err}", options.out.display()))?;
|
fs::write(&options.out, rendered).map_err(|err| format!("{}: {err}", options.out.display()))?;
|
||||||
println!("{}", options.out.display());
|
println!("{}", options.out.display());
|
||||||
let strict_failures = audit.strict_failures();
|
let strict_failures = audit.strict_failures();
|
||||||
if options.strict && (!strict_failures.is_empty() || !audit.unknown_coverage.is_empty()) {
|
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();
|
let unverified = audit.unverified();
|
||||||
format!(
|
let report = AcceptanceAuditJson {
|
||||||
concat!(
|
schema_version: "fparkan-acceptance-coverage-v1",
|
||||||
"{{\n",
|
commit_sha: &audit.commit_sha,
|
||||||
" \"schema_version\": \"fparkan-acceptance-coverage-v1\",\n",
|
git_dirty: audit.git_dirty,
|
||||||
" \"commit_sha\": \"{}\",\n",
|
runner_identity: &audit.runner_identity,
|
||||||
" \"git_dirty\": {},\n",
|
rust_toolchain: &audit.rust_toolchain,
|
||||||
" \"runner_identity\": \"{}\",\n",
|
msrv: &audit.msrv,
|
||||||
" \"rust_toolchain\": \"{}\",\n",
|
required_total: audit.required_total,
|
||||||
" \"msrv\": \"{}\",\n",
|
covered_total: audit.covered.len(),
|
||||||
" \"required_total\": {},\n",
|
partial_total: audit.partial.len(),
|
||||||
" \"covered_total\": {},\n",
|
blocked_total: audit.blocked.len(),
|
||||||
" \"partial_total\": {},\n",
|
omitted_total: audit.omitted.len(),
|
||||||
" \"blocked_total\": {},\n",
|
missing_total: audit.missing.len(),
|
||||||
" \"omitted_total\": {},\n",
|
unverified_total: unverified.len(),
|
||||||
" \"missing_total\": {},\n",
|
unknown_coverage_total: audit.unknown_coverage.len(),
|
||||||
" \"unverified_total\": {},\n",
|
by_stage: &audit.by_stage,
|
||||||
" \"unknown_coverage_total\": {},\n",
|
covered: &audit.covered,
|
||||||
" \"by_stage\": {},\n",
|
partial: &audit.partial,
|
||||||
" \"covered\": {},\n",
|
blocked: &audit.blocked,
|
||||||
" \"partial\": {},\n",
|
omitted: &audit.omitted,
|
||||||
" \"blocked\": {},\n",
|
missing: &audit.missing,
|
||||||
" \"omitted\": {},\n",
|
unknown_coverage: &audit.unknown_coverage,
|
||||||
" \"missing\": {},\n",
|
coverage_evidence: &audit.coverage_evidence,
|
||||||
" \"unknown_coverage\": {},\n",
|
};
|
||||||
" \"coverage_evidence\": {}\n",
|
serde_json::to_string_pretty(&report)
|
||||||
"}}\n"
|
.map(|json| format!("{json}\n"))
|
||||||
),
|
.map_err(|err| format!("acceptance audit serialization failed: {err}"))
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_git_commit_sha() -> String {
|
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> {
|
fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> {
|
||||||
let roots = if options.suite == TestSuite::Licensed {
|
let roots = if options.suite == TestSuite::Licensed {
|
||||||
Some(load_licensed_roots(options.manifest.as_deref())?)
|
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() {
|
if let Some(parent) = options.out.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
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()))?;
|
fs::write(&options.out, report).map_err(|err| format!("{}: {err}", options.out.display()))?;
|
||||||
println!("{}", options.out.display());
|
println!("{}", options.out.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_acceptance_report(options: &AcceptanceOptions) -> String {
|
#[derive(Serialize)]
|
||||||
let packages = stage_report_packages(options.stage)
|
struct AcceptanceLicensedCorpusReport<'a> {
|
||||||
.unwrap_or_default()
|
root: &'a str,
|
||||||
.into_iter()
|
parts: [&'a str; 2],
|
||||||
.map(|package| format!(" \"{package}\""))
|
}
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",\n");
|
#[derive(Serialize)]
|
||||||
let corpus = if options.suite == TestSuite::Licensed {
|
struct AcceptanceReportJson {
|
||||||
"\n \"licensed_corpus\": {\n \"root\": \"redacted\",\n \"parts\": [\"IS\", \"IS2\"]\n },"
|
schema_version: &'static str,
|
||||||
} else {
|
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!(
|
serde_json::to_string_pretty(&report)
|
||||||
concat!(
|
.map(|json| format!("{json}\n"))
|
||||||
"{{\n",
|
.map_err(|err| format!("acceptance report serialization failed: {err}"))
|
||||||
" \"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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_report_packages(stage: Stage) -> Result<Vec<String>, String> {
|
fn stage_report_packages(stage: Stage) -> Result<Vec<String>, String> {
|
||||||
@@ -2488,7 +2452,8 @@ mod tests {
|
|||||||
manifest: Some(PathBuf::from("/private/corpora.toml")),
|
manifest: Some(PathBuf::from("/private/corpora.toml")),
|
||||||
out: PathBuf::from("target/report.json"),
|
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("\"root\": \"redacted\""));
|
||||||
assert!(!report.contains("/private/game"));
|
assert!(!report.contains("/private/game"));
|
||||||
@@ -2581,7 +2546,7 @@ mod tests {
|
|||||||
.coverage_evidence
|
.coverage_evidence
|
||||||
.insert("S0-ARCH-001".to_string(), "quoted \"value\"".to_string());
|
.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("quoted \\\"value\\\""));
|
||||||
assert!(json.contains("\"commit_sha\": \"0123456789abcdef0123456789abcdef01234567\""));
|
assert!(json.contains("\"commit_sha\": \"0123456789abcdef0123456789abcdef01234567\""));
|
||||||
|
|||||||
Reference in New Issue
Block a user