feat: add Vulkan loader probe to smoke runner
This commit is contained in:
@@ -77,6 +77,7 @@ jobs:
|
|||||||
--platform "${{ matrix.smoke_platform }}"
|
--platform "${{ matrix.smoke_platform }}"
|
||||||
--out "target/fparkan/native-smoke/${{ runner.os }}.json"
|
--out "target/fparkan/native-smoke/${{ runner.os }}.json"
|
||||||
--status blocked
|
--status blocked
|
||||||
|
--probe-loader
|
||||||
--reason "native Vulkan smoke runner is not enabled on this CI lane yet"
|
--reason "native Vulkan smoke runner is not enabled on this CI lane yet"
|
||||||
- name: Upload acceptance evidence
|
- name: Upload acceptance evidence
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
||||||
//! Native Vulkan smoke runner entrypoint.
|
//! Native Vulkan smoke runner entrypoint.
|
||||||
|
|
||||||
use fparkan_render_vulkan::{triangle_shader_manifest, validate_shader_manifest};
|
use fparkan_render_vulkan::{
|
||||||
|
probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest,
|
||||||
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -35,8 +37,9 @@ fn main() {
|
|||||||
|
|
||||||
fn run(args: &[String]) -> Result<String, String> {
|
fn run(args: &[String]) -> Result<String, String> {
|
||||||
let options = SmokeOptions::parse(args)?;
|
let options = SmokeOptions::parse(args)?;
|
||||||
validate_smoke_options(&options)?;
|
let bootstrap = VulkanBootstrapProbe::run(&options);
|
||||||
let report = render_smoke_report_json(&options)?;
|
validate_smoke_options(&options, &bootstrap)?;
|
||||||
|
let report = render_smoke_report_json(&options, &bootstrap)?;
|
||||||
if let Some(parent) = options.out.parent() {
|
if let Some(parent) = options.out.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,7 @@ struct SmokeOptions {
|
|||||||
frames: u32,
|
frames: u32,
|
||||||
resize_count: u32,
|
resize_count: u32,
|
||||||
validation_error_count: Option<u32>,
|
validation_error_count: Option<u32>,
|
||||||
|
probe_loader: bool,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ impl SmokeOptions {
|
|||||||
let mut frames = 0;
|
let mut frames = 0;
|
||||||
let mut resize_count = 0;
|
let mut resize_count = 0;
|
||||||
let mut validation_error_count = None;
|
let mut validation_error_count = None;
|
||||||
|
let mut probe_loader = false;
|
||||||
let mut reason = None;
|
let mut reason = None;
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
@@ -104,6 +109,9 @@ impl SmokeOptions {
|
|||||||
.ok_or_else(|| "--validation-error-count requires a value".to_string())?;
|
.ok_or_else(|| "--validation-error-count requires a value".to_string())?;
|
||||||
validation_error_count = Some(parse_u32("--validation-error-count", value)?);
|
validation_error_count = Some(parse_u32("--validation-error-count", value)?);
|
||||||
}
|
}
|
||||||
|
"--probe-loader" => {
|
||||||
|
probe_loader = true;
|
||||||
|
}
|
||||||
"--reason" => {
|
"--reason" => {
|
||||||
let value = iter
|
let value = iter
|
||||||
.next()
|
.next()
|
||||||
@@ -120,6 +128,7 @@ impl SmokeOptions {
|
|||||||
frames,
|
frames,
|
||||||
resize_count,
|
resize_count,
|
||||||
validation_error_count,
|
validation_error_count,
|
||||||
|
probe_loader,
|
||||||
reason,
|
reason,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -131,6 +140,55 @@ fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
|
|||||||
.map_err(|_| format!("invalid {name} value: {value}"))
|
.map_err(|_| format!("invalid {name} value: {value}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
struct VulkanBootstrapProbe {
|
||||||
|
loader_status: VulkanLoaderStatus,
|
||||||
|
instance_api: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanBootstrapProbe {
|
||||||
|
fn run(options: &SmokeOptions) -> Self {
|
||||||
|
if !options.probe_loader {
|
||||||
|
return Self {
|
||||||
|
loader_status: VulkanLoaderStatus::Skipped,
|
||||||
|
instance_api: None,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match probe_vulkan_loader() {
|
||||||
|
Ok(report) => Self {
|
||||||
|
loader_status: VulkanLoaderStatus::Available,
|
||||||
|
instance_api: Some(format_api_version(report.instance_api_version)),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => Self {
|
||||||
|
loader_status: VulkanLoaderStatus::Unavailable,
|
||||||
|
instance_api: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum VulkanLoaderStatus {
|
||||||
|
Skipped,
|
||||||
|
Available,
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanLoaderStatus {
|
||||||
|
const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Skipped => "skipped",
|
||||||
|
Self::Available => "available",
|
||||||
|
Self::Unavailable => "unavailable",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum SmokePlatform {
|
enum SmokePlatform {
|
||||||
Windows,
|
Windows,
|
||||||
@@ -180,7 +238,10 @@ impl SmokeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_smoke_options(options: &SmokeOptions) -> Result<(), String> {
|
fn validate_smoke_options(
|
||||||
|
options: &SmokeOptions,
|
||||||
|
bootstrap: &VulkanBootstrapProbe,
|
||||||
|
) -> Result<(), String> {
|
||||||
match options.status {
|
match options.status {
|
||||||
SmokeStatus::Blocked => {
|
SmokeStatus::Blocked => {
|
||||||
if options
|
if options
|
||||||
@@ -205,12 +266,20 @@ fn validate_smoke_options(options: &SmokeOptions) -> Result<(), String> {
|
|||||||
"passed native smoke report requires --validation-error-count 0".to_string(),
|
"passed native smoke report requires --validation-error-count 0".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if bootstrap.loader_status != VulkanLoaderStatus::Available {
|
||||||
|
return Err(
|
||||||
|
"passed native smoke report requires successful --probe-loader".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> {
|
fn render_smoke_report_json(
|
||||||
|
options: &SmokeOptions,
|
||||||
|
bootstrap: &VulkanBootstrapProbe,
|
||||||
|
) -> Result<String, String> {
|
||||||
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
||||||
.map_err(|err| format!("shader manifest: {err}"))?;
|
.map_err(|err| format!("shader manifest: {err}"))?;
|
||||||
let validation_error_count = options
|
let validation_error_count = options
|
||||||
@@ -220,6 +289,14 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> {
|
|||||||
.reason
|
.reason
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(|| "null".to_string(), |value| json_string(value));
|
.map_or_else(|| "null".to_string(), |value| json_string(value));
|
||||||
|
let instance_api = bootstrap
|
||||||
|
.instance_api
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| "null".to_string(), |value| json_string(value));
|
||||||
|
let bootstrap_error = bootstrap
|
||||||
|
.error
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| "null".to_string(), |value| json_string(value));
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
concat!(
|
concat!(
|
||||||
"{{\n",
|
"{{\n",
|
||||||
@@ -232,6 +309,9 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> {
|
|||||||
" \"resize_count\": {},\n",
|
" \"resize_count\": {},\n",
|
||||||
" \"validation_error_count\": {},\n",
|
" \"validation_error_count\": {},\n",
|
||||||
" \"shader_manifest_hash\": \"{}\",\n",
|
" \"shader_manifest_hash\": \"{}\",\n",
|
||||||
|
" \"vulkan_loader_status\": \"{}\",\n",
|
||||||
|
" \"vulkan_instance_api\": {},\n",
|
||||||
|
" \"vulkan_bootstrap_error\": {},\n",
|
||||||
" \"reason\": {}\n",
|
" \"reason\": {}\n",
|
||||||
"}}\n"
|
"}}\n"
|
||||||
),
|
),
|
||||||
@@ -244,10 +324,20 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> {
|
|||||||
options.resize_count,
|
options.resize_count,
|
||||||
validation_error_count,
|
validation_error_count,
|
||||||
json_escape(&shader_manifest.manifest_hash),
|
json_escape(&shader_manifest.manifest_hash),
|
||||||
|
bootstrap.loader_status.as_str(),
|
||||||
|
instance_api,
|
||||||
|
bootstrap_error,
|
||||||
reason
|
reason
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_api_version(version: u32) -> String {
|
||||||
|
let major = version >> 22;
|
||||||
|
let minor = (version >> 12) & 0x03ff;
|
||||||
|
let patch = version & 0x0fff;
|
||||||
|
format!("{major}.{minor}.{patch}")
|
||||||
|
}
|
||||||
|
|
||||||
fn current_git_commit_sha() -> String {
|
fn current_git_commit_sha() -> String {
|
||||||
Command::new("git")
|
Command::new("git")
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
@@ -300,14 +390,23 @@ mod tests {
|
|||||||
"target/native.json",
|
"target/native.json",
|
||||||
"--status",
|
"--status",
|
||||||
"blocked",
|
"blocked",
|
||||||
|
"--probe-loader",
|
||||||
"--reason",
|
"--reason",
|
||||||
"runner unavailable",
|
"runner unavailable",
|
||||||
]))?;
|
]))?;
|
||||||
|
|
||||||
assert_eq!(options.platform, SmokePlatform::Linux);
|
assert_eq!(options.platform, SmokePlatform::Linux);
|
||||||
assert_eq!(options.status, SmokeStatus::Blocked);
|
assert_eq!(options.status, SmokeStatus::Blocked);
|
||||||
|
assert!(options.probe_loader);
|
||||||
assert_eq!(options.reason.as_deref(), Some("runner unavailable"));
|
assert_eq!(options.reason.as_deref(), Some("runner unavailable"));
|
||||||
validate_smoke_options(&options)
|
validate_smoke_options(
|
||||||
|
&options,
|
||||||
|
&VulkanBootstrapProbe {
|
||||||
|
loader_status: VulkanLoaderStatus::Unavailable,
|
||||||
|
instance_api: None,
|
||||||
|
error: Some("Vulkan loader is unavailable".to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -329,13 +428,51 @@ mod tests {
|
|||||||
.expect("options");
|
.expect("options");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_smoke_options(&options),
|
validate_smoke_options(
|
||||||
|
&options,
|
||||||
|
&VulkanBootstrapProbe {
|
||||||
|
loader_status: VulkanLoaderStatus::Available,
|
||||||
|
instance_api: Some("1.3.0".to_string()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
Err("passed native smoke report requires --frames >= 300".to_string())
|
Err("passed native smoke report requires --frames >= 300".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_report_includes_shader_manifest_hash() -> Result<(), String> {
|
fn rejects_passed_without_loader_probe() {
|
||||||
|
let options = SmokeOptions::parse(&strings(&[
|
||||||
|
"--platform",
|
||||||
|
"linux",
|
||||||
|
"--out",
|
||||||
|
"target/native.json",
|
||||||
|
"--status",
|
||||||
|
"passed",
|
||||||
|
"--frames",
|
||||||
|
"300",
|
||||||
|
"--resize-count",
|
||||||
|
"1",
|
||||||
|
"--validation-error-count",
|
||||||
|
"0",
|
||||||
|
]))
|
||||||
|
.expect("options");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
validate_smoke_options(
|
||||||
|
&options,
|
||||||
|
&VulkanBootstrapProbe {
|
||||||
|
loader_status: VulkanLoaderStatus::Skipped,
|
||||||
|
instance_api: None,
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Err("passed native smoke report requires successful --probe-loader".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> {
|
||||||
let options = SmokeOptions::parse(&strings(&[
|
let options = SmokeOptions::parse(&strings(&[
|
||||||
"--platform",
|
"--platform",
|
||||||
"macos",
|
"macos",
|
||||||
@@ -347,13 +484,30 @@ mod tests {
|
|||||||
"runner unavailable",
|
"runner unavailable",
|
||||||
]))?;
|
]))?;
|
||||||
|
|
||||||
let json = render_smoke_report_json(&options)?;
|
let json = render_smoke_report_json(
|
||||||
|
&options,
|
||||||
|
&VulkanBootstrapProbe {
|
||||||
|
loader_status: VulkanLoaderStatus::Unavailable,
|
||||||
|
instance_api: None,
|
||||||
|
error: Some("Vulkan loader is unavailable: dlopen failed".to_string()),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\""));
|
assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\""));
|
||||||
assert!(json.contains("\"platform\": \"macos\""));
|
assert!(json.contains("\"platform\": \"macos\""));
|
||||||
assert!(json.contains("\"status\": \"blocked\""));
|
assert!(json.contains("\"status\": \"blocked\""));
|
||||||
assert!(json.contains("\"shader_manifest_hash\": \""));
|
assert!(json.contains("\"shader_manifest_hash\": \""));
|
||||||
|
assert!(json.contains("\"vulkan_loader_status\": \"unavailable\""));
|
||||||
|
assert!(json.contains("\"vulkan_instance_api\": null"));
|
||||||
|
assert!(json.contains(
|
||||||
|
"\"vulkan_bootstrap_error\": \"Vulkan loader is unavailable: dlopen failed\""
|
||||||
|
));
|
||||||
assert!(json.contains("\"reason\": \"runner unavailable\""));
|
assert!(json.contains("\"reason\": \"runner unavailable\""));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_vulkan_api_version() {
|
||||||
|
assert_eq!(format_api_version((1 << 22) | (3 << 12) | 280), "1.3.280");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_
|
|||||||
S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
|
S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
|
||||||
S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
|
S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
|
||||||
S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
||||||
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_hash
|
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
|
||||||
|
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
|
||||||
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
|
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
|
||||||
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
|
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
|
||||||
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
|
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
|
||||||
|
|||||||
|
@@ -51,6 +51,7 @@
|
|||||||
`S0-VK-021`
|
`S0-VK-021`
|
||||||
`S0-VK-022`
|
`S0-VK-022`
|
||||||
`S0-VK-023`
|
`S0-VK-023`
|
||||||
|
`S0-VK-024`
|
||||||
`S0-LIMIT-001`
|
`S0-LIMIT-001`
|
||||||
`S0-LIMIT-002`
|
`S0-LIMIT-002`
|
||||||
`L1-P1-NRES-001`
|
`L1-P1-NRES-001`
|
||||||
|
|||||||
Reference in New Issue
Block a user