From 7f2c92f42ac701bebebb50a59d55cc5937250454 Mon Sep 17 00:00:00 2001 From: Erich Gubler Date: Fri, 27 Oct 2023 09:43:01 -0400 Subject: [PATCH] feat(cli)!: normalize exps. in meta. to be: by platform, by build profile --- Cargo.lock | 29 ++++ Cargo.toml | 1 + src/bin/moz-webgpu-cts/main.rs | 103 ++++++----- src/bin/moz-webgpu-cts/metadata.rs | 263 +++++++++++++++++++++-------- src/bin/moz-webgpu-cts/shared.rs | 55 ++++++ 5 files changed, 337 insertions(+), 114 deletions(-) create mode 100644 src/bin/moz-webgpu-cts/shared.rs diff --git a/Cargo.lock b/Cargo.lock index 38efac8..05a470c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "same-file" version = "1.0.6" @@ -671,6 +677,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.31", +] + [[package]] name = "supports-color" version = "2.1.0" @@ -849,6 +877,7 @@ dependencies = [ "natord", "path-dsl", "regex", + "strum", "thiserror", "wax", ] diff --git a/Cargo.toml b/Cargo.toml index 1f94113..ee55e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ miette = { version = "5.10.0", features = ["fancy"] } natord = "1.0.9" path-dsl = "0.6.1" regex = "1.9.5" +strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.49" wax = "0.6.0" diff --git a/src/bin/moz-webgpu-cts/main.rs b/src/bin/moz-webgpu-cts/main.rs index f5b8fc9..e039132 100644 --- a/src/bin/moz-webgpu-cts/main.rs +++ b/src/bin/moz-webgpu-cts/main.rs @@ -1,7 +1,9 @@ mod metadata; +mod shared; -use self::metadata::{ - AnalyzeableProps, Applicability, Expectation, Platform, SubtestOutcome, Test, TestOutcome, +use self::{ + metadata::{AnalyzeableProps, Platform, SubtestOutcome, Test, TestOutcome}, + shared::{Expectation, MaybeCollapsed}, }; use std::{ @@ -22,10 +24,7 @@ use path_dsl::path; use regex::Regex; use wax::Glob; use whippit::{ - metadata::{ - properties::{ConditionalValue, PropertyValue}, - SectionHeader, Subtest, - }, + metadata::{SectionHeader, Subtest}, reexport::chumsky::prelude::Rich, }; @@ -381,27 +380,35 @@ fn run(cli: Cli) -> ExitCode { }) }) }; - match expectations { - PropertyValue::Unconditional(exp) => { - apply_to_all_platforms(&mut analysis, exp) - } - PropertyValue::Conditional(ConditionalValue { - conditions, - fallback, - }) => { - for (condition, exp) in conditions { - let Applicability { - platform, - build_profile: _, - } = condition; - if let Some(platform) = platform { - apply_to_specific_platforms(&mut analysis, platform, exp) - } else { + + match expectations.into_inner() { + MaybeCollapsed::Collapsed(exps) => match exps { + MaybeCollapsed::Collapsed(exp) => { + apply_to_all_platforms(&mut analysis, exp) + } + MaybeCollapsed::Expanded(by_build_profile) => { + for (_build_profile, exp) in by_build_profile { apply_to_all_platforms(&mut analysis, exp) } } - if let Some(fallback) = fallback { - apply_to_all_platforms(&mut analysis, fallback) + }, + MaybeCollapsed::Expanded(by_platform) => { + for (platform, exp_by_build_profile) in by_platform { + // TODO: has a lot in common with above cases. Refactor out? + match exp_by_build_profile { + MaybeCollapsed::Collapsed(exp) => { + apply_to_specific_platforms(&mut analysis, platform, exp) + } + MaybeCollapsed::Expanded(by_build_profile) => { + for (_build_profile, exp) in by_build_profile { + apply_to_specific_platforms( + &mut analysis, + platform, + exp, + ) + } + } + } } } } @@ -474,27 +481,39 @@ fn run(cli: Cli) -> ExitCode { ) }) }; - match expectations { - PropertyValue::Unconditional(exp) => { - apply_to_all_platforms(&mut analysis, exp) - } - PropertyValue::Conditional(ConditionalValue { - conditions, - fallback, - }) => { - for (condition, exp) in conditions { - let Applicability { - platform, - build_profile: _, - } = condition; - if let Some(platform) = platform { - apply_to_specific_platforms(&mut analysis, platform, exp) - } else { + + match expectations.into_inner() { + MaybeCollapsed::Collapsed(exps) => match exps { + MaybeCollapsed::Collapsed(exp) => { + apply_to_all_platforms(&mut analysis, exp) + } + MaybeCollapsed::Expanded(by_build_profile) => { + for (_build_profile, exp) in by_build_profile { apply_to_all_platforms(&mut analysis, exp) } } - if let Some(fallback) = fallback { - apply_to_all_platforms(&mut analysis, fallback) + }, + MaybeCollapsed::Expanded(by_platform) => { + for (platform, exp_by_build_profile) in by_platform { + // TODO: has a lot in common with above cases. Refactor out? + match exp_by_build_profile { + MaybeCollapsed::Collapsed(exp) => { + apply_to_specific_platforms( + &mut analysis, + platform, + exp, + ) + } + MaybeCollapsed::Expanded(by_build_profile) => { + for (_build_profile, exp) in by_build_profile { + apply_to_specific_platforms( + &mut analysis, + platform, + exp, + ) + } + } + } } } } diff --git a/src/bin/moz-webgpu-cts/metadata.rs b/src/bin/moz-webgpu-cts/metadata.rs index 0962c66..3aa1869 100644 --- a/src/bin/moz-webgpu-cts/metadata.rs +++ b/src/bin/moz-webgpu-cts/metadata.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, fmt::{self, Display}, hash::Hash, }; @@ -11,8 +12,8 @@ use chumsky::{ Boxed, IterParser, Parser, }; use format::lazy_format; -use indexmap::IndexSet; use joinery::JoinableIterator; +use strum::{EnumIter, IntoEnumIterator}; use whippit::metadata::{ self, properties::{ @@ -21,6 +22,8 @@ use whippit::metadata::{ ParseError, }; +use crate::shared::{Expectation, MaybeCollapsed, NormalizedExpectationPropertyValue}; + #[cfg(test)] use {chumsky::text::newline, insta::assert_debug_snapshot}; @@ -68,7 +71,7 @@ fn format_test(test: &Test) -> impl Display + '_ { fn format_properties(indentation: u8, property: &AnalyzeableProps) -> impl Display + '_ where - Out: Display, + Out: Default + Display + Eq + PartialEq, { lazy_format!(move |f| { let indent = lazy_format!(move |f| write!( @@ -82,59 +85,82 @@ where is_disabled, expectations, } = property; + if *is_disabled { writeln!(f, "{indent}disabled: true")?; } - if let Some(expectations) = expectations { - write!(f, "{indent}expected:")?; - - match expectations { - PropertyValue::Unconditional(exp) => writeln!(f, " {}", format_exp(exp))?, - PropertyValue::Conditional(ConditionalValue { - conditions, - fallback, - }) => { - writeln!(f)?; - if !conditions.is_empty() { - for (applicability, expectation) in conditions { - let Applicability { - platform, - build_profile, - } = applicability; - let platform = platform.map(|p| { - let platform_str = match p { - Platform::Windows => "win", - Platform::Linux => "linux", - Platform::MacOs => "mac", - }; - lazy_format!(move |f| write!(f, "os == {platform_str:?}")) - }); - let build_profile = build_profile.as_ref().map(|p| -> &'static str { - match p { - BuildProfile::Debug => "debug", - BuildProfile::Optimized => "not debug", - } - }); - writeln!( - f, - "{indent} if {}: {}", - platform - .as_ref() - .map(|p| -> &dyn Display { p }) - .into_iter() - .chain( - build_profile - .as_ref() - .into_iter() - .map(|s| -> &dyn Display { s }) - ) - .join_with(" and "), - format_exp(expectation) - )?; + + if let Some(exps) = expectations { + fn if_not_default( + exp: &Expectation, + f: impl FnOnce() -> fmt::Result, + ) -> fmt::Result + where + Out: Default + Eq + PartialEq, + { + if !matches!(exp, Expectation::Permanent(perma) if perma == &Default::default()) { + f() + } else { + Ok(()) + } + } + + let expected = lazy_format!("{indent}expected"); + + let rhs = format_exp; + + let NormalizedExpectationPropertyValue(exps) = exps; + let r#if = lazy_format!("{indent} if"); + let disp_build_profile = |build_profile| match build_profile { + BuildProfile::Debug => "debug", + BuildProfile::Optimized => "not debug", + }; + match exps { + MaybeCollapsed::Collapsed(exps) => match exps { + MaybeCollapsed::Collapsed(exps) => { + if_not_default(exps, || writeln!(f, "{expected}: {}", rhs(exps)))?; + } + MaybeCollapsed::Expanded(by_build_profile) => { + writeln!(f, "{expected}:")?; + debug_assert!(!by_build_profile.is_empty()); + for (build_profile, exps) in by_build_profile { + let build_profile = disp_build_profile(*build_profile); + if_not_default(exps, || { + writeln!(f, "{if} {build_profile}: {}", rhs(exps)) + })?; } } - if let Some(fallback) = fallback { - writeln!(f, "{indent} {}", format_exp(fallback))?; + }, + MaybeCollapsed::Expanded(by_platform) => { + writeln!(f, "{expected}:")?; + debug_assert!(!by_platform.is_empty()); + for (platform, exps) in by_platform { + let platform = { + let platform_str = match platform { + Platform::Windows => "win", + Platform::Linux => "linux", + Platform::MacOs => "mac", + }; + lazy_format!(move |f| write!(f, "os == {platform_str:?}")) + }; + match exps { + MaybeCollapsed::Collapsed(exps) => if_not_default(exps, || { + writeln!(f, "{if} {platform}: {}", rhs(exps)) + })?, + MaybeCollapsed::Expanded(by_build_profile) => { + debug_assert!(!by_build_profile.is_empty()); + for (build_profile, exps) in by_build_profile { + let build_profile = disp_build_profile(*build_profile); + if_not_default(exps, || { + writeln!( + f, + "{if} {platform} and {build_profile}: {}", + rhs(exps) + ) + })?; + } + } + } } } } @@ -158,14 +184,14 @@ where }) } -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Platform { Windows, Linux, MacOs, } -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum BuildProfile { Debug, Optimized, @@ -174,7 +200,7 @@ pub enum BuildProfile { #[derive(Clone, Debug)] pub struct AnalyzeableProps { pub is_disabled: bool, - pub expectations: Option>>, + pub expectations: Option>, } impl Default for AnalyzeableProps { @@ -186,10 +212,13 @@ impl Default for AnalyzeableProps { } } -impl<'a, Exp> AnalyzeableProps { +impl<'a, Out> AnalyzeableProps +where + Out: Clone + Default + Eq + PartialEq + Hash, +{ fn insert( &mut self, - prop: AnalyzeableProp, + prop: AnalyzeableProp, emitter: &mut chumsky::input::Emitter>, ) { let Self { @@ -199,12 +228,64 @@ impl<'a, Exp> AnalyzeableProps { match prop { AnalyzeableProp::Expected(val) => { - if expectations.replace(val).is_some() { + if expectations.is_some() { emitter.emit(Rich::custom( todo!("duplicate `expected` key detected"), "duplicate `expected` key detected", - )) + )); + return; } + expectations.replace(match val { + PropertyValue::Unconditional(exp) => NormalizedExpectationPropertyValue( + MaybeCollapsed::Collapsed(MaybeCollapsed::Collapsed(exp)), + ), + PropertyValue::Conditional(val) => { + let ConditionalValue { + conditions, + fallback, + } = val; + if conditions.is_empty() { + NormalizedExpectationPropertyValue(MaybeCollapsed::Collapsed( + MaybeCollapsed::Collapsed(fallback.expect(concat!( + "at least one condition or fallback not present ", + "in conditional `expected` property value" + ))), + )) + } else { + let fully_expanded = Platform::iter() + .filter_map(|p| { + let by_build_profile = BuildProfile::iter() + .filter_map(|bp| { + let mut matched = None; + + for (applicability, val) in &conditions { + let Applicability { + platform, + build_profile, + } = applicability; + if platform.as_ref().map_or(true, |p2| *p2 == p) + && build_profile + .as_ref() + .map_or(true, |bp2| *bp2 == bp) + { + matched = Some(val.clone()); + } + } + matched + .or(fallback.clone()) + .map(|matched| (bp, matched)) + }) + .collect::>(); + (!by_build_profile.is_empty()) + .then_some(by_build_profile) + .map(|tree| (p, tree)) + }) + .collect(); + + NormalizedExpectationPropertyValue::from_full(fully_expanded).unwrap() + } + } + }); } AnalyzeableProp::Disabled => { if *is_disabled { @@ -219,6 +300,59 @@ impl<'a, Exp> AnalyzeableProps { } } +impl NormalizedExpectationPropertyValue +where + Out: Clone + Default + Eq + PartialEq, +{ + fn from_full( + mut outcomes: BTreeMap>>, + ) -> Option { + if outcomes.is_empty() { + return None; + } + + let normalize_by_build_profile = + |exp_by_build_profile: BTreeMap>| { + let default = &Default::default(); + let mut iter = + BuildProfile::iter().map(|bp| exp_by_build_profile.get(&bp).unwrap_or(default)); + let first_exp = iter.next().unwrap(); + + for exp in iter { + if exp != first_exp { + return MaybeCollapsed::Expanded(exp_by_build_profile); + } + } + + MaybeCollapsed::Collapsed(first_exp.clone()) + }; + + let mut iter = Platform::iter().map(|p| { + ( + p, + normalize_by_build_profile(outcomes.remove(&p).unwrap_or_default()), + ) + }); + let (first_platform, first_normalized_by_build_profile) = iter.next().unwrap(); + let mut normalized_expanded = BTreeMap::new(); + while let Some((platform, normalized_by_build_profile)) = iter.next() { + let is_consistent = normalized_by_build_profile != first_normalized_by_build_profile; + normalized_expanded.insert(platform, normalized_by_build_profile); + if is_consistent { + normalized_expanded.insert(first_platform, first_normalized_by_build_profile); + normalized_expanded.extend(iter); + return Some(NormalizedExpectationPropertyValue( + MaybeCollapsed::Expanded(normalized_expanded), + )); + } + } + + return Some(NormalizedExpectationPropertyValue( + MaybeCollapsed::Collapsed(first_normalized_by_build_profile), + )); + } +} + #[derive(Clone, Debug, Default)] pub struct Applicability { pub platform: Option, @@ -392,21 +526,6 @@ impl AnalyzeableProp { } } -#[derive(Clone, Debug)] -pub enum Expectation { - Permanent(Out), - Intermittent(IndexSet), -} - -impl Default for Expectation -where - Out: Default, -{ - fn default() -> Self { - Self::Permanent(Default::default()) - } -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum TestOutcome { Ok, diff --git a/src/bin/moz-webgpu-cts/shared.rs b/src/bin/moz-webgpu-cts/shared.rs new file mode 100644 index 0000000..d4d9bf9 --- /dev/null +++ b/src/bin/moz-webgpu-cts/shared.rs @@ -0,0 +1,55 @@ +use std::{collections::BTreeMap}; + +use crate::metadata::{BuildProfile, Platform}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Expectation { + Permanent(Out), + Intermittent(Vec), +} + +impl Default for Expectation +where + Out: Default, +{ + fn default() -> Self { + Self::Permanent(Default::default()) + } +} + +/// Similar to the ubiquitous `enum Either`, but with the implication that `Collapsed` values are +/// abbreviations of equivalent `Expanded` values. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MaybeCollapsed { + Collapsed(C), + Expanded(E), +} + +/// A normalized representation of [`Expectation`]s in [`AnalyzeableProps`]. +/// +/// Yes, the type is _gnarly_. Sorry about that. This is some complex domain, okay? 😆😭 +/// +/// [`AnalyzeableProps`]: crate::metadata::AnalyzeableProps +#[derive(Clone, Debug)] +pub struct NormalizedExpectationPropertyValue( + pub(crate) MaybeCollapsed< + MaybeCollapsed, BTreeMap>>, + BTreeMap< + Platform, + MaybeCollapsed, BTreeMap>>, + >, + >, +); + +/// Data from a [`NormalizedExpectationPropertyValue`]. +pub type NormalizedExpectationPropertyValueData = MaybeCollapsed< + MaybeCollapsed, BTreeMap>>, + BTreeMap, BTreeMap>>>, +>; + +impl NormalizedExpectationPropertyValue { + pub fn into_inner(self) -> NormalizedExpectationPropertyValueData { + let Self(inner) = self; + inner + } +}