ci: tighten stage 0 acceptance gates
This commit is contained in:
+14
-10
@@ -2,19 +2,21 @@ name: fparkan-ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [devel, main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [devel, main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
msrv-backend-neutral:
|
||||
name: MSRV backend-neutral crates
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
||||
with:
|
||||
toolchain: 1.87.0
|
||||
- name: Test backend-neutral crates
|
||||
@@ -48,6 +50,7 @@ jobs:
|
||||
stage0-matrix:
|
||||
name: Stage 0-2 CI (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -61,12 +64,13 @@ jobs:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
||||
with:
|
||||
toolchain-file: rust-toolchain.toml
|
||||
toolchain: 1.87.0
|
||||
components: clippy,rustfmt
|
||||
- name: Install cargo-deny
|
||||
run: cargo install cargo-deny --locked
|
||||
run: cargo install cargo-deny --version 0.19.9 --locked
|
||||
- name: Run canonical CI gate
|
||||
run: cargo xtask ci
|
||||
- name: Record native Vulkan smoke status
|
||||
@@ -81,10 +85,10 @@ jobs:
|
||||
--reason "native Vulkan smoke runner is not enabled on this CI lane yet"
|
||||
- name: Upload acceptance evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: stage-0-2-acceptance-${{ matrix.os }}
|
||||
path: |
|
||||
target/fparkan/acceptance/stage-0-2-audit.json
|
||||
target/fparkan/native-smoke/*.json
|
||||
if-no-files-found: ignore
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -345,14 +345,20 @@ impl VulkanLogicalDeviceProbe {
|
||||
#[must_use]
|
||||
pub fn graphics_queue(&self) -> vk::Queue {
|
||||
// SAFETY: The queue-family index belongs to this live logical device.
|
||||
unsafe { self.device.get_device_queue(self.report.graphics_queue_family, 0) }
|
||||
unsafe {
|
||||
self.device
|
||||
.get_device_queue(self.report.graphics_queue_family, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the presentation queue selected by the Stage 0 policy.
|
||||
#[must_use]
|
||||
pub fn present_queue(&self) -> vk::Queue {
|
||||
// SAFETY: The queue-family index belongs to this live logical device.
|
||||
unsafe { self.device.get_device_queue(self.report.present_queue_family, 0) }
|
||||
unsafe {
|
||||
self.device
|
||||
.get_device_queue(self.report.present_queue_family, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a shared reference to the live logical device.
|
||||
@@ -440,8 +446,12 @@ pub enum VulkanSmokeRunError {
|
||||
impl std::fmt::Display for VulkanSmokeRunError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::AcquireImage { result } => write!(f, "failed to acquire swapchain image: {result}"),
|
||||
Self::PresentImage { result } => write!(f, "failed to present swapchain image: {result}"),
|
||||
Self::AcquireImage { result } => {
|
||||
write!(f, "failed to acquire swapchain image: {result}")
|
||||
}
|
||||
Self::PresentImage { result } => {
|
||||
write!(f, "failed to present swapchain image: {result}")
|
||||
}
|
||||
Self::RecreateSwapchain { result } => {
|
||||
write!(f, "failed to recreate swapchain: {result}")
|
||||
}
|
||||
@@ -464,9 +474,11 @@ pub fn run_vulkan_smoke_pass(
|
||||
let timeout_ns = u64::MAX;
|
||||
|
||||
let image_available = vk::SemaphoreCreateInfo::default();
|
||||
let image_ready = unsafe { device.device().create_semaphore(&image_available, None) }
|
||||
.map_err(|error| VulkanSmokeRunError::RecreateSwapchain {
|
||||
result: format!("{error:?}"),
|
||||
let image_ready =
|
||||
unsafe { device.device().create_semaphore(&image_available, None) }.map_err(|error| {
|
||||
VulkanSmokeRunError::RecreateSwapchain {
|
||||
result: format!("{error:?}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let recreate_interval = if recreate_count == 0 {
|
||||
@@ -479,18 +491,27 @@ pub fn run_vulkan_smoke_pass(
|
||||
let mut created = 0_u32;
|
||||
|
||||
for frame in 0..frames {
|
||||
if recreate_interval > 0 && frame > 0 && frame % recreate_interval == 0 && created < recreate_count {
|
||||
swapchain = create_vulkan_swapchain_probe(instance, surface, device)
|
||||
.map_err(|error| VulkanSmokeRunError::RecreateSwapchain {
|
||||
result: error.to_string(),
|
||||
if recreate_interval > 0
|
||||
&& frame > 0
|
||||
&& frame % recreate_interval == 0
|
||||
&& created < recreate_count
|
||||
{
|
||||
swapchain =
|
||||
create_vulkan_swapchain_probe(instance, surface, device).map_err(|error| {
|
||||
VulkanSmokeRunError::RecreateSwapchain {
|
||||
result: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
created = created.saturating_add(1);
|
||||
}
|
||||
|
||||
let image_index = unsafe {
|
||||
swapchain
|
||||
.loader()
|
||||
.acquire_next_image(swapchain.swapchain(), timeout_ns, image_ready, vk::Fence::null())
|
||||
swapchain.loader().acquire_next_image(
|
||||
swapchain.swapchain(),
|
||||
timeout_ns,
|
||||
image_ready,
|
||||
vk::Fence::null(),
|
||||
)
|
||||
}
|
||||
.map(|(index, _)| index)
|
||||
.map_err(|error| VulkanSmokeRunError::AcquireImage {
|
||||
@@ -513,10 +534,11 @@ pub fn run_vulkan_smoke_pass(
|
||||
result: format!("{error:?}"),
|
||||
})?;
|
||||
|
||||
unsafe { device.device().queue_wait_idle(render_queue) }
|
||||
.map_err(|error| VulkanSmokeRunError::PresentImage {
|
||||
unsafe { device.device().queue_wait_idle(render_queue) }.map_err(|error| {
|
||||
VulkanSmokeRunError::PresentImage {
|
||||
result: format!("{error:?}"),
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
|
||||
swaps = swaps.saturating_add(1);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
|
||||
use fparkan_render_vulkan::{
|
||||
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
|
||||
create_vulkan_swapchain_probe, probe_vulkan_loader, run_vulkan_smoke_pass,
|
||||
triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
|
||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSwapchainProbe,
|
||||
triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe,
|
||||
VulkanLogicalDeviceProbe, VulkanSwapchainProbe,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -67,9 +67,13 @@ fn run(args: &[String]) -> Result<String, String> {
|
||||
return Err("passed native smoke report requires frames to be advanced".to_string());
|
||||
}
|
||||
if smoke_run.validation_error_count
|
||||
!= options.validation_error_count.unwrap_or(smoke_run.validation_error_count)
|
||||
!= options
|
||||
.validation_error_count
|
||||
.unwrap_or(smoke_run.validation_error_count)
|
||||
{
|
||||
return Err("passed native smoke report requires validation errors to be zero".to_string());
|
||||
return Err(
|
||||
"passed native smoke report requires validation errors to be zero".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let report = render_smoke_report_json(&options, &bootstrap)?;
|
||||
@@ -443,12 +447,13 @@ impl VulkanBootstrapProbe {
|
||||
options: &SmokeOptions,
|
||||
instance: &VulkanInstanceProbe,
|
||||
window_handles: Option<NativeWindowHandles>,
|
||||
) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe>
|
||||
{
|
||||
) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe> {
|
||||
if options.probes.vulkan.includes_surface()
|
||||
&& self.instance_status == VulkanInstanceStatus::Created
|
||||
{
|
||||
match create_vulkan_surface_probe(instance, window_handles).map_err(|err| err.to_string()) {
|
||||
match create_vulkan_surface_probe(instance, window_handles)
|
||||
.map_err(|err| err.to_string())
|
||||
{
|
||||
Ok(surface) => {
|
||||
self.surface_status = VulkanSurfaceStatus::Created;
|
||||
return Some(surface);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
[graph]
|
||||
all-features = true
|
||||
|
||||
[advisories]
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "deny"
|
||||
deny = [
|
||||
{ name = "native-tls" },
|
||||
{ name = "openssl" },
|
||||
{ name = "openssl-sys" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
unlicensed = "deny"
|
||||
copyleft = "allow"
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"GPL-2.0-only",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
@@ -0,0 +1,47 @@
|
||||
schema = 1
|
||||
|
||||
[stages]
|
||||
"0" = [
|
||||
"fparkan-binary",
|
||||
"fparkan-corpus",
|
||||
"fparkan-diagnostics",
|
||||
"fparkan-platform",
|
||||
"fparkan-platform-winit",
|
||||
"fparkan-render",
|
||||
"fparkan-render-vulkan",
|
||||
"fparkan-test-support",
|
||||
"fparkan-vulkan-smoke",
|
||||
"xtask",
|
||||
]
|
||||
"1" = [
|
||||
"fparkan-cli",
|
||||
"fparkan-inspection",
|
||||
"fparkan-nres",
|
||||
"fparkan-path",
|
||||
"fparkan-resource",
|
||||
"fparkan-rsli",
|
||||
"fparkan-vfs",
|
||||
]
|
||||
"2" = [
|
||||
"fparkan-prototype",
|
||||
]
|
||||
"3" = [
|
||||
"fparkan-assets",
|
||||
"fparkan-material",
|
||||
"fparkan-msh",
|
||||
"fparkan-texm",
|
||||
"fparkan-viewer",
|
||||
]
|
||||
"4" = [
|
||||
"fparkan-animation",
|
||||
"fparkan-fx",
|
||||
]
|
||||
"5" = [
|
||||
"fparkan-game",
|
||||
"fparkan-headless",
|
||||
"fparkan-mission-format",
|
||||
"fparkan-runtime",
|
||||
"fparkan-terrain",
|
||||
"fparkan-terrain-format",
|
||||
"fparkan-world",
|
||||
]
|
||||
+277
-103
@@ -37,11 +37,24 @@ const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT";
|
||||
const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_2_roadmap.md";
|
||||
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
|
||||
const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-2-audit.json";
|
||||
const STAGE_PACKAGE_MANIFEST: &str = "fixtures/acceptance/stage_packages.toml";
|
||||
const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["linux", "macos", "windows"];
|
||||
const APPROVED_REGISTRY_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
const SUPPLY_CHAIN_BANNED_PACKAGES: &[&str] = &["native-tls", "openssl", "openssl-sys"];
|
||||
const PINNED_RUST_TOOLCHAIN: &str = "1.87.0";
|
||||
const WORKSPACE_MSRV: &str = "1.87";
|
||||
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<_>>();
|
||||
@@ -180,13 +193,27 @@ fn run_cargo_fmt_check() -> Result<(), String> {
|
||||
|
||||
fn run_cargo_deny() -> Result<(), String> {
|
||||
let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into());
|
||||
let available = Command::new(&cargo_deny).arg("--version").status();
|
||||
match available {
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(_) | Err(_) => {
|
||||
eprintln!("cargo-deny is unavailable; running built-in supply-chain policy fallback");
|
||||
return run_builtin_supply_chain_policy(Path::new("."));
|
||||
}
|
||||
let version_output = Command::new(&cargo_deny)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"cargo-deny is required; install cargo-deny {PINNED_CARGO_DENY_VERSION} or set {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV}=1 for the built-in fallback: {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)
|
||||
@@ -208,6 +235,21 @@ fn run_cargo_deny() -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
const PINNED_CARGO_DENY_VERSION: &str = "0.19.9";
|
||||
|
||||
fn handle_cargo_deny_fallback(reason: String) -> Result<(), String> {
|
||||
if std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() {
|
||||
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 {
|
||||
Err(format!(
|
||||
"{reason}; install cargo-deny {PINNED_CARGO_DENY_VERSION} or explicitly opt into the fallback with {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV}=1"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_builtin_supply_chain_policy(root: &Path) -> Result<(), String> {
|
||||
let mut failures = Vec::new();
|
||||
validate_workspace_license(root, &mut failures)?;
|
||||
@@ -473,12 +515,7 @@ fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<()
|
||||
if !manifest.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let metadata = 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))?;
|
||||
let metadata = workspace_metadata(root)?;
|
||||
if metadata.workspace_members.is_empty() {
|
||||
failures.push(format!(
|
||||
"{}: cargo metadata produced no workspace members",
|
||||
@@ -486,6 +523,18 @@ fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<()
|
||||
));
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1185,40 +1234,6 @@ enum Stage {
|
||||
Number(u8),
|
||||
}
|
||||
|
||||
const ALL_WORKSPACE_PACKAGES: &[&str] = &[
|
||||
"fparkan-animation",
|
||||
"fparkan-assets",
|
||||
"fparkan-binary",
|
||||
"fparkan-corpus",
|
||||
"fparkan-diagnostics",
|
||||
"fparkan-fx",
|
||||
"fparkan-material",
|
||||
"fparkan-mission-format",
|
||||
"fparkan-msh",
|
||||
"fparkan-nres",
|
||||
"fparkan-path",
|
||||
"fparkan-platform",
|
||||
"fparkan-prototype",
|
||||
"fparkan-render",
|
||||
"fparkan-resource",
|
||||
"fparkan-rsli",
|
||||
"fparkan-runtime",
|
||||
"fparkan-terrain",
|
||||
"fparkan-terrain-format",
|
||||
"fparkan-test-support",
|
||||
"fparkan-texm",
|
||||
"fparkan-vfs",
|
||||
"fparkan-world",
|
||||
"fparkan-platform-winit",
|
||||
"fparkan-render-vulkan",
|
||||
"fparkan-cli",
|
||||
"fparkan-game",
|
||||
"fparkan-headless",
|
||||
"fparkan-vulkan-smoke",
|
||||
"fparkan-viewer",
|
||||
"xtask",
|
||||
];
|
||||
|
||||
impl Stage {
|
||||
fn parse(value: &str) -> Result<Self, String> {
|
||||
if value == "all" {
|
||||
@@ -1390,9 +1405,10 @@ fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String
|
||||
}
|
||||
|
||||
fn parse_audit_options(args: &[String]) -> Result<AuditOptions, String> {
|
||||
let mut roadmap = PathBuf::from("FPARKAN_ARCHITECTURE_ROADMAP_STAGES_0_5.md");
|
||||
let mut coverage = PathBuf::from("fixtures/acceptance/coverage.tsv");
|
||||
let mut out = PathBuf::from("target")
|
||||
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")
|
||||
@@ -1478,25 +1494,55 @@ fn read_native_smoke_reports(dir: &Path) -> Result<BTreeMap<String, serde_json::
|
||||
.map_err(|err| format!("{}: {err}", path.display()))?;
|
||||
let platform = json_string_field(&json, "platform")
|
||||
.map_err(|err| format!("{}: {err}", path.display()))?;
|
||||
reports.insert(platform.to_string(), json);
|
||||
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>) -> 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);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1561,7 +1607,14 @@ fn validate_native_smoke_report(
|
||||
expect_u64_at_least(platform, report, "swapchain_recreate_count", 1, failures);
|
||||
expect_u64_field(platform, report, "validation_error_count", 0, failures);
|
||||
expect_nonempty_string(platform, report, "commit_sha", failures);
|
||||
expect_nonempty_string(platform, report, "rust_toolchain", failures);
|
||||
expect_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_nonempty_string(platform, report, "shader_manifest_hash", failures);
|
||||
expect_nonempty_string(platform, report, "vulkan_device_name", failures);
|
||||
@@ -1741,13 +1794,20 @@ impl AcceptanceAudit {
|
||||
self.partial
|
||||
.iter()
|
||||
.chain(&self.blocked)
|
||||
.chain(&self.omitted)
|
||||
.chain(&self.missing)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn strict_failures(&self) -> Vec<String> {
|
||||
self.partial.iter().chain(&self.missing).cloned().collect()
|
||||
self.partial
|
||||
.iter()
|
||||
.chain(&self.blocked)
|
||||
.chain(&self.omitted)
|
||||
.chain(&self.missing)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1855,7 +1915,7 @@ fn build_acceptance_audit(
|
||||
|
||||
AcceptanceAudit {
|
||||
commit_sha: current_git_commit_sha(),
|
||||
rust_toolchain: PINNED_RUST_TOOLCHAIN.to_string(),
|
||||
rust_toolchain: measured_rust_toolchain_version(),
|
||||
msrv: WORKSPACE_MSRV.to_string(),
|
||||
required_total: required.len(),
|
||||
covered,
|
||||
@@ -1930,6 +1990,23 @@ fn current_git_commit_sha() -> String {
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
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 render_string_usize_map(values: &BTreeMap<String, usize>) -> String {
|
||||
let pairs = values
|
||||
.iter()
|
||||
@@ -1994,6 +2071,7 @@ fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> {
|
||||
|
||||
fn render_acceptance_report(options: &AcceptanceOptions) -> String {
|
||||
let packages = stage_report_packages(options.stage)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|package| format!(" \"{package}\""))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2023,10 +2101,12 @@ fn render_acceptance_report(options: &AcceptanceOptions) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn stage_report_packages(stage: Stage) -> Vec<&'static str> {
|
||||
fn stage_report_packages(stage: Stage) -> Result<Vec<String>, String> {
|
||||
let workspace_root = workspace_root_path();
|
||||
match stage {
|
||||
Stage::All => ALL_WORKSPACE_PACKAGES.to_vec(),
|
||||
Stage::Number(number) => stage_packages(number).unwrap_or(&[]).to_vec(),
|
||||
Stage::All => workspace_package_names(&workspace_root)
|
||||
.map(|packages| packages.into_iter().collect::<Vec<_>>()),
|
||||
Stage::Number(number) => stage_packages(number),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2052,12 +2132,19 @@ fn run_stage_tests(
|
||||
}
|
||||
Stage::Number(number) => {
|
||||
for package in stage_packages(number)? {
|
||||
let mut args = vec!["test", "-p", package, "--locked", "--offline"];
|
||||
args.extend(suffix.iter().copied());
|
||||
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 {
|
||||
cargo_with_env(&args, &envs)?;
|
||||
let borrowed = args.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
cargo_with_env(&borrowed, &envs)?;
|
||||
} else {
|
||||
cargo(&args)?;
|
||||
cargo_owned(&args)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2065,43 +2152,108 @@ fn run_stage_tests(
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_packages(stage: u8) -> Result<&'static [&'static str], String> {
|
||||
match stage {
|
||||
0 => Ok(&[
|
||||
"fparkan-corpus",
|
||||
"fparkan-diagnostics",
|
||||
"fparkan-test-support",
|
||||
]),
|
||||
1 => Ok(&[
|
||||
"fparkan-binary",
|
||||
"fparkan-path",
|
||||
"fparkan-nres",
|
||||
"fparkan-rsli",
|
||||
"fparkan-resource",
|
||||
"fparkan-vfs",
|
||||
]),
|
||||
2 => Ok(&["fparkan-prototype"]),
|
||||
3 => Ok(&[
|
||||
"fparkan-msh",
|
||||
"fparkan-material",
|
||||
"fparkan-texm",
|
||||
"fparkan-assets",
|
||||
"fparkan-render",
|
||||
"fparkan-viewer",
|
||||
]),
|
||||
4 => Ok(&["fparkan-animation", "fparkan-fx"]),
|
||||
5 => Ok(&[
|
||||
"fparkan-terrain-format",
|
||||
"fparkan-terrain",
|
||||
"fparkan-mission-format",
|
||||
"fparkan-world",
|
||||
"fparkan-runtime",
|
||||
"fparkan-headless",
|
||||
"fparkan-game",
|
||||
"fparkan-vulkan-smoke",
|
||||
]),
|
||||
_ => Err(format!("stage out of range: {stage}")),
|
||||
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)]
|
||||
@@ -2233,6 +2385,10 @@ mod tests {
|
||||
assert_eq!(audit.missing, ["S0-ARCH-002"]);
|
||||
assert_eq!(audit.unknown_coverage, ["S9-UNKNOWN-001"]);
|
||||
assert_eq!(audit.by_stage.get("S0"), Some(&2));
|
||||
assert_eq!(
|
||||
audit.strict_failures(),
|
||||
strings(&["L5-RG40-001", "L3-DEVICE-001", "S0-ARCH-002"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2273,7 +2429,7 @@ mod tests {
|
||||
serde_json::json!({
|
||||
"schema_version": "fparkan-native-smoke-v1",
|
||||
"commit_sha": "0123456789abcdef0123456789abcdef01234567",
|
||||
"rust_toolchain": "1.87.0",
|
||||
"rust_toolchain": measured_rust_toolchain_version(),
|
||||
"target_triple": format!("{platform}-test-target"),
|
||||
"platform": platform,
|
||||
"status": "passed",
|
||||
@@ -2311,7 +2467,7 @@ mod tests {
|
||||
serde_json::json!({
|
||||
"schema_version": "fparkan-native-smoke-v1",
|
||||
"commit_sha": "0123456789abcdef0123456789abcdef01234567",
|
||||
"rust_toolchain": "1.87.0",
|
||||
"rust_toolchain": measured_rust_toolchain_version(),
|
||||
"target_triple": "aarch64-apple-darwin",
|
||||
"platform": "macos",
|
||||
"status": "blocked",
|
||||
@@ -2412,13 +2568,31 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_stage_packages() {
|
||||
assert!(stage_packages(3).is_ok_and(|packages| packages.contains(&"fparkan-assets")));
|
||||
assert!(stage_packages(3).is_ok_and(|packages| packages.contains(&"fparkan-viewer")));
|
||||
assert!(stage_packages(5).is_ok_and(|packages| packages.contains(&"fparkan-runtime")));
|
||||
assert!(stage_packages(5).is_ok_and(|packages| packages.contains(&"fparkan-game")));
|
||||
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 parses_manifest_dependencies_for_arch_policy() {
|
||||
let manifest = r#"
|
||||
|
||||
Reference in New Issue
Block a user