use std::{
collections::{BTreeMap, BTreeSet},
path::Path,
};
use crate::coverage_report::TestProvenance;
pub const REQUEST_WORKER: &str = "supercov-request";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServedTest {
pub worker: String,
pub test_id: String,
pub retry: usize,
pub name: String,
pub title: String,
pub file: String,
pub status: String,
pub expected_status: String,
pub provenance: TestProvenance,
pub window: Option<(i64, i64)>,
}
pub type PhaseStart = (String, usize, Attempt);
pub type Attempt = (String, String, usize);
pub struct InProcess {
by_transport: BTreeMap<String, (Vec<(usize, Attempt)>, bool)>,
}
impl InProcess {
pub fn new(phases: &[PhaseStart]) -> Self {
let mut by_transport = BTreeMap::<String, (Vec<_>, bool)>::new();
for (transport, seq, attempt) in phases {
by_transport
.entry(transport.clone())
.or_default()
.0
.push((*seq, attempt.clone()));
}
for (starts, interleaved) in by_transport.values_mut() {
starts.sort();
let mut left = BTreeSet::new();
for pair in starts.windows(2) {
if pair[0].1 != pair[1].1 {
left.insert(pair[0].1.clone());
*interleaved |= left.contains(&pair[1].1);
}
}
}
Self { by_transport }
}
pub fn owner(&self, transport: &str, seq: usize) -> Option<&Attempt> {
let (starts, interleaved) = self.by_transport.get(transport)?;
if *interleaved {
return None;
}
let before = starts.partition_point(|(start, _)| *start < seq);
before.checked_sub(1).map(|index| &starts[index].1)
}
}
pub fn owner<'a>(
served: &'a [ServedTest],
answered: Option<&BTreeSet<(String, String, usize)>>,
at: i64,
) -> Option<&'a ServedTest> {
let mut running = served.iter().filter(|test| {
answered.is_none_or(|attempts| {
attempts.contains(&(test.worker.clone(), test.test_id.clone(), test.retry))
}) && test
.window
.is_some_and(|(start, end)| start <= at && at <= end)
});
match (running.next(), running.next()) {
(Some(only), None) => Some(only),
_ => None,
}
}
pub fn read(evidence_directory: &Path) -> Vec<ServedTest> {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Scope {
worker_id: String,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Attempt {
test_id: String,
scope: Scope,
test: String,
test_file: String,
title: String,
retry: usize,
status: String,
expected_status: Option<String>,
provenance: crate::coverage_report::TestProvenance,
#[serde(default)]
phases: Vec<Phase>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Phase {
started_at_ms: i64,
ended_at_ms: Option<i64>,
}
let mut served = BTreeMap::new();
let Ok(entries) = std::fs::read_dir(evidence_directory) else {
return Vec::new();
};
let mut files = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with("playwright-worker-") && name.ends_with(".mcdc.jsonl")
})
})
.collect::<Vec<_>>();
files.sort();
for file in files {
let Ok(text) = std::fs::read_to_string(&file) else {
continue;
};
for attempt in text
.lines()
.filter_map(|line| serde_json::from_str::<Attempt>(line).ok())
{
let worker = format!("playwright:{}", attempt.scope.worker_id);
let start = attempt.phases.iter().map(|phase| phase.started_at_ms).min();
let end = attempt
.phases
.iter()
.map(|phase| phase.ended_at_ms.unwrap_or(phase.started_at_ms))
.max();
served
.entry((worker.clone(), attempt.test_id.clone(), attempt.retry))
.or_insert(ServedTest {
worker,
test_id: attempt.test_id,
retry: attempt.retry,
name: attempt.test,
title: attempt.title,
file: attempt.test_file,
status: attempt.status,
expected_status: attempt.expected_status.unwrap_or_else(|| "passed".into()),
provenance: attempt.provenance,
window: start.zip(end),
});
}
}
served.into_values().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_request_in_process_is_the_test_running_before_it() {
let attempt = |test: &str| ("main".to_owned(), test.to_owned(), 0);
let phases = [
("a".to_owned(), 1, attempt("one")),
("a".to_owned(), 2, attempt("one")),
("a".to_owned(), 5, attempt("one")),
("a".to_owned(), 6, attempt("two")),
("a".to_owned(), 7, attempt("two")),
];
let order = InProcess::new(&phases);
assert_eq!(order.owner("a", 0), None, "before any test");
assert_eq!(order.owner("a", 3), Some(&attempt("one")));
assert_eq!(
order.owner("a", 6),
Some(&attempt("one")),
"as the next test begins"
);
assert_eq!(order.owner("a", 8), Some(&attempt("two")));
assert_eq!(order.owner("b", 3), None, "another process ran no test");
let interleaved = InProcess::new(&[
("a".to_owned(), 1, attempt("one")),
("a".to_owned(), 2, attempt("two")),
("a".to_owned(), 3, attempt("one")),
]);
assert_eq!(interleaved.owner("a", 4), None);
}
}