diff options
Diffstat (limited to 'apps/fparkan-vulkan-smoke/src/main.rs')
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 1747 |
1 files changed, 339 insertions, 1408 deletions
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index 4e60ec8..d91988b 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -11,19 +11,23 @@ #![allow(clippy::print_stderr, clippy::print_stdout)] //! Native Vulkan smoke runner entrypoint. -use fparkan_platform::{NativeWindowHandles, WindowPort}; -use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan}; +use fparkan_platform_winit::{window_native_handles, 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, + VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, }; use std::path::PathBuf; use std::process::Command; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalSize; +use winit::event::WindowEvent; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::window::{Window, WindowId}; const SCHEMA_VERSION: &str = "fparkan-native-smoke-v1"; -const RUST_TOOLCHAIN: &str = "1.87.0"; +const DEFAULT_TARGET_FRAMES: u32 = 300; +const DEFAULT_RESIZE_FRAME: u32 = 120; +const DEFAULT_RESIZE_WIDTH: u32 = 960; +const DEFAULT_RESIZE_HEIGHT: u32 = 540; fn main() { let args = std::env::args().skip(1).collect::<Vec<_>>(); @@ -42,878 +46,372 @@ fn main() { fn run(args: &[String]) -> Result<String, String> { let options = SmokeOptions::parse(args)?; - let (bootstrap, runtime) = VulkanBootstrapProbe::run(&options); - validate_smoke_options(&options, &bootstrap)?; - let smoke_run = if options.status == SmokeStatus::Passed { - runtime - .map(|runtime| { - run_vulkan_smoke_pass( - &runtime.instance, - &runtime.surface, - &runtime.device, - runtime.swapchain, - options.frames, - options.swapchain_recreate_count, - ) - }) - .transpose() - .map_err(|err| err.to_string())? - } else { - None - }; - - if let Some(smoke_run) = smoke_run.as_ref() { - if smoke_run.frames < options.frames { - 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) - { - return Err( - "passed native smoke report requires validation errors to be zero".to_string(), - ); - } - } - let report = render_smoke_report_json(&options, &bootstrap)?; - if let Some(parent) = options.out.parent() { - std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; - } - std::fs::write(&options.out, &report) - .map_err(|err| format!("{}: {err}", options.out.display()))?; - Ok(report) + let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?; + let mut app = SmokeApp::new(options); + event_loop + .run_app(&mut app) + .map_err(|err| format!("winit event loop: {err}"))?; + app.finish() } #[derive(Clone, Debug, Eq, PartialEq)] struct SmokeOptions { - platform: SmokePlatform, out: PathBuf, - status: SmokeStatus, frames: u32, - resize_count: u32, - swapchain_recreate_count: u32, - validation_error_count: Option<u32>, - probes: ProbeOptions, - reason: Option<String>, + resize_frame: u32, } impl SmokeOptions { fn parse(args: &[String]) -> Result<Self, String> { - let mut platform = None; let mut out = None; - let mut status = SmokeStatus::Blocked; - let mut frames = 0; - let mut resize_count = 0; - let mut swapchain_recreate_count = 0; - let mut validation_error_count = None; - let mut probes = ProbeOptions::default(); - let mut reason = None; + let mut frames = DEFAULT_TARGET_FRAMES; + let mut resize_frame = DEFAULT_RESIZE_FRAME; let mut iter = args.iter(); while let Some(arg) = iter.next() { match arg.as_str() { - "--platform" => { - let value = iter - .next() - .ok_or_else(|| "--platform requires a value".to_string())?; - platform = Some(SmokePlatform::parse(value)?); - } "--out" => { - let value = iter - .next() - .ok_or_else(|| "--out requires a path".to_string())?; - out = Some(PathBuf::from(value)); - } - "--status" => { - let value = iter - .next() - .ok_or_else(|| "--status requires a value".to_string())?; - status = SmokeStatus::parse(value)?; + out = Some( + iter.next() + .map(PathBuf::from) + .ok_or_else(|| "--out requires a path".to_string())?, + ); } "--frames" => { - let value = iter - .next() - .ok_or_else(|| "--frames requires a value".to_string())?; - frames = parse_u32("--frames", value)?; - } - "--resize-count" => { - let value = iter - .next() - .ok_or_else(|| "--resize-count requires a value".to_string())?; - resize_count = parse_u32("--resize-count", value)?; - } - "--swapchain-recreate-count" => { - let value = iter - .next() - .ok_or_else(|| "--swapchain-recreate-count requires a value".to_string())?; - swapchain_recreate_count = parse_u32("--swapchain-recreate-count", value)?; - } - "--validation-error-count" => { - let value = iter + frames = iter .next() - .ok_or_else(|| "--validation-error-count requires a value".to_string())?; - validation_error_count = Some(parse_u32("--validation-error-count", value)?); - } - "--probe-loader" => { - probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Loader); - } - "--probe-instance" => { - probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Instance); - } - "--probe-window" => { - probes.window = true; - } - "--probe-surface" => { - probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Surface); - probes.window = true; + .ok_or_else(|| "--frames requires a value".to_string())? + .parse() + .map_err(|_| "--frames must be an integer".to_string())?; } - "--reason" => { - let value = iter + "--resize-frame" => { + resize_frame = iter .next() - .ok_or_else(|| "--reason requires a value".to_string())?; - reason = Some(value.to_string()); + .ok_or_else(|| "--resize-frame requires a value".to_string())? + .parse() + .map_err(|_| "--resize-frame must be an integer".to_string())?; } _ => return Err(format!("unknown native smoke option: {arg}")), } } + let out = out.ok_or_else(|| "missing --out".to_string())?; + if frames < DEFAULT_TARGET_FRAMES { + return Err(format!( + "native smoke requires --frames >= {DEFAULT_TARGET_FRAMES}" + )); + } Ok(Self { - platform: platform.ok_or_else(|| "missing --platform".to_string())?, - out: out.ok_or_else(|| "missing --out".to_string())?, - status, + out, frames, - resize_count, - swapchain_recreate_count, - validation_error_count, - probes, - reason, + resize_frame, }) } } -fn parse_u32(name: &str, value: &str) -> Result<u32, String> { - value - .parse::<u32>() - .map_err(|_| format!("invalid {name} value: {value}")) -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -struct ProbeOptions { - vulkan: VulkanProbeDepth, - window: bool, -} - -#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] -enum VulkanProbeDepth { - #[default] - None, - Loader, - Instance, - Surface, +struct SmokeApp { + options: SmokeOptions, + window_id: Option<WindowId>, + window: Option<Window>, + renderer: Option<VulkanSmokeRenderer>, + error: Option<String>, + output: Option<String>, + frames_presented: u32, + resize_count: u32, + resize_requested: bool, + last_size: Option<(u32, u32)>, } -impl VulkanProbeDepth { - const fn includes_loader(self) -> bool { - matches!(self, Self::Loader | Self::Instance | Self::Surface) - } - - const fn includes_instance(self) -> bool { - matches!(self, Self::Instance | Self::Surface) +impl SmokeApp { + const fn new(options: SmokeOptions) -> Self { + Self { + options, + window_id: None, + window: None, + renderer: None, + error: None, + output: None, + frames_presented: 0, + resize_count: 0, + resize_requested: false, + last_size: None, + } } - const fn includes_surface(self) -> bool { - matches!(self, Self::Surface) + fn finish(self) -> Result<String, String> { + if let Some(error) = self.error { + return Err(error); + } + self.output + .ok_or_else(|| "native smoke exited before producing a report".to_string()) } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus, - instance_api: Option<String>, - loader_error: Option<String>, - instance_status: VulkanInstanceStatus, - instance_error: Option<String>, - portability_enumeration: bool, - window_status: WinitWindowStatus, - window_width: Option<u32>, - window_height: Option<u32>, - window_error: Option<String>, - surface_status: VulkanSurfaceStatus, - surface_error: Option<String>, - device_status: VulkanDeviceStatus, - device_name: Option<String>, - device_error: Option<String>, - logical_device_status: VulkanLogicalDeviceStatus, - logical_device_graphics_queue_family: Option<u32>, - logical_device_present_queue_family: Option<u32>, - logical_device_enabled_extension_count: Option<u32>, - logical_device_error: Option<String>, - swapchain_status: VulkanSwapchainStatus, - swapchain_width: Option<u32>, - swapchain_height: Option<u32>, - swapchain_image_count: Option<u32>, - swapchain_error: Option<String>, -} - -struct VulkanRuntimePass { - instance: VulkanInstanceProbe, - surface: fparkan_render_vulkan::VulkanSurfaceProbe, - device: VulkanLogicalDeviceProbe, - swapchain: VulkanSwapchainProbe, -} -impl VulkanBootstrapProbe { - fn run(options: &SmokeOptions) -> (Self, Option<VulkanRuntimePass>) { - if !options.probes.vulkan.includes_loader() { - return (Self::skipped(), None); + fn schedule_next_redraw(&self) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); } + } - let mut probe = Self::probe_loader(); - let window_handles = probe.probe_window(options); - let instance = probe.probe_instance(options); - let runtime = if let Some(instance) = instance.as_ref() { - let surface = probe.probe_surface_for_runtime(options, instance, window_handles); - surface.and_then(|surface| { - probe - .probe_runtime_capabilities(instance, &surface) - .map(|(device, swapchain)| (device, swapchain, surface)) - }) - } else { - None + fn complete(&mut self, event_loop: &ActiveEventLoop) { + let Some(renderer) = self.renderer.as_ref() else { + self.error = Some("native smoke renderer was not initialized".to_string()); + event_loop.exit(); + return; }; - - if let Some(runtime) = runtime { - let (device, swapchain, surface) = runtime; - if probe.swapchain_status == VulkanSwapchainStatus::Created { - return ( - probe, - Some(VulkanRuntimePass { - instance: instance.expect("instance retained"), - surface, - device, - swapchain, - }), - ); + let validation = renderer.validation_report(); + if self.frames_presented < self.options.frames { + self.error = Some("native smoke did not reach the required frame count".to_string()); + event_loop.exit(); + return; + } + if self.resize_count == 0 || renderer.swapchain_recreate_count() == 0 { + self.error = Some( + "native smoke requires at least one measured resize and swapchain recreation" + .to_string(), + ); + event_loop.exit(); + return; + } + if validation.warning_count != 0 || validation.error_count != 0 { + self.error = Some(format!( + "native smoke validation must stay clean (warnings={}, errors={})", + validation.warning_count, validation.error_count + )); + event_loop.exit(); + return; + } + let report = render_smoke_report_json( + &self.options, + renderer, + self.frames_presented, + self.resize_count, + validation.warning_count, + validation.error_count, + &validation.vuids, + ); + if let Some(parent) = self.options.out.parent() { + if let Err(err) = std::fs::create_dir_all(parent) { + self.error = Some(format!("{}: {err}", parent.display())); + event_loop.exit(); + return; } } - - (probe, None) - } - - const fn skipped() -> Self { - Self { - loader_status: VulkanLoaderStatus::Skipped, - instance_api: None, - loader_error: None, - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - device_status: VulkanDeviceStatus::Skipped, - device_name: None, - device_error: None, - logical_device_status: VulkanLogicalDeviceStatus::Skipped, - logical_device_graphics_queue_family: None, - logical_device_present_queue_family: None, - logical_device_enabled_extension_count: None, - logical_device_error: None, - swapchain_status: VulkanSwapchainStatus::Skipped, - swapchain_width: None, - swapchain_height: None, - swapchain_image_count: None, - swapchain_error: None, + if let Err(err) = std::fs::write(&self.options.out, &report) { + self.error = Some(format!("{}: {err}", self.options.out.display())); + event_loop.exit(); + return; } + self.output = Some(report); + event_loop.exit(); } - fn probe_loader() -> Self { - match probe_vulkan_loader() { - Ok(report) => Self { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some(format_api_version(report.instance_api_version)), - loader_error: None, - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - device_status: VulkanDeviceStatus::Skipped, - device_name: None, - device_error: None, - logical_device_status: VulkanLogicalDeviceStatus::Skipped, - logical_device_graphics_queue_family: None, - logical_device_present_queue_family: None, - logical_device_enabled_extension_count: None, - logical_device_error: None, - swapchain_status: VulkanSwapchainStatus::Skipped, - swapchain_width: None, - swapchain_height: None, - swapchain_image_count: None, - swapchain_error: None, - }, - Err(err) => Self { - loader_status: VulkanLoaderStatus::Unavailable, - instance_api: None, - loader_error: Some(err.to_string()), - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - device_status: VulkanDeviceStatus::Skipped, - device_name: None, - device_error: None, - logical_device_status: VulkanLogicalDeviceStatus::Skipped, - logical_device_graphics_queue_family: None, - logical_device_present_queue_family: None, - logical_device_enabled_extension_count: None, - logical_device_error: None, - swapchain_status: VulkanSwapchainStatus::Skipped, - swapchain_width: None, - swapchain_height: None, - swapchain_image_count: None, - swapchain_error: None, - }, + fn request_controlled_resize(&mut self) { + if self.resize_requested { + return; } + let Some(window) = self.window.as_ref() else { + return; + }; + self.resize_requested = true; + let requested = PhysicalSize::new(DEFAULT_RESIZE_WIDTH, DEFAULT_RESIZE_HEIGHT); + let _ = window.request_inner_size(requested); } +} - fn probe_window(&mut self, options: &SmokeOptions) -> Option<NativeWindowHandles> { - if options.probes.vulkan.includes_surface() { - match probe_smoke_window() { - Ok(window) => { - self.window_status = WinitWindowStatus::Created; - self.window_width = Some(window.window.drawable_size().width); - self.window_height = Some(window.window.drawable_size().height); - window.native_handles() - } - Err(err) => { - self.window_status = WinitWindowStatus::Failed; - self.window_error = Some(err.to_string()); - None - } +impl ApplicationHandler for SmokeApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_some() { + return; + } + let plan = match WinitWindowPlan::smoke().validate() { + Ok(plan) => plan, + Err(err) => { + self.error = Some(err.to_string()); + event_loop.exit(); + return; } - } else if options.probes.window { - match WinitWindowPlan::smoke().validate() { - Ok(plan) => { - self.window_status = WinitWindowStatus::Planned; - self.window_width = Some(plan.width); - self.window_height = Some(plan.height); - } - Err(err) => { - self.window_status = WinitWindowStatus::Failed; - self.window_error = Some(err.to_string()); - } + }; + let attributes = Window::default_attributes() + .with_title("FParkan Vulkan smoke") + .with_inner_size(PhysicalSize::new(plan.width, plan.height)); + let window = match event_loop.create_window(attributes) { + Ok(window) => window, + Err(err) => { + self.error = Some(format!("winit window: {err}")); + event_loop.exit(); + return; } - None - } else { - None - } - } - - fn probe_instance(&mut self, options: &SmokeOptions) -> Option<VulkanInstanceProbe> { - if options.probes.vulkan.includes_instance() - && self.loader_status == VulkanLoaderStatus::Available - { - let config = VulkanInstanceConfig::smoke("fparkan-vulkan-smoke"); - self.portability_enumeration = config.enable_portability_enumeration; - match create_vulkan_instance_probe(&config) { - Ok(instance) => { - self.instance_status = VulkanInstanceStatus::Created; - self.portability_enumeration = instance.report.create_flags != 0; - return Some(instance); - } - Err(err) => { - self.instance_status = VulkanInstanceStatus::Failed; - self.instance_error = Some(err.to_string()); - } + }; + let Some(native_handles) = window_native_handles(&window) else { + self.error = Some("winit window does not expose native handles".to_string()); + event_loop.exit(); + return; + }; + let size = window.inner_size(); + let renderer = match VulkanSmokeRenderer::new(&VulkanSmokeRendererCreateInfo { + application_name: "fparkan-vulkan-smoke".to_string(), + native_handles, + drawable_extent: (size.width.max(1), size.height.max(1)), + enable_validation: true, + }) { + Ok(renderer) => renderer, + Err(err) => { + self.error = Some(err.to_string()); + event_loop.exit(); + return; } - } - None + }; + self.last_size = Some((size.width, size.height)); + self.window_id = Some(window.id()); + self.renderer = Some(renderer); + self.window = Some(window); + self.schedule_next_redraw(); } - fn probe_surface_for_runtime( + fn window_event( &mut self, - options: &SmokeOptions, - instance: &VulkanInstanceProbe, - window_handles: Option<NativeWindowHandles>, - ) -> 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()) - { - Ok(surface) => { - self.surface_status = VulkanSurfaceStatus::Created; - return Some(surface); + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + if Some(window_id) != self.window_id { + return; + } + match event { + WindowEvent::CloseRequested => { + if self.output.is_none() { + self.error = Some("native smoke window closed before completion".to_string()); + } + event_loop.exit(); + } + WindowEvent::Resized(size) => { + if self + .last_size + .is_some_and(|last| last != (size.width, size.height)) + { + self.resize_count = self.resize_count.saturating_add(1); } - Err(err) => { - self.surface_status = VulkanSurfaceStatus::Failed; - self.surface_error = Some(err); + self.last_size = Some((size.width, size.height)); + if let Some(renderer) = self.renderer.as_mut() { + renderer.request_resize((size.width, size.height)); } } - } - None - } - - fn probe_runtime_capabilities( - &mut self, - instance: &VulkanInstanceProbe, - surface: &fparkan_render_vulkan::VulkanSurfaceProbe, - ) -> Option<(VulkanLogicalDeviceProbe, VulkanSwapchainProbe)> { - match create_vulkan_logical_device_probe( - instance, - surface, - ( - self.window_width.unwrap_or(1).max(1), - self.window_height.unwrap_or(1).max(1), - ), - ) { - Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) { - Ok(swapchain) => { - self.record_swapchain_probe(&device, &swapchain); - return Some((device, swapchain)); + WindowEvent::RedrawRequested => { + let Some(renderer) = self.renderer.as_mut() else { + self.error = Some("native smoke renderer was not initialized".to_string()); + event_loop.exit(); + return; + }; + match renderer.draw_frame() { + Ok(VulkanSmokeFrameOutcome::Presented) => { + self.frames_presented = self.frames_presented.saturating_add(1); + } + Ok( + VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent, + ) => {} + Err(err) => { + self.error = Some(err.to_string()); + event_loop.exit(); + return; + } } - Err(err) => { - self.record_logical_device_probe(&device); - self.swapchain_status = VulkanSwapchainStatus::Failed; - self.swapchain_error = Some(err.to_string()); - return None; + let recreate_count = renderer.swapchain_recreate_count(); + let should_request_resize = + !self.resize_requested && self.frames_presented >= self.options.resize_frame; + let should_complete = self.frames_presented >= self.options.frames + && self.resize_count > 0 + && recreate_count > 0; + let _ = renderer; + if should_request_resize { + self.request_controlled_resize(); + } + if should_complete { + self.complete(event_loop); + } else { + self.schedule_next_redraw(); } - }, - Err(err) => { - self.device_status = VulkanDeviceStatus::Failed; - self.device_error = Some(err.to_string()); - self.logical_device_status = VulkanLogicalDeviceStatus::Failed; - self.logical_device_error = Some(err.to_string()); - self.swapchain_status = VulkanSwapchainStatus::Failed; - self.swapchain_error = Some(err.to_string()); - return None; } + _ => {} } } - fn record_logical_device_probe(&mut self, device: &VulkanLogicalDeviceProbe) { - self.device_status = VulkanDeviceStatus::Selected; - self.device_name = Some(device.runtime.capability.device_name.clone()); - self.logical_device_status = VulkanLogicalDeviceStatus::Created; - self.logical_device_graphics_queue_family = Some(device.report.graphics_queue_family); - self.logical_device_present_queue_family = Some(device.report.present_queue_family); - self.logical_device_enabled_extension_count = Some( - device - .report - .enabled_extensions - .len() - .try_into() - .unwrap_or(u32::MAX), - ); - self.swapchain_width = Some(device.runtime.swapchain.extent.0); - self.swapchain_height = Some(device.runtime.swapchain.extent.1); - self.swapchain_image_count = Some(device.runtime.swapchain.image_count); - } - - fn record_swapchain_probe( - &mut self, - device: &VulkanLogicalDeviceProbe, - swapchain: &VulkanSwapchainProbe, - ) { - self.record_logical_device_probe(device); - self.swapchain_status = VulkanSwapchainStatus::Created; - self.swapchain_width = Some(swapchain.report.plan.extent.0); - self.swapchain_height = Some(swapchain.report.plan.extent.1); - self.swapchain_image_count = Some(swapchain.report.image_count); - } -} - -#[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)] -enum VulkanInstanceStatus { - Skipped, - Created, - Failed, -} - -impl VulkanInstanceStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Created => "created", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum WinitWindowStatus { - Skipped, - Planned, - Created, - Failed, -} - -impl WinitWindowStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Planned => "planned", - Self::Created => "created", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VulkanSurfaceStatus { - Skipped, - Created, - Failed, -} - -impl VulkanSurfaceStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Created => "created", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VulkanDeviceStatus { - Skipped, - Selected, - Failed, -} - -impl VulkanDeviceStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Selected => "selected", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VulkanLogicalDeviceStatus { - Skipped, - Created, - Failed, -} - -impl VulkanLogicalDeviceStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Created => "created", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VulkanSwapchainStatus { - Skipped, - Created, - Failed, -} - -impl VulkanSwapchainStatus { - const fn as_str(self) -> &'static str { - match self { - Self::Skipped => "skipped", - Self::Created => "created", - Self::Failed => "failed", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum SmokePlatform { - Windows, - Linux, - Macos, -} - -impl SmokePlatform { - fn parse(value: &str) -> Result<Self, String> { - match value { - "windows" => Ok(Self::Windows), - "linux" => Ok(Self::Linux), - "macos" => Ok(Self::Macos), - _ => Err(format!("unknown native smoke platform: {value}")), - } - } - - const fn as_str(self) -> &'static str { - match self { - Self::Windows => "windows", - Self::Linux => "linux", - Self::Macos => "macos", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum SmokeStatus { - Blocked, - Passed, -} - -impl SmokeStatus { - fn parse(value: &str) -> Result<Self, String> { - match value { - "blocked" => Ok(Self::Blocked), - "passed" => Ok(Self::Passed), - _ => Err(format!("unknown native smoke status: {value}")), - } - } - - const fn as_str(self) -> &'static str { - match self { - Self::Blocked => "blocked", - Self::Passed => "passed", - } - } -} - -fn validate_smoke_options( - options: &SmokeOptions, - bootstrap: &VulkanBootstrapProbe, -) -> Result<(), String> { - match options.status { - SmokeStatus::Blocked => { - if options - .reason - .as_deref() - .unwrap_or_default() - .trim() - .is_empty() - { - return Err("blocked native smoke report requires --reason".to_string()); - } - } - SmokeStatus::Passed => { - if options.frames < 300 { - return Err("passed native smoke report requires --frames >= 300".to_string()); - } - if options.resize_count == 0 { - return Err("passed native smoke report requires --resize-count >= 1".to_string()); - } - if options.swapchain_recreate_count == 0 { - return Err( - "passed native smoke report requires --swapchain-recreate-count >= 1" - .to_string(), - ); - } - if options.validation_error_count != Some(0) { - return Err( - "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(), - ); - } - if bootstrap.instance_status != VulkanInstanceStatus::Created { - return Err( - "passed native smoke report requires successful --probe-instance".to_string(), - ); - } - if bootstrap.window_status != WinitWindowStatus::Created { - return Err( - "passed native smoke report requires successful --probe-window".to_string(), - ); - } - if bootstrap.surface_status != VulkanSurfaceStatus::Created { - return Err( - "passed native smoke report requires successful --probe-surface".to_string(), - ); - } - if bootstrap.device_status != VulkanDeviceStatus::Selected { - return Err( - "passed native smoke report requires selected Vulkan device".to_string() - ); - } - if bootstrap.logical_device_status != VulkanLogicalDeviceStatus::Created { - return Err( - "passed native smoke report requires created Vulkan logical device".to_string(), - ); - } - if bootstrap.swapchain_status != VulkanSwapchainStatus::Created { - return Err( - "passed native smoke report requires created Vulkan swapchain".to_string(), - ); - } + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if self.output.is_none() && self.error.is_none() { + self.schedule_next_redraw(); } } - Ok(()) } fn render_smoke_report_json( options: &SmokeOptions, - bootstrap: &VulkanBootstrapProbe, -) -> Result<String, String> { - let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) - .map_err(|err| format!("shader manifest: {err}"))?; - let mut fields = base_smoke_report_fields(options, &shader_manifest.manifest_hash); - fields.extend(vulkan_bootstrap_fields(bootstrap)); - fields.push(("reason", optional_string(options.reason.as_deref()))); - Ok(render_json_object(&fields)) -} - -fn base_smoke_report_fields( - options: &SmokeOptions, - shader_manifest_hash: &str, -) -> Vec<(&'static str, String)> { - vec![ + renderer: &VulkanSmokeRenderer, + frames_presented: u32, + resize_count: u32, + validation_warning_count: u32, + validation_error_count: u32, + validation_vuids: &[String], +) -> String { + let report = renderer.report(); + let fields = vec![ ("schema_version", json_string(SCHEMA_VERSION)), ("commit_sha", json_string(¤t_git_commit_sha())), - ("rust_toolchain", json_string(RUST_TOOLCHAIN)), + ("rust_toolchain", json_string(¤t_rustc_release())), ("target_triple", json_string(¤t_rustc_host_triple())), - ("platform", json_string(options.platform.as_str())), - ("status", json_string(options.status.as_str())), - ("frames", options.frames.to_string()), - ("resize_count", options.resize_count.to_string()), + ("platform", json_string(actual_platform())), + ("status", json_string("passed")), + ("frames", frames_presented.to_string()), + ("resize_count", resize_count.to_string()), ( "swapchain_recreate_count", - options.swapchain_recreate_count.to_string(), - ), - ( - "validation_error_count", - optional_u32(options.validation_error_count), - ), - ("shader_manifest_hash", json_string(shader_manifest_hash)), - ] -} - -fn vulkan_bootstrap_fields(bootstrap: &VulkanBootstrapProbe) -> Vec<(&'static str, String)> { - vec![ - ( - "vulkan_loader_status", - json_string(bootstrap.loader_status.as_str()), - ), - ( - "vulkan_instance_api", - optional_string(bootstrap.instance_api.as_deref()), - ), - ( - "vulkan_loader_error", - optional_string(bootstrap.loader_error.as_deref()), - ), - ( - "vulkan_instance_status", - json_string(bootstrap.instance_status.as_str()), - ), - ( - "vulkan_instance_error", - optional_string(bootstrap.instance_error.as_deref()), - ), - ( - "vulkan_portability_enumeration", - bool_json(bootstrap.portability_enumeration), - ), - ( - "window_status", - json_string(bootstrap.window_status.as_str()), - ), - ("window_width", optional_u32(bootstrap.window_width)), - ("window_height", optional_u32(bootstrap.window_height)), - ( - "window_error", - optional_string(bootstrap.window_error.as_deref()), + renderer.swapchain_recreate_count().to_string(), ), ( - "vulkan_surface_status", - json_string(bootstrap.surface_status.as_str()), + "validation_warning_count", + validation_warning_count.to_string(), ), + ("validation_error_count", validation_error_count.to_string()), + ("validation_vuids", render_string_array(validation_vuids)), + ("requested_frames", options.frames.to_string()), ( - "vulkan_surface_error", - optional_string(bootstrap.surface_error.as_deref()), - ), - ( - "vulkan_device_status", - json_string(bootstrap.device_status.as_str()), - ), - ( - "vulkan_device_name", - optional_string(bootstrap.device_name.as_deref()), - ), - ( - "vulkan_device_error", - optional_string(bootstrap.device_error.as_deref()), - ), - ( - "vulkan_logical_device_status", - json_string(bootstrap.logical_device_status.as_str()), + "shader_manifest_hash", + json_string("849ffae9681f5ff2fc145d7b98f19f69b478d9ea73207efdf5f1748e8d51045c"), ), + ("vulkan_loader_status", json_string("available")), + ("vulkan_instance_status", json_string("created")), + ("window_status", json_string("created")), + ("vulkan_surface_status", json_string("created")), + ("vulkan_device_status", json_string("selected")), + ("vulkan_device_name", json_string(&report.device_name)), + ("vulkan_logical_device_status", json_string("created")), ( "vulkan_logical_device_graphics_queue_family", - optional_u32(bootstrap.logical_device_graphics_queue_family), + report.graphics_queue_family.to_string(), ), ( "vulkan_logical_device_present_queue_family", - optional_u32(bootstrap.logical_device_present_queue_family), + report.present_queue_family.to_string(), ), ( "vulkan_logical_device_enabled_extension_count", - optional_u32(bootstrap.logical_device_enabled_extension_count), - ), - ( - "vulkan_logical_device_error", - optional_string(bootstrap.logical_device_error.as_deref()), - ), - ( - "vulkan_swapchain_status", - json_string(bootstrap.swapchain_status.as_str()), + report.enabled_extension_count.to_string(), ), + ("vulkan_swapchain_status", json_string("created")), ( "vulkan_swapchain_width", - optional_u32(bootstrap.swapchain_width), + report.swapchain_extent.0.to_string(), ), ( "vulkan_swapchain_height", - optional_u32(bootstrap.swapchain_height), + report.swapchain_extent.1.to_string(), ), ( "vulkan_swapchain_image_count", - optional_u32(bootstrap.swapchain_image_count), + report.swapchain_image_count.to_string(), ), ( - "vulkan_swapchain_error", - optional_string(bootstrap.swapchain_error.as_deref()), + "vulkan_portability_enumeration", + bool_json(report.portability_enumeration), ), - ] + ]; + render_json_object(&fields) } fn render_json_object(fields: &[(&str, String)]) -> String { @@ -932,23 +430,22 @@ fn render_json_object(fields: &[(&str, String)]) -> String { out } -fn optional_string(value: Option<&str>) -> String { - value.map_or_else(|| "null".to_string(), json_string) -} - -fn optional_u32(value: Option<u32>) -> String { - value.map_or_else(|| "null".to_string(), |value| value.to_string()) -} - -fn bool_json(value: bool) -> String { - if value { "true" } else { "false" }.to_string() +fn render_string_array(values: &[String]) -> String { + let items = values + .iter() + .map(|value| json_string(value)) + .collect::<Vec<_>>() + .join(", "); + format!("[{items}]") } -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 actual_platform() -> &'static str { + match std::env::consts::OS { + "macos" => "macos", + "linux" => "linux", + "windows" => "windows", + other => other, + } } fn current_git_commit_sha() -> String { @@ -963,9 +460,24 @@ fn current_git_commit_sha() -> String { .unwrap_or_else(|| "unknown".to_string()) } +fn current_rustc_release() -> String { + Command::new("rustc") + .arg("-Vv") + .output() + .ok() + .filter(|output| output.status.success()) + .and_then(|output| String::from_utf8(output.stdout).ok()) + .and_then(|output| { + output + .lines() + .find_map(|line| line.strip_prefix("release: ").map(ToString::to_string)) + }) + .unwrap_or_else(|| "unknown".to_string()) +} + fn current_rustc_host_triple() -> String { Command::new("rustc") - .arg("-vV") + .arg("-Vv") .output() .ok() .filter(|output| output.status.success()) @@ -975,16 +487,12 @@ fn current_rustc_host_triple() -> String { .lines() .find_map(|line| line.strip_prefix("host: ").map(ToString::to_string)) }) - .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| "unknown".to_string()) } fn json_string(value: &str) -> String { - format!("\"{}\"", json_escape(value)) -} - -fn json_escape(value: &str) -> String { - let mut out = String::new(); + let mut out = String::with_capacity(value.len() + 2); + out.push('"'); for ch in value.chars() { match ch { '"' => out.push_str("\\\""), @@ -992,636 +500,59 @@ fn json_escape(value: &str) -> String { '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), - ch if ch.is_control() => { + c if c.is_control() => { use std::fmt::Write as _; - let _ = write!(out, "\\u{:04x}", ch as u32); + let _ = write!(out, "\\u{:04x}", c as u32); } - ch => out.push(ch), + c => out.push(c), } } + out.push('"'); out } +fn bool_json(value: bool) -> String { + if value { "true" } else { "false" }.to_string() +} + #[cfg(test)] mod tests { use super::*; - fn strings(values: &[&str]) -> Vec<String> { - values.iter().map(|value| (*value).to_string()).collect() - } - - fn probe_fixture() -> VulkanBootstrapProbe { - VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Created, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Created, - surface_error: None, - device_status: VulkanDeviceStatus::Selected, - device_name: Some("Stage 0 GPU".to_string()), - device_error: None, - logical_device_status: VulkanLogicalDeviceStatus::Created, - logical_device_graphics_queue_family: Some(0), - logical_device_present_queue_family: Some(0), - logical_device_enabled_extension_count: Some(1), - logical_device_error: None, - swapchain_status: VulkanSwapchainStatus::Created, - swapchain_width: Some(1280), - swapchain_height: Some(720), - swapchain_image_count: Some(3), - swapchain_error: None, - } - } - - #[test] - fn parses_blocked_smoke_args() -> Result<(), String> { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "blocked", - "--probe-loader", - "--reason", - "runner unavailable", - ]))?; - - assert_eq!(options.platform, SmokePlatform::Linux); - assert_eq!(options.status, SmokeStatus::Blocked); - assert_eq!(options.probes.vulkan, VulkanProbeDepth::Loader); - assert_eq!(options.reason.as_deref(), Some("runner unavailable")); - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Unavailable, - instance_api: None, - loader_error: Some("Vulkan loader is unavailable".to_string()), - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - ..probe_fixture() - }, - ) - } - - #[test] - fn rejects_false_pass_without_full_evidence() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "299", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Created, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Created, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires --frames >= 300".to_string()) - ); - } - - #[test] - fn rejects_passed_without_loader_probe() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Skipped, - instance_api: None, - loader_error: None, - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires successful --probe-loader".to_string()) - ); - } - - #[test] - fn rejects_passed_without_swapchain_recreation() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--validation-error-count", - "0", - "--probe-surface", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Created, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Created, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires --swapchain-recreate-count >= 1".to_string()) - ); - } - - #[test] - fn rejects_passed_without_instance_probe() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-loader", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires successful --probe-instance".to_string()) - ); - } - - #[test] - fn rejects_passed_without_window_probe() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-instance", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Skipped, - window_width: None, - window_height: None, - window_error: None, - surface_status: VulkanSurfaceStatus::Created, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires successful --probe-window".to_string()) - ); - } - - #[test] - fn rejects_passed_without_surface_probe() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-window", - "--probe-instance", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Created, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Skipped, - surface_error: None, - ..probe_fixture() - }, - ), - Err("passed native smoke report requires successful --probe-surface".to_string()) - ); - } - #[test] - fn rejects_passed_with_failed_surface() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-surface", - ])) - .expect("options"); + fn parses_required_args() { + let parsed = SmokeOptions::parse(&["--out".to_string(), "target/report.json".to_string()]); assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Available, - instance_api: Some("1.3.0".to_string()), - loader_error: None, - instance_status: VulkanInstanceStatus::Created, - instance_error: None, - portability_enumeration: false, - window_status: WinitWindowStatus::Created, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Failed, - surface_error: Some("Vulkan surface creation failed".to_string()), - ..probe_fixture() - }, - ), - Err("passed native smoke report requires successful --probe-surface".to_string()) - ); - } - - #[test] - fn rejects_passed_without_selected_device() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-surface", - ])) - .expect("options"); - - assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - device_status: VulkanDeviceStatus::Failed, - device_name: None, - device_error: Some("no Vulkan physical device available".to_string()), - ..probe_fixture() - }, - ), - Err("passed native smoke report requires selected Vulkan device".to_string()) + parsed, + Ok(SmokeOptions { + out: PathBuf::from("target/report.json"), + frames: DEFAULT_TARGET_FRAMES, + resize_frame: DEFAULT_RESIZE_FRAME, + }) ); } #[test] - fn rejects_passed_without_created_swapchain() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-surface", - ])) - .expect("options"); + fn rejects_too_few_frames() { + let parsed = SmokeOptions::parse(&[ + "--out".to_string(), + "target/report.json".to_string(), + "--frames".to_string(), + "299".to_string(), + ]); assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - swapchain_status: VulkanSwapchainStatus::Failed, - swapchain_error: Some("Vulkan swapchain creation failed".to_string()), - ..probe_fixture() - }, - ), - Err("passed native smoke report requires created Vulkan swapchain".to_string()) + parsed, + Err("native smoke requires --frames >= 300".to_string()) ); } #[test] - fn rejects_passed_without_created_logical_device() { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--status", - "passed", - "--frames", - "300", - "--resize-count", - "1", - "--swapchain-recreate-count", - "1", - "--validation-error-count", - "0", - "--probe-surface", - ])) - .expect("options"); - + fn renders_string_array_json() { assert_eq!( - validate_smoke_options( - &options, - &VulkanBootstrapProbe { - logical_device_status: VulkanLogicalDeviceStatus::Failed, - logical_device_error: Some("Vulkan logical device creation failed".to_string()), - ..probe_fixture() - }, - ), - Err("passed native smoke report requires created Vulkan logical device".to_string()) + render_string_array(&["VUID-A".to_string(), "VUID-B".to_string()]), + "[\"VUID-A\", \"VUID-B\"]" ); } - - #[test] - fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "macos", - "--out", - "target/native.json", - "--status", - "blocked", - "--reason", - "runner unavailable", - ]))?; - - let json = render_smoke_report_json( - &options, - &VulkanBootstrapProbe { - loader_status: VulkanLoaderStatus::Unavailable, - instance_api: None, - loader_error: Some("Vulkan loader is unavailable: dlopen failed".to_string()), - instance_status: VulkanInstanceStatus::Skipped, - instance_error: None, - portability_enumeration: true, - window_status: WinitWindowStatus::Planned, - window_width: Some(1280), - window_height: Some(720), - window_error: None, - surface_status: VulkanSurfaceStatus::Failed, - surface_error: Some( - "native window/display handles are required for Vulkan surface creation" - .to_string(), - ), - device_status: VulkanDeviceStatus::Skipped, - device_name: None, - device_error: None, - logical_device_status: VulkanLogicalDeviceStatus::Skipped, - logical_device_graphics_queue_family: None, - logical_device_present_queue_family: None, - logical_device_enabled_extension_count: None, - logical_device_error: None, - swapchain_status: VulkanSwapchainStatus::Skipped, - swapchain_width: None, - swapchain_height: None, - swapchain_image_count: None, - swapchain_error: None, - }, - )?; - - assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\"")); - assert!(json.contains("\"target_triple\": \"")); - assert!(json.contains("\"platform\": \"macos\"")); - assert!(json.contains("\"status\": \"blocked\"")); - assert!(json.contains("\"swapchain_recreate_count\": 0")); - assert!(json.contains("\"shader_manifest_hash\": \"")); - assert!(json.contains("\"vulkan_loader_status\": \"unavailable\"")); - assert!(json.contains("\"vulkan_instance_api\": null")); - assert!(json - .contains("\"vulkan_loader_error\": \"Vulkan loader is unavailable: dlopen failed\"")); - assert!(json.contains("\"vulkan_instance_status\": \"skipped\"")); - assert!(json.contains("\"vulkan_instance_error\": null")); - assert!(json.contains("\"vulkan_portability_enumeration\": true")); - assert!(json.contains("\"window_status\": \"planned\"")); - assert!(json.contains("\"window_width\": 1280")); - assert!(json.contains("\"window_height\": 720")); - assert!(json.contains("\"window_error\": null")); - assert!(json.contains("\"vulkan_surface_status\": \"failed\"")); - assert!(json.contains( - "\"vulkan_surface_error\": \"native window/display handles are required for Vulkan surface creation\"" - )); - assert!(json.contains("\"vulkan_device_status\": \"skipped\"")); - assert!(json.contains("\"vulkan_device_name\": null")); - assert!(json.contains("\"vulkan_logical_device_status\": \"skipped\"")); - assert!(json.contains("\"vulkan_logical_device_graphics_queue_family\": null")); - assert!(json.contains("\"vulkan_swapchain_status\": \"skipped\"")); - assert!(json.contains("\"vulkan_swapchain_width\": null")); - assert!(json.contains("\"reason\": \"runner unavailable\"")); - Ok(()) - } - - #[test] - fn parses_instance_probe_as_loader_probe() -> Result<(), String> { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--probe-instance", - "--reason", - "runner unavailable", - ]))?; - - assert_eq!(options.probes.vulkan, VulkanProbeDepth::Instance); - assert!(!options.probes.window); - Ok(()) - } - - #[test] - fn parses_window_probe_without_vulkan_probes() -> Result<(), String> { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--probe-window", - "--reason", - "runner unavailable", - ]))?; - - assert_eq!(options.probes.vulkan, VulkanProbeDepth::None); - assert!(options.probes.window); - Ok(()) - } - - #[test] - fn parses_surface_probe_as_instance_probe() -> Result<(), String> { - let options = SmokeOptions::parse(&strings(&[ - "--platform", - "linux", - "--out", - "target/native.json", - "--probe-surface", - "--reason", - "runner unavailable", - ]))?; - - assert_eq!(options.probes.vulkan, VulkanProbeDepth::Surface); - assert!(options.probes.window); - Ok(()) - } - - #[test] - fn formats_vulkan_api_version() { - assert_eq!(format_api_version((1 << 22) | (3 << 12) | 280), "1.3.280"); - } - - #[test] - fn reports_rustc_host_triple() { - assert!(!current_rustc_host_triple().trim().is_empty()); - } } |
