test_split.rs (6093B)
1 //! Functionality for splitting CTS tests at certain paths into more tests and files in 2 //! [`crate::main`]. 3 4 use std::collections::BTreeSet; 5 6 /// The value portion of a key-value pair used in [`crate::main`] to split tests into more tests or 7 /// files. 8 #[derive(Debug)] 9 pub(crate) struct Config<O> { 10 /// The new base name used for the sibling directory that this entry will split off from the 11 /// original CTS test. 12 /// 13 /// Chunking WPT tests happens at a directory level in Firefox's CI. If we split into a child 14 /// directory, instead of a sibling directory, then we would actually cause the child test to 15 /// be run in multiple chunks. Therefore, it's required to split tests into siblings. 16 pub new_sibling_basename: &'static str, 17 /// How to split the test this entry refers to. 18 pub split_by: SplitBy<O>, 19 } 20 21 impl<O> Config<O> { 22 pub fn map_observed_values<T>(self, f: impl FnOnce(O) -> T) -> Config<T> { 23 let Self { 24 new_sibling_basename, 25 split_by, 26 } = self; 27 Config { 28 new_sibling_basename, 29 split_by: split_by.map_observed_values(f), 30 } 31 } 32 } 33 34 /// A [`Config::split_by`] value. 35 #[derive(Debug)] 36 pub(crate) enum SplitBy<O> { 37 FirstParam { 38 /// The name of the first parameter in the test, to be used as validation that we are 39 /// actually pointing to the correct test. 40 expected_name: &'static str, 41 /// The method by which a test should be divided. 42 split_to: SplitParamsTo, 43 /// Values collected during [`Entry::process`], corresponding to a new test entry each. 44 observed_values: O, 45 }, 46 } 47 48 impl SplitBy<()> { 49 /// Convenience for constructing [`SplitBy::FirstParam`]. 50 pub fn first_param(name: &'static str, split_to: SplitParamsTo) -> Self { 51 Self::FirstParam { 52 expected_name: name, 53 split_to, 54 observed_values: (), 55 } 56 } 57 } 58 59 impl<O> SplitBy<O> { 60 pub fn map_observed_values<T>(self, f: impl FnOnce(O) -> T) -> SplitBy<T> { 61 match self { 62 Self::FirstParam { 63 expected_name, 64 split_to, 65 observed_values, 66 } => { 67 let observed_values = f(observed_values); 68 SplitBy::FirstParam { 69 expected_name, 70 split_to, 71 observed_values, 72 } 73 } 74 } 75 } 76 } 77 78 /// A [SplitBy::FirstParam::split_to]. 79 #[derive(Debug)] 80 pub(crate) enum SplitParamsTo { 81 /// Place new test entries as siblings in the same file. 82 SeparateTestsInSameFile, 83 } 84 85 #[derive(Debug)] 86 pub(crate) struct Entry<'a> { 87 /// Whether this 88 pub seen: SeenIn, 89 pub config: Config<BTreeSet<&'a str>>, 90 } 91 92 impl Entry<'_> { 93 pub fn from_config(config: Config<()>) -> Self { 94 Self { 95 seen: SeenIn::nowhere(), 96 config: config.map_observed_values(|()| BTreeSet::new()), 97 } 98 } 99 } 100 101 /// An [`Entry::seen`]. 102 #[derive(Debug, Default)] 103 pub(crate) struct SeenIn { 104 pub listing: bool, 105 pub wpt_files: bool, 106 } 107 108 impl SeenIn { 109 /// Default value: all seen locations set to `false`. 110 pub fn nowhere() -> Self { 111 Self::default() 112 } 113 } 114 115 impl<'a> Entry<'a> { 116 /// Accumulates a line from the test listing script in upstream CTS. 117 /// 118 /// Line is expected to have a full CTS test path, including at least one case parameter, i.e.: 119 /// 120 /// ``` 121 /// webgpu:path,to,test,group:test:param1="value1";… 122 /// ``` 123 /// 124 /// Note that `;…` is not strictly necessary, when there is only a single case parameter for 125 /// the test. 126 /// 127 /// See [`crate::main`] for more details on how upstream CTS' listing script is invoked. 128 pub(crate) fn process_listing_line( 129 &mut self, 130 test_group_and_later_path: &'a str, 131 ) -> miette::Result<()> { 132 let rest = test_group_and_later_path; 133 134 let Self { seen, config } = self; 135 136 let Config { 137 new_sibling_basename: _, 138 split_by, 139 } = config; 140 141 match split_by { 142 SplitBy::FirstParam { 143 ref expected_name, 144 split_to: _, 145 observed_values, 146 } => { 147 // NOTE: This only parses strings with no escaped characters. We may need different 148 // values later, at which point we'll have to consider what to do here. 149 let (ident, rest) = rest.split_once("=").ok_or_else(|| { 150 miette::diagnostic!("failed to get start of value of first arg") 151 })?; 152 153 if ident != *expected_name { 154 return Err(miette::diagnostic!( 155 "expected {:?}, got {:?}", 156 expected_name, 157 ident 158 ) 159 .into()); 160 } 161 162 let value = rest 163 .split_once(';') 164 .map(|(value, _rest)| value) 165 .unwrap_or(rest); 166 167 // TODO: parse as JSON? 168 169 observed_values.insert(value); 170 } 171 } 172 seen.listing = true; 173 174 Ok(()) 175 } 176 } 177 178 /// Iterate over `entries`' [`seen` members](Entry::seen), accumulating one of their fields and 179 /// panicking if any are `false` 180 /// 181 /// Used in [`crate::main`] to check that a specific [`Entry::seen`] fields have been set to `true` 182 /// for each entry configured. 183 pub(crate) fn assert_seen<'a>( 184 what: &'static str, 185 entries: impl Iterator<Item = (&'a &'a str, &'a Entry<'a>)>, 186 mut in_: impl FnMut(&'a SeenIn) -> &'a bool, 187 ) { 188 let mut unseen = Vec::new(); 189 entries.for_each(|(test_path, entry)| { 190 if !*in_(&entry.seen) { 191 unseen.push(test_path); 192 } 193 }); 194 if !unseen.is_empty() { 195 panic!( 196 concat!( 197 "did not find the following test split config. entries ", 198 "in {}: {:#?}", 199 ), 200 what, unseen 201 ); 202 } 203 }