fix(vulkan-smoke): emit structured timeout reports
This commit is contained in:
@@ -118,6 +118,7 @@ jobs:
|
|||||||
run: >
|
run: >
|
||||||
cargo run -p fparkan-vulkan-smoke --locked --
|
cargo run -p fparkan-vulkan-smoke --locked --
|
||||||
--out "target/fparkan/native-smoke/${{ matrix.smoke_platform }}.json"
|
--out "target/fparkan/native-smoke/${{ matrix.smoke_platform }}.json"
|
||||||
|
--timeout-seconds 120
|
||||||
- name: Upload acceptance audit
|
- name: Upload acceptance audit
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ FPARKAN_CORPORA_MANIFEST=/private/tmp/fparkan-corpora.toml \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -p fparkan-vulkan-smoke --locked -- \
|
cargo run -p fparkan-vulkan-smoke --locked -- \
|
||||||
--out target/fparkan/native-smoke/local.json
|
--out target/fparkan/native-smoke/local.json \
|
||||||
|
--timeout-seconds 120
|
||||||
```
|
```
|
||||||
|
|
||||||
Перед запуском убедитесь, что на машине доступен Vulkan loader и рабочий ICD:
|
Перед запуском убедитесь, что на машине доступен Vulkan loader и рабочий ICD:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use fparkan_render_vulkan::{
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
@@ -29,6 +30,7 @@ const DEFAULT_TARGET_FRAMES: u32 = 300;
|
|||||||
const DEFAULT_RESIZE_FRAME: u32 = 120;
|
const DEFAULT_RESIZE_FRAME: u32 = 120;
|
||||||
const DEFAULT_RESIZE_WIDTH: u32 = 960;
|
const DEFAULT_RESIZE_WIDTH: u32 = 960;
|
||||||
const DEFAULT_RESIZE_HEIGHT: u32 = 540;
|
const DEFAULT_RESIZE_HEIGHT: u32 = 540;
|
||||||
|
const DEFAULT_TIMEOUT_SECONDS: u64 = 120;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||||
@@ -49,9 +51,9 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
let options = SmokeOptions::parse(args)?;
|
let options = SmokeOptions::parse(args)?;
|
||||||
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
||||||
let mut app = SmokeApp::new(options);
|
let mut app = SmokeApp::new(options);
|
||||||
event_loop
|
if let Err(err) = event_loop.run_app(&mut app) {
|
||||||
.run_app(&mut app)
|
app.error = Some(format!("winit event loop: {err}"));
|
||||||
.map_err(|err| format!("winit event loop: {err}"))?;
|
}
|
||||||
app.finish()
|
app.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ struct SmokeOptions {
|
|||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
frames: u32,
|
frames: u32,
|
||||||
resize_frame: u32,
|
resize_frame: u32,
|
||||||
|
timeout_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmokeOptions {
|
impl SmokeOptions {
|
||||||
@@ -67,6 +70,7 @@ impl SmokeOptions {
|
|||||||
let mut out = None;
|
let mut out = None;
|
||||||
let mut frames = DEFAULT_TARGET_FRAMES;
|
let mut frames = DEFAULT_TARGET_FRAMES;
|
||||||
let mut resize_frame = DEFAULT_RESIZE_FRAME;
|
let mut resize_frame = DEFAULT_RESIZE_FRAME;
|
||||||
|
let mut timeout_seconds = DEFAULT_TIMEOUT_SECONDS;
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
@@ -91,6 +95,13 @@ impl SmokeOptions {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "--resize-frame must be an integer".to_string())?;
|
.map_err(|_| "--resize-frame must be an integer".to_string())?;
|
||||||
}
|
}
|
||||||
|
"--timeout-seconds" => {
|
||||||
|
timeout_seconds = iter
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "--timeout-seconds requires a value".to_string())?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "--timeout-seconds must be an integer".to_string())?;
|
||||||
|
}
|
||||||
_ => return Err(format!("unknown native smoke option: {arg}")),
|
_ => return Err(format!("unknown native smoke option: {arg}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,10 +111,14 @@ impl SmokeOptions {
|
|||||||
"native smoke requires --frames >= {DEFAULT_TARGET_FRAMES}"
|
"native smoke requires --frames >= {DEFAULT_TARGET_FRAMES}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if timeout_seconds == 0 {
|
||||||
|
return Err("native smoke requires --timeout-seconds >= 1".to_string());
|
||||||
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
out,
|
out,
|
||||||
frames,
|
frames,
|
||||||
resize_frame,
|
resize_frame,
|
||||||
|
timeout_seconds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +134,11 @@ struct SmokeApp {
|
|||||||
resize_count: u32,
|
resize_count: u32,
|
||||||
resize_requested: bool,
|
resize_requested: bool,
|
||||||
last_size: Option<(u32, u32)>,
|
last_size: Option<(u32, u32)>,
|
||||||
|
started_at: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmokeApp {
|
impl SmokeApp {
|
||||||
const fn new(options: SmokeOptions) -> Self {
|
fn new(options: SmokeOptions) -> Self {
|
||||||
Self {
|
Self {
|
||||||
options,
|
options,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
@@ -134,15 +150,20 @@ impl SmokeApp {
|
|||||||
resize_count: 0,
|
resize_count: 0,
|
||||||
resize_requested: false,
|
resize_requested: false,
|
||||||
last_size: None,
|
last_size: None,
|
||||||
|
started_at: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(self) -> Result<String, String> {
|
fn finish(mut self) -> Result<String, String> {
|
||||||
if let Some(error) = self.error {
|
if let Some(output) = self.output.take() {
|
||||||
return Err(error);
|
return Ok(output);
|
||||||
}
|
}
|
||||||
self.output
|
let error = self
|
||||||
.ok_or_else(|| "native smoke exited before producing a report".to_string())
|
.error
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "native smoke exited before producing a report".to_string());
|
||||||
|
self.write_failure_report(&error)?;
|
||||||
|
Err(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_next_redraw(&self) {
|
fn schedule_next_redraw(&self) {
|
||||||
@@ -151,6 +172,124 @@ impl SmokeApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_report(&self, report: &str) -> Result<(), String> {
|
||||||
|
if let Some(parent) = self.options.out.parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|err| format!("{}: {err}", parent.display()))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&self.options.out, report)
|
||||||
|
.map_err(|err| format!("{}: {err}", self.options.out.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_report(
|
||||||
|
&self,
|
||||||
|
status: &'static str,
|
||||||
|
failure_reason: Option<&str>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let renderer = self.renderer.as_ref();
|
||||||
|
let validation = renderer.map_or(
|
||||||
|
fparkan_render_vulkan::VulkanValidationReport {
|
||||||
|
warning_count: 0,
|
||||||
|
error_count: 0,
|
||||||
|
vuids: Vec::new(),
|
||||||
|
},
|
||||||
|
VulkanSmokeRenderer::validation_report,
|
||||||
|
);
|
||||||
|
let smoke_report = SmokeReport {
|
||||||
|
schema_version: SCHEMA_VERSION,
|
||||||
|
commit_sha: current_git_commit_sha(),
|
||||||
|
git_dirty: current_git_dirty(),
|
||||||
|
runner_identity: measured_runner_identity(),
|
||||||
|
rust_toolchain: current_rustc_release(),
|
||||||
|
target_triple: current_rustc_host_triple(),
|
||||||
|
platform: actual_platform(),
|
||||||
|
status,
|
||||||
|
failure_reason,
|
||||||
|
frames: self.frames_presented,
|
||||||
|
resize_count: self.resize_count,
|
||||||
|
swapchain_recreate_count: renderer
|
||||||
|
.map_or(0, VulkanSmokeRenderer::swapchain_recreate_count),
|
||||||
|
validation_warning_count: validation.warning_count,
|
||||||
|
validation_error_count: validation.error_count,
|
||||||
|
validation_vuids: &validation.vuids,
|
||||||
|
requested_frames: self.options.frames,
|
||||||
|
timeout_seconds: self.options.timeout_seconds,
|
||||||
|
shader_manifest_hash: renderer
|
||||||
|
.map_or("", |value| value.report().shader_manifest_hash.as_str()),
|
||||||
|
vulkan_loader_status: if renderer.is_some() {
|
||||||
|
"available"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_instance_status: if renderer.is_some() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
window_status: if self.window.is_some() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_surface_status: if renderer.is_some() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_device_status: if renderer.is_some() {
|
||||||
|
"selected"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_device_name: renderer.map_or("", |value| value.report().device_name.as_str()),
|
||||||
|
vulkan_logical_device_status: if renderer.is_some() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_logical_device_graphics_queue_family: renderer
|
||||||
|
.map_or(0, |value| value.report().graphics_queue_family),
|
||||||
|
vulkan_logical_device_present_queue_family: renderer
|
||||||
|
.map_or(0, |value| value.report().present_queue_family),
|
||||||
|
vulkan_logical_device_enabled_extension_count: renderer
|
||||||
|
.map_or(0, |value| value.report().enabled_extension_count),
|
||||||
|
vulkan_swapchain_status: if renderer.is_some() {
|
||||||
|
"created"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
},
|
||||||
|
vulkan_swapchain_width: renderer.map_or(0, |value| value.report().swapchain_extent.0),
|
||||||
|
vulkan_swapchain_height: renderer.map_or(0, |value| value.report().swapchain_extent.1),
|
||||||
|
vulkan_swapchain_image_count: renderer
|
||||||
|
.map_or(0, |value| value.report().swapchain_image_count),
|
||||||
|
vulkan_portability_enumeration: renderer
|
||||||
|
.is_some_and(|value| value.report().portability_enumeration),
|
||||||
|
};
|
||||||
|
serde_json::to_string_pretty(&smoke_report)
|
||||||
|
.map(|json| format!("{json}\n"))
|
||||||
|
.map_err(|err| format!("native smoke report serialization failed: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_failure_report(&self, failure_reason: &str) -> Result<(), String> {
|
||||||
|
let report = self.render_report("failed", Some(failure_reason))?;
|
||||||
|
self.write_report(&report)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn abort_if_timed_out(&mut self, event_loop: &ActiveEventLoop) -> bool {
|
||||||
|
if self.output.is_some() || self.error.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.started_at.elapsed() <= Duration::from_secs(self.options.timeout_seconds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.error = Some(format!(
|
||||||
|
"native smoke timed out after {} seconds",
|
||||||
|
self.options.timeout_seconds
|
||||||
|
));
|
||||||
|
event_loop.exit();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn complete(&mut self, event_loop: &ActiveEventLoop) {
|
fn complete(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
let Some(renderer) = self.renderer.as_ref() else {
|
let Some(renderer) = self.renderer.as_ref() else {
|
||||||
self.error = Some("native smoke renderer was not initialized".to_string());
|
self.error = Some("native smoke renderer was not initialized".to_string());
|
||||||
@@ -179,15 +318,8 @@ impl SmokeApp {
|
|||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let report = match render_smoke_report_json(
|
let _ = renderer;
|
||||||
&self.options,
|
let report = match self.render_report("passed", None) {
|
||||||
renderer,
|
|
||||||
self.frames_presented,
|
|
||||||
self.resize_count,
|
|
||||||
validation.warning_count,
|
|
||||||
validation.error_count,
|
|
||||||
&validation.vuids,
|
|
||||||
) {
|
|
||||||
Ok(report) => report,
|
Ok(report) => report,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.error = Some(err);
|
self.error = Some(err);
|
||||||
@@ -195,15 +327,8 @@ impl SmokeApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(parent) = self.options.out.parent() {
|
if let Err(err) = self.write_report(&report) {
|
||||||
if let Err(err) = std::fs::create_dir_all(parent) {
|
self.error = Some(err);
|
||||||
self.error = Some(format!("{}: {err}", parent.display()));
|
|
||||||
event_loop.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = std::fs::write(&self.options.out, &report) {
|
|
||||||
self.error = Some(format!("{}: {err}", self.options.out.display()));
|
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,6 +351,9 @@ impl SmokeApp {
|
|||||||
|
|
||||||
impl ApplicationHandler for SmokeApp {
|
impl ApplicationHandler for SmokeApp {
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.abort_if_timed_out(event_loop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.window.is_some() {
|
if self.window.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -280,6 +408,9 @@ impl ApplicationHandler for SmokeApp {
|
|||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
event: WindowEvent,
|
event: WindowEvent,
|
||||||
) {
|
) {
|
||||||
|
if self.abort_if_timed_out(event_loop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if Some(window_id) != self.window_id {
|
if Some(window_id) != self.window_id {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -341,7 +472,10 @@ impl ApplicationHandler for SmokeApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.abort_if_timed_out(event_loop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.output.is_none() && self.error.is_none() {
|
if self.output.is_none() && self.error.is_none() {
|
||||||
self.schedule_next_redraw();
|
self.schedule_next_redraw();
|
||||||
}
|
}
|
||||||
@@ -357,7 +491,9 @@ struct SmokeReport<'a> {
|
|||||||
rust_toolchain: String,
|
rust_toolchain: String,
|
||||||
target_triple: String,
|
target_triple: String,
|
||||||
platform: &'static str,
|
platform: &'static str,
|
||||||
status: &'static str,
|
status: &'a str,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
failure_reason: Option<&'a str>,
|
||||||
frames: u32,
|
frames: u32,
|
||||||
resize_count: u32,
|
resize_count: u32,
|
||||||
swapchain_recreate_count: u32,
|
swapchain_recreate_count: u32,
|
||||||
@@ -365,72 +501,25 @@ struct SmokeReport<'a> {
|
|||||||
validation_error_count: u32,
|
validation_error_count: u32,
|
||||||
validation_vuids: &'a [String],
|
validation_vuids: &'a [String],
|
||||||
requested_frames: u32,
|
requested_frames: u32,
|
||||||
|
timeout_seconds: u64,
|
||||||
shader_manifest_hash: &'a str,
|
shader_manifest_hash: &'a str,
|
||||||
vulkan_loader_status: &'static str,
|
vulkan_loader_status: &'a str,
|
||||||
vulkan_instance_status: &'static str,
|
vulkan_instance_status: &'a str,
|
||||||
window_status: &'static str,
|
window_status: &'a str,
|
||||||
vulkan_surface_status: &'static str,
|
vulkan_surface_status: &'a str,
|
||||||
vulkan_device_status: &'static str,
|
vulkan_device_status: &'a str,
|
||||||
vulkan_device_name: &'a str,
|
vulkan_device_name: &'a str,
|
||||||
vulkan_logical_device_status: &'static str,
|
vulkan_logical_device_status: &'a str,
|
||||||
vulkan_logical_device_graphics_queue_family: u32,
|
vulkan_logical_device_graphics_queue_family: u32,
|
||||||
vulkan_logical_device_present_queue_family: u32,
|
vulkan_logical_device_present_queue_family: u32,
|
||||||
vulkan_logical_device_enabled_extension_count: u32,
|
vulkan_logical_device_enabled_extension_count: u32,
|
||||||
vulkan_swapchain_status: &'static str,
|
vulkan_swapchain_status: &'a str,
|
||||||
vulkan_swapchain_width: u32,
|
vulkan_swapchain_width: u32,
|
||||||
vulkan_swapchain_height: u32,
|
vulkan_swapchain_height: u32,
|
||||||
vulkan_swapchain_image_count: u32,
|
vulkan_swapchain_image_count: u32,
|
||||||
vulkan_portability_enumeration: bool,
|
vulkan_portability_enumeration: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_smoke_report_json(
|
|
||||||
options: &SmokeOptions,
|
|
||||||
renderer: &VulkanSmokeRenderer,
|
|
||||||
frames_presented: u32,
|
|
||||||
resize_count: u32,
|
|
||||||
validation_warning_count: u32,
|
|
||||||
validation_error_count: u32,
|
|
||||||
validation_vuids: &[String],
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let report = renderer.report();
|
|
||||||
let smoke_report = SmokeReport {
|
|
||||||
schema_version: SCHEMA_VERSION,
|
|
||||||
commit_sha: current_git_commit_sha(),
|
|
||||||
git_dirty: current_git_dirty(),
|
|
||||||
runner_identity: measured_runner_identity(),
|
|
||||||
rust_toolchain: current_rustc_release(),
|
|
||||||
target_triple: current_rustc_host_triple(),
|
|
||||||
platform: actual_platform(),
|
|
||||||
status: "passed",
|
|
||||||
frames: frames_presented,
|
|
||||||
resize_count,
|
|
||||||
swapchain_recreate_count: renderer.swapchain_recreate_count(),
|
|
||||||
validation_warning_count,
|
|
||||||
validation_error_count,
|
|
||||||
validation_vuids,
|
|
||||||
requested_frames: options.frames,
|
|
||||||
shader_manifest_hash: &report.shader_manifest_hash,
|
|
||||||
vulkan_loader_status: "available",
|
|
||||||
vulkan_instance_status: "created",
|
|
||||||
window_status: "created",
|
|
||||||
vulkan_surface_status: "created",
|
|
||||||
vulkan_device_status: "selected",
|
|
||||||
vulkan_device_name: &report.device_name,
|
|
||||||
vulkan_logical_device_status: "created",
|
|
||||||
vulkan_logical_device_graphics_queue_family: report.graphics_queue_family,
|
|
||||||
vulkan_logical_device_present_queue_family: report.present_queue_family,
|
|
||||||
vulkan_logical_device_enabled_extension_count: report.enabled_extension_count,
|
|
||||||
vulkan_swapchain_status: "created",
|
|
||||||
vulkan_swapchain_width: report.swapchain_extent.0,
|
|
||||||
vulkan_swapchain_height: report.swapchain_extent.1,
|
|
||||||
vulkan_swapchain_image_count: report.swapchain_image_count,
|
|
||||||
vulkan_portability_enumeration: report.portability_enumeration,
|
|
||||||
};
|
|
||||||
serde_json::to_string_pretty(&smoke_report)
|
|
||||||
.map(|json| format!("{json}\n"))
|
|
||||||
.map_err(|err| format!("native smoke report serialization failed: {err}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn actual_platform() -> &'static str {
|
fn actual_platform() -> &'static str {
|
||||||
match std::env::consts::OS {
|
match std::env::consts::OS {
|
||||||
"macos" => "macos",
|
"macos" => "macos",
|
||||||
@@ -521,6 +610,7 @@ mod tests {
|
|||||||
out: PathBuf::from("target/report.json"),
|
out: PathBuf::from("target/report.json"),
|
||||||
frames: DEFAULT_TARGET_FRAMES,
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
resize_frame: DEFAULT_RESIZE_FRAME,
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
|
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -540,6 +630,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_timeout_seconds() {
|
||||||
|
let parsed = SmokeOptions::parse(&[
|
||||||
|
"--out".to_string(),
|
||||||
|
"target/report.json".to_string(),
|
||||||
|
"--timeout-seconds".to_string(),
|
||||||
|
"45".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Ok(SmokeOptions {
|
||||||
|
out: PathBuf::from("target/report.json"),
|
||||||
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
|
timeout_seconds: 45,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_timeout_seconds() {
|
||||||
|
let parsed = SmokeOptions::parse(&[
|
||||||
|
"--out".to_string(),
|
||||||
|
"target/report.json".to_string(),
|
||||||
|
"--timeout-seconds".to_string(),
|
||||||
|
"0".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Err("native smoke requires --timeout-seconds >= 1".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_deprecated_self_assertion_flags() {
|
fn rejects_deprecated_self_assertion_flags() {
|
||||||
for flag in [
|
for flag in [
|
||||||
@@ -571,6 +696,7 @@ mod tests {
|
|||||||
target_triple: "aarch64-apple-darwin".to_string(),
|
target_triple: "aarch64-apple-darwin".to_string(),
|
||||||
platform: "macos",
|
platform: "macos",
|
||||||
status: "passed",
|
status: "passed",
|
||||||
|
failure_reason: None,
|
||||||
frames: 300,
|
frames: 300,
|
||||||
resize_count: 1,
|
resize_count: 1,
|
||||||
swapchain_recreate_count: 1,
|
swapchain_recreate_count: 1,
|
||||||
@@ -578,6 +704,7 @@ mod tests {
|
|||||||
validation_error_count: 0,
|
validation_error_count: 0,
|
||||||
validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()],
|
validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()],
|
||||||
requested_frames: 300,
|
requested_frames: 300,
|
||||||
|
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
||||||
shader_manifest_hash: "deadbeef",
|
shader_manifest_hash: "deadbeef",
|
||||||
vulkan_loader_status: "available",
|
vulkan_loader_status: "available",
|
||||||
vulkan_instance_status: "created",
|
vulkan_instance_status: "created",
|
||||||
@@ -601,4 +728,51 @@ mod tests {
|
|||||||
assert!(json.contains("\"validation_vuids\": ["));
|
assert!(json.contains("\"validation_vuids\": ["));
|
||||||
assert!(json.contains("\"vulkan_device_name\": \"Apple GPU\""));
|
assert!(json.contains("\"vulkan_device_name\": \"Apple GPU\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finish_writes_failure_artifact() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"fparkan-native-smoke-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("time")
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).expect("temp dir");
|
||||||
|
let out = root.join("report.json");
|
||||||
|
|
||||||
|
let result = SmokeApp {
|
||||||
|
options: SmokeOptions {
|
||||||
|
out: out.clone(),
|
||||||
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
|
timeout_seconds: 7,
|
||||||
|
},
|
||||||
|
window_id: None,
|
||||||
|
window: None,
|
||||||
|
renderer: None,
|
||||||
|
error: Some("native smoke timed out after 7 seconds".to_string()),
|
||||||
|
output: None,
|
||||||
|
frames_presented: 42,
|
||||||
|
resize_count: 0,
|
||||||
|
resize_requested: false,
|
||||||
|
last_size: None,
|
||||||
|
started_at: Instant::now(),
|
||||||
|
}
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err("native smoke timed out after 7 seconds".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = std::fs::read_to_string(&out).expect("failure report");
|
||||||
|
assert!(json.contains("\"status\": \"failed\""));
|
||||||
|
assert!(json.contains("\"failure_reason\": \"native smoke timed out after 7 seconds\""));
|
||||||
|
assert!(json.contains("\"timeout_seconds\": 7"));
|
||||||
|
|
||||||
|
std::fs::remove_file(out).expect("cleanup report");
|
||||||
|
std::fs::remove_dir(root).expect("cleanup dir");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user