aboutsummaryrefslogtreecommitdiff
path: root/xtask/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'xtask/src/main.rs')
-rw-r--r--xtask/src/main.rs3861
1 files changed, 0 insertions, 3861 deletions
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
deleted file mode 100644
index e2f4a53..0000000
--- a/xtask/src/main.rs
+++ /dev/null
@@ -1,3861 +0,0 @@
-#![forbid(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::cast_possible_truncation,
- clippy::cast_possible_wrap,
- clippy::cast_precision_loss,
- clippy::expect_used,
- clippy::float_cmp,
- clippy::identity_op,
- clippy::too_many_lines,
- clippy::uninlined_format_args,
- clippy::map_unwrap_or,
- clippy::needless_raw_string_hashes,
- clippy::semicolon_if_nothing_returned,
- clippy::type_complexity,
- clippy::panic,
- clippy::unwrap_used
- )
-)]
-#![allow(clippy::print_stderr, clippy::print_stdout)]
-//! Repository automation for `FParkan`.
-
-use cargo_metadata::MetadataCommand;
-use fparkan_binary::{sha256, sha256_hex};
-use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
-use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use std::time::{SystemTime, UNIX_EPOCH};
-
-const CORPORA_MANIFEST_ENV: &str = "FPARKAN_CORPORA_MANIFEST";
-const PART1_ROOT_ENV: &str = "FPARKAN_CORPUS_PART1_ROOT";
-const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT";
-const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_roadmap.md";
-const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
-const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-audit.json";
-const SHADER_MANIFEST_REPORT: &str = "adapters/fparkan-render-vulkan/shaders/manifest.json";
-const STAGE_PACKAGE_MANIFEST: &str = "fixtures/acceptance/stage_packages.toml";
-const SUPPLY_CHAIN_POLICY_CONFIG: &str = "deny.toml";
-/// Windows is the only supported native runtime platform.
-const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["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.97.1";
-const WORKSPACE_MSRV: &str = "1.97";
-const ALLOW_SUPPLY_CHAIN_FALLBACK_ENV: &str = "FPARKAN_ALLOW_SUPPLY_CHAIN_FALLBACK";
-
-fn workspace_root_path() -> PathBuf {
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")))
- .to_path_buf()
-}
-
-fn workspace_relative_path(path: &str) -> PathBuf {
- workspace_root_path().join(path)
-}
-
-fn main() {
- let args = std::env::args().skip(1).collect::<Vec<_>>();
- let code = match run(&args) {
- Ok(()) => 0,
- Err(err) => {
- eprintln!("{err}");
- 2
- }
- };
- std::process::exit(code);
-}
-
-fn run(args: &[String]) -> Result<(), String> {
- match args {
- [cmd] if cmd == "ci" => {
- run_cargo_fmt_check()?;
- run_policy(Path::new("."))?;
- run_shader_provenance_verification()?;
- cargo(&["test", "--workspace", "--all-targets", "--all-features", "--locked"])?;
- cargo(&[
- "clippy",
- "--workspace",
- "--all-targets",
- "--all-features",
- "--locked",
- "--",
- "-D",
- "warnings",
- ])?;
- run_cargo_doc()?;
- run_cargo_deny()?;
- run_acceptance_audit(&AuditOptions {
- roadmap: PathBuf::from(CI_ACCEPTANCE_ROADMAP),
- coverage: PathBuf::from(CI_ACCEPTANCE_COVERAGE),
- out: PathBuf::from(CI_ACCEPTANCE_REPORT),
- strict: true,
- })?;
- Ok(())
- }
- [cmd] if cmd == "policy" => run_policy(Path::new(".")),
- [cmd] if cmd == "shader-provenance" => run_shader_provenance_verification(),
- [cmd, subcmd, rest @ ..] if cmd == "acceptance" && subcmd == "report" => {
- let options = parse_acceptance_options(rest)?;
- run_acceptance_report(&options)
- }
- [cmd, subcmd, rest @ ..] if cmd == "acceptance" && subcmd == "audit" => {
- 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)
- }
- [cmd, suite, rest @ ..] if cmd == "test" && suite == "synthetic" => {
- let options = parse_test_options(rest, PathBuf::from("testdata"))?;
- run_stage_tests(options.stage, TestSuite::Synthetic, None)
- }
- [cmd, suite, rest @ ..] if cmd == "test" && suite == "licensed" => {
- let options = parse_test_options(rest, PathBuf::from("testdata"))?;
- let roots = load_licensed_roots(options.manifest.as_deref())?;
- run_stage_tests(options.stage, TestSuite::Licensed, Some(&roots))
- }
- [cmd, subcmd, rest @ ..] if cmd == "corpus" && subcmd == "baseline" => {
- run_corpus_baseline(&parse_corpus_baseline_options(rest)?)
- }
- _ => 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> [--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> [--out <path>]"
- .to_string(),
- ),
- }
-}
-
-fn cargo(args: &[&str]) -> Result<(), String> {
- let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let status = Command::new(cargo)
- .args(args)
- .status()
- .map_err(|err| format!("failed to run cargo: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo exited with {status}"))
- }
-}
-
-fn cargo_owned(args: &[String]) -> Result<(), String> {
- let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let status = Command::new(cargo)
- .args(args)
- .status()
- .map_err(|err| format!("failed to run cargo: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo exited with {status}"))
- }
-}
-
-fn cargo_with_env(args: &[&str], envs: &[(&str, &Path)]) -> Result<(), String> {
- let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let mut command = Command::new(cargo);
- command.args(args);
- for (key, value) in envs {
- command.env(key, value);
- }
- let status = command
- .status()
- .map_err(|err| format!("failed to run cargo: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo exited with {status}"))
- }
-}
-
-fn run_cargo_fmt_check() -> Result<(), String> {
- let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let status = Command::new(cargo)
- .args(["fmt", "--all", "--", "--check"])
- .status()
- .map_err(|err| format!("failed to run rustfmt: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo fmt exited with {status}"))
- }
-}
-
-fn run_shader_provenance_verification() -> Result<(), String> {
- let manifest_path = workspace_relative_path(SHADER_MANIFEST_REPORT);
- let manifest = load_shader_manifest(&manifest_path)?;
- let compiler_path = resolve_required_tool("FPARKAN_GLSLANG_VALIDATOR", &manifest.compiler)?;
- let validator_path = resolve_required_tool("FPARKAN_SPIRV_VAL", &manifest.validator)?;
-
- verify_tool_metadata(&compiler_path, &manifest.compiler)?;
- verify_tool_metadata(&validator_path, &manifest.validator)?;
-
- let out_dir = shader_provenance_output_dir();
- if out_dir.exists() {
- fs::remove_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?;
- }
- fs::create_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?;
-
- for module in &manifest.modules {
- verify_shader_module(&manifest, module, &compiler_path, &validator_path, &out_dir)?;
- }
- Ok(())
-}
-
-fn load_shader_manifest(path: &Path) -> Result<ShaderManifestJson, String> {
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let manifest = serde_json::from_str::<ShaderManifestJson>(&text)
- .map_err(|err| format!("{}: invalid shader manifest JSON: {err}", path.display()))?;
- if manifest.modules.is_empty() {
- return Err(format!(
- "{}: shader manifest must describe at least one module",
- 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)
-}
-
-fn resolve_required_tool(
- env_var: &str,
- manifest: &ShaderToolManifestJson,
-) -> Result<PathBuf, String> {
- let requested = std::env::var(env_var).unwrap_or_else(|_| manifest.name.clone());
- resolve_tool_path(&requested).ok_or_else(|| {
- format!(
- "required shader tool {} is unavailable (set {env_var} to override path)",
- manifest.name
- )
- })
-}
-
-fn resolve_tool_path(tool: &str) -> Option<PathBuf> {
- let candidate = Path::new(tool);
- if candidate.components().count() > 1 {
- return candidate.is_file().then(|| candidate.to_path_buf());
- }
-
- if let Some(path) = find_tool_on_path(tool) {
- return Some(path);
- }
-
- windows_vulkan_sdk_tool(tool)
-}
-
-fn find_tool_on_path(tool: &str) -> Option<PathBuf> {
- let path = std::env::var_os("PATH")?;
- let extensions: &[&str] = if cfg!(windows) { &[".exe", ""] } else { &[""] };
- for directory in std::env::split_paths(&path) {
- for extension in extensions {
- let candidate = directory.join(format!("{tool}{extension}"));
- if candidate.is_file() {
- return Some(candidate);
- }
- }
- }
- None
-}
-
-#[cfg(windows)]
-fn windows_vulkan_sdk_tool(tool: &str) -> Option<PathBuf> {
- let mut roots = Vec::new();
- if let Some(root) = std::env::var_os("VULKAN_SDK") {
- roots.push(PathBuf::from(root));
- }
- let sdk_root = PathBuf::from(r"C:\VulkanSDK");
- let mut versions = fs::read_dir(&sdk_root)
- .ok()?
- .filter_map(Result::ok)
- .map(|entry| entry.path())
- .filter(|path| path.is_dir())
- .collect::<Vec<_>>();
- versions.sort();
- versions.reverse();
- roots.extend(versions);
-
- roots
- .into_iter()
- .map(|root| root.join("Bin").join(format!("{tool}.exe")))
- .find(|candidate| candidate.is_file())
-}
-
-#[cfg(not(windows))]
-fn windows_vulkan_sdk_tool(_tool: &str) -> Option<PathBuf> {
- None
-}
-
-fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Result<(), String> {
- let actual_name = path
- .file_name()
- .and_then(|value| value.to_str())
- .ok_or_else(|| format!("{}: invalid tool filename", path.display()))?;
- let actual_name = actual_name.strip_suffix(".exe").unwrap_or(actual_name);
- if actual_name != manifest.name {
- return Err(format!(
- "{}: tool name mismatch, expected {}, found {}",
- path.display(),
- manifest.name,
- actual_name
- ));
- }
- let actual_version = tool_version(path)?;
- let actual_sha256 = sha256_file(path)?;
- if actual_version != manifest.version || actual_sha256 != manifest.binary_sha256 {
- eprintln!(
- "warning: {} is non-canonical shader tooling (expected version {:?}, SHA-256 {}; found version {:?}, SHA-256 {}). Checked-in SPIR-V hashes remain mandatory.",
- path.display(),
- manifest.version,
- manifest.binary_sha256,
- actual_version,
- actual_sha256
- );
- }
- Ok(())
-}
-
-fn tool_version(path: &Path) -> Result<String, String> {
- let output = Command::new(path)
- .arg("--version")
- .output()
- .map_err(|err| format!("{} --version: {err}", path.display()))?;
- if !output.status.success() {
- return Err(format!(
- "{} --version exited with {}",
- path.display(),
- output.status
- ));
- }
- let stdout = String::from_utf8(output.stdout)
- .map_err(|err| format!("{} --version: invalid UTF-8: {err}", path.display()))?;
- stdout
- .lines()
- .find(|line| !line.trim().is_empty())
- .map(str::trim)
- .map(|line| {
- line.strip_prefix("Glslang Version: ")
- .unwrap_or(line)
- .to_string()
- })
- .ok_or_else(|| format!("{} --version returned no version line", path.display()))
-}
-
-fn verify_shader_module(
- manifest: &ShaderManifestJson,
- module: &ShaderModuleManifestJson,
- compiler_path: &Path,
- validator_path: &Path,
- out_dir: &Path,
-) -> Result<(), String> {
- let source_path = workspace_relative_path(&module.source_path);
- let checked_in_spirv_path = workspace_relative_path(&module.spirv_path);
- let generated_spirv_path = out_dir.join(format!("{}.spv", module.name));
-
- let source_sha256 = sha256_canonical_text_file(&source_path)?;
- if source_sha256 != module.source_sha256 {
- return Err(format!(
- "{}: source SHA-256 mismatch, expected {}, found {}",
- source_path.display(),
- module.source_sha256,
- source_sha256
- ));
- }
-
- let checked_in_spirv_sha256 = sha256_file(&checked_in_spirv_path)?;
- if checked_in_spirv_sha256 != module.sha256 {
- return Err(format!(
- "{}: checked-in SPIR-V SHA-256 mismatch, expected {}, found {}",
- checked_in_spirv_path.display(),
- module.sha256,
- checked_in_spirv_sha256
- ));
- }
-
- compile_shader_module(
- compiler_path,
- &manifest.target_env,
- module,
- &source_path,
- &generated_spirv_path,
- )?;
- validate_shader_module(validator_path, &manifest.target_env, &generated_spirv_path)?;
-
- let generated_spirv_sha256 = sha256_file(&generated_spirv_path)?;
- if generated_spirv_sha256 != module.sha256 {
- return Err(format!(
- "{}: generated SPIR-V SHA-256 mismatch, expected {}, found {}",
- generated_spirv_path.display(),
- module.sha256,
- generated_spirv_sha256
- ));
- }
- Ok(())
-}
-
-fn compile_shader_module(
- compiler_path: &Path,
- target_env: &str,
- module: &ShaderModuleManifestJson,
- source_path: &Path,
- output_path: &Path,
-) -> Result<(), String> {
- let stage = glslang_stage(&module.stage).ok_or_else(|| {
- format!(
- "{}: unsupported shader stage {:?}",
- source_path.display(),
- module.stage
- )
- })?;
- let output = Command::new(compiler_path)
- .args(["-V", "--target-env", target_env, "-S", stage, "-e"])
- .arg(&module.entry_point)
- .arg(source_path)
- .arg("-o")
- .arg(output_path)
- .output()
- .map_err(|err| {
- format!(
- "{}: shader compile failed to start: {err}",
- source_path.display()
- )
- })?;
- if !output.status.success() {
- return Err(format!(
- "{}: shader compile failed:\n{}{}",
- source_path.display(),
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
- Ok(())
-}
-
-fn validate_shader_module(
- validator_path: &Path,
- target_env: &str,
- module_path: &Path,
-) -> Result<(), String> {
- let output = Command::new(validator_path)
- .args(["--target-env", target_env])
- .arg(module_path)
- .output()
- .map_err(|err| {
- format!(
- "{}: shader validation failed to start: {err}",
- module_path.display()
- )
- })?;
- if !output.status.success() {
- return Err(format!(
- "{}: shader validation failed:\n{}{}",
- module_path.display(),
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
- Ok(())
-}
-
-fn glslang_stage(stage: &str) -> Option<&'static str> {
- match stage {
- "vertex" => Some("vert"),
- "fragment" => Some("frag"),
- _ => None,
- }
-}
-
-fn sha256_file(path: &Path) -> Result<String, String> {
- let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- Ok(sha256_hex(&sha256(&bytes)))
-}
-
-fn sha256_canonical_text_file(path: &Path) -> Result<String, String> {
- let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let mut canonical = Vec::with_capacity(bytes.len());
- let mut index = 0;
- while index < bytes.len() {
- if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') {
- canonical.push(b'\n');
- index += 2;
- } else {
- canonical.push(bytes[index]);
- index += 1;
- }
- }
- Ok(sha256_hex(&sha256(&canonical)))
-}
-
-fn shader_provenance_output_dir() -> PathBuf {
- let nonce = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .as_nanos();
- workspace_root_path()
- .join("target")
- .join("fparkan")
- .join("shader-provenance")
- .join(format!("{}-{}", std::process::id(), nonce))
-}
-
-fn run_cargo_deny() -> Result<(), String> {
- validate_supply_chain_policy_config(&workspace_relative_path(SUPPLY_CHAIN_POLICY_CONFIG))?;
- let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into());
- let version_output = match Command::new(&cargo_deny).arg("--version").output() {
- Ok(output) => output,
- Err(err) => {
- return handle_cargo_deny_fallback(&format!(
- "failed to run cargo-deny --version: {err}"
- ));
- }
- };
- if !version_output.status.success() {
- return handle_cargo_deny_fallback(&format!(
- "cargo-deny --version exited with {}",
- version_output.status
- ));
- }
- let version_text = String::from_utf8(version_output.stdout)
- .map_err(|err| format!("cargo-deny --version produced invalid UTF-8: {err}"))?;
- if !version_text.contains(PINNED_CARGO_DENY_VERSION) {
- return handle_cargo_deny_fallback(&format!(
- "cargo-deny version mismatch: expected {PINNED_CARGO_DENY_VERSION}, found {}",
- version_text.trim()
- ));
- }
-
- let status = Command::new(cargo_deny)
- .args(["check", "advisories", "bans", "licenses", "sources"])
- .status()
- .map_err(|err| format!("failed to run cargo-deny: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo-deny exited with {status}"))
- }
-}
-
-const PINNED_CARGO_DENY_VERSION: &str = "0.19.9";
-
-fn handle_cargo_deny_fallback(reason: &str) -> Result<(), String> {
- if allow_supply_chain_fallback() {
- eprintln!(
- "{reason}; running built-in supply-chain policy fallback because {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is set"
- );
- run_builtin_supply_chain_policy(Path::new("."))
- } else if std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && ci_env_active() {
- Err(format!(
- "{reason}; {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is for local developer convenience only and is forbidden when CI is set"
- ))
- } else {
- Err(format!(
- "{reason}; install cargo-deny {PINNED_CARGO_DENY_VERSION} or explicitly opt into the fallback with {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV}=1"
- ))
- }
-}
-
-fn validate_supply_chain_policy_config(path: &Path) -> Result<(), String> {
- if path.is_file() {
- Ok(())
- } else {
- Err(format!(
- "reviewed supply-chain policy config is missing: {}",
- path.display()
- ))
- }
-}
-
-fn allow_supply_chain_fallback() -> bool {
- std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && !ci_env_active()
-}
-
-fn ci_env_active() -> bool {
- ci_env_value_is_active(std::env::var("CI").ok().as_deref())
-}
-
-fn ci_env_value_is_active(value: Option<&str>) -> bool {
- value.is_some_and(|value| {
- let trimmed = value.trim();
- !trimmed.is_empty() && trimmed != "0" && !trimmed.eq_ignore_ascii_case("false")
- })
-}
-
-fn run_builtin_supply_chain_policy(root: &Path) -> Result<(), String> {
- let mut failures = Vec::new();
- validate_workspace_license(root, &mut failures)?;
- validate_lockfile_supply_chain(root, &mut failures)?;
- if failures.is_empty() {
- Ok(())
- } else {
- Err(format!(
- "built-in supply-chain policy failed:\n{}",
- failures.join("\n")
- ))
- }
-}
-
-fn run_cargo_doc() -> Result<(), String> {
- let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let status = Command::new(cargo)
- .args([
- "doc",
- "--workspace",
- "--all-features",
- "--locked",
- "--no-deps",
- ])
- .env(
- "RUSTDOCFLAGS",
- "-D warnings -D rustdoc::broken_intra_doc_links",
- )
- .status()
- .map_err(|err| format!("failed to run cargo doc: {err}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("cargo doc exited with {status}"))
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct LicensedCorpusRoots {
- part1: PathBuf,
- part2: PathBuf,
-}
-
-impl LicensedCorpusRoots {
- fn envs(&self) -> [(&str, &Path); 2] {
- [
- (PART1_ROOT_ENV, self.part1.as_path()),
- (PART2_ROOT_ENV, self.part2.as_path()),
- ]
- }
-}
-
-fn load_licensed_roots(manifest: Option<&Path>) -> Result<LicensedCorpusRoots, String> {
- let manifest = manifest
- .map(Path::to_path_buf)
- .or_else(|| std::env::var_os(CORPORA_MANIFEST_ENV).map(PathBuf::from))
- .ok_or_else(|| {
- format!(
- "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>"
- )
- })?;
- parse_licensed_manifest(&manifest)
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct LicensedManifest {
- schema: Option<u8>,
- #[serde(rename = "corpus")]
- corpora: Vec<CorpusEntry>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct CorpusEntry {
- id: String,
- kind: CorpusKind,
- root: String,
- expected_profile: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "lowercase")]
-enum CorpusKind {
- Part1,
- Part2,
-}
-
-fn parse_licensed_manifest(path: &Path) -> Result<LicensedCorpusRoots, String> {
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let manifest: LicensedManifest = toml::from_str(&text)
- .map_err(|err| format!("failed to parse {}: {err}", path.display()))?;
- if manifest.schema.is_some_and(|version| version != 1) {
- return Err(format!(
- "unsupported corpora manifest schema {} (expected 1)",
- manifest.schema.unwrap_or(1)
- ));
- }
-
- let mut part1 = None;
- let mut part2 = None;
-
- for entry in manifest.corpora {
- match entry.kind {
- CorpusKind::Part1 => {
- let root = PathBuf::from(entry.root);
- assign_manifest_root(&mut part1, root, "part1")?;
- }
- CorpusKind::Part2 => {
- let root = PathBuf::from(entry.root);
- assign_manifest_root(&mut part2, root, "part2")?;
- }
- }
- if entry.expected_profile.is_none() {
- return Err(format!(
- "{}: corpus entry '{}' must define expected_profile",
- path.display(),
- entry.id
- ));
- }
- }
-
- let roots = LicensedCorpusRoots {
- part1: part1
- .ok_or_else(|| "licensed manifest is missing part1 corpus entry".to_string())?,
- part2: part2
- .ok_or_else(|| "licensed manifest is missing part2 corpus entry".to_string())?,
- };
- validate_licensed_part("part1", &roots.part1)?;
- validate_licensed_part("part2", &roots.part2)?;
- Ok(roots)
-}
-
-fn assign_manifest_root(
- target: &mut Option<PathBuf>,
- root: PathBuf,
- kind: &str,
-) -> Result<(), String> {
- if target.replace(root).is_some() {
- return Err(format!("licensed manifest contains duplicate {kind} root"));
- }
- Ok(())
-}
-
-fn validate_licensed_part(kind: &str, root: &Path) -> Result<(), String> {
- if root.is_dir() {
- Ok(())
- } else {
- Err(format!(
- "licensed corpus {kind} root is missing: {}",
- root.display()
- ))
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct CorpusBaselineOptions {
- root: PathBuf,
- out: Option<PathBuf>,
-}
-
-fn parse_corpus_baseline_options(args: &[String]) -> Result<CorpusBaselineOptions, String> {
- let mut root = None;
- let mut out = None;
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--root" => {
- root = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--root requires a path".to_string())?,
- );
- }
- "--out" => {
- out = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--out requires a path".to_string())?,
- );
- }
- other => return Err(format!("unknown corpus baseline option: {other}")),
- }
- }
- Ok(CorpusBaselineOptions {
- root: root.ok_or_else(|| "missing --root".to_string())?,
- out,
- })
-}
-
-fn run_corpus_baseline(options: &CorpusBaselineOptions) -> Result<(), String> {
- let manifest =
- discover(&options.root, DiscoverOptions::default()).map_err(|error| error.to_string())?;
- let baseline = report(&options.root, &manifest).map_err(|error| error.to_string())?;
- let json = render_report_json(&baseline);
- if let Some(out) = &options.out {
- if let Some(parent) = out.parent() {
- fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?;
- }
- fs::write(out, json).map_err(|error| format!("{}: {error}", out.display()))?;
- println!("{}", out.display());
- } else {
- println!("{json}");
- }
- Ok(())
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct PackageOptions {
- target: String,
- app: AppPackage,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum AppPackage {
- Cli,
- Game,
- Headless,
- Viewer,
-}
-
-impl AppPackage {
- fn parse(value: &str) -> Result<Self, String> {
- match value {
- "cli" => Ok(Self::Cli),
- "game" => Ok(Self::Game),
- "headless" => Ok(Self::Headless),
- "viewer" => Ok(Self::Viewer),
- _ => Err(format!("unknown app: {value}")),
- }
- }
-
- fn package(self) -> &'static str {
- match self {
- Self::Cli => "fparkan-cli",
- Self::Game => "fparkan-game",
- Self::Headless => "fparkan-headless",
- Self::Viewer => "fparkan-viewer",
- }
- }
-}
-
-fn parse_package_options(args: &[String]) -> Result<PackageOptions, String> {
- let mut target = None;
- let mut app = None;
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--target" => {
- target = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--target requires a value".to_string())?,
- );
- }
- "--app" => {
- let value = iter
- .next()
- .ok_or_else(|| "--app requires a value".to_string())?;
- app = Some(AppPackage::parse(value)?);
- }
- _ => return Err(format!("unknown package option: {arg}")),
- }
- }
- Ok(PackageOptions {
- target: target.ok_or_else(|| "missing --target".to_string())?,
- app: app.ok_or_else(|| "missing --app".to_string())?,
- })
-}
-
-fn run_package(options: &PackageOptions) -> Result<(), String> {
- cargo_owned(&[
- "build".to_string(),
- "-p".to_string(),
- options.app.package().to_string(),
- "--release".to_string(),
- "--locked".to_string(),
- "--offline".to_string(),
- "--target".to_string(),
- options.target.clone(),
- ])
-}
-
-fn run_policy(root: &Path) -> Result<(), String> {
- let mut failures = Vec::new();
- validate_toolchain_policy(root, &mut failures)?;
- scan_policy_dir(root, &mut failures)?;
- validate_cargo_metadata(root, &mut failures)?;
- validate_cargo_metadata_dependency_closures(root, &mut failures)?;
- validate_lockfile(root, &mut failures);
- validate_workspace_license(root, &mut failures)?;
- validate_dependency_boundaries(root, &mut failures)?;
- if failures.is_empty() {
- Ok(())
- } else {
- Err(format!("workspace policy failed:\n{}", failures.join("\n")))
- }
-}
-
-fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let manifest = root.join("Cargo.toml");
- if !manifest.exists() {
- return Ok(());
- }
- let metadata = workspace_metadata(root)?;
- if metadata.workspace_members.is_empty() {
- failures.push(format!(
- "{}: cargo metadata produced no workspace members",
- manifest.display()
- ));
- return Ok(());
- }
- let stage_manifest_path = root.join(STAGE_PACKAGE_MANIFEST);
- let stage_manifest = load_stage_package_manifest(&stage_manifest_path)?;
- let workspace_packages = metadata
- .workspace_packages()
- .iter()
- .map(|package| package.name.to_string())
- .collect::<BTreeSet<_>>();
- if let Err(err) =
- validate_stage_package_entries(&stage_manifest, &workspace_packages, &stage_manifest_path)
- {
- failures.push(err);
- }
- Ok(())
-}
-
-fn validate_cargo_metadata_dependency_closures(
- root: &Path,
- failures: &mut Vec<String>,
-) -> Result<(), String> {
- let metadata = workspace_metadata(root)?;
- let deps_by_package = workspace_dependency_names(&metadata);
- validate_package_closure_excludes("fparkan-headless", &deps_by_package, failures);
- Ok(())
-}
-
-/// Returns declared direct dependencies for each workspace package from Cargo's
-/// own metadata model. Cargo, rather than a source-text heuristic, resolves
-/// package identities and workspace membership before this policy sees them.
-fn workspace_dependency_names(
- metadata: &cargo_metadata::Metadata,
-) -> BTreeMap<String, BTreeSet<String>> {
- metadata
- .workspace_packages()
- .iter()
- .map(|package| {
- (
- package.name.to_string(),
- package
- .dependencies
- .iter()
- .map(|dependency| dependency.name.clone())
- .collect(),
- )
- })
- .collect()
-}
-
-fn validate_package_closure_excludes(
- package: &str,
- deps_by_package: &BTreeMap<String, BTreeSet<String>>,
- failures: &mut Vec<String>,
-) {
- if !deps_by_package.contains_key(package) {
- failures.push(format!(
- "workspace manifest graph missing package {package}"
- ));
- return;
- }
- let closure = dependency_closure_names(package, deps_by_package);
- if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&closure) {
- failures.push(format!(
- "workspace manifest closure: package {package} depends on forbidden platform/render dependency {forbidden}"
- ));
- }
-}
-
-fn dependency_closure_names(
- root: &str,
- deps_by_package: &BTreeMap<String, BTreeSet<String>>,
-) -> BTreeSet<String> {
- let mut seen = BTreeSet::new();
- let mut names = BTreeSet::new();
- let mut stack = deps_by_package
- .get(root)
- .cloned()
- .unwrap_or_default()
- .into_iter()
- .collect::<Vec<_>>();
- while let Some(name) = stack.pop() {
- if !seen.insert(name.clone()) {
- continue;
- }
- names.insert(name.clone());
- if let Some(deps) = deps_by_package.get(&name) {
- stack.extend(deps.iter().cloned());
- }
- }
- names
-}
-
-fn validate_toolchain_policy(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let toolchain_path = root.join("rust-toolchain.toml");
- let toolchain_text = fs::read_to_string(&toolchain_path)
- .map_err(|err| format!("{}: {err}", toolchain_path.display()))?;
- let toolchain = toml::from_str::<RustToolchainToml>(&toolchain_text)
- .map_err(|err| format!("{}: invalid TOML: {err}", toolchain_path.display()))?;
- if toolchain.toolchain.channel != PINNED_RUST_TOOLCHAIN {
- failures.push(format!(
- "{}: toolchain channel must be exact {PINNED_RUST_TOOLCHAIN}",
- toolchain_path.display()
- ));
- }
- if !is_exact_rust_patch_version(&toolchain.toolchain.channel) {
- failures.push(format!(
- "{}: toolchain channel must include major.minor.patch, not a moving channel",
- toolchain_path.display()
- ));
- }
-
- let manifest_path = root.join("Cargo.toml");
- let manifest_text = fs::read_to_string(&manifest_path)
- .map_err(|err| format!("{}: {err}", manifest_path.display()))?;
- let manifest = toml::from_str::<WorkspaceManifestToml>(&manifest_text)
- .map_err(|err| format!("{}: invalid TOML: {err}", manifest_path.display()))?;
- if manifest.workspace.package.rust_version != WORKSPACE_MSRV {
- failures.push(format!(
- "{}: workspace.package.rust-version must be {WORKSPACE_MSRV}",
- manifest_path.display()
- ));
- }
- if !PINNED_RUST_TOOLCHAIN.starts_with(&format!("{}.", manifest.workspace.package.rust_version))
- {
- failures.push(format!(
- "{}: workspace.package.rust-version must match pinned toolchain major.minor",
- manifest_path.display()
- ));
- }
- Ok(())
-}
-
-fn is_exact_rust_patch_version(value: &str) -> bool {
- let parts = value.split('.').collect::<Vec<_>>();
- parts.len() == 3
- && parts
- .iter()
- .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct RustToolchainToml {
- toolchain: RustToolchainTable,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct RustToolchainTable {
- channel: String,
- #[allow(dead_code)]
- components: Option<Vec<String>>,
- #[allow(dead_code)]
- targets: Option<Vec<String>>,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceManifestToml {
- workspace: WorkspaceTable,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceTable {
- package: WorkspacePackageTable,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspacePackageTable {
- #[serde(rename = "rust-version")]
- rust_version: String,
-}
-
-fn validate_lockfile(root: &Path, failures: &mut Vec<String>) {
- let lockfile = root.join("Cargo.lock");
- if !lockfile.is_file() {
- failures.push(format!(
- "{}: workspace lockfile is required for locked/offline builds",
- lockfile.display()
- ));
- }
-}
-
-fn validate_lockfile_supply_chain(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let lockfile = root.join("Cargo.lock");
- let packages = read_lockfile_packages(&lockfile)?;
- for package in packages {
- if let Some(source) = package.source.as_deref() {
- if source != APPROVED_REGISTRY_SOURCE {
- failures.push(format!(
- "{}: package {} {} uses unapproved source {source}",
- lockfile.display(),
- package.name,
- package.version
- ));
- }
- }
- if SUPPLY_CHAIN_BANNED_PACKAGES.contains(&package.name.as_str()) {
- failures.push(format!(
- "{}: package {} {} is banned by built-in supply-chain policy",
- lockfile.display(),
- package.name,
- package.version
- ));
- }
- }
- Ok(())
-}
-
-fn read_lockfile_packages(lockfile: &Path) -> Result<Vec<CargoLockPackage>, String> {
- let text =
- fs::read_to_string(lockfile).map_err(|err| format!("{}: {err}", lockfile.display()))?;
- let parsed = toml::from_str::<CargoLock>(&text)
- .map_err(|err| format!("{}: invalid Cargo.lock TOML: {err}", lockfile.display()))?;
- Ok(parsed.package)
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoLock {
- package: Vec<CargoLockPackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoLockPackage {
- name: String,
- version: String,
- source: Option<String>,
-}
-
-fn validate_workspace_license(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let manifest = root.join("Cargo.toml");
- let license = fs::read_to_string(root.join("LICENSE.txt"))
- .map_err(|err| format!("{}: {err}", root.join("LICENSE.txt").display()))?;
- let expected = if license.contains("GNU GENERAL PUBLIC LICENSE")
- && license.contains("Version 2, June 1991")
- {
- "GPL-2.0-only"
- } else {
- failures.push(format!(
- "{}: unsupported repository license text",
- root.join("LICENSE.txt").display()
- ));
- return Ok(());
- };
-
- let mut manifests = Vec::new();
- collect_cargo_manifests(root, &mut manifests)?;
- manifests.push(manifest);
- manifests.sort();
- manifests.dedup();
-
- for manifest in manifests {
- let policy = load_cargo_manifest_policy(&manifest)?;
- let explicit_license = policy.license();
- let is_root = manifest == root.join("Cargo.toml");
- if is_root {
- if explicit_license.as_deref() != Some(expected) {
- failures.push(format!(
- "{}: workspace.package license must be {expected}",
- manifest.display()
- ));
- }
- } else if let Some(license) = explicit_license {
- if license != expected {
- failures.push(format!(
- "{}: package license {license} does not match repository license {expected}",
- manifest.display()
- ));
- }
- }
- }
- Ok(())
-}
-
-fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let mut manifests = Vec::new();
- collect_cargo_manifests(root, &mut manifests)?;
- for manifest in manifests {
- let policy = load_cargo_manifest_policy(&manifest)?;
- let Some(package) = policy.package_name() else {
- continue;
- };
- if is_removed_legacy_adapter_manifest(root, &manifest) {
- failures.push(format!(
- "{}: legacy SDL/OpenGL adapter crate must be removed: {package}",
- manifest.display()
- ));
- continue;
- }
- let dependencies = policy.dependency_names();
- if !is_adapter_like_package(package) && !is_app_package(package) {
- for dependency in &dependencies {
- if is_forbidden_gui_dependency(dependency) {
- failures.push(format!(
- "{}: package {package} depends on forbidden GUI/adapter package {dependency}",
- manifest.display()
- ));
- }
- }
- }
- if is_app_package(package) {
- if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) {
- failures.push(format!(
- "{}: app package {package} depends on parser crate {forbidden}",
- manifest.display()
- ));
- }
- }
- if package == "fparkan-headless" {
- if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&dependencies) {
- failures.push(format!(
- "{}: headless package {package} depends on platform/render bridge dependency {forbidden}",
- manifest.display()
- ));
- }
- }
-
- if package == "fparkan-runtime" {
- if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) {
- failures.push(format!(
- "{}: runtime package {package} depends on parser crate {forbidden}",
- manifest.display()
- ));
- }
- if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&dependencies) {
- failures.push(format!(
- "{}: runtime package {package} depends on forbidden platform/driver crate {forbidden}",
- manifest.display()
- ));
- }
- }
-
- if package == "fparkan-prototype" {
- if let Some(forbidden) = first_forbidden_visual_dependency(&dependencies) {
- failures.push(format!(
- "{}: prototype package {package} depends on forbidden visual parser {forbidden}",
- manifest.display()
- ));
- }
- }
- }
- Ok(())
-}
-
-fn is_app_package(package: &str) -> bool {
- matches!(
- package,
- "fparkan-cli"
- | "fparkan-game"
- | "fparkan-headless"
- | "fparkan-vulkan-smoke"
- | "fparkan-viewer"
- )
-}
-
-fn is_adapter_like_package(package: &str) -> bool {
- matches!(package, "fparkan-platform-winit" | "fparkan-render-vulkan")
-}
-
-fn first_forbidden_parser_dependency(dependencies: &BTreeSet<String>) -> Option<&str> {
- [
- "fparkan-msh",
- "fparkan-nres",
- "fparkan-rsli",
- "fparkan-terrain-format",
- "fparkan-texm",
- "fparkan-mission-format",
- "fparkan-material",
- "fparkan-fx",
- ]
- .iter()
- .find_map(|forbidden| {
- if dependencies.contains(*forbidden) {
- Some(*forbidden)
- } else {
- None
- }
- })
-}
-
-fn first_forbidden_visual_dependency(dependencies: &BTreeSet<String>) -> Option<&str> {
- [
- "fparkan-msh",
- "fparkan-material",
- "fparkan-texm",
- "fparkan-fx",
- "fparkan-terrain-format",
- ]
- .iter()
- .find_map(|forbidden| {
- if dependencies.contains(*forbidden) {
- Some(*forbidden)
- } else {
- None
- }
- })
-}
-
-fn first_forbidden_platform_bridge_dependency(dependencies: &BTreeSet<String>) -> Option<&str> {
- [
- "fparkan-platform-winit",
- "fparkan-render-vulkan",
- "winit",
- "ash",
- "ash-window",
- ]
- .iter()
- .find_map(|forbidden| {
- if dependencies.contains(*forbidden) {
- Some(*forbidden)
- } else {
- None
- }
- })
-}
-
-fn is_forbidden_domain_dependency(dependency: &str) -> bool {
- matches!(
- dependency,
- "fparkan-cli"
- | "fparkan-game"
- | "fparkan-headless"
- | "fparkan-viewer"
- | "fparkan-platform-sdl"
- | "fparkan-render-gl"
- | "sdl2"
- | "gl"
- | "glow"
- | "glium"
- | "glutin"
- )
-}
-
-fn is_forbidden_gui_dependency(dependency: &str) -> bool {
- is_forbidden_domain_dependency(dependency) || is_forbidden_platform_dependency(dependency)
-}
-
-fn is_forbidden_platform_dependency(dependency: &str) -> bool {
- matches!(
- dependency,
- "fparkan-platform-winit" | "fparkan-render-vulkan" | "winit" | "ash" | "ash-window"
- )
-}
-
-fn collect_cargo_manifests(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
- 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 should_skip_policy_path(&path) {
- continue;
- }
- let file_type = entry
- .file_type()
- .map_err(|err| format!("{}: {err}", path.display()))?;
- if file_type.is_dir() {
- collect_cargo_manifests(&path, out)?;
- } else if file_type.is_file()
- && path
- .file_name()
- .and_then(|name| name.to_str())
- .is_some_and(|name| name == "Cargo.toml")
- {
- out.push(path);
- }
- }
- Ok(())
-}
-
-/// Typed Cargo manifest subset used by the architecture policy.
-///
-/// Dependency values deliberately remain `toml::Value`: the policy only needs
-/// keys and Cargo accepts several value forms (version string, inline table,
-/// or workspace inheritance).
-#[derive(Debug, Default, Deserialize)]
-struct CargoManifestPolicy {
- package: Option<CargoPackagePolicy>,
- workspace: Option<CargoWorkspacePolicy>,
- dependencies: Option<BTreeMap<String, toml::Value>>,
- #[serde(rename = "dev-dependencies")]
- dev_dependencies: Option<BTreeMap<String, toml::Value>>,
- #[serde(rename = "build-dependencies")]
- build_dependencies: Option<BTreeMap<String, toml::Value>>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoPackagePolicy {
- name: String,
- license: Option<toml::Value>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoWorkspacePolicy {
- package: Option<CargoWorkspacePackagePolicy>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoWorkspacePackagePolicy {
- license: Option<toml::Value>,
-}
-
-impl CargoManifestPolicy {
- fn package_name(&self) -> Option<&str> {
- self.package.as_ref().map(|package| package.name.as_str())
- }
-
- fn license(&self) -> Option<String> {
- self.package
- .as_ref()
- .and_then(|package| cargo_license_string(package.license.as_ref()))
- .or_else(|| {
- self.workspace
- .as_ref()
- .and_then(|workspace| workspace.package.as_ref())
- .and_then(|package| cargo_license_string(package.license.as_ref()))
- })
- }
-
- fn dependency_names(&self) -> BTreeSet<String> {
- [
- self.dependencies.as_ref(),
- self.dev_dependencies.as_ref(),
- self.build_dependencies.as_ref(),
- ]
- .into_iter()
- .flatten()
- .flat_map(|dependencies| dependencies.keys().cloned())
- .collect()
- }
-}
-
-fn cargo_license_string(value: Option<&toml::Value>) -> Option<String> {
- value.and_then(toml::Value::as_str).map(ToString::to_string)
-}
-
-fn load_cargo_manifest_policy(path: &Path) -> Result<CargoManifestPolicy, String> {
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- toml::from_str(&text).map_err(|err| format!("{}: invalid Cargo TOML: {err}", path.display()))
-}
-
-fn is_removed_legacy_adapter_manifest(root: &Path, manifest: &Path) -> bool {
- let normalized = manifest.strip_prefix(root).unwrap_or(manifest);
- normalized.starts_with("adapters/fparkan-platform-sdl")
- || normalized.starts_with("adapters/fparkan-render-gl")
-}
-
-fn scan_policy_dir(dir: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let entries = match fs::read_dir(dir) {
- Ok(entries) => entries,
- Err(err) => return Err(format!("{}: {err}", dir.display())),
- };
- for entry in entries {
- let entry = entry.map_err(|err| format!("{}: {err}", dir.display()))?;
- let path = entry.path();
- if should_skip_policy_path(&path) {
- continue;
- }
- let file_type = entry
- .file_type()
- .map_err(|err| format!("{}: {err}", path.display()))?;
- if file_type.is_dir() {
- if is_forbidden_generic_crate_dir(&path) {
- failures.push(format!(
- "{}: package under crates/ must use the fparkan-* prefix",
- path.display()
- ));
- }
- scan_policy_dir(&path, failures)?;
- } else if file_type.is_file() {
- scan_repository_file_policy(&path, failures)?;
- if is_policy_source(&path) {
- scan_policy_file(&path, failures)?;
- }
- }
- }
- Ok(())
-}
-
-fn should_skip_policy_path(path: &Path) -> bool {
- path.file_name()
- .and_then(|name| name.to_str())
- .is_some_and(|name| {
- matches!(
- name,
- ".git" | "target" | "testdata" | ".idea" | ".vscode" | ".DS_Store"
- )
- })
-}
-
-fn is_policy_source(path: &Path) -> bool {
- path.extension()
- .and_then(|ext| ext.to_str())
- .is_some_and(|ext| matches!(ext, "rs" | "toml"))
-}
-
-fn is_forbidden_generic_crate_dir(path: &Path) -> bool {
- path.parent()
- .and_then(Path::file_name)
- .and_then(|name| name.to_str())
- .is_some_and(|name| name == "crates")
- && path
- .file_name()
- .and_then(|name| name.to_str())
- .is_some_and(|name| !name.starts_with("fparkan-"))
-}
-
-fn scan_repository_file_policy(path: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- if path
- .extension()
- .and_then(|ext| ext.to_str())
- .is_some_and(|ext| ext == "py")
- {
- failures.push(format!(
- "{}: Python source file is forbidden",
- path.display()
- ));
- }
-
- let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- if bytes.starts_with(b"#!") {
- let first_line = bytes
- .split(|byte| *byte == b'\n')
- .next()
- .unwrap_or_default()
- .to_ascii_lowercase();
- if first_line
- .windows("python".len())
- .any(|window| window == b"python")
- {
- failures.push(format!("{}: Python shebang is forbidden", path.display()));
- }
- }
- if is_workflow_file(path) {
- let text = String::from_utf8_lossy(&bytes).to_ascii_lowercase();
- if text.contains("python") {
- failures.push(format!("{}: Python CI step is forbidden", path.display()));
- }
- }
- Ok(())
-}
-
-fn is_workflow_file(path: &Path) -> bool {
- let mut previous = None;
- for component in path.components() {
- let name = component.as_os_str().to_string_lossy();
- if previous.as_deref() == Some(".github") && name == "workflows" {
- return true;
- }
- previous = Some(name.into_owned());
- }
- false
-}
-
-fn scan_policy_file(path: &Path, failures: &mut Vec<String>) -> Result<(), String> {
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let lower = text.to_ascii_lowercase();
- if lower.contains(concat!("app.", "notion.com")) || lower.contains(concat!("385e", "79f2")) {
- failures.push(format!(
- "{}: external knowledge-base reference in source",
- path.display()
- ));
- }
- validate_unsafe_module_attribute_policy(path, &text, failures);
- let mut previous_line_has_safety_comment = false;
- for (index, line) in text.lines().enumerate() {
- let trimmed = line.trim_start();
- if is_comment_line(trimmed) {
- previous_line_has_safety_comment = has_safety_comment(trimmed);
- continue;
- }
- if trimmed.is_empty() {
- previous_line_has_safety_comment = false;
- continue;
- }
- if contains_unsafe_construct(trimmed)
- && !is_authorized_unsafe_construct(path, trimmed, previous_line_has_safety_comment)
- {
- failures.push(format!(
- "{}:{}: unsafe construct in workspace source",
- path.display(),
- index + 1
- ));
- }
- previous_line_has_safety_comment = false;
- }
- Ok(())
-}
-
-fn validate_unsafe_module_attribute_policy(path: &Path, text: &str, failures: &mut Vec<String>) {
- let declares_unsafe_module_boundary = text
- .lines()
- .map(str::trim_start)
- .any(|line| line.starts_with("#![allow(unsafe_code)]"));
- match (
- is_audited_unsafe_source(path),
- declares_unsafe_module_boundary,
- ) {
- (true, false) => failures.push(format!(
- "{}: audited Vulkan FFI module must declare #![allow(unsafe_code)]",
- path.display()
- )),
- (false, true) => failures.push(format!(
- "{}: #![allow(unsafe_code)] is only permitted in audited Vulkan FFI modules",
- path.display()
- )),
- (true, true) | (false, false) => {}
- }
-}
-
-fn contains_unsafe_construct(line: &str) -> bool {
- line.contains(concat!("un", "safe {"))
- || line.contains(concat!("un", "safe fn"))
- || line.contains(concat!("un", "safe impl"))
- || line.contains(concat!("extern ", "\"C\""))
-}
-
-fn is_comment_line(line: &str) -> bool {
- line.starts_with("//") || line.starts_with("//!") || line.starts_with("///")
-}
-
-fn has_safety_comment(line: &str) -> bool {
- line.contains("SAFETY:")
-}
-
-const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[
- "adapters/fparkan-render-vulkan/src/ffi/instance.rs",
- "adapters/fparkan-render-vulkan/src/ffi/capabilities.rs",
- "adapters/fparkan-render-vulkan/src/ffi/resources.rs",
- "adapters/fparkan-render-vulkan/src/ffi/runtime.rs",
- "adapters/fparkan-render-vulkan/src/ffi/smoke.rs",
- "adapters/fparkan-render-vulkan/src/ffi/swapchain.rs",
- "adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs",
- "adapters/fparkan-render-vulkan/src/ffi/surface.rs",
- "adapters/fparkan-render-vulkan/src/ffi/validation.rs",
-];
-
-fn is_audited_unsafe_source(path: &Path) -> bool {
- let as_path = path.as_os_str().to_string_lossy().replace('\\', "/");
- AUDITED_UNSAFE_SOURCE_FILES
- .iter()
- .any(|candidate| as_path.ends_with(candidate))
-}
-
-fn is_authorized_unsafe_construct(
- path: &Path,
- line: &str,
- previous_line_has_safety_comment: bool,
-) -> bool {
- if !is_audited_unsafe_source(path) {
- return false;
- }
- previous_line_has_safety_comment || has_safety_comment(line)
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum Stage {
- All,
- Number(u8),
-}
-
-impl Stage {
- fn parse(value: &str) -> Result<Self, String> {
- if value == "all" {
- return Ok(Self::All);
- }
- let stage = value
- .parse::<u8>()
- .map_err(|_| format!("invalid stage: {value}"))?;
- if stage <= 5 {
- Ok(Self::Number(stage))
- } else {
- Err(format!("stage out of range: {stage}"))
- }
- }
-}
-
-impl fmt::Display for Stage {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::All => f.write_str("all"),
- Self::Number(stage) => write!(f, "{stage}"),
- }
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct TestOptions {
- stage: Stage,
- root: PathBuf,
- manifest: Option<PathBuf>,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum TestSuite {
- Licensed,
- Synthetic,
-}
-
-impl TestSuite {
- fn parse(value: &str) -> Result<Self, String> {
- match value {
- "licensed" => Ok(Self::Licensed),
- "synthetic" => Ok(Self::Synthetic),
- _ => Err(format!("unknown suite: {value}")),
- }
- }
-
- fn as_str(self) -> &'static str {
- match self {
- Self::Licensed => "licensed",
- Self::Synthetic => "synthetic",
- }
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct AcceptanceOptions {
- suite: TestSuite,
- stage: Stage,
- root: PathBuf,
- manifest: Option<PathBuf>,
- out: PathBuf,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct AuditOptions {
- roadmap: PathBuf,
- coverage: PathBuf,
- out: PathBuf,
- strict: bool,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct NativeSmokeAuditOptions {
- dir: PathBuf,
- expected_commit: String,
- expected_shader_manifest_hash: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct ShaderManifestJson {
- target_env: String,
- compiler: ShaderToolManifestJson,
- validator: ShaderToolManifestJson,
- modules: Vec<ShaderModuleManifestJson>,
- manifest_hash: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct ShaderToolManifestJson {
- name: String,
- version: String,
- binary_sha256: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct ShaderModuleManifestJson {
- name: String,
- stage: String,
- entry_point: String,
- source_path: String,
- source_sha256: String,
- spirv_path: String,
- sha256: String,
-}
-
-fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOptions, String> {
- let mut options = TestOptions {
- stage: Stage::All,
- root: default_root,
- manifest: None,
- };
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--stage" => {
- let value = iter
- .next()
- .ok_or_else(|| "--stage requires a value".to_string())?;
- options.stage = Stage::parse(value)?;
- }
- "--root" => {
- let value = iter
- .next()
- .ok_or_else(|| "--root requires a path".to_string())?;
- options.root = PathBuf::from(value);
- }
- "--manifest" => {
- let value = iter
- .next()
- .ok_or_else(|| "--manifest requires a path".to_string())?;
- options.manifest = Some(PathBuf::from(value));
- }
- _ => return Err(format!("unknown test option: {arg}")),
- }
- }
- Ok(options)
-}
-
-fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String> {
- let mut suite = None;
- let mut stage = Stage::All;
- let mut root = PathBuf::from("testdata");
- let mut manifest = None;
- let mut out = None;
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--suite" => {
- let value = iter
- .next()
- .ok_or_else(|| "--suite requires a value".to_string())?;
- suite = Some(TestSuite::parse(value)?);
- }
- "--stage" => {
- let value = iter
- .next()
- .ok_or_else(|| "--stage requires a value".to_string())?;
- stage = Stage::parse(value)?;
- }
- "--root" => {
- let value = iter
- .next()
- .ok_or_else(|| "--root requires a path".to_string())?;
- root = PathBuf::from(value);
- }
- "--manifest" => {
- let value = iter
- .next()
- .ok_or_else(|| "--manifest requires a path".to_string())?;
- manifest = Some(PathBuf::from(value));
- }
- "--out" => {
- let value = iter
- .next()
- .ok_or_else(|| "--out requires a path".to_string())?;
- out = Some(PathBuf::from(value));
- }
- _ => return Err(format!("unknown acceptance option: {arg}")),
- }
- }
-
- let suite = suite.ok_or_else(|| "missing --suite".to_string())?;
- let out = out.unwrap_or_else(|| {
- PathBuf::from("target")
- .join("fparkan")
- .join("reports")
- .join("acceptance")
- .join(format!("{}-stage-{}.json", suite.as_str(), stage))
- });
- Ok(AcceptanceOptions {
- suite,
- stage,
- root,
- manifest,
- out,
- })
-}
-
-fn parse_audit_options(args: &[String]) -> Result<AuditOptions, String> {
- let mut roadmap = workspace_relative_path(CI_ACCEPTANCE_ROADMAP);
- let mut coverage = workspace_relative_path(CI_ACCEPTANCE_COVERAGE);
- let mut out = workspace_root_path()
- .join("target")
- .join("fparkan")
- .join("reports")
- .join("acceptance")
- .join("coverage-audit.json");
- let mut strict = false;
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--roadmap" => {
- let value = iter
- .next()
- .ok_or_else(|| "--roadmap requires a path".to_string())?;
- roadmap = PathBuf::from(value);
- }
- "--coverage" => {
- let value = iter
- .next()
- .ok_or_else(|| "--coverage requires a path".to_string())?;
- coverage = PathBuf::from(value);
- }
- "--out" => {
- let value = iter
- .next()
- .ok_or_else(|| "--out requires a path".to_string())?;
- out = PathBuf::from(value);
- }
- "--strict" => strict = true,
- _ => return Err(format!("unknown audit option: {arg}")),
- }
- }
- Ok(AuditOptions {
- roadmap,
- coverage,
- out,
- strict,
- })
-}
-
-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() {
- "--dir" => {
- let value = iter
- .next()
- .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.clone_from(value);
- }
- "--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.clone_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())?,
- 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,
- &options.expected_commit,
- &options.expected_shader_manifest_hash,
- );
- 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()))?;
- let platform = platform.to_string();
- if reports.insert(platform.clone(), json).is_some() {
- return Err(format!(
- "{}: duplicate native smoke report for platform {platform}",
- path.display()
- ));
- }
- }
- Ok(reports)
-}
-
-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();
- 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);
- 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\""));
- } else {
- commit_shas.insert(commit_sha.to_string());
- }
- }
- if let Ok(toolchain) = json_string_field(report, "rust_toolchain") {
- rust_toolchains.insert(toolchain.to_string());
- }
- }
- for platform in reports.keys() {
- if !REQUIRED_NATIVE_SMOKE_PLATFORMS.contains(&platform.as_str()) {
- failures.push(format!("{platform}: unexpected native smoke platform"));
- }
- }
- if commit_shas.len() > 1 {
- failures.push(format!(
- "native smoke reports disagree on commit_sha: {}",
- commit_shas.into_iter().collect::<Vec<_>>().join(", ")
- ));
- }
- if rust_toolchains.len() > 1 {
- failures.push(format!(
- "native smoke reports disagree on rust_toolchain: {}",
- rust_toolchains.into_iter().collect::<Vec<_>>().join(", ")
- ));
- }
- 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,
- failures: &mut Vec<String>,
-) {
- validate_native_smoke_status_fields(platform, report, failures);
- validate_native_smoke_provenance_fields(platform, report, failures);
- validate_native_smoke_runtime_fields(platform, report, failures);
-}
-
-fn validate_native_smoke_status_fields(
- 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", "created", failures);
- expect_string_field(
- platform,
- report,
- "vulkan_surface_status",
- "created",
- failures,
- );
- expect_string_field(
- platform,
- report,
- "vulkan_device_status",
- "selected",
- failures,
- );
- expect_string_field(
- platform,
- report,
- "vulkan_logical_device_status",
- "created",
- failures,
- );
- expect_string_field(
- platform,
- report,
- "vulkan_swapchain_status",
- "created",
- failures,
- );
-}
-
-fn validate_native_smoke_provenance_fields(
- platform: &str,
- report: &serde_json::Value,
- failures: &mut Vec<String>,
-) {
- expect_commit_sha(platform, report, failures);
- expect_bool_field(platform, report, "git_dirty", failures);
- expect_bool_field_value(platform, report, "git_dirty", false, failures);
- expect_nonempty_string(platform, report, "runner_identity", failures);
- expect_nonempty_string(platform, report, "runner_architecture", failures);
- expect_runner_architecture_matches_platform(platform, report, failures);
- expect_string_field(
- platform,
- report,
- "rust_toolchain",
- &measured_rust_toolchain_version(),
- failures,
- );
- expect_string_field(platform, report, "platform", platform, failures);
- expect_nonempty_string(platform, report, "target_triple", failures);
- expect_target_triple_matches_platform(platform, report, failures);
-}
-
-fn validate_native_smoke_runtime_fields(
- platform: &str,
- report: &serde_json::Value,
- failures: &mut Vec<String>,
-) {
- 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_warning_count", 0, failures);
- expect_u64_field(platform, report, "validation_error_count", 0, failures);
- expect_nonempty_string(platform, report, "shader_manifest_hash", failures);
- expect_nonempty_string(platform, report, "vulkan_device_name", failures);
- expect_u64_at_least(
- platform,
- report,
- "vulkan_logical_device_enabled_extension_count",
- 1,
- failures,
- );
- expect_u64_at_least(
- platform,
- report,
- "vulkan_logical_device_graphics_queue_family",
- 0,
- failures,
- );
- expect_u64_at_least(
- platform,
- report,
- "vulkan_logical_device_present_queue_family",
- 0,
- failures,
- );
- expect_u64_at_least(platform, report, "vulkan_swapchain_width", 1, failures);
- expect_u64_at_least(platform, report, "vulkan_swapchain_height", 1, failures);
- expect_u64_at_least(
- platform,
- report,
- "vulkan_swapchain_image_count",
- 2,
- 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_bool_field(
- platform: &str,
- report: &serde_json::Value,
- field: &str,
- failures: &mut Vec<String>,
-) {
- match report.get(field) {
- Some(serde_json::Value::Bool(_)) => {}
- Some(_) => failures.push(format!("{platform}: {field} must be a boolean")),
- None => failures.push(format!("{platform}: missing {field}")),
- }
-}
-
-fn expect_bool_field_value(
- platform: &str,
- report: &serde_json::Value,
- field: &str,
- expected: bool,
- failures: &mut Vec<String>,
-) {
- match report.get(field) {
- Some(serde_json::Value::Bool(actual)) if *actual == expected => {}
- Some(serde_json::Value::Bool(actual)) => {
- failures.push(format!(
- "{platform}: {field} expected {expected}, found {actual}"
- ));
- }
- Some(_) => failures.push(format!("{platform}: {field} must be a boolean")),
- None => failures.push(format!("{platform}: missing {field}")),
- }
-}
-
-fn expect_commit_sha(platform: &str, report: &serde_json::Value, failures: &mut Vec<String>) {
- match report.get("commit_sha") {
- Some(serde_json::Value::String(commit_sha))
- if commit_sha.len() == 40 && commit_sha.chars().all(|ch| ch.is_ascii_hexdigit()) => {}
- Some(serde_json::Value::String(_)) => failures.push(format!(
- "{platform}: commit_sha must be a 40-character lowercase or uppercase hex string"
- )),
- Some(_) => failures.push(format!("{platform}: commit_sha must be a string")),
- None => failures.push(format!("{platform}: missing commit_sha")),
- }
-}
-
-fn expect_target_triple_matches_platform(
- platform: &str,
- report: &serde_json::Value,
- failures: &mut Vec<String>,
-) {
- let Ok(target_triple) = json_string_field(report, "target_triple") else {
- return;
- };
- let matches_platform = match platform {
- "windows" => target_triple.contains("windows"),
- _ => true,
- };
- if !matches_platform {
- failures.push(format!(
- "{platform}: target_triple {target_triple:?} does not match platform"
- ));
- }
-}
-
-fn expect_runner_architecture_matches_platform(
- platform: &str,
- report: &serde_json::Value,
- failures: &mut Vec<String>,
-) {
- let Ok(runner_architecture) = json_string_field(report, "runner_architecture") else {
- return;
- };
- let matches_platform = !runner_architecture.trim().is_empty();
- if !matches_platform {
- failures.push(format!(
- "{platform}: runner_architecture {runner_architecture:?} does not match platform policy"
- ));
- }
-}
-
-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()))?;
- let required = extract_acceptance_ids(&roadmap_text);
- let coverage = read_coverage_manifest(&options.coverage)?;
- let audit = build_acceptance_audit(&required, &coverage);
- if let Some(parent) = options.out.parent() {
- fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.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()) {
- Err(format!(
- "acceptance coverage incomplete: {} strict failures, {} unknown",
- strict_failures.len(),
- audit.unknown_coverage.len()
- ))
- } else {
- Ok(())
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct CoverageEntry {
- status: CoverageStatus,
- evidence: String,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum CoverageStatus {
- Covered,
- CoveredPlanning,
- CoveredStub,
- CoveredGpu,
- Partial,
- Blocked,
- Omitted,
-}
-
-impl CoverageStatus {
- fn parse(value: &str) -> Result<Self, String> {
- match value {
- "covered" => Ok(Self::Covered),
- "covered-planning" => Ok(Self::CoveredPlanning),
- "covered-stub" => Ok(Self::CoveredStub),
- "covered-gpu" => Ok(Self::CoveredGpu),
- "partial" => Ok(Self::Partial),
- "blocked" => Ok(Self::Blocked),
- "omitted" => Ok(Self::Omitted),
- _ => Err(format!("unknown coverage status: {value}")),
- }
- }
-
- const fn as_str(self) -> &'static str {
- match self {
- Self::Covered => "covered",
- Self::CoveredPlanning => "covered-planning",
- Self::CoveredStub => "covered-stub",
- Self::CoveredGpu => "covered-gpu",
- Self::Partial => "partial",
- Self::Blocked => "blocked",
- Self::Omitted => "omitted",
- }
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct AcceptanceAudit {
- commit_sha: String,
- git_dirty: bool,
- runner_identity: String,
- rust_toolchain: String,
- msrv: String,
- required_total: usize,
- covered: Vec<String>,
- covered_planning: Vec<String>,
- covered_stub: Vec<String>,
- covered_gpu: Vec<String>,
- partial: Vec<String>,
- blocked: Vec<String>,
- omitted: Vec<String>,
- missing: Vec<String>,
- invalid_coverage: Vec<String>,
- unknown_coverage: Vec<String>,
- coverage_evidence: BTreeMap<String, String>,
- by_stage: BTreeMap<String, usize>,
-}
-
-impl AcceptanceAudit {
- fn covered_total(&self) -> usize {
- self.covered.len()
- + self.covered_planning.len()
- + self.covered_stub.len()
- + self.covered_gpu.len()
- }
-
- fn unverified(&self) -> Vec<String> {
- self.partial
- .iter()
- .chain(&self.blocked)
- .chain(&self.omitted)
- .chain(&self.missing)
- .chain(&self.invalid_coverage)
- .cloned()
- .collect()
- }
-
- fn strict_failures(&self) -> Vec<String> {
- self.partial
- .iter()
- .chain(&self.blocked)
- .chain(&self.omitted)
- .chain(&self.missing)
- .chain(&self.invalid_coverage)
- .cloned()
- .collect()
- }
-}
-
-fn extract_acceptance_ids(text: &str) -> BTreeSet<String> {
- let mut ids = BTreeSet::new();
- for segment in text.split('`') {
- if is_acceptance_id(segment) {
- ids.insert(segment.to_string());
- }
- }
- ids
-}
-
-fn is_acceptance_id(value: &str) -> bool {
- let bytes = value.as_bytes();
- bytes.len() >= 9
- && matches!(bytes[0], b'S' | b'L')
- && matches!(bytes[1], b'0'..=b'5')
- && bytes[2] == b'-'
- && bytes.iter().all(|byte| {
- byte.is_ascii_uppercase() || byte.is_ascii_digit() || *byte == b'-' || *byte == b'_'
- })
-}
-
-fn read_coverage_manifest(path: &Path) -> Result<BTreeMap<String, CoverageEntry>, String> {
- if !path.exists() {
- return Ok(BTreeMap::new());
- }
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let mut entries = BTreeMap::new();
- for (line_number, line) in text.lines().enumerate() {
- let trimmed = line.trim();
- if trimmed.is_empty() || trimmed.starts_with('#') {
- continue;
- }
- let mut fields = trimmed.splitn(3, '\t');
- let id = fields
- .next()
- .ok_or_else(|| format!("{}:{}: missing id", path.display(), line_number + 1))?;
- let status = fields
- .next()
- .ok_or_else(|| format!("{}:{}: missing status", path.display(), line_number + 1))?;
- let evidence = fields
- .next()
- .ok_or_else(|| format!("{}:{}: missing evidence", path.display(), line_number + 1))?;
- if evidence.trim().is_empty() {
- return Err(format!(
- "{}:{}: empty evidence",
- path.display(),
- line_number + 1
- ));
- }
- if !is_acceptance_id(id) {
- return Err(format!(
- "{}:{}: invalid acceptance id: {id}",
- path.display(),
- line_number + 1
- ));
- }
- entries.insert(
- id.to_string(),
- CoverageEntry {
- status: CoverageStatus::parse(status)?,
- evidence: evidence.to_string(),
- },
- );
- }
- Ok(entries)
-}
-
-fn build_acceptance_audit(
- required: &BTreeSet<String>,
- coverage: &BTreeMap<String, CoverageEntry>,
-) -> AcceptanceAudit {
- let mut covered = Vec::new();
- let mut covered_planning = Vec::new();
- let mut covered_stub = Vec::new();
- let mut covered_gpu = Vec::new();
- let mut partial = Vec::new();
- let mut blocked = Vec::new();
- let mut omitted = Vec::new();
- let mut missing = Vec::new();
- let mut by_stage = BTreeMap::new();
- let mut coverage_evidence = BTreeMap::new();
- let required_scopes = required
- .iter()
- .filter_map(|id| id.get(0..2).map(ToString::to_string))
- .collect::<BTreeSet<_>>();
-
- for id in required {
- let stage = id
- .get(0..2)
- .map_or_else(|| "??".to_string(), ToString::to_string);
- *by_stage.entry(stage).or_insert(0) += 1;
- match coverage.get(id).map(|entry| entry.status) {
- Some(CoverageStatus::Covered) => covered.push(id.clone()),
- Some(CoverageStatus::CoveredPlanning) => covered_planning.push(id.clone()),
- Some(CoverageStatus::CoveredStub) => covered_stub.push(id.clone()),
- Some(CoverageStatus::CoveredGpu) => covered_gpu.push(id.clone()),
- Some(CoverageStatus::Partial) => partial.push(id.clone()),
- Some(CoverageStatus::Blocked) => blocked.push(id.clone()),
- Some(CoverageStatus::Omitted) => omitted.push(id.clone()),
- None => missing.push(id.clone()),
- }
- if let Some(entry) = coverage.get(id) {
- coverage_evidence.insert(id.clone(), entry.evidence.clone());
- }
- }
-
- let invalid_coverage = coverage
- .iter()
- .filter_map(|(id, entry)| coverage_semantic_failure(id, entry.status))
- .collect();
-
- let unknown_coverage = coverage
- .keys()
- .filter(|id| {
- !required.contains(*id)
- && id
- .get(0..2)
- .is_some_and(|scope| required_scopes.contains(scope))
- })
- .cloned()
- .collect();
-
- AcceptanceAudit {
- commit_sha: current_git_commit_sha(),
- git_dirty: current_git_dirty(),
- runner_identity: measured_runner_identity(),
- rust_toolchain: measured_rust_toolchain_version(),
- msrv: WORKSPACE_MSRV.to_string(),
- required_total: required.len(),
- covered,
- covered_planning,
- covered_stub,
- covered_gpu,
- partial,
- blocked,
- omitted,
- missing,
- invalid_coverage,
- unknown_coverage,
- coverage_evidence,
- by_stage,
- }
-}
-
-fn coverage_semantic_failure(id: &str, status: CoverageStatus) -> Option<String> {
- if is_stage3_or_later(id)
- && matches!(
- status,
- CoverageStatus::CoveredPlanning | CoverageStatus::CoveredStub
- )
- && has_gpu_delivery_token(id)
- {
- return Some(format!(
- "{id}: {} cannot satisfy Stage 3+ GPU/pixel acceptance; use covered-gpu or leave the row non-green",
- status.as_str()
- ));
- }
-
- if matches!(status, CoverageStatus::CoveredStub)
- && is_fx_lifecycle_compatibility_id(id)
- && !id.contains("STUB")
- {
- return Some(format!(
- "{id}: covered-stub is reserved for explicit STUB-only FX lifecycle acceptance rows"
- ));
- }
-
- None
-}
-
-fn is_stage3_or_later(id: &str) -> bool {
- id.as_bytes().get(1).is_some_and(|stage| *stage >= b'3')
-}
-
-fn has_gpu_delivery_token(id: &str) -> bool {
- id.split('-').any(|token| {
- matches!(
- token,
- "VK" | "GPU" | "DRAW" | "PIXEL" | "VALIDATION" | "RENDERED"
- )
- })
-}
-
-fn is_fx_lifecycle_compatibility_id(id: &str) -> bool {
- id.starts_with("S4-FX-") || id.starts_with("L4-P1-EFFECT-") || id.starts_with("L4-P2-EFFECT-")
-}
-
-#[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,
- covered_planning_total: usize,
- covered_stub_total: usize,
- covered_gpu_total: usize,
- partial_total: usize,
- blocked_total: usize,
- omitted_total: usize,
- missing_total: usize,
- unverified_total: usize,
- invalid_coverage_total: usize,
- unknown_coverage_total: usize,
- by_stage: &'a BTreeMap<String, usize>,
- covered: &'a [String],
- covered_planning: &'a [String],
- covered_stub: &'a [String],
- covered_gpu: &'a [String],
- partial: &'a [String],
- blocked: &'a [String],
- omitted: &'a [String],
- missing: &'a [String],
- invalid_coverage: &'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 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_total(),
- covered_planning_total: audit.covered_planning.len(),
- covered_stub_total: audit.covered_stub.len(),
- covered_gpu_total: audit.covered_gpu.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(),
- invalid_coverage_total: audit.invalid_coverage.len(),
- unknown_coverage_total: audit.unknown_coverage.len(),
- by_stage: &audit.by_stage,
- covered: &audit.covered,
- covered_planning: &audit.covered_planning,
- covered_stub: &audit.covered_stub,
- covered_gpu: &audit.covered_gpu,
- partial: &audit.partial,
- blocked: &audit.blocked,
- omitted: &audit.omitted,
- missing: &audit.missing,
- invalid_coverage: &audit.invalid_coverage,
- 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 {
- Command::new("git")
- .args(["rev-parse", "HEAD"])
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .map(|value| value.trim().to_string())
- .filter(|value| value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit()))
- .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"])
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .is_some_and(|output| !output.trim().is_empty())
-}
-
-fn measured_rust_toolchain_version() -> String {
- Command::new("rustc")
- .args(["-Vv"])
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .and_then(|stdout| {
- stdout.lines().find_map(|line| {
- line.strip_prefix("release:")
- .map(str::trim)
- .map(ToString::to_string)
- })
- })
- .unwrap_or_else(|| PINNED_RUST_TOOLCHAIN.to_string())
-}
-
-fn measured_runner_identity() -> String {
- if std::env::var_os("GITHUB_ACTIONS").is_some() {
- let run_id = std::env::var("GITHUB_RUN_ID").unwrap_or_else(|_| "unknown-run".to_string());
- let job = std::env::var("GITHUB_JOB").unwrap_or_else(|_| "unknown-job".to_string());
- format!("github-actions/{run_id}/{job}")
- } else if std::env::var_os("CI").is_some() {
- let job = std::env::var("CI_JOB_NAME")
- .or_else(|_| std::env::var("BUILD_ID"))
- .unwrap_or_else(|_| "generic-ci".to_string());
- format!("ci/{job}")
- } else {
- format!("local/{}", std::env::consts::OS)
- }
-}
-
-fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> {
- let roots = if options.suite == TestSuite::Licensed {
- Some(load_licensed_roots(options.manifest.as_deref())?)
- } else {
- None
- };
- run_stage_tests(options.stage, options.suite, roots.as_ref())?;
-
- 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)?;
- fs::write(&options.out, report).map_err(|err| format!("{}: {err}", options.out.display()))?;
- println!("{}", options.out.display());
- Ok(())
-}
-
-#[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(),
- };
- 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> {
- let workspace_root = workspace_root_path();
- match stage {
- Stage::All => workspace_package_names(&workspace_root)
- .map(|packages| packages.into_iter().collect::<Vec<_>>()),
- Stage::Number(number) => stage_packages(number),
- }
-}
-
-fn run_stage_tests(
- stage: Stage,
- suite: TestSuite,
- roots: Option<&LicensedCorpusRoots>,
-) -> Result<(), String> {
- let mut suffix = Vec::new();
- if suite == TestSuite::Licensed {
- suffix.extend(["--", "--ignored"]);
- }
- let envs = roots.map(LicensedCorpusRoots::envs);
- match stage {
- Stage::All => {
- let mut args = vec!["test", "--workspace", "--locked", "--offline"];
- args.extend(suffix);
- if let Some(envs) = envs {
- cargo_with_env(&args, &envs)
- } else {
- cargo(&args)
- }
- }
- Stage::Number(number) => {
- for package in stage_packages(number)? {
- let mut args = vec![
- "test".to_string(),
- "-p".to_string(),
- package,
- "--locked".to_string(),
- "--offline".to_string(),
- ];
- args.extend(suffix.iter().map(|value| (*value).to_string()));
- if let Some(envs) = envs {
- let borrowed = args.iter().map(String::as_str).collect::<Vec<_>>();
- cargo_with_env(&borrowed, &envs)?;
- } else {
- cargo_owned(&args)?;
- }
- }
- Ok(())
- }
- }
-}
-
-fn stage_packages(stage: u8) -> Result<Vec<String>, String> {
- let manifest_path = workspace_relative_path(STAGE_PACKAGE_MANIFEST);
- let manifest = load_stage_package_manifest(&manifest_path)?;
- let packages = manifest
- .stages
- .get(&stage.to_string())
- .cloned()
- .ok_or_else(|| format!("stage out of range: {stage}"))?;
- validate_stage_package_entries(
- &manifest,
- &workspace_package_names(&workspace_root_path())?,
- &manifest_path,
- )?;
- Ok(packages)
-}
-
-fn workspace_package_names(root: &Path) -> Result<BTreeSet<String>, String> {
- let metadata = workspace_metadata(root)?;
- Ok(metadata
- .workspace_packages()
- .iter()
- .map(|package| package.name.to_string())
- .collect())
-}
-
-fn workspace_metadata(root: &Path) -> Result<cargo_metadata::Metadata, String> {
- let manifest = root.join("Cargo.toml");
- MetadataCommand::new()
- .manifest_path(&manifest)
- .no_deps()
- .other_options(["--offline".to_string(), "--locked".to_string()])
- .exec()
- .map_err(|error| format!("{}: cargo metadata failed: {}", manifest.display(), error))
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct StagePackageManifest {
- schema: Option<u8>,
- stages: BTreeMap<String, Vec<String>>,
-}
-
-fn load_stage_package_manifest(path: &Path) -> Result<StagePackageManifest, String> {
- let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let manifest = toml::from_str::<StagePackageManifest>(&text)
- .map_err(|err| format!("failed to parse {}: {err}", path.display()))?;
- if manifest.schema != Some(1) {
- return Err(format!(
- "{}: unsupported stage package manifest schema {:?} (expected 1)",
- path.display(),
- manifest.schema
- ));
- }
- Ok(manifest)
-}
-
-fn validate_stage_package_entries(
- manifest: &StagePackageManifest,
- workspace_packages: &BTreeSet<String>,
- path: &Path,
-) -> Result<(), String> {
- let required_stages = (0_u8..=5_u8)
- .map(|stage| stage.to_string())
- .collect::<BTreeSet<_>>();
- let declared_stages = manifest.stages.keys().cloned().collect::<BTreeSet<_>>();
- if declared_stages != required_stages {
- return Err(format!(
- "{}: stage package manifest must declare stages 0 through 5 exactly once",
- path.display()
- ));
- }
-
- let mut assigned = BTreeSet::new();
- for (stage, packages) in &manifest.stages {
- for package in packages {
- if !workspace_packages.contains(package) {
- return Err(format!(
- "{}: stage {stage} references unknown package {package}",
- path.display()
- ));
- }
- if !assigned.insert(package.clone()) {
- return Err(format!(
- "{}: package {package} is assigned to multiple stages",
- path.display()
- ));
- }
- }
- }
-
- let missing = workspace_packages
- .difference(&assigned)
- .cloned()
- .collect::<Vec<_>>();
- if !missing.is_empty() {
- return Err(format!(
- "{}: stage package manifest is missing workspace packages: {}",
- path.display(),
- missing.join(", ")
- ));
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn strings(values: &[&str]) -> Vec<String> {
- values.iter().map(|value| (*value).to_string()).collect()
- }
-
- fn temp_dir(name: &str) -> PathBuf {
- let suffix = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map_or(0, |duration| duration.as_nanos());
- std::env::temp_dir().join(format!("fparkan-xtask-{name}-{suffix}"))
- }
-
- #[test]
- fn parses_stage_and_root_options() {
- let args = strings(&["--stage", "3", "--root", "fixtures"]);
- let parsed = parse_test_options(&args, PathBuf::from("testdata"));
-
- assert_eq!(
- parsed,
- Ok(TestOptions {
- stage: Stage::Number(3),
- root: PathBuf::from("fixtures"),
- manifest: None,
- })
- );
- }
-
- #[test]
- fn parses_corpus_baseline_output_option() {
- let options = parse_corpus_baseline_options(&strings(&[
- "--root",
- "C:/GOG Games/Parkan - Iron Strategy",
- "--out",
- "target/fparkan/corpus/part1.json",
- ]));
-
- assert_eq!(
- options,
- Ok(CorpusBaselineOptions {
- root: PathBuf::from("C:/GOG Games/Parkan - Iron Strategy"),
- out: Some(PathBuf::from("target/fparkan/corpus/part1.json")),
- })
- );
- }
-
- #[test]
- fn parses_acceptance_report_options() {
- let parsed = parse_acceptance_options(&strings(&[
- "--suite",
- "licensed",
- "--stage",
- "5",
- "--manifest",
- "corpora.toml",
- "--out",
- "target/report.json",
- ]));
-
- assert_eq!(
- parsed,
- Ok(AcceptanceOptions {
- suite: TestSuite::Licensed,
- stage: Stage::Number(5),
- root: PathBuf::from("testdata"),
- manifest: Some(PathBuf::from("corpora.toml")),
- out: PathBuf::from("target/report.json"),
- })
- );
- }
-
- #[test]
- fn acceptance_report_redacts_licensed_root() {
- let options = AcceptanceOptions {
- suite: TestSuite::Licensed,
- stage: Stage::Number(0),
- root: PathBuf::from("/private/game"),
- manifest: Some(PathBuf::from("/private/corpora.toml")),
- out: PathBuf::from("target/report.json"),
- };
- let report =
- render_acceptance_report(&options).expect("acceptance report should serialize");
-
- assert!(report.contains("\"root\": \"redacted\""));
- assert!(!report.contains("/private/game"));
- assert!(report.contains("\"fparkan-corpus\""));
- }
-
- #[test]
- fn extracts_acceptance_ids_from_backticks_only() {
- let ids =
- extract_acceptance_ids("`S0-ARCH-001` text S0-ARCH-002 `L5-P1-MISSION-001` `bad`");
-
- assert!(ids.contains("S0-ARCH-001"));
- assert!(ids.contains("L5-P1-MISSION-001"));
- assert!(!ids.contains("S0-ARCH-002"));
- assert_eq!(ids.len(), 2);
- }
-
- #[test]
- fn builds_acceptance_audit_counts() {
- let required = ["S0-ARCH-001", "S0-ARCH-002", "L3-GPU-001", "L5-RG40-001"]
- .into_iter()
- .map(str::to_string)
- .collect::<BTreeSet<_>>();
- let coverage = [
- (
- "S0-ARCH-001".to_string(),
- CoverageEntry {
- status: CoverageStatus::Covered,
- evidence: "cargo xtask policy".to_string(),
- },
- ),
- (
- "L3-GPU-001".to_string(),
- CoverageEntry {
- status: CoverageStatus::CoveredPlanning,
- evidence: "planning-only evidence".to_string(),
- },
- ),
- (
- "L5-RG40-001".to_string(),
- CoverageEntry {
- status: CoverageStatus::Blocked,
- evidence: "device not attached".to_string(),
- },
- ),
- (
- "S0-ARCH-099".to_string(),
- CoverageEntry {
- status: CoverageStatus::Partial,
- evidence: "bad id".to_string(),
- },
- ),
- ]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let audit = build_acceptance_audit(&required, &coverage);
-
- assert_eq!(audit.covered, ["S0-ARCH-001"]);
- assert_eq!(audit.covered_planning, ["L3-GPU-001"]);
- assert_eq!(audit.blocked, ["L5-RG40-001"]);
- assert_eq!(audit.missing, ["S0-ARCH-002"]);
- assert_eq!(audit.unknown_coverage, ["S0-ARCH-099"]);
- assert_eq!(audit.by_stage.get("S0"), Some(&2));
- assert_eq!(
- audit.strict_failures(),
- vec![
- "L5-RG40-001".to_string(),
- "S0-ARCH-002".to_string(),
- "L3-GPU-001: covered-planning cannot satisfy Stage 3+ GPU/pixel acceptance; use covered-gpu or leave the row non-green".to_string(),
- ]
- );
- }
-
- #[test]
- fn covered_stub_requires_explicit_stub_fx_acceptance_row() {
- let required = ["S4-FX-018"]
- .into_iter()
- .map(str::to_string)
- .collect::<BTreeSet<_>>();
- let coverage = [(
- "S4-FX-018".to_string(),
- CoverageEntry {
- status: CoverageStatus::CoveredStub,
- evidence: "reference stub output".to_string(),
- },
- )]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let audit = build_acceptance_audit(&required, &coverage);
-
- assert_eq!(audit.covered_stub, ["S4-FX-018"]);
- assert_eq!(
- audit.invalid_coverage,
- vec![
- "S4-FX-018: covered-stub is reserved for explicit STUB-only FX lifecycle acceptance rows"
- .to_string()
- ]
- );
- }
-
- #[test]
- fn audit_json_escapes_evidence() {
- let mut audit = AcceptanceAudit {
- commit_sha: "0123456789abcdef0123456789abcdef01234567".to_string(),
- git_dirty: false,
- runner_identity: "windows-local/stage0".to_string(),
- rust_toolchain: PINNED_RUST_TOOLCHAIN.to_string(),
- msrv: WORKSPACE_MSRV.to_string(),
- required_total: 1,
- covered: vec!["S0-ARCH-001".to_string()],
- covered_planning: Vec::new(),
- covered_stub: Vec::new(),
- covered_gpu: Vec::new(),
- partial: Vec::new(),
- blocked: Vec::new(),
- omitted: Vec::new(),
- missing: Vec::new(),
- invalid_coverage: Vec::new(),
- unknown_coverage: Vec::new(),
- coverage_evidence: BTreeMap::new(),
- by_stage: BTreeMap::new(),
- };
- audit
- .coverage_evidence
- .insert("S0-ARCH-001".to_string(), "quoted \"value\"".to_string());
-
- let json = render_audit_json(&audit).expect("acceptance audit should serialize");
-
- assert!(json.contains("quoted \\\"value\\\""));
- assert!(json.contains("\"commit_sha\": \"0123456789abcdef0123456789abcdef01234567\""));
- assert!(json.contains("\"git_dirty\": false"));
- assert!(json.contains("\"runner_identity\": \"windows-local/stage0\""));
- assert!(json.contains("\"rust_toolchain\": \"1.97.1\""));
- assert!(json.contains("\"msrv\": \"1.97\""));
- }
-
- #[test]
- fn native_smoke_audit_accepts_complete_required_platform_pass() {
- let expected_commit = "0123456789abcdef0123456789abcdef01234567";
- let expected_shader_manifest_hash =
- "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c";
- let reports = ["windows"]
- .into_iter()
- .map(|platform| {
- let target_triple = match platform {
- "windows" => "x86_64-pc-windows-msvc",
- _ => "unknown-target",
- };
- (
- platform.to_string(),
- serde_json::json!({
- "schema_version": "fparkan-native-smoke-v1",
- "commit_sha": "0123456789abcdef0123456789abcdef01234567",
- "git_dirty": false,
- "runner_identity": "windows-local/stage0",
- "runner_architecture": "x86_64",
- "rust_toolchain": measured_rust_toolchain_version(),
- "target_triple": target_triple,
- "platform": platform,
- "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": format!("{platform} 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": false,
- "vulkan_portability_subset_enabled": false
- }),
- )
- })
- .collect::<BTreeMap<_, _>>();
-
- 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 = [(
- "windows".to_string(),
- serde_json::json!({
- "schema_version": "fparkan-native-smoke-v1",
- "commit_sha": "unknown",
- "git_dirty": true,
- "runner_identity": "",
- "runner_architecture": "x86_64",
- "rust_toolchain": measured_rust_toolchain_version(),
- "target_triple": "x86_64-unknown-linux-gnu",
- "platform": "windows",
- "status": "blocked",
- "frames": 0,
- "resize_count": 0,
- "swapchain_recreate_count": 0,
- "validation_warning_count": null,
- "validation_error_count": null,
- "shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c",
- "vulkan_loader_status": "unavailable",
- "vulkan_instance_status": "skipped",
- "window_status": "planned",
- "vulkan_surface_status": "skipped",
- "vulkan_device_status": "skipped",
- "vulkan_swapchain_status": "skipped"
- }),
- )]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let failures =
- audit_native_smoke_reports(&reports, expected_commit, expected_shader_manifest_hash);
-
- assert!(failures
- .contains(&"windows: status expected \"passed\", found \"blocked\"".to_string()));
- assert!(failures.contains(
- &"windows: commit_sha must be a 40-character lowercase or uppercase hex string"
- .to_string()
- ));
- assert!(failures.contains(&"windows: git_dirty expected false, found true".to_string()));
- assert!(failures.contains(&"windows: runner_identity must be non-empty".to_string()));
- assert!(failures.contains(
- &"windows: target_triple \"x86_64-unknown-linux-gnu\" does not match platform"
- .to_string()
- ));
- assert!(failures.contains(&"windows: frames expected >= 300, found 0".to_string()));
- assert!(failures
- .contains(&"windows: validation_error_count must be an unsigned integer".to_string()));
- }
-
- #[test]
- fn native_smoke_audit_rejects_stale_commit_sha() {
- let expected_shader_manifest_hash =
- "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c";
- let reports = [(
- "windows".to_string(),
- serde_json::json!({
- "schema_version": "fparkan-native-smoke-v1",
- "commit_sha": "fedcba98765432100123456789abcdef01234567",
- "git_dirty": false,
- "runner_identity": "windows-local/stage0",
- "runner_architecture": "x86_64",
- "rust_toolchain": measured_rust_toolchain_version(),
- "target_triple": "x86_64-pc-windows-msvc",
- "platform": "windows",
- "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": "Windows test 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": false,
- "vulkan_portability_subset_enabled": false
- }),
- )]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let failures = audit_native_smoke_reports(
- &reports,
- "0123456789abcdef0123456789abcdef01234567",
- expected_shader_manifest_hash,
- );
-
- assert!(failures.contains(
- &"windows: commit_sha expected \"0123456789abcdef0123456789abcdef01234567\", found \"fedcba98765432100123456789abcdef01234567\"".to_string()
- ));
- }
-
- #[test]
- fn native_smoke_audit_rejects_stale_shader_manifest_hash() {
- let expected_commit = "0123456789abcdef0123456789abcdef01234567";
- let reports = [(
- "windows".to_string(),
- serde_json::json!({
- "schema_version": "fparkan-native-smoke-v1",
- "commit_sha": expected_commit,
- "git_dirty": false,
- "runner_identity": "windows-local/stage0",
- "runner_architecture": "x86_64",
- "rust_toolchain": measured_rust_toolchain_version(),
- "target_triple": "x86_64-pc-windows-msvc",
- "platform": "windows",
- "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": "Windows test 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": false,
- "vulkan_portability_subset_enabled": false
- }),
- )]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let failures = audit_native_smoke_reports(
- &reports,
- expected_commit,
- "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c",
- );
-
- assert!(failures.contains(
- &"windows: 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"));
-
- assert_eq!(
- parsed,
- Ok(TestOptions {
- stage: Stage::All,
- root: PathBuf::from("testdata"),
- manifest: None,
- })
- );
- }
-
- #[test]
- fn parses_licensed_corpora_manifest() -> Result<(), String> {
- let root = temp_dir("manifest");
- let part1 = root.join("IS");
- let part2 = root.join("IS2");
- fs::create_dir_all(&part1).map_err(|err| err.to_string())?;
- fs::create_dir_all(&part2).map_err(|err| err.to_string())?;
- let manifest = root.join("corpora.toml");
- fs::write(
- &manifest,
- format!(
- "schema = 1\n\n[[corpus]]\nid = \"part1-local\"\nkind = \"part1\"\nroot = '{}'\nexpected_profile = \"parkan-is-part1\"\n\n[[corpus]]\nid = \"part2-local\"\nkind = \"part2\"\nroot = '{}'\nexpected_profile = \"parkan-is-part2\"\n",
- part1.display(),
- part2.display()
- ),
- )
- .map_err(|err| err.to_string())?;
-
- assert_eq!(
- parse_licensed_manifest(&manifest)?,
- LicensedCorpusRoots { part1, part2 }
- );
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn licensed_roots_require_manifest_configuration() {
- let previous = std::env::var_os(CORPORA_MANIFEST_ENV);
- std::env::remove_var(CORPORA_MANIFEST_ENV);
-
- assert_eq!(
- load_licensed_roots(None),
- Err(format!(
- "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>"
- ))
- );
-
- if let Some(value) = previous {
- std::env::set_var(CORPORA_MANIFEST_ENV, value);
- }
- }
-
- #[test]
- fn rejects_unknown_stage() {
- assert_eq!(Stage::parse("6"), Err("stage out of range: 6".to_string()));
- assert_eq!(
- Stage::parse("assets"),
- Err("invalid stage: assets".to_string())
- );
- }
-
- #[test]
- fn maps_stage_packages() {
- assert!(stage_packages(0)
- .is_ok_and(|packages| packages.contains(&"fparkan-platform".to_string())));
- assert!(stage_packages(0)
- .is_ok_and(|packages| packages.contains(&"fparkan-vulkan-smoke".to_string())));
- assert!(stage_packages(1)
- .is_ok_and(|packages| packages.contains(&"fparkan-inspection".to_string())));
- assert!(stage_packages(5)
- .is_ok_and(|packages| packages.contains(&"fparkan-runtime".to_string())));
- assert!(
- stage_packages(5).is_ok_and(|packages| packages.contains(&"fparkan-game".to_string()))
- );
- assert_eq!(stage_packages(9), Err("stage out of range: 9".to_string()));
- }
-
- #[test]
- fn stage_package_manifest_covers_workspace_once() -> Result<(), String> {
- let manifest_path = workspace_relative_path(STAGE_PACKAGE_MANIFEST);
- let manifest = load_stage_package_manifest(&manifest_path)?;
- let workspace_packages = workspace_package_names(&workspace_root_path())?;
-
- validate_stage_package_entries(&manifest, &workspace_packages, &manifest_path)?;
-
- Ok(())
- }
-
- #[test]
- fn workspace_metadata_keeps_headless_closure_platform_free() -> Result<(), String> {
- let metadata = workspace_metadata(&workspace_root_path())?;
- let dependencies = workspace_dependency_names(&metadata);
- let mut failures = Vec::new();
-
- validate_package_closure_excludes("fparkan-headless", &dependencies, &mut failures);
-
- assert!(failures.is_empty(), "{failures:?}");
- Ok(())
- }
-
- #[test]
- fn parses_manifest_dependencies_for_arch_policy() {
- let manifest = r#"
-[package]
-name = "fparkan-example"
-
-[dependencies]
-fparkan-render = { path = "../fparkan-render" }
-"quoted-dep" = "1"
-
-[dev-dependencies]
-fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
-"#;
-
- let policy = toml::from_str::<CargoManifestPolicy>(manifest)
- .expect("typed policy manifest should parse");
- assert_eq!(policy.package_name(), Some("fparkan-example"));
- let deps = policy.dependency_names();
- assert!(deps.contains("fparkan-render"));
- assert!(deps.contains("quoted-dep"));
- assert!(deps.contains("fparkan-render-vulkan"));
- }
-
- #[test]
- fn typed_manifest_policy_accepts_workspace_inherited_license() {
- let manifest = r#"
-[package]
-name = "fparkan-example"
-license.workspace = true
-"#;
-
- let policy = toml::from_str::<CargoManifestPolicy>(manifest)
- .expect("workspace inheritance should be valid Cargo TOML");
-
- assert_eq!(policy.package_name(), Some("fparkan-example"));
- assert_eq!(policy.license(), None);
- }
-
- #[test]
- fn workspace_manifest_closure_detects_transitive_platform_bridge() {
- let deps_by_package = [
- (
- "fparkan-headless".to_string(),
- ["fparkan-runtime".to_string()].into_iter().collect(),
- ),
- (
- "fparkan-runtime".to_string(),
- ["fparkan-render-vulkan".to_string()].into_iter().collect(),
- ),
- ("fparkan-render-vulkan".to_string(), BTreeSet::new()),
- ]
- .into_iter()
- .collect::<BTreeMap<_, _>>();
-
- let closure = dependency_closure_names("fparkan-headless", &deps_by_package);
-
- assert!(closure.contains("fparkan-runtime"));
- assert_eq!(
- first_forbidden_platform_bridge_dependency(&closure),
- Some("fparkan-render-vulkan")
- );
- }
-
- #[test]
- fn toolchain_policy_rejects_moving_toolchain() -> Result<(), String> {
- let root = temp_dir("toolchain-moving");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
- fs::write(
- root.join("rust-toolchain.toml"),
- "[toolchain]\nchannel = \"stable\"\n",
- )
- .map_err(|err| err.to_string())?;
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\n[workspace.package]\nrust-version = \"1.97\"\n",
- )
- .map_err(|err| err.to_string())?;
-
- let mut failures = Vec::new();
- validate_toolchain_policy(&root, &mut failures)?;
-
- assert_eq!(failures.len(), 2);
- assert!(failures[0].contains("must be exact"));
- assert!(failures[1].contains("major.minor.patch"));
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn toolchain_policy_rejects_msrv_mismatch() -> Result<(), String> {
- let root = temp_dir("toolchain-msrv");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
- fs::write(
- root.join("rust-toolchain.toml"),
- format!("[toolchain]\nchannel = \"{PINNED_RUST_TOOLCHAIN}\"\n"),
- )
- .map_err(|err| err.to_string())?;
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\n[workspace.package]\nrust-version = \"1.86\"\n",
- )
- .map_err(|err| err.to_string())?;
-
- let mut failures = Vec::new();
- validate_toolchain_policy(&root, &mut failures)?;
-
- assert_eq!(failures.len(), 2);
- assert!(failures[0].contains("rust-version must be"));
- assert!(failures[1].contains("must match pinned toolchain"));
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn lockfile_supply_chain_rejects_unapproved_sources() -> Result<(), String> {
- let root = temp_dir("lockfile-source");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
- fs::write(
- root.join("Cargo.lock"),
- r#"
-[[package]]
-name = "external"
-version = "1.0.0"
-source = "git+https://example.invalid/repo"
-"#,
- )
- .map_err(|err| err.to_string())?;
-
- let mut failures = Vec::new();
- validate_lockfile_supply_chain(&root, &mut failures)?;
-
- assert_eq!(failures.len(), 1);
- assert!(failures[0].contains("uses unapproved source"));
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn lockfile_supply_chain_rejects_banned_packages() -> Result<(), String> {
- let root = temp_dir("lockfile-ban");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
- fs::write(
- root.join("Cargo.lock"),
- format!(
- "[[package]]\nname = \"openssl\"\nversion = \"0.10.0\"\nsource = \"{APPROVED_REGISTRY_SOURCE}\"\n"
- ),
- )
- .map_err(|err| err.to_string())?;
-
- let mut failures = Vec::new();
- validate_lockfile_supply_chain(&root, &mut failures)?;
-
- assert_eq!(failures.len(), 1);
- assert!(failures[0].contains("is banned"));
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn supply_chain_policy_config_must_exist() -> Result<(), String> {
- let root = temp_dir("supply-chain-config");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
-
- let missing = root.join("deny.toml");
- assert!(validate_supply_chain_policy_config(&missing).is_err());
-
- fs::write(&missing, "[graph]\nall-features = true\n").map_err(|err| err.to_string())?;
- assert_eq!(validate_supply_chain_policy_config(&missing), Ok(()));
-
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-
- #[test]
- fn ci_env_truthy_values_are_detected() {
- assert!(ci_env_value_is_active(Some("true")));
- assert!(ci_env_value_is_active(Some("1")));
- assert!(ci_env_value_is_active(Some("yes")));
- assert!(!ci_env_value_is_active(None));
- assert!(!ci_env_value_is_active(Some("")));
- assert!(!ci_env_value_is_active(Some("0")));
- assert!(!ci_env_value_is_active(Some("false")));
- assert!(!ci_env_value_is_active(Some(" FALSE ")));
- }
-
- #[test]
- fn detects_forbidden_domain_dependencies() {
- assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
- assert!(is_forbidden_domain_dependency("sdl2"));
- assert!(is_forbidden_domain_dependency("fparkan-platform-sdl"));
- assert!(!is_forbidden_domain_dependency("fparkan-render"));
- assert!(!is_forbidden_domain_dependency("fparkan-platform"));
- }
-
- #[test]
- fn parses_package_options() {
- assert_eq!(
- parse_package_options(&strings(&[
- "--target",
- "aarch64-apple-darwin",
- "--app",
- "viewer"
- ])),
- Ok(PackageOptions {
- target: "aarch64-apple-darwin".to_string(),
- app: AppPackage::Viewer,
- })
- );
- assert_eq!(
- parse_package_options(&strings(&["--target", "x", "--app", "bad"])),
- Err("unknown app: bad".to_string())
- );
- }
-
- #[test]
- fn app_packages_map_to_cargo_packages() {
- assert_eq!(AppPackage::Cli.package(), "fparkan-cli");
- assert_eq!(AppPackage::Game.package(), "fparkan-game");
- assert_eq!(AppPackage::Headless.package(), "fparkan-headless");
- assert_eq!(AppPackage::Viewer.package(), "fparkan-viewer");
- }
-
- #[test]
- fn policy_source_detection_is_scoped_to_code_files() {
- assert!(is_policy_source(Path::new("src/main.rs")));
- assert!(is_policy_source(Path::new("Cargo.toml")));
- assert!(!is_policy_source(Path::new("README.md")));
- assert!(should_skip_policy_path(Path::new("target")));
- assert!(should_skip_policy_path(Path::new("testdata")));
- assert!(!should_skip_policy_path(Path::new("crates/experimental")));
- assert!(!should_skip_policy_path(Path::new("crates/fparkan-render")));
- }
-
- #[test]
- fn unsafe_construct_detector_ignores_lints_and_comments() {
- assert!(contains_unsafe_construct(concat!(
- "un",
- "safe fn call() {}"
- )));
- assert!(contains_unsafe_construct(concat!(
- "let value = un",
- "safe { call() };"
- )));
- assert!(!contains_unsafe_construct("#![forbid(unsafe_code)]"));
- }
-
- #[test]
- fn unsafe_module_attributes_must_match_audited_ffi_allowlist() {
- let audited = Path::new("adapters/fparkan-render-vulkan/src/ffi/runtime.rs");
- let non_audited = Path::new("adapters/fparkan-render-vulkan/src/ffi.rs");
-
- let mut failures = Vec::new();
- validate_unsafe_module_attribute_policy(audited, "mod x;\n", &mut failures);
- assert_eq!(
- failures,
- vec![format!(
- "{}: audited Vulkan FFI module must declare #![allow(unsafe_code)]",
- audited.display()
- )]
- );
-
- failures.clear();
- validate_unsafe_module_attribute_policy(
- non_audited,
- "#![allow(unsafe_code)]\nmod runtime;\n",
- &mut failures,
- );
- assert_eq!(
- failures,
- vec![format!(
- "{}: #![allow(unsafe_code)] is only permitted in audited Vulkan FFI modules",
- non_audited.display()
- )]
- );
- }
-
- #[test]
- fn audited_unsafe_allowlist_accepts_windows_separators() {
- let path = Path::new(r".\adapters\fparkan-render-vulkan\src\ffi\runtime.rs");
-
- assert!(is_audited_unsafe_source(path));
- }
-
- #[test]
- fn shader_source_hash_normalizes_windows_line_endings() -> Result<(), String> {
- let root = temp_dir("shader-source-hash");
- fs::create_dir_all(&root).map_err(|err| err.to_string())?;
- let lf = root.join("lf.glsl");
- let crlf = root.join("crlf.glsl");
- fs::write(&lf, b"#version 450\nvoid main() {}\n").map_err(|err| err.to_string())?;
- fs::write(&crlf, b"#version 450\r\nvoid main() {}\r\n").map_err(|err| err.to_string())?;
-
- assert_eq!(
- sha256_canonical_text_file(&lf)?,
- sha256_canonical_text_file(&crlf)?
- );
-
- fs::remove_dir_all(root).map_err(|err| err.to_string())?;
- Ok(())
- }
-}