wgpu_test/
expectations.rs

1use core::fmt;
2
3/// Conditions under which a test should fail or be skipped.
4///
5/// By passing a `FailureCase` to [`TestParameters::expect_fail`][expect_fail], you can
6/// mark a test as expected to fail under the indicated conditions. By
7/// passing it to [`TestParameters::skip`][skip], you can request that the
8/// test be skipped altogether.
9///
10/// If a field is `None`, then that field does not restrict matches. For
11/// example:
12///
13/// ```
14/// # use wgpu_test::*;
15/// FailureCase {
16///     backends: Some(wgpu::Backends::DX12),
17///     vendor: None,
18///     adapter: Some("RTX"),
19///     driver: None,
20///     reasons: vec![FailureReason::validation_error().with_message("Some error substring")],
21///     behavior: FailureBehavior::AssertFailure,
22/// }
23/// # ;
24/// ```
25///
26/// This applies to all cards with `"RTX'` in their name on either
27/// Direct3D backend, no matter the vendor ID or driver name.
28///
29/// The strings given here need only appear as a substring in the
30/// corresponding [`AdapterInfo`] fields. The comparison is
31/// case-insensitive.
32///
33/// The default value of `FailureCase` applies to any test case. That
34/// is, there are no criteria to constrain the match.
35///
36/// [skip]: super::TestParameters::skip
37/// [expect_fail]: super::TestParameters::expect_fail
38/// [`AdapterInfo`]: wgpu::AdapterInfo
39#[derive(Default, Clone, PartialEq)]
40pub struct FailureCase {
41    /// Backends expected to fail, or `None` for any backend.
42    ///
43    /// If this is `None`, or if the test is using one of the backends
44    /// in `backends`, then this `FailureCase` applies.
45    pub backends: Option<wgpu::Backends>,
46
47    /// Vendor expected to fail, or `None` for any vendor.
48    ///
49    /// If `Some`, this must match [`AdapterInfo::device`], which is
50    /// usually the PCI device id. Otherwise, this `FailureCase`
51    /// applies regardless of vendor.
52    ///
53    /// [`AdapterInfo::device`]: wgpu::AdapterInfo::device
54    pub vendor: Option<u32>,
55
56    /// Name of adapter expected to fail, or `None` for any adapter name.
57    ///
58    /// If this is `Some(s)` and `s` is a substring of
59    /// [`AdapterInfo::name`], then this `FailureCase` applies. If
60    /// this is `None`, the adapter name isn't considered.
61    ///
62    /// [`AdapterInfo::name`]: wgpu::AdapterInfo::name
63    pub adapter: Option<&'static str>,
64
65    /// Name of driver expected to fail, or `None` for any driver name.
66    ///
67    /// If this is `Some(s)` and `s` is a substring of
68    /// [`AdapterInfo::driver`], then this `FailureCase` applies. If
69    /// this is `None`, the driver name isn't considered.
70    ///
71    /// [`AdapterInfo::driver`]: wgpu::AdapterInfo::driver
72    pub driver: Option<&'static str>,
73
74    /// Reason why the test is expected to fail.
75    ///
76    /// If this does not match, the failure will not match this case.
77    ///
78    /// If no reasons are pushed, will match any failure.
79    pub reasons: Vec<FailureReason>,
80
81    /// Behavior after this case matches a failure.
82    pub behavior: FailureBehavior,
83}
84
85impl FailureCase {
86    /// Create a new failure case.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// This case applies to all tests.
92    pub fn always() -> Self {
93        FailureCase::default()
94    }
95
96    /// This case applies to no tests.
97    pub fn never() -> Self {
98        FailureCase {
99            backends: Some(wgpu::Backends::empty()),
100            ..FailureCase::default()
101        }
102    }
103
104    /// Tests running on any of the given backends.
105    pub fn backend(backends: wgpu::Backends) -> Self {
106        FailureCase {
107            backends: Some(backends),
108            ..FailureCase::default()
109        }
110    }
111
112    /// Tests running on `adapter`.
113    ///
114    /// For this case to apply, the `adapter` string must appear as a substring
115    /// of the adapter's [`AdapterInfo::name`]. The comparison is
116    /// case-insensitive.
117    ///
118    /// [`AdapterInfo::name`]: wgpu::AdapterInfo::name
119    pub fn adapter(adapter: &'static str) -> Self {
120        FailureCase {
121            adapter: Some(adapter),
122            ..FailureCase::default()
123        }
124    }
125
126    /// Tests running on `backend` and `adapter`.
127    ///
128    /// For this case to apply, the test must be using an adapter for one of the
129    /// given `backend` bits, and `adapter` string must appear as a substring of
130    /// the adapter's [`AdapterInfo::name`]. The string comparison is
131    /// case-insensitive.
132    ///
133    /// [`AdapterInfo::name`]: wgpu::AdapterInfo::name
134    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    /// Tests running under WebGL.
143    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    /// Tests running on the MoltenVK Vulkan driver on macOS.
152    pub fn molten_vk() -> Self {
153        FailureCase {
154            backends: Some(wgpu::Backends::VULKAN),
155            driver: Some("MoltenVK"),
156            ..FailureCase::default()
157        }
158    }
159
160    /// Tests running on the KosmicKrisp Vulkan driver on macOS.
161    pub fn kosmic_krisp() -> Self {
162        FailureCase {
163            backends: Some(wgpu::Backends::VULKAN),
164            driver: Some("KosmicKrisp"),
165            ..FailureCase::default()
166        }
167    }
168
169    /// Tests running on either Vulkan driver on macOS.
170    pub fn mac_vulkan(f: impl Fn(FailureCase) -> FailureCase) -> Vec<Self> {
171        vec![f(FailureCase::molten_vk()), f(FailureCase::kosmic_krisp())]
172    }
173
174    /// Return the reasons why this case should fail.
175    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    /// Matches this failure case against the given validation error substring.
184    ///
185    /// Substrings are matched case-insensitively.
186    ///
187    /// If multiple reasons are pushed, will match any of them.
188    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    /// Matches this failure case against the given panic substring.
195    ///
196    /// Substrings are matched case-insensitively.
197    ///
198    /// If multiple reasons are pushed, will match any of them.
199    pub fn panic(mut self, msg: &'static str) -> Self {
200        self.reasons.push(FailureReason::panic().with_message(msg));
201        self
202    }
203
204    /// Test is flaky with the given configuration. Do not assert failure.
205    ///
206    /// Use this _very_ sparyingly, and match as tightly as you can, including giving a specific failure message.
207    pub fn flaky(self) -> Self {
208        FailureCase {
209            behavior: FailureBehavior::Ignore,
210            ..self
211        }
212    }
213
214    /// Test whether `self` applies to `info`.
215    ///
216    /// If it does, return a `FailureReasons` whose set bits indicate
217    /// why. If it doesn't, return `None`.
218    ///
219    /// The caller is responsible for converting the string-valued
220    /// fields of `info` to lower case, to ensure case-insensitive
221    /// matching.
222    pub(crate) fn applies_to_adapter(
223        &self,
224        info: &wgpu::AdapterInfo,
225    ) -> Option<FailureApplicationReasons> {
226        let mut reasons = FailureApplicationReasons::empty();
227
228        if let Some(backends) = self.backends {
229            if !backends.contains(wgpu::Backends::from(info.backend)) {
230                return None;
231            }
232            reasons.set(FailureApplicationReasons::BACKEND, true);
233        }
234        if let Some(vendor) = self.vendor {
235            if vendor != info.vendor {
236                return None;
237            }
238            reasons.set(FailureApplicationReasons::VENDOR, true);
239        }
240        if let Some(adapter) = self.adapter {
241            let adapter = adapter.to_lowercase();
242            if !info.name.contains(&adapter) {
243                return None;
244            }
245            reasons.set(FailureApplicationReasons::ADAPTER, true);
246        }
247        if let Some(driver) = self.driver {
248            let driver = driver.to_lowercase();
249            if !info.driver.contains(&driver) {
250                return None;
251            }
252            reasons.set(FailureApplicationReasons::DRIVER, true);
253        }
254
255        // If we got this far but no specific reasons were triggered, then this
256        // must be a wildcard.
257        if reasons.is_empty() {
258            Some(FailureApplicationReasons::ALWAYS)
259        } else {
260            Some(reasons)
261        }
262    }
263
264    /// Returns true if the given failure "satisfies" this failure case.
265    pub(crate) fn matches_failure(&self, failure: &FailureResult) -> bool {
266        for reason in self.reasons() {
267            let kind_matched = reason.kind.is_none_or(|kind| kind == failure.kind);
268
269            let message_matched =
270                reason
271                    .message
272                    .is_none_or(|message| matches!(&failure.message, Some(actual) if actual.to_lowercase().contains(&message.to_lowercase())));
273
274            if kind_matched && message_matched {
275                let message = failure.message.as_deref().unwrap_or("*no message*");
276                log::error!("Matched {} {message}", failure.kind);
277                return true;
278            }
279        }
280
281        false
282    }
283}
284
285bitflags::bitflags! {
286    /// Reason why a test matches a given failure case.
287    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
288    pub struct FailureApplicationReasons: u8 {
289        const BACKEND = 1 << 0;
290        const VENDOR = 1 << 1;
291        const ADAPTER = 1 << 2;
292        const DRIVER = 1 << 3;
293        const ALWAYS = 1 << 4;
294    }
295}
296
297/// Reason why a test is expected to fail.
298///
299/// If the test fails for a different reason, the given FailureCase will be ignored.
300#[derive(Default, Debug, Clone, PartialEq)]
301pub struct FailureReason {
302    /// Match a particular kind of failure result.
303    ///
304    /// If `None`, match any result kind.
305    kind: Option<FailureResultKind>,
306    /// Match a particular message of a failure result.
307    ///
308    /// If `None`, matches any message. If `Some`, a case-insensitive sub-string
309    /// test is performed. Allowing `"error occurred"` to match a message like
310    /// `"An unexpected Error occurred!"`.
311    message: Option<&'static str>,
312}
313
314impl FailureReason {
315    /// Match any failure reason.
316    const ANY: Self = Self {
317        kind: None,
318        message: None,
319    };
320
321    /// Match a validation error.
322    #[allow(dead_code, reason = "Not constructed on wasm")]
323    pub fn validation_error() -> Self {
324        Self {
325            kind: Some(FailureResultKind::ValidationError),
326            message: None,
327        }
328    }
329
330    /// Match a panic.
331    pub fn panic() -> Self {
332        Self {
333            kind: Some(FailureResultKind::Panic),
334            message: None,
335        }
336    }
337
338    /// Match an error with a message.
339    ///
340    /// If specified, a case-insensitive sub-string test is performed. Allowing
341    /// `"error occurred"` to match a message like `"An unexpected Error
342    /// occurred!"`.
343    pub fn with_message(self, message: &'static str) -> Self {
344        Self {
345            message: Some(message),
346            ..self
347        }
348    }
349}
350
351#[derive(Default, Clone, PartialEq)]
352pub enum FailureBehavior {
353    /// Assert that the test fails for the given reason.
354    ///
355    /// If the test passes, the test harness will panic.
356    #[default]
357    AssertFailure,
358    /// Ignore the matching failure.
359    ///
360    /// This is useful for tests that flake in a very specific way,
361    /// but sometimes succeed, so we can't assert that they always fail.
362    Ignore,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq)]
366pub(crate) enum FailureResultKind {
367    #[allow(dead_code, reason = "Not constructed on wasm")]
368    ValidationError,
369    Panic,
370}
371
372impl fmt::Display for FailureResultKind {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        match self {
375            FailureResultKind::ValidationError => write!(f, "Validation Error"),
376            FailureResultKind::Panic => write!(f, "Panic"),
377        }
378    }
379}
380
381#[derive(Debug)]
382pub(crate) struct FailureResult {
383    kind: FailureResultKind,
384    message: Option<String>,
385}
386
387impl FailureResult {
388    /// Failure result is a panic.
389    pub(super) fn panic() -> Self {
390        Self {
391            kind: FailureResultKind::Panic,
392            message: None,
393        }
394    }
395
396    /// Failure result is a validation error.
397    #[allow(dead_code, reason = "Not constructed on wasm")]
398    pub(super) fn validation_error() -> Self {
399        Self {
400            kind: FailureResultKind::ValidationError,
401            message: None,
402        }
403    }
404
405    /// Message associated with a failure result.
406    pub(super) fn with_message(self, message: impl fmt::Display) -> Self {
407        Self {
408            kind: self.kind,
409            message: Some(message.to_string()),
410        }
411    }
412}
413
414#[derive(PartialEq, Clone, Copy, Debug)]
415pub(crate) enum ExpectationMatchResult {
416    Panic,
417    Complete,
418}
419
420/// Compares if the actual failures match the expected failures.
421pub(crate) fn expectations_match_failures(
422    expectations: &[FailureCase],
423    mut actual: Vec<FailureResult>,
424) -> ExpectationMatchResult {
425    // Start with the assumption that we will pass.
426    let mut result = ExpectationMatchResult::Complete;
427
428    // Run through all expected failures.
429    for expected_failure in expectations {
430        // If any of the failures match.
431        let mut matched = false;
432
433        // Iterate through the failures.
434        //
435        // In reverse, to be able to use swap_remove.
436        actual.retain(|failure| {
437            // If the failure matches, remove it from the list of failures, as we expected it.
438            let matches = expected_failure.matches_failure(failure);
439
440            if matches {
441                matched = true;
442            }
443
444            // Retain removes on false, so flip the bool so we remove on failure.
445            !matches
446        });
447
448        // If we didn't match our expected failure against any of the actual failures,
449        // and this failure is not flaky, then we need to panic, as we got an unexpected success.
450        if !matched && matches!(expected_failure.behavior, FailureBehavior::AssertFailure) {
451            result = ExpectationMatchResult::Panic;
452            log::error!(
453                "Expected to fail due to {:?}, but did not fail",
454                expected_failure.reasons()
455            );
456        }
457    }
458
459    // If we have any failures left, then we got an unexpected failure
460    // and we need to panic.
461    if !actual.is_empty() {
462        result = ExpectationMatchResult::Panic;
463        for failure in actual {
464            let message = failure.message.as_deref().unwrap_or("*no message*");
465            log::error!("{}: {message}", failure.kind);
466        }
467    }
468
469    result
470}
471
472#[cfg(test)]
473mod test {
474    use crate::{
475        expectations::{ExpectationMatchResult, FailureResult},
476        init::init_logger,
477        FailureCase,
478    };
479
480    fn validation_err(msg: &'static str) -> FailureResult {
481        FailureResult::validation_error().with_message(msg)
482    }
483
484    fn panic(msg: &'static str) -> FailureResult {
485        FailureResult::panic().with_message(msg)
486    }
487
488    #[test]
489    fn simple_match() {
490        init_logger();
491
492        // -- Unexpected failure --
493
494        let expectation = vec![];
495        let actual = vec![FailureResult::validation_error()];
496
497        assert_eq!(
498            super::expectations_match_failures(&expectation, actual),
499            ExpectationMatchResult::Panic
500        );
501
502        // -- Missing expected failure --
503
504        let expectation = vec![FailureCase::always()];
505        let actual = vec![];
506
507        assert_eq!(
508            super::expectations_match_failures(&expectation, actual),
509            ExpectationMatchResult::Panic
510        );
511
512        // -- Expected failure (validation) --
513
514        let expectation = vec![FailureCase::always()];
515        let actual = vec![FailureResult::validation_error()];
516
517        assert_eq!(
518            super::expectations_match_failures(&expectation, actual),
519            ExpectationMatchResult::Complete
520        );
521
522        // -- Expected failure (panic) --
523
524        let expectation = vec![FailureCase::always()];
525        let actual = vec![FailureResult::panic()];
526
527        assert_eq!(
528            super::expectations_match_failures(&expectation, actual),
529            ExpectationMatchResult::Complete
530        );
531    }
532
533    #[test]
534    fn substring_match() {
535        init_logger();
536
537        // -- Matching Substring --
538
539        let expectation: Vec<FailureCase> =
540            vec![FailureCase::always().validation_error("Some StrIng")];
541        let actual = vec![FailureResult::validation_error().with_message(
542            "a very long string that contains sOmE sTrInG of different capitalization",
543        )];
544
545        assert_eq!(
546            super::expectations_match_failures(&expectation, actual),
547            ExpectationMatchResult::Complete
548        );
549
550        // -- Non-Matching Substring --
551
552        let expectation = vec![FailureCase::always().validation_error("Some String")];
553        let actual = vec![validation_err("a very long string that doesn't contain it")];
554
555        assert_eq!(
556            super::expectations_match_failures(&expectation, actual),
557            ExpectationMatchResult::Panic
558        );
559    }
560
561    #[test]
562    fn ignore_flaky() {
563        init_logger();
564
565        let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
566        let actual = vec![validation_err("some blah")];
567
568        assert_eq!(
569            super::expectations_match_failures(&expectation, actual),
570            ExpectationMatchResult::Complete
571        );
572
573        let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
574        let actual = vec![];
575
576        assert_eq!(
577            super::expectations_match_failures(&expectation, actual),
578            ExpectationMatchResult::Complete
579        );
580    }
581
582    #[test]
583    fn matches_multiple_errors() {
584        init_logger();
585
586        // -- matches all matching errors --
587
588        let expectation = vec![FailureCase::always().validation_error("blah")];
589        let actual = vec![
590            validation_err("some blah"),
591            validation_err("some other blah"),
592        ];
593
594        assert_eq!(
595            super::expectations_match_failures(&expectation, actual),
596            ExpectationMatchResult::Complete
597        );
598
599        // -- but not all errors --
600
601        let expectation = vec![FailureCase::always().validation_error("blah")];
602        let actual = vec![
603            validation_err("some blah"),
604            validation_err("some other blah"),
605            validation_err("something else"),
606        ];
607
608        assert_eq!(
609            super::expectations_match_failures(&expectation, actual),
610            ExpectationMatchResult::Panic
611        );
612    }
613
614    #[test]
615    fn multi_reason_error() {
616        init_logger();
617
618        let expectation = vec![FailureCase::default()
619            .validation_error("blah")
620            .panic("panik")];
621        let actual = vec![
622            validation_err("my blah blah validation error"),
623            panic("my panik"),
624        ];
625
626        assert_eq!(
627            super::expectations_match_failures(&expectation, actual),
628            ExpectationMatchResult::Complete
629        );
630    }
631}