1use std::{future::Future, num::NonZeroUsize, pin::Pin};
2
3use futures_util::stream::{FuturesUnordered, StreamExt as _};
4use rootcause::Report;
5use rootcause::report_collection::ReportCollection;
6
7use crate::BrowserTestError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum BrowserTestParallelism {
12 #[default]
14 Sequential,
15
16 Parallel(NonZeroUsize),
22}
23
24impl BrowserTestParallelism {
25 pub(crate) const fn max_parallel_tests(self) -> NonZeroUsize {
26 match self {
27 Self::Sequential => NonZeroUsize::MIN,
28 Self::Parallel(max_parallel_tests) => max_parallel_tests,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum BrowserTestFailurePolicy {
36 #[default]
41 FailFast,
42
43 RunAll,
45}
46
47pub(crate) struct BrowserTestExecution {
48 pub(crate) test_index: usize,
49 pub(crate) result: Result<(), Report<BrowserTestError>>,
50}
51
52pub(crate) type BrowserTestExecutionFuture<'a> =
53 Pin<Box<dyn Future<Output = BrowserTestExecution> + Send + 'a>>;
54
55pub(crate) async fn run_test_executions_sequential<'a>(
56 failure_policy: BrowserTestFailurePolicy,
57 executions: impl IntoIterator<Item = BrowserTestExecutionFuture<'a>>,
58) -> Result<(), Report<BrowserTestError>> {
59 let mut failures = BrowserTestFailures::default();
60
61 for execution in executions {
62 let execution = execution.await;
63 match execution.result {
64 Ok(()) => {}
65 Err(err) => {
66 if failure_policy == BrowserTestFailurePolicy::FailFast {
67 return Err(err);
68 }
69 failures.push(execution.test_index, err);
70 }
71 }
72 }
73
74 failures.into_result()
75}
76
77pub(crate) async fn run_test_executions_parallel<'a>(
78 failure_policy: BrowserTestFailurePolicy,
79 executions: impl IntoIterator<Item = BrowserTestExecutionFuture<'a>>,
80 max_parallel_tests: NonZeroUsize,
81) -> Result<(), Report<BrowserTestError>> {
82 let mut tests = executions.into_iter();
83 let mut running = FuturesUnordered::new();
84 let mut keep_starting = true;
85 let mut collected_failures = BrowserTestFailures::default();
86
87 while running.len() < max_parallel_tests.get() {
89 match tests.next() {
90 None => break,
91 Some(test) => running.push(test),
92 }
93 }
94
95 while let Some(execution) = running.next().await {
98 if let Err(err) = execution.result {
99 if failure_policy == BrowserTestFailurePolicy::FailFast {
100 keep_starting = false;
101 }
102 collected_failures.push(execution.test_index, err);
103 }
104
105 if keep_starting && let Some(test) = tests.next() {
106 running.push(test);
107 }
108 }
109
110 collected_failures.into_result()
111}
112
113#[derive(Default)]
114struct BrowserTestFailures {
115 failures: Vec<(usize, Report<BrowserTestError>)>,
116}
117
118impl BrowserTestFailures {
119 fn push(&mut self, test_index: usize, failure: Report<BrowserTestError>) {
120 self.failures.push((test_index, failure));
121 }
122
123 fn into_result(mut self) -> Result<(), Report<BrowserTestError>> {
124 if self.failures.is_empty() {
125 return Ok(());
126 }
127
128 let failed_tests = self.failures.len();
129 self.failures
130 .sort_by_key(|(test_index, _failure)| *test_index);
131
132 let mut failure_collection = ReportCollection::with_capacity(failed_tests);
133 for (_test_index, failure) in self.failures {
134 failure_collection.push(failure.into_cloneable());
135 }
136
137 Err(failure_collection.context(BrowserTestError::RunTests { failed_tests }))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::sync::{
144 Arc,
145 atomic::{AtomicUsize, Ordering},
146 };
147
148 use assertr::prelude::*;
149
150 use super::*;
151
152 #[test]
153 fn parallelism_max_parallel_tests_treats_sequential_as_one() {
154 assert_that!(
155 BrowserTestParallelism::Sequential
156 .max_parallel_tests()
157 .get()
158 )
159 .is_equal_to(1);
160
161 let max_parallel_tests =
162 NonZeroUsize::new(3).expect("literal parallelism should be non-zero");
163 assert_that!(
164 BrowserTestParallelism::Parallel(max_parallel_tests)
165 .max_parallel_tests()
166 .get()
167 )
168 .is_equal_to(3);
169 }
170
171 #[test]
172 fn browser_test_failures_returns_ok_when_empty() {
173 let failures = BrowserTestFailures::default();
174
175 assert_that!(failures.into_result()).is_ok();
176 }
177
178 #[test]
179 fn browser_test_failures_returns_aggregate_report_with_children() {
180 let mut failures = BrowserTestFailures::default();
181 failures.push(
182 0,
183 Report::new(BrowserTestError::RunTest {
184 test_name: "login".to_owned(),
185 }),
186 );
187 failures.push(
188 1,
189 Report::new(BrowserTestError::RunTest {
190 test_name: "checkout".to_owned(),
191 }),
192 );
193
194 let err = failures
195 .into_result()
196 .expect_err("non-empty failure collection should fail");
197
198 assert_that!(err.to_string())
199 .contains(BrowserTestError::RunTests { failed_tests: 2 }.to_string());
200 assert_that!(err.children().len()).is_equal_to(2);
201 }
202
203 #[test]
204 fn sequential_fail_fast_stops_after_first_failure() {
205 let runtime = current_thread_runtime();
206 let first = Arc::new(AtomicUsize::new(0));
207 let second = Arc::new(AtomicUsize::new(0));
208
209 let result = runtime.block_on(run_test_executions_sequential(
210 BrowserTestFailurePolicy::FailFast,
211 [
212 tracked_execution(first.clone(), failing_execution(0, "first")),
213 tracked_execution(second.clone(), passing_execution(1)),
214 ],
215 ));
216
217 assert_that!(result).is_err();
218 assert_that!(first.load(Ordering::SeqCst)).is_equal_to(1);
219 assert_that!(second.load(Ordering::SeqCst)).is_equal_to(0);
220 }
221
222 #[test]
223 fn sequential_run_all_continues_after_failure_and_panic() {
224 let runtime = current_thread_runtime();
225 let first = Arc::new(AtomicUsize::new(0));
226 let second = Arc::new(AtomicUsize::new(0));
227 let third = Arc::new(AtomicUsize::new(0));
228
229 let err = runtime
230 .block_on(run_test_executions_sequential(
231 BrowserTestFailurePolicy::RunAll,
232 [
233 tracked_execution(first.clone(), failing_execution(0, "first")),
234 tracked_execution(second.clone(), panicked_execution(1, "second")),
235 tracked_execution(third.clone(), passing_execution(2)),
236 ],
237 ))
238 .expect_err("run-all should report collected failures");
239
240 assert_that!(first.load(Ordering::SeqCst)).is_equal_to(1);
241 assert_that!(second.load(Ordering::SeqCst)).is_equal_to(1);
242 assert_that!(third.load(Ordering::SeqCst)).is_equal_to(1);
243 assert_that!(err.children().len()).is_equal_to(2);
244 assert_that!(err.to_string())
245 .contains(BrowserTestError::RunTests { failed_tests: 2 }.to_string());
246 }
247
248 #[test]
249 fn parallel_fail_fast_stops_starting_new_tests_but_waits_for_running_tests() {
250 let runtime = current_thread_runtime();
251 let first = Arc::new(AtomicUsize::new(0));
252 let second = Arc::new(AtomicUsize::new(0));
253 let third = Arc::new(AtomicUsize::new(0));
254
255 let err = runtime
256 .block_on(run_test_executions_parallel(
257 BrowserTestFailurePolicy::FailFast,
258 [
259 tracked_execution(first.clone(), failing_execution(0, "first")),
260 tracked_execution(second.clone(), failing_execution(1, "second")),
261 tracked_execution(third.clone(), passing_execution(2)),
262 ],
263 NonZeroUsize::new(2).expect("literal parallelism should be non-zero"),
264 ))
265 .expect_err("fail-fast should report failures from already-running tests");
266
267 assert_that!(err.children().len()).is_equal_to(2);
268 assert_that!(first.load(Ordering::SeqCst)).is_equal_to(1);
269 assert_that!(second.load(Ordering::SeqCst)).is_equal_to(1);
270 assert_that!(third.load(Ordering::SeqCst)).is_equal_to(0);
271 }
272
273 #[test]
274 fn parallel_run_all_starts_every_test() {
275 let runtime = current_thread_runtime();
276 let first = Arc::new(AtomicUsize::new(0));
277 let second = Arc::new(AtomicUsize::new(0));
278 let third = Arc::new(AtomicUsize::new(0));
279
280 runtime
281 .block_on(run_test_executions_parallel(
282 BrowserTestFailurePolicy::RunAll,
283 [
284 tracked_execution(first.clone(), passing_execution(0)),
285 tracked_execution(second.clone(), passing_execution(1)),
286 tracked_execution(third.clone(), passing_execution(2)),
287 ],
288 NonZeroUsize::new(2).expect("literal parallelism should be non-zero"),
289 ))
290 .expect("all passing executions should succeed");
291
292 assert_that!(first.load(Ordering::SeqCst)).is_equal_to(1);
293 assert_that!(second.load(Ordering::SeqCst)).is_equal_to(1);
294 assert_that!(third.load(Ordering::SeqCst)).is_equal_to(1);
295 }
296
297 fn current_thread_runtime() -> tokio::runtime::Runtime {
298 tokio::runtime::Builder::new_current_thread()
299 .build()
300 .expect("current-thread runtime should build")
301 }
302
303 fn tracked_execution(
304 counter: Arc<AtomicUsize>,
305 execution: BrowserTestExecution,
306 ) -> BrowserTestExecutionFuture<'static> {
307 Box::pin(async move {
308 counter.fetch_add(1, Ordering::SeqCst);
309 execution
310 })
311 }
312
313 fn passing_execution(test_index: usize) -> BrowserTestExecution {
314 BrowserTestExecution {
315 test_index,
316 result: Ok(()),
317 }
318 }
319
320 fn failing_execution(test_index: usize, test_name: &str) -> BrowserTestExecution {
321 BrowserTestExecution {
322 test_index,
323 result: Err(Report::new(BrowserTestError::RunTest {
324 test_name: test_name.to_owned(),
325 })),
326 }
327 }
328
329 fn panicked_execution(test_index: usize, test_name: &str) -> BrowserTestExecution {
330 BrowserTestExecution {
331 test_index,
332 result: Err(Report::new(BrowserTestError::Panic {
333 test_name: test_name.to_owned(),
334 message: "boom".to_owned(),
335 })),
336 }
337 }
338}