1use std::{
6 collections::{BTreeMap, BTreeSet},
7 path::Path,
8};
9
10use crate::coverage_report::TestProvenance;
11
12pub const REQUEST_WORKER: &str = "supercov-request";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ServedTest {
21 pub worker: String,
23 pub test_id: String,
24 pub retry: usize,
25 pub name: String,
27 pub title: String,
28 pub file: String,
29 pub status: String,
30 pub expected_status: String,
31 pub provenance: TestProvenance,
32 pub window: Option<(i64, i64)>,
35}
36
37pub type PhaseStart = (String, usize, Attempt);
40
41pub type Attempt = (String, String, usize);
43
44pub struct InProcess {
51 by_transport: BTreeMap<String, (Vec<(usize, Attempt)>, bool)>,
54}
55
56impl InProcess {
57 pub fn new(phases: &[PhaseStart]) -> Self {
58 let mut by_transport = BTreeMap::<String, (Vec<_>, bool)>::new();
59 for (transport, seq, attempt) in phases {
60 by_transport
61 .entry(transport.clone())
62 .or_default()
63 .0
64 .push((*seq, attempt.clone()));
65 }
66 for (starts, interleaved) in by_transport.values_mut() {
67 starts.sort();
68 let mut left = BTreeSet::new();
69 for pair in starts.windows(2) {
70 if pair[0].1 != pair[1].1 {
71 left.insert(pair[0].1.clone());
72 *interleaved |= left.contains(&pair[1].1);
73 }
74 }
75 }
76 Self { by_transport }
77 }
78
79 pub fn owner(&self, transport: &str, seq: usize) -> Option<&Attempt> {
80 let (starts, interleaved) = self.by_transport.get(transport)?;
81 if *interleaved {
82 return None;
83 }
84 let before = starts.partition_point(|(start, _)| *start < seq);
85 before.checked_sub(1).map(|index| &starts[index].1)
86 }
87}
88
89pub fn owner<'a>(
95 served: &'a [ServedTest],
96 answered: Option<&BTreeSet<(String, String, usize)>>,
97 at: i64,
98) -> Option<&'a ServedTest> {
99 let mut running = served.iter().filter(|test| {
100 answered.is_none_or(|attempts| {
101 attempts.contains(&(test.worker.clone(), test.test_id.clone(), test.retry))
102 }) && test
103 .window
104 .is_some_and(|(start, end)| start <= at && at <= end)
105 });
106 match (running.next(), running.next()) {
107 (Some(only), None) => Some(only),
108 _ => None,
109 }
110}
111
112pub fn read(evidence_directory: &Path) -> Vec<ServedTest> {
115 #[derive(serde::Deserialize)]
116 #[serde(rename_all = "camelCase")]
117 struct Scope {
118 worker_id: String,
119 }
120 #[derive(serde::Deserialize)]
121 #[serde(rename_all = "camelCase")]
122 struct Attempt {
123 test_id: String,
124 scope: Scope,
125 test: String,
126 test_file: String,
127 title: String,
128 retry: usize,
129 status: String,
130 expected_status: Option<String>,
131 provenance: crate::coverage_report::TestProvenance,
132 #[serde(default)]
133 phases: Vec<Phase>,
134 }
135 #[derive(serde::Deserialize)]
136 #[serde(rename_all = "camelCase")]
137 struct Phase {
138 started_at_ms: i64,
139 ended_at_ms: Option<i64>,
140 }
141 let mut served = BTreeMap::new();
142 let Ok(entries) = std::fs::read_dir(evidence_directory) else {
143 return Vec::new();
144 };
145 let mut files = entries
146 .filter_map(Result::ok)
147 .map(|entry| entry.path())
148 .filter(|path| {
149 path.file_name()
150 .and_then(|name| name.to_str())
151 .is_some_and(|name| {
152 name.starts_with("playwright-worker-") && name.ends_with(".mcdc.jsonl")
153 })
154 })
155 .collect::<Vec<_>>();
156 files.sort();
157 for file in files {
158 let Ok(text) = std::fs::read_to_string(&file) else {
159 continue;
160 };
161 for attempt in text
162 .lines()
163 .filter_map(|line| serde_json::from_str::<Attempt>(line).ok())
164 {
165 let worker = format!("playwright:{}", attempt.scope.worker_id);
166 let start = attempt.phases.iter().map(|phase| phase.started_at_ms).min();
167 let end = attempt
168 .phases
169 .iter()
170 .map(|phase| phase.ended_at_ms.unwrap_or(phase.started_at_ms))
171 .max();
172 served
173 .entry((worker.clone(), attempt.test_id.clone(), attempt.retry))
174 .or_insert(ServedTest {
175 worker,
176 test_id: attempt.test_id,
177 retry: attempt.retry,
178 name: attempt.test,
179 title: attempt.title,
180 file: attempt.test_file,
181 status: attempt.status,
182 expected_status: attempt.expected_status.unwrap_or_else(|| "passed".into()),
183 provenance: attempt.provenance,
184 window: start.zip(end),
185 });
186 }
187 }
188 served.into_values().collect()
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
200 fn a_request_in_process_is_the_test_running_before_it() {
201 let attempt = |test: &str| ("main".to_owned(), test.to_owned(), 0);
202 let phases = [
203 ("a".to_owned(), 1, attempt("one")),
204 ("a".to_owned(), 2, attempt("one")),
205 ("a".to_owned(), 5, attempt("one")),
206 ("a".to_owned(), 6, attempt("two")),
207 ("a".to_owned(), 7, attempt("two")),
208 ];
209 let order = InProcess::new(&phases);
210 assert_eq!(order.owner("a", 0), None, "before any test");
211 assert_eq!(order.owner("a", 3), Some(&attempt("one")));
212 assert_eq!(
213 order.owner("a", 6),
214 Some(&attempt("one")),
215 "as the next test begins"
216 );
217 assert_eq!(order.owner("a", 8), Some(&attempt("two")));
218 assert_eq!(order.owner("b", 3), None, "another process ran no test");
219 let interleaved = InProcess::new(&[
221 ("a".to_owned(), 1, attempt("one")),
222 ("a".to_owned(), 2, attempt("two")),
223 ("a".to_owned(), 3, attempt("one")),
224 ]);
225 assert_eq!(interleaved.owner("a", 4), None);
226 }
227}