//! Functionality for splitting CTS tests at certain paths into more tests and files in //! [`crate::main`]. use std::collections::BTreeSet; /// The value portion of a key-value pair used in [`crate::main`] to split tests into more tests or /// files. #[derive(Debug)] pub(crate) struct Config { /// The new base name used for the sibling directory that this entry will split off from the /// original CTS test. /// /// Chunking WPT tests happens at a directory level in Firefox's CI. If we split into a child /// directory, instead of a sibling directory, then we would actually cause the child test to /// be run in multiple chunks. Therefore, it's required to split tests into siblings. pub new_sibling_basename: &'static str, /// How to split the test this entry refers to. pub split_by: SplitBy, } impl Config { pub fn map_observed_values(self, f: impl FnOnce(O) -> T) -> Config { let Self { new_sibling_basename, split_by, } = self; Config { new_sibling_basename, split_by: split_by.map_observed_values(f), } } } /// A [`Config::split_by`] value. #[derive(Debug)] pub(crate) enum SplitBy { FirstParam { /// The name of the first parameter in the test, to be used as validation that we are /// actually pointing to the correct test. expected_name: &'static str, /// The method by which a test should be divided. split_to: SplitParamsTo, /// Values collected during [`Entry::process`], corresponding to a new test entry each. observed_values: O, }, } impl SplitBy<()> { /// Convenience for constructing [`SplitBy::FirstParam`]. pub fn first_param(name: &'static str, split_to: SplitParamsTo) -> Self { Self::FirstParam { expected_name: name, split_to, observed_values: (), } } } impl SplitBy { pub fn map_observed_values(self, f: impl FnOnce(O) -> T) -> SplitBy { match self { Self::FirstParam { expected_name, split_to, observed_values, } => { let observed_values = f(observed_values); SplitBy::FirstParam { expected_name, split_to, observed_values, } } } } } /// A [SplitBy::FirstParam::split_to]. #[derive(Debug)] pub(crate) enum SplitParamsTo { /// Place new test entries as siblings in the same file. SeparateTestsInSameFile, } #[derive(Debug)] pub(crate) struct Entry<'a> { /// Whether this pub seen: SeenIn, pub config: Config>, } impl Entry<'_> { pub fn from_config(config: Config<()>) -> Self { Self { seen: SeenIn::nowhere(), config: config.map_observed_values(|()| BTreeSet::new()), } } } /// An [`Entry::seen`]. #[derive(Debug, Default)] pub(crate) struct SeenIn { pub listing: bool, pub wpt_files: bool, } impl SeenIn { /// Default value: all seen locations set to `false`. pub fn nowhere() -> Self { Self::default() } } impl<'a> Entry<'a> { /// Accumulates a line from the test listing script in upstream CTS. /// /// Line is expected to have a full CTS test path, including at least one case parameter, i.e.: /// /// ``` /// webgpu:path,to,test,group:test:param1="value1";… /// ``` /// /// Note that `;…` is not strictly necessary, when there is only a single case parameter for /// the test. /// /// See [`crate::main`] for more details on how upstream CTS' listing script is invoked. pub(crate) fn process_listing_line( &mut self, test_group_and_later_path: &'a str, ) -> miette::Result<()> { let rest = test_group_and_later_path; let Self { seen, config } = self; let Config { new_sibling_basename: _, split_by, } = config; match split_by { SplitBy::FirstParam { ref expected_name, split_to: _, observed_values, } => { // NOTE: This only parses strings with no escaped characters. We may need different // values later, at which point we'll have to consider what to do here. let (ident, rest) = rest.split_once("=").ok_or_else(|| { miette::diagnostic!("failed to get start of value of first arg") })?; if ident != *expected_name { return Err(miette::diagnostic!( "expected {:?}, got {:?}", expected_name, ident ) .into()); } let value = rest .split_once(';') .map(|(value, _rest)| value) .unwrap_or(rest); // TODO: parse as JSON? observed_values.insert(value); } } seen.listing = true; Ok(()) } } /// Iterate over `entries`' [`seen` members](Entry::seen), accumulating one of their fields and /// panicking if any are `false` /// /// Used in [`crate::main`] to check that a specific [`Entry::seen`] fields have been set to `true` /// for each entry configured. pub(crate) fn assert_seen<'a>( what: &'static str, entries: impl Iterator)>, mut in_: impl FnMut(&'a SeenIn) -> &'a bool, ) { let mut unseen = Vec::new(); entries.for_each(|(test_path, entry)| { if !*in_(&entry.seen) { unseen.push(test_path); } }); if !unseen.is_empty() { panic!( concat!( "did not find the following test split config. entries ", "in {}: {:#?}", ), what, unseen ); } }