mirror of
https://github.com/gfx-rs/wgpu.git
synced 2024-11-21 22:33:49 +00:00
586215ab2e
* build(deps): bump crate-ci/typos from 1.22.9 to 1.23.1 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.22.9 to 1.23.1. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.22.9...v1.23.1) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * typo fixes --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andreas Reich <r_andreas2@web.de>
618 lines
19 KiB
Rust
618 lines
19 KiB
Rust
use core::fmt;
|
|
|
|
/// Conditions under which a test should fail or be skipped.
|
|
///
|
|
/// By passing a `FailureCase` to [`TestParameters::expect_fail`][expect_fail], you can
|
|
/// mark a test as expected to fail under the indicated conditions. By
|
|
/// passing it to [`TestParameters::skip`][skip], you can request that the
|
|
/// test be skipped altogether.
|
|
///
|
|
/// If a field is `None`, then that field does not restrict matches. For
|
|
/// example:
|
|
///
|
|
/// ```
|
|
/// # use wgpu_test::*;
|
|
/// FailureCase {
|
|
/// backends: Some(wgpu::Backends::DX12),
|
|
/// vendor: None,
|
|
/// adapter: Some("RTX"),
|
|
/// driver: None,
|
|
/// reasons: vec![FailureReason::validation_error().with_message("Some error substring")],
|
|
/// behavior: FailureBehavior::AssertFailure,
|
|
/// }
|
|
/// # ;
|
|
/// ```
|
|
///
|
|
/// This applies to all cards with `"RTX'` in their name on either
|
|
/// Direct3D backend, no matter the vendor ID or driver name.
|
|
///
|
|
/// The strings given here need only appear as a substring in the
|
|
/// corresponding [`AdapterInfo`] fields. The comparison is
|
|
/// case-insensitive.
|
|
///
|
|
/// The default value of `FailureCase` applies to any test case. That
|
|
/// is, there are no criteria to constrain the match.
|
|
///
|
|
/// [skip]: super::TestParameters::skip
|
|
/// [expect_fail]: super::TestParameters::expect_fail
|
|
/// [`AdapterInfo`]: wgt::AdapterInfo
|
|
#[derive(Default, Clone)]
|
|
pub struct FailureCase {
|
|
/// Backends expected to fail, or `None` for any backend.
|
|
///
|
|
/// If this is `None`, or if the test is using one of the backends
|
|
/// in `backends`, then this `FailureCase` applies.
|
|
pub backends: Option<wgpu::Backends>,
|
|
|
|
/// Vendor expected to fail, or `None` for any vendor.
|
|
///
|
|
/// If `Some`, this must match [`AdapterInfo::device`], which is
|
|
/// usually the PCI device id. Otherwise, this `FailureCase`
|
|
/// applies regardless of vendor.
|
|
///
|
|
/// [`AdapterInfo::device`]: wgt::AdapterInfo::device
|
|
pub vendor: Option<u32>,
|
|
|
|
/// Name of adapter expected to fail, or `None` for any adapter name.
|
|
///
|
|
/// If this is `Some(s)` and `s` is a substring of
|
|
/// [`AdapterInfo::name`], then this `FailureCase` applies. If
|
|
/// this is `None`, the adapter name isn't considered.
|
|
///
|
|
/// [`AdapterInfo::name`]: wgt::AdapterInfo::name
|
|
pub adapter: Option<&'static str>,
|
|
|
|
/// Name of driver expected to fail, or `None` for any driver name.
|
|
///
|
|
/// If this is `Some(s)` and `s` is a substring of
|
|
/// [`AdapterInfo::driver`], then this `FailureCase` applies. If
|
|
/// this is `None`, the driver name isn't considered.
|
|
///
|
|
/// [`AdapterInfo::driver`]: wgt::AdapterInfo::driver
|
|
pub driver: Option<&'static str>,
|
|
|
|
/// Reason why the test is expected to fail.
|
|
///
|
|
/// If this does not match, the failure will not match this case.
|
|
///
|
|
/// If no reasons are pushed, will match any failure.
|
|
pub reasons: Vec<FailureReason>,
|
|
|
|
/// Behavior after this case matches a failure.
|
|
pub behavior: FailureBehavior,
|
|
}
|
|
|
|
impl FailureCase {
|
|
/// Create a new failure case.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// This case applies to all tests.
|
|
pub fn always() -> Self {
|
|
FailureCase::default()
|
|
}
|
|
|
|
/// This case applies to no tests.
|
|
pub fn never() -> Self {
|
|
FailureCase {
|
|
backends: Some(wgpu::Backends::empty()),
|
|
..FailureCase::default()
|
|
}
|
|
}
|
|
|
|
/// Tests running on any of the given backends.
|
|
pub fn backend(backends: wgpu::Backends) -> Self {
|
|
FailureCase {
|
|
backends: Some(backends),
|
|
..FailureCase::default()
|
|
}
|
|
}
|
|
|
|
/// Tests running on `adapter`.
|
|
///
|
|
/// For this case to apply, the `adapter` string must appear as a substring
|
|
/// of the adapter's [`AdapterInfo::name`]. The comparison is
|
|
/// case-insensitive.
|
|
///
|
|
/// [`AdapterInfo::name`]: wgt::AdapterInfo::name
|
|
pub fn adapter(adapter: &'static str) -> Self {
|
|
FailureCase {
|
|
adapter: Some(adapter),
|
|
..FailureCase::default()
|
|
}
|
|
}
|
|
|
|
/// Tests running on `backend` and `adapter`.
|
|
///
|
|
/// For this case to apply, the test must be using an adapter for one of the
|
|
/// given `backend` bits, and `adapter` string must appear as a substring of
|
|
/// the adapter's [`AdapterInfo::name`]. The string comparison is
|
|
/// case-insensitive.
|
|
///
|
|
/// [`AdapterInfo::name`]: wgt::AdapterInfo::name
|
|
pub fn backend_adapter(backends: wgpu::Backends, adapter: &'static str) -> Self {
|
|
FailureCase {
|
|
backends: Some(backends),
|
|
adapter: Some(adapter),
|
|
..FailureCase::default()
|
|
}
|
|
}
|
|
|
|
/// Tests running under WebGL.
|
|
pub fn webgl2() -> Self {
|
|
#[cfg(target_arch = "wasm32")]
|
|
let case = FailureCase::backend(wgpu::Backends::GL);
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
let case = FailureCase::never();
|
|
case
|
|
}
|
|
|
|
/// Tests running on the MoltenVK Vulkan driver on macOS.
|
|
pub fn molten_vk() -> Self {
|
|
FailureCase {
|
|
backends: Some(wgpu::Backends::VULKAN),
|
|
driver: Some("MoltenVK"),
|
|
..FailureCase::default()
|
|
}
|
|
}
|
|
|
|
/// Return the reasons why this case should fail.
|
|
pub fn reasons(&self) -> &[FailureReason] {
|
|
if self.reasons.is_empty() {
|
|
std::array::from_ref(&FailureReason::ANY)
|
|
} else {
|
|
&self.reasons
|
|
}
|
|
}
|
|
|
|
/// Matches this failure case against the given validation error substring.
|
|
///
|
|
/// Substrings are matched case-insensitively.
|
|
///
|
|
/// If multiple reasons are pushed, will match any of them.
|
|
pub fn validation_error(mut self, msg: &'static str) -> Self {
|
|
self.reasons
|
|
.push(FailureReason::validation_error().with_message(msg));
|
|
self
|
|
}
|
|
|
|
/// Matches this failure case against the given panic substring.
|
|
///
|
|
/// Substrings are matched case-insensitively.
|
|
///
|
|
/// If multiple reasons are pushed, will match any of them.
|
|
pub fn panic(mut self, msg: &'static str) -> Self {
|
|
self.reasons.push(FailureReason::panic().with_message(msg));
|
|
self
|
|
}
|
|
|
|
/// Test is flaky with the given configuration. Do not assert failure.
|
|
///
|
|
/// Use this _very_ sparyingly, and match as tightly as you can, including giving a specific failure message.
|
|
pub fn flaky(self) -> Self {
|
|
FailureCase {
|
|
behavior: FailureBehavior::Ignore,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Test whether `self` applies to `info`.
|
|
///
|
|
/// If it does, return a `FailureReasons` whose set bits indicate
|
|
/// why. If it doesn't, return `None`.
|
|
///
|
|
/// The caller is responsible for converting the string-valued
|
|
/// fields of `info` to lower case, to ensure case-insensitive
|
|
/// matching.
|
|
pub(crate) fn applies_to_adapter(
|
|
&self,
|
|
info: &wgt::AdapterInfo,
|
|
) -> Option<FailureApplicationReasons> {
|
|
let mut reasons = FailureApplicationReasons::empty();
|
|
|
|
if let Some(backends) = self.backends {
|
|
if !backends.contains(wgpu::Backends::from(info.backend)) {
|
|
return None;
|
|
}
|
|
reasons.set(FailureApplicationReasons::BACKEND, true);
|
|
}
|
|
if let Some(vendor) = self.vendor {
|
|
if vendor != info.vendor {
|
|
return None;
|
|
}
|
|
reasons.set(FailureApplicationReasons::VENDOR, true);
|
|
}
|
|
if let Some(adapter) = self.adapter {
|
|
let adapter = adapter.to_lowercase();
|
|
if !info.name.contains(&adapter) {
|
|
return None;
|
|
}
|
|
reasons.set(FailureApplicationReasons::ADAPTER, true);
|
|
}
|
|
if let Some(driver) = self.driver {
|
|
let driver = driver.to_lowercase();
|
|
if !info.driver.contains(&driver) {
|
|
return None;
|
|
}
|
|
reasons.set(FailureApplicationReasons::DRIVER, true);
|
|
}
|
|
|
|
// If we got this far but no specific reasons were triggered, then this
|
|
// must be a wildcard.
|
|
if reasons.is_empty() {
|
|
Some(FailureApplicationReasons::ALWAYS)
|
|
} else {
|
|
Some(reasons)
|
|
}
|
|
}
|
|
|
|
/// Returns true if the given failure "satisfies" this failure case.
|
|
pub(crate) fn matches_failure(&self, failure: &FailureResult) -> bool {
|
|
for reason in self.reasons() {
|
|
let kind_matched = reason.kind.map_or(true, |kind| kind == failure.kind);
|
|
|
|
let message_matched =
|
|
reason
|
|
.message
|
|
.map_or(true, |message| matches!(&failure.message, Some(actual) if actual.to_lowercase().contains(&message.to_lowercase())));
|
|
|
|
if kind_matched && message_matched {
|
|
let message = failure.message.as_deref().unwrap_or("*no message*");
|
|
log::error!("Matched {} {message}", failure.kind);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
bitflags::bitflags! {
|
|
/// Reason why a test matches a given failure case.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
|
pub struct FailureApplicationReasons: u8 {
|
|
const BACKEND = 1 << 0;
|
|
const VENDOR = 1 << 1;
|
|
const ADAPTER = 1 << 2;
|
|
const DRIVER = 1 << 3;
|
|
const ALWAYS = 1 << 4;
|
|
}
|
|
}
|
|
|
|
/// Reason why a test is expected to fail.
|
|
///
|
|
/// If the test fails for a different reason, the given FailureCase will be ignored.
|
|
#[derive(Default, Debug, Clone, PartialEq)]
|
|
pub struct FailureReason {
|
|
/// Match a particular kind of failure result.
|
|
///
|
|
/// If `None`, match any result kind.
|
|
kind: Option<FailureResultKind>,
|
|
/// Match a particular message of a failure result.
|
|
///
|
|
/// If `None`, matches any message. If `Some`, a case-insensitive sub-string
|
|
/// test is performed. Allowing `"error occurred"` to match a message like
|
|
/// `"An unexpected Error occurred!"`.
|
|
message: Option<&'static str>,
|
|
}
|
|
|
|
impl FailureReason {
|
|
/// Match any failure reason.
|
|
const ANY: Self = Self {
|
|
kind: None,
|
|
message: None,
|
|
};
|
|
|
|
/// Match a validation error.
|
|
#[allow(dead_code)] // Not constructed on wasm
|
|
pub fn validation_error() -> Self {
|
|
Self {
|
|
kind: Some(FailureResultKind::ValidationError),
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
/// Match a panic.
|
|
pub fn panic() -> Self {
|
|
Self {
|
|
kind: Some(FailureResultKind::Panic),
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
/// Match an error with a message.
|
|
///
|
|
/// If specified, a case-insensitive sub-string test is performed. Allowing
|
|
/// `"error occurred"` to match a message like `"An unexpected Error
|
|
/// occurred!"`.
|
|
pub fn with_message(self, message: &'static str) -> Self {
|
|
Self {
|
|
message: Some(message),
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub enum FailureBehavior {
|
|
/// Assert that the test fails for the given reason.
|
|
///
|
|
/// If the test passes, the test harness will panic.
|
|
#[default]
|
|
AssertFailure,
|
|
/// Ignore the matching failure.
|
|
///
|
|
/// This is useful for tests that flake in a very specific way,
|
|
/// but sometimes succeed, so we can't assert that they always fail.
|
|
Ignore,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub(crate) enum FailureResultKind {
|
|
#[allow(dead_code)] // Not constructed on wasm
|
|
ValidationError,
|
|
Panic,
|
|
}
|
|
|
|
impl fmt::Display for FailureResultKind {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
FailureResultKind::ValidationError => write!(f, "Validation Error"),
|
|
FailureResultKind::Panic => write!(f, "Panic"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FailureResult {
|
|
kind: FailureResultKind,
|
|
message: Option<String>,
|
|
}
|
|
|
|
impl FailureResult {
|
|
/// Failure result is a panic.
|
|
pub(super) fn panic() -> Self {
|
|
Self {
|
|
kind: FailureResultKind::Panic,
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
/// Failure result is a validation error.
|
|
#[allow(dead_code)] // Not constructed on wasm
|
|
pub(super) fn validation_error() -> Self {
|
|
Self {
|
|
kind: FailureResultKind::ValidationError,
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
/// Message associated with a failure result.
|
|
pub(super) fn with_message(self, message: impl fmt::Display) -> Self {
|
|
Self {
|
|
kind: self.kind,
|
|
message: Some(message.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
|
pub(crate) enum ExpectationMatchResult {
|
|
Panic,
|
|
Complete,
|
|
}
|
|
|
|
/// Compares if the actual failures match the expected failures.
|
|
pub(crate) fn expectations_match_failures(
|
|
expectations: &[FailureCase],
|
|
mut actual: Vec<FailureResult>,
|
|
) -> ExpectationMatchResult {
|
|
// Start with the assumption that we will pass.
|
|
let mut result = ExpectationMatchResult::Complete;
|
|
|
|
// Run through all expected failures.
|
|
for expected_failure in expectations {
|
|
// If any of the failures match.
|
|
let mut matched = false;
|
|
|
|
// Iterate through the failures.
|
|
//
|
|
// In reverse, to be able to use swap_remove.
|
|
actual.retain(|failure| {
|
|
// If the failure matches, remove it from the list of failures, as we expected it.
|
|
let matches = expected_failure.matches_failure(failure);
|
|
|
|
if matches {
|
|
matched = true;
|
|
}
|
|
|
|
// Retain removes on false, so flip the bool so we remove on failure.
|
|
!matches
|
|
});
|
|
|
|
// If we didn't match our expected failure against any of the actual failures,
|
|
// and this failure is not flaky, then we need to panic, as we got an unexpected success.
|
|
if !matched && matches!(expected_failure.behavior, FailureBehavior::AssertFailure) {
|
|
result = ExpectationMatchResult::Panic;
|
|
log::error!(
|
|
"Expected to fail due to {:?}, but did not fail",
|
|
expected_failure.reasons()
|
|
);
|
|
}
|
|
}
|
|
|
|
// If we have any failures left, then we got an unexpected failure
|
|
// and we need to panic.
|
|
if !actual.is_empty() {
|
|
result = ExpectationMatchResult::Panic;
|
|
for failure in actual {
|
|
let message = failure.message.as_deref().unwrap_or("*no message*");
|
|
log::error!("{}: {message}", failure.kind);
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{
|
|
expectations::{ExpectationMatchResult, FailureResult},
|
|
init::init_logger,
|
|
FailureCase,
|
|
};
|
|
|
|
fn validation_err(msg: &'static str) -> FailureResult {
|
|
FailureResult::validation_error().with_message(msg)
|
|
}
|
|
|
|
fn panic(msg: &'static str) -> FailureResult {
|
|
FailureResult::panic().with_message(msg)
|
|
}
|
|
|
|
#[test]
|
|
fn simple_match() {
|
|
init_logger();
|
|
|
|
// -- Unexpected failure --
|
|
|
|
let expectation = vec![];
|
|
let actual = vec![FailureResult::validation_error()];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Panic
|
|
);
|
|
|
|
// -- Missing expected failure --
|
|
|
|
let expectation = vec![FailureCase::always()];
|
|
let actual = vec![];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Panic
|
|
);
|
|
|
|
// -- Expected failure (validation) --
|
|
|
|
let expectation = vec![FailureCase::always()];
|
|
let actual = vec![FailureResult::validation_error()];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
|
|
// -- Expected failure (panic) --
|
|
|
|
let expectation = vec![FailureCase::always()];
|
|
let actual = vec![FailureResult::panic()];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn substring_match() {
|
|
init_logger();
|
|
|
|
// -- Matching Substring --
|
|
|
|
let expectation: Vec<FailureCase> =
|
|
vec![FailureCase::always().validation_error("Some StrIng")];
|
|
let actual = vec![FailureResult::validation_error().with_message(
|
|
"a very long string that contains sOmE sTrInG of different capitalization",
|
|
)];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
|
|
// -- Non-Matching Substring --
|
|
|
|
let expectation = vec![FailureCase::always().validation_error("Some String")];
|
|
let actual = vec![validation_err("a very long string that doesn't contain it")];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Panic
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignore_flaky() {
|
|
init_logger();
|
|
|
|
let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
|
|
let actual = vec![validation_err("some blah")];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
|
|
let expectation = vec![FailureCase::always().validation_error("blah").flaky()];
|
|
let actual = vec![];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matches_multiple_errors() {
|
|
init_logger();
|
|
|
|
// -- matches all matching errors --
|
|
|
|
let expectation = vec![FailureCase::always().validation_error("blah")];
|
|
let actual = vec![
|
|
validation_err("some blah"),
|
|
validation_err("some other blah"),
|
|
];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
|
|
// -- but not all errors --
|
|
|
|
let expectation = vec![FailureCase::always().validation_error("blah")];
|
|
let actual = vec![
|
|
validation_err("some blah"),
|
|
validation_err("some other blah"),
|
|
validation_err("something else"),
|
|
];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Panic
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn multi_reason_error() {
|
|
init_logger();
|
|
|
|
let expectation = vec![FailureCase::default()
|
|
.validation_error("blah")
|
|
.panic("panik")];
|
|
let actual = vec![
|
|
validation_err("my blah blah validation error"),
|
|
panic("my panik"),
|
|
];
|
|
|
|
assert_eq!(
|
|
super::expectations_match_failures(&expectation, actual),
|
|
ExpectationMatchResult::Complete
|
|
);
|
|
}
|
|
}
|