1use core::fmt;
2
3#[derive(Default, Clone, PartialEq)]
40pub struct FailureCase {
41 pub backends: Option<wgpu::Backends>,
46
47 pub vendor: Option<u32>,
55
56 pub adapter: Option<&'static str>,
64
65 pub driver: Option<&'static str>,
73
74 pub reasons: Vec<FailureReason>,
80
81 pub behavior: FailureBehavior,
83}
84
85impl FailureCase {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn always() -> Self {
93 FailureCase::default()
94 }
95
96 pub fn never() -> Self {
98 FailureCase {
99 backends: Some(wgpu::Backends::empty()),
100 ..FailureCase::default()
101 }
102 }
103
104 pub fn backend(backends: wgpu::Backends) -> Self {
106 FailureCase {
107 backends: Some(backends),
108 ..FailureCase::default()
109 }
110 }
111
112 pub fn adapter(adapter: &'static str) -> Self {
120 FailureCase {
121 adapter: Some(adapter),
122 ..FailureCase::default()
123 }
124 }
125
126 pub fn backend_adapter(backends: wgpu::Backends, adapter: &'static str) -> Self {
135 FailureCase {
136 backends: Some(backends),
137 adapter: Some(adapter),
138 ..FailureCase::default()
139 }
140 }
141
142 pub fn webgl2() -> Self {
144 #[cfg(target_arch = "wasm32")]
145 let case = FailureCase::backend(wgpu::Backends::GL);
146 #[cfg(not(target_arch = "wasm32"))]
147 let case = FailureCase::never();
148 case
149 }
150
151 pub fn molten_vk() -> Self {
153 FailureCase {
154 backends: Some(wgpu::Backends::VULKAN),
155 driver: Some("MoltenVK"),
156 ..FailureCase::default()
157 }
158 }
159
160 pub fn kosmic_krisp() -> Self {
162 FailureCase {
163 backends: Some(wgpu::Backends::VULKAN),
164 driver: Some("KosmicKrisp"),
165 ..FailureCase::default()
166 }
167 }
168
169 pub fn mac_vulkan(f: impl Fn(FailureCase) -> FailureCase) -> Vec<Self> {
171 vec![f(FailureCase::molten_vk()), f(FailureCase::kosmic_krisp())]
172 }
173
174 pub fn reasons(&self) -> &[FailureReason] {
176 if self.reasons.is_empty() {
177 std::array::from_ref(&FailureReason::ANY)
178 } else {
179 &self.reasons
180 }
181 }
182
183 pub fn validation_error(mut self, msg: &'static str) -> Self {
189 self.reasons
190 .push(FailureReason::validation_error().with_message(msg));
191 self
192 }
193
194 pub fn panic(mut self, msg: &'static str) -> Self {
200 self.reasons.push(FailureReason::panic().with_message(msg));
201 self
202 }
203
204 pub fn unexpected_error(mut self, msg: &'static str) -> Self {
214 self.reasons.push(FailureReason::panic().with_message(msg));
215 self.reasons.push(FailureReason::panic().with_message(
216 "Device lost: Unexpected error variant (driver implementation is at fault)",
217 ));
218 self
219 }
220
221 pub fn flaky(self) -> Self {
225 FailureCase {
226 behavior: FailureBehavior::Ignore,
227 ..self
228 }
229 }
230
231 pub(crate) fn applies_to_adapter(
240 &self,
241 info: &wgpu::AdapterInfo,
242 ) -> Option<FailureApplicationReasons> {
243 let mut reasons = FailureApplicationReasons::empty();
244
245 if let Some(backends) = self.backends {
246 if !backends.contains(wgpu::Backends::from(info.backend)) {
247 return None;
248 }
249 reasons.set(FailureApplicationReasons::BACKEND, true);
250 }
251 if let Some(vendor) = self.vendor {
252 if vendor != info.vendor {
253 return None;
254 }
255 reasons.set(FailureApplicationReasons::VENDOR, true);
256 }
257 if let Some(adapter) = self.adapter {
258 let adapter = adapter.to_lowercase();
259 if !info.name.contains(&adapter) {
260 return None;
261 }
262 reasons.set(FailureApplicationReasons::ADAPTER, true);
263 }
264 if let Some(driver) = self.driver {
265 let driver = driver.to_lowercase();
266 if !info.driver.contains(&driver) {
267 return None;
268 }
269 reasons.set(FailureApplicationReasons::DRIVER, true);
270 }
271
272 if reasons.is_empty() {
275 Some(FailureApplicationReasons::ALWAYS)
276 } else {
277 Some(reasons)
278 }
279 }
280
281 pub(crate) fn matches_failure(&self, failure: &FailureResult) -> bool {
283 for reason in self.reasons() {
284 let kind_matched = reason.kind.is_none_or(|kind| kind == failure.kind);
285
286 let message_matched =
287 reason
288 .message
289 .is_none_or(|message| matches!(&failure.message, Some(actual) if actual.to_lowercase().contains(&message.to_lowercase())));
290
291 if kind_matched && message_matched {
292 let message = failure.message.as_deref().unwrap_or("*no message*");
293 log::error!("Matched {} {message}", failure.kind);
294 return true;
295 }
296 }
297
298 false
299 }
300}
301
302bitflags::bitflags! {
303 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
305 pub struct FailureApplicationReasons: u8 {
306 const BACKEND = 1 << 0;
307 const VENDOR = 1 << 1;
308 const ADAPTER = 1 << 2;
309 const DRIVER = 1 << 3;
310 const ALWAYS = 1 << 4;
311 }
312}
313
314#[derive(Default, Debug, Clone, PartialEq)]
318pub struct FailureReason {
319 kind: Option<FailureResultKind>,
323 message: Option<&'static str>,
329}
330
331impl FailureReason {
332 const ANY: Self = Self {
334 kind: None,
335 message: None,
336 };
337
338 #[allow(dead_code, reason = "Not constructed on wasm")]
340 pub fn validation_error() -> Self {
341 Self {
342 kind: Some(FailureResultKind::ValidationError),
343 message: None,
344 }
345 }
346
347 pub fn panic() -> Self {
349 Self {
350 kind: Some(FailureResultKind::Panic),
351 message: None,
352 }
353 }
354
355 pub fn with_message(self, message: &'static str) -> Self {
361 Self {
362 message: Some(message),
363 ..self
364 }
365 }
366}
367
368#[derive(Default, Clone, PartialEq)]
369pub enum FailureBehavior {
370 #[default]
374 AssertFailure,
375 Ignore,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq)]
383pub(crate) enum FailureResultKind {
384 #[allow(dead_code, reason = "Not constructed on wasm")]
385 ValidationError,
386 Panic,
387}
388
389impl fmt::Display for FailureResultKind {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 match self {
392 FailureResultKind::ValidationError => write!(f, "Validation Error"),
393 FailureResultKind::Panic => write!(f, "Panic"),
394 }
395 }
396}
397
398#[derive(Debug)]
399pub(crate) struct FailureResult {
400 kind: FailureResultKind,
401 message: Option<String>,
402}
403
404impl FailureResult {
405 pub(super) fn panic() -> Self {
407 Self {
408 kind: FailureResultKind::Panic,
409 message: None,
410 }
411 }
412
413 #[allow(dead_code, reason = "Not constructed on wasm")]
415 pub(super) fn validation_error() -> Self {
416 Self {
417 kind: FailureResultKind::ValidationError,
418 message: None,
419 }
420 }
421
422 pub(super) fn with_message(self, message: impl fmt::Display) -> Self {
424 Self {
425 kind: self.kind,
426 message: Some(message.to_string()),
427 }
428 }
429}
430
431#[derive(PartialEq, Clone, Copy, Debug)]
432pub(crate) enum ExpectationMatchResult {
433 Panic,
434 Complete,
435}
436
437pub(crate) fn expectations_match_failures(
439 expectations: &[FailureCase],
440 mut actual: Vec<FailureResult>,
441) -> ExpectationMatchResult {
442 let mut result = ExpectationMatchResult::Complete;
444
445 for expected_failure in expectations {
447 let mut matched = false;
449
450 actual.retain(|failure| {
454 let matches = expected_failure.matches_failure(failure);
456
457 if matches {
458 matched = true;
459 }
460
461 !matches
463 });
464
465 if !matched && matches!(expected_failure.behavior, FailureBehavior::AssertFailure) {
468 result = ExpectationMatchResult::Panic;
469 log::error!(
470 "Expected to fail due to {:?}, but did not fail",
471 expected_failure.reasons()
472 );
473 }
474 }
475
476 if !actual.is_empty() {
479 result = ExpectationMatchResult::Panic;
480 for failure in actual {
481 let message = failure.message.as_deref().unwrap_or("*no message*");
482 log::error!("{}: {message}", failure.kind);
483 }
484 }
485
486 result
487}
488
489#[cfg(test)]
490mod test {
491 use crate::{
492 expectations::{ExpectationMatchResult, FailureResult},
493 init::init_logger,
494 FailureCase,
495 };
496
497 fn validation_err(msg: &'static str) -> FailureResult {
498 FailureResult::validation_error().with_message(msg)
499 }
500
501 fn panic(msg: &'static str) -> FailureResult {
502 FailureResult::panic().with_message(msg)
503 }
504
505 #[test]
506 fn simple_match() {
507 init_logger();
508
509 let expectation = vec![];
512 let actual = vec![FailureResult::validation_error()];
513
514 assert_eq!(
515 super::expectations_match_failures(&expectation, actual),
516 ExpectationMatchResult::Panic
517 );
518
519 let expectation = vec![FailureCase::always()];
522 let actual = vec![];
523
524 assert_eq!(
525 super::expectations_match_failures(&expectation, actual),
526 ExpectationMatchResult::Panic
527 );
528
529 let expectation = vec![FailureCase::always()];
532 let actual = vec![FailureResult::validation_error()];
533
534 assert_eq!(
535 super::expectations_match_failures(&expectation, actual),
536 ExpectationMatchResult::Complete
537 );
538
539 let expectation = vec![FailureCase::always()];
542 let actual = vec![FailureResult::panic()];
543
544 assert_eq!(
545 super::expectations_match_failures(&expectation, actual),
546 ExpectationMatchResult::Complete
547 );
548 }
549
550 #[test]
551 fn substring_match() {
552 init_logger();
553
554 let expectation: Vec<FailureCase> =
557 vec![FailureCase::always().validation_error("Some StrIng")];
558 let actual = vec![FailureResult::validation_error().with_message(
559 "a very long string that contains sOmE sTrInG of different capitalization",
560 )];
561
562 assert_eq!(
563 super::expectations_match_failures(&expectation, actual),
564 ExpectationMatchResult::Complete
565 );
566
567 let expectation = vec![FailureCase::always().validation_error("Some String")];
570 let actual = vec![validation_err("a very long string that doesn't contain it")];
571
572 assert_eq!(
573 super::expectations_match_failures(&expectation, actual),
574 ExpectationMatchResult::Panic
575 );
576 }
577
578 #[test]
579 fn ignore_flaky() {
580 init_logger();
581
582 let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
583 let actual = vec![validation_err("some blah")];
584
585 assert_eq!(
586 super::expectations_match_failures(&expectation, actual),
587 ExpectationMatchResult::Complete
588 );
589
590 let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
591 let actual = vec![];
592
593 assert_eq!(
594 super::expectations_match_failures(&expectation, actual),
595 ExpectationMatchResult::Complete
596 );
597 }
598
599 #[test]
600 fn matches_multiple_errors() {
601 init_logger();
602
603 let expectation = vec![FailureCase::always().validation_error("blah")];
606 let actual = vec![
607 validation_err("some blah"),
608 validation_err("some other blah"),
609 ];
610
611 assert_eq!(
612 super::expectations_match_failures(&expectation, actual),
613 ExpectationMatchResult::Complete
614 );
615
616 let expectation = vec![FailureCase::always().validation_error("blah")];
619 let actual = vec![
620 validation_err("some blah"),
621 validation_err("some other blah"),
622 validation_err("something else"),
623 ];
624
625 assert_eq!(
626 super::expectations_match_failures(&expectation, actual),
627 ExpectationMatchResult::Panic
628 );
629 }
630
631 #[test]
632 fn multi_reason_error() {
633 init_logger();
634
635 let expectation = vec![FailureCase::default()
636 .validation_error("blah")
637 .panic("panik")];
638 let actual = vec![
639 validation_err("my blah blah validation error"),
640 panic("my panik"),
641 ];
642
643 assert_eq!(
644 super::expectations_match_failures(&expectation, actual),
645 ExpectationMatchResult::Complete
646 );
647 }
648}