Skip to main content

supercov_engine/
served_tests.rs

1//! The browser tests a measured server answered: what the browser runner
2//! reported for each attempt, which a Python or Ruby server's run takes its
3//! tests from, and the rule that gives a request naming no test to one.
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    path::Path,
8};
9
10use crate::coverage_report::TestProvenance;
11
12/// The worker a server's runtime records a request that names no test
13/// under, numbered within its process.
14pub const REQUEST_WORKER: &str = "supercov-request";
15
16/// A browser test whose requests a measured server answered: what the
17/// browser runner reported for it, which no runner of the server's language
18/// saw.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ServedTest {
21    /// As the server's runtime names the worker: `playwright:<worker id>`.
22    pub worker: String,
23    pub test_id: String,
24    pub retry: usize,
25    /// The test's name, title and file as the browser run gives them.
26    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    /// When the attempt ran, in milliseconds since the epoch, first and
33    /// last moment: what a request that named no test is judged by.
34    pub window: Option<(i64, i64)>,
35}
36
37/// One phase starting: in which process (its transport), where in its
38/// records, and of which attempt (worker, test, retry).
39pub type PhaseStart = (String, usize, Attempt);
40
41/// A test attempt: worker, test, retry.
42pub type Attempt = (String, String, usize);
43
44/// The suite's own test attempts, in the order each process ran them: what
45/// a request to a server the suite runs in its own process -- a live server
46/// a browser or the test itself calls -- is judged by. A process writes its
47/// records in order, so the attempt whose phase began last before the
48/// request did is the one running when it came: exactly, where the attempts
49/// were run one after another, and no one's where they interleave.
50pub struct InProcess {
51    /// By transport: where each phase began and whose, and whether the
52    /// process ran attempts at once.
53    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
89/// The attempt a request that named no test belongs to: the one running when
90/// it came, if only one was, among the attempts its process answered a
91/// request of where it answered any -- with a server per worker, the
92/// worker's test, whatever other workers ran meanwhile -- and among every
93/// attempt otherwise.
94pub 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
112/// Each browser test attempt the suite reported, read from the Playwright
113/// fixture's evidence lines.
114pub 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    // A request to a server the suite runs itself is the test's whose phase
196    // began last before it in that process: by record, which a millisecond
197    // clock cannot tell apart when a test ends, the next starts and the
198    // request comes all in one millisecond.
199    #[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        // Tests that ran at once in one process are no one's.
220        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}