feat: audit native smoke artifacts

This commit is contained in:
2026-06-23 23:51:38 +04:00
parent ed2b540abf
commit 54f07ee3be
5 changed files with 270 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@ repository.workspace = true
fparkan-corpus = { path = "../crates/fparkan-corpus" }
cargo_metadata = "0.21"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
[lints]
+266 -1
View File
@@ -37,6 +37,7 @@ const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT";
const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_2_roadmap.md";
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-2-audit.json";
const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["linux", "macos", "windows"];
const APPROVED_REGISTRY_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index";
const SUPPLY_CHAIN_BANNED_PACKAGES: &[&str] = &["native-tls", "openssl", "openssl-sys"];
const PINNED_RUST_TOOLCHAIN: &str = "1.87.0";
@@ -89,6 +90,10 @@ fn run(args: &[String]) -> Result<(), String> {
let options = parse_audit_options(rest)?;
run_acceptance_audit(&options)
}
[cmd, subcmd, rest @ ..] if cmd == "native-smoke" && subcmd == "audit" => {
let options = parse_native_smoke_audit_options(rest)?;
run_native_smoke_audit(&options)
}
[cmd, rest @ ..] if cmd == "package" => {
let options = parse_package_options(rest)?;
run_package(&options)
@@ -111,7 +116,7 @@ fn run(args: &[String]) -> Result<(), String> {
Ok(())
}
_ => Err(
"usage: cargo xtask ci | policy | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | 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 | 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>"
.to_string(),
),
}
@@ -1286,6 +1291,11 @@ struct AuditOptions {
strict: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct NativeSmokeAuditOptions {
dir: PathBuf,
}
fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOptions, String> {
let mut options = TestOptions {
stage: Stage::All,
@@ -1421,6 +1431,193 @@ 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 iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--dir" => {
let value = iter
.next()
.ok_or_else(|| "--dir requires a path".to_string())?;
dir = Some(PathBuf::from(value));
}
_ => return Err(format!("unknown native-smoke audit option: {arg}")),
}
}
Ok(NativeSmokeAuditOptions {
dir: dir.ok_or_else(|| "native-smoke audit requires --dir".to_string())?,
})
}
fn run_native_smoke_audit(options: &NativeSmokeAuditOptions) -> Result<(), String> {
let reports = read_native_smoke_reports(&options.dir)?;
let failures = audit_native_smoke_reports(&reports);
if failures.is_empty() {
println!("native smoke artifacts passed: {}", options.dir.display());
Ok(())
} else {
Err(format!(
"native smoke artifacts incomplete:\n{}",
failures.join("\n")
))
}
}
fn read_native_smoke_reports(dir: &Path) -> Result<BTreeMap<String, serde_json::Value>, String> {
let mut reports = BTreeMap::new();
let entries = fs::read_dir(dir).map_err(|err| format!("{}: {err}", dir.display()))?;
for entry in entries {
let entry = entry.map_err(|err| format!("{}: {err}", dir.display()))?;
let path = entry.path();
if path.extension().and_then(|value| value.to_str()) != Some("json") {
continue;
}
let text = fs::read_to_string(&path).map_err(|err| format!("{}: {err}", path.display()))?;
let json = serde_json::from_str::<serde_json::Value>(&text)
.map_err(|err| format!("{}: {err}", path.display()))?;
let platform = json_string_field(&json, "platform")
.map_err(|err| format!("{}: {err}", path.display()))?;
reports.insert(platform.to_string(), json);
}
Ok(reports)
}
fn audit_native_smoke_reports(reports: &BTreeMap<String, serde_json::Value>) -> Vec<String> {
let mut failures = Vec::new();
for platform in REQUIRED_NATIVE_SMOKE_PLATFORMS {
let Some(report) = reports.get(*platform) else {
failures.push(format!("{platform}: missing native smoke report"));
continue;
};
validate_native_smoke_report(platform, report, &mut failures);
}
for platform in reports.keys() {
if !REQUIRED_NATIVE_SMOKE_PLATFORMS.contains(&platform.as_str()) {
failures.push(format!("{platform}: unexpected native smoke platform"));
}
}
failures
}
fn validate_native_smoke_report(
platform: &str,
report: &serde_json::Value,
failures: &mut Vec<String>,
) {
expect_string_field(
platform,
report,
"schema_version",
"fparkan-native-smoke-v1",
failures,
);
expect_string_field(platform, report, "status", "passed", failures);
expect_string_field(
platform,
report,
"vulkan_loader_status",
"available",
failures,
);
expect_string_field(
platform,
report,
"vulkan_instance_status",
"created",
failures,
);
expect_string_field(platform, report, "window_status", "planned", failures);
expect_string_field(
platform,
report,
"vulkan_surface_status",
"planned",
failures,
);
expect_u64_at_least(platform, report, "frames", 300, failures);
expect_u64_at_least(platform, report, "resize_count", 1, failures);
expect_u64_at_least(platform, report, "swapchain_recreate_count", 1, failures);
expect_u64_field(platform, report, "validation_error_count", 0, failures);
expect_nonempty_string(platform, report, "commit_sha", failures);
expect_nonempty_string(platform, report, "rust_toolchain", failures);
expect_nonempty_string(platform, report, "target_triple", failures);
expect_nonempty_string(platform, report, "shader_manifest_hash", failures);
}
fn expect_string_field(
platform: &str,
report: &serde_json::Value,
field: &str,
expected: &str,
failures: &mut Vec<String>,
) {
match json_string_field(report, field) {
Ok(actual) if actual == expected => {}
Ok(actual) => failures.push(format!(
"{platform}: {field} expected {expected:?}, found {actual:?}"
)),
Err(err) => failures.push(format!("{platform}: {err}")),
}
}
fn expect_nonempty_string(
platform: &str,
report: &serde_json::Value,
field: &str,
failures: &mut Vec<String>,
) {
match json_string_field(report, field) {
Ok(value) if !value.trim().is_empty() => {}
Ok(_) => failures.push(format!("{platform}: {field} must be non-empty")),
Err(err) => failures.push(format!("{platform}: {err}")),
}
}
fn expect_u64_at_least(
platform: &str,
report: &serde_json::Value,
field: &str,
minimum: u64,
failures: &mut Vec<String>,
) {
match json_u64_field(report, field) {
Ok(value) if value >= minimum => {}
Ok(value) => failures.push(format!(
"{platform}: {field} expected >= {minimum}, found {value}"
)),
Err(err) => failures.push(format!("{platform}: {err}")),
}
}
fn expect_u64_field(
platform: &str,
report: &serde_json::Value,
field: &str,
expected: u64,
failures: &mut Vec<String>,
) {
match json_u64_field(report, field) {
Ok(value) if value == expected => {}
Ok(value) => failures.push(format!(
"{platform}: {field} expected {expected}, found {value}"
)),
Err(err) => failures.push(format!("{platform}: {err}")),
}
}
fn json_string_field<'a>(json: &'a serde_json::Value, field: &str) -> Result<&'a str, String> {
json.get(field)
.and_then(serde_json::Value::as_str)
.ok_or_else(|| format!("{field} must be a string"))
}
fn json_u64_field(json: &serde_json::Value, field: &str) -> Result<u64, String> {
json.get(field)
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| format!("{field} must be an unsigned integer"))
}
fn run_acceptance_audit(options: &AuditOptions) -> Result<(), String> {
let roadmap_text = fs::read_to_string(&options.roadmap)
.map_err(|err| format!("{}: {err}", options.roadmap.display()))?;
@@ -2014,6 +2211,74 @@ mod tests {
assert!(json.contains("\"msrv\": \"1.87\""));
}
#[test]
fn native_smoke_audit_accepts_complete_three_platform_pass() {
let reports = ["linux", "macos", "windows"]
.into_iter()
.map(|platform| {
(
platform.to_string(),
serde_json::json!({
"schema_version": "fparkan-native-smoke-v1",
"commit_sha": "0123456789abcdef0123456789abcdef01234567",
"rust_toolchain": "1.87.0",
"target_triple": format!("{platform}-test-target"),
"platform": platform,
"status": "passed",
"frames": 300,
"resize_count": 1,
"swapchain_recreate_count": 1,
"validation_error_count": 0,
"shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c",
"vulkan_loader_status": "available",
"vulkan_instance_status": "created",
"window_status": "planned",
"vulkan_surface_status": "planned"
}),
)
})
.collect::<BTreeMap<_, _>>();
assert_eq!(audit_native_smoke_reports(&reports), Vec::<String>::new());
}
#[test]
fn native_smoke_audit_rejects_blocked_or_incomplete_reports() {
let reports = [(
"macos".to_string(),
serde_json::json!({
"schema_version": "fparkan-native-smoke-v1",
"commit_sha": "0123456789abcdef0123456789abcdef01234567",
"rust_toolchain": "1.87.0",
"target_triple": "aarch64-apple-darwin",
"platform": "macos",
"status": "blocked",
"frames": 0,
"resize_count": 0,
"swapchain_recreate_count": 0,
"validation_error_count": null,
"shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c",
"vulkan_loader_status": "unavailable",
"vulkan_instance_status": "skipped",
"window_status": "planned",
"vulkan_surface_status": "skipped"
}),
)]
.into_iter()
.collect::<BTreeMap<_, _>>();
let failures = audit_native_smoke_reports(&reports);
assert!(failures.contains(&"linux: missing native smoke report".to_string()));
assert!(failures.contains(&"windows: missing native smoke report".to_string()));
assert!(
failures.contains(&"macos: status expected \"passed\", found \"blocked\"".to_string())
);
assert!(failures.contains(&"macos: frames expected >= 300, found 0".to_string()));
assert!(failures
.contains(&"macos: validation_error_count must be an unsigned integer".to_string()));
}
#[test]
fn defaults_to_all_stage_and_testdata_root() {
let args = Vec::new();