Skip to main content

supercov_engine/
owned_server.rs

1//! A server a browser suite drove, measured by a runtime Supercov owns: Go
2//! and the JVM. Their probes store into one array, which cannot tell two
3//! requests apart, so the runtime sweeps it on a clock into windows, and each
4//! window is credited, by time, to the one browser test whose span it
5//! overlaps. What no single test's window holds is no test's.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    fs,
10    io::Write,
11    path::{Path, PathBuf},
12};
13
14use serde::Deserialize;
15use supercov_contracts::FrontendRunDeclaration;
16
17use crate::{
18    coverage_report::{CoverageManifest, CoverageModelDeclaration},
19    evidence_archive::write_archive,
20    frontend_protocol::validate_frontend_report_request,
21    go_instrumenter::GoProbe,
22    lifecycle::{note_kept_evidence, publish_run, remove_stored_tree_deferred},
23    owned_evidence::{
24        OwnedEvidence, OwnedRunInputs, OwnedTestEvidence, OwnedTestOutcome, PackedVector,
25        build_frontend_run,
26    },
27    run_store::{RawEvidenceMetadata, RunIntegrity, RunMetadata},
28    served_tests::ServedTest,
29};
30
31/// One sweep of a server's probes, as the runtime appended it.
32#[derive(Deserialize)]
33struct ServerWindow {
34    start: i64,
35    end: i64,
36    /// Pairs: probe id, mask.
37    probes: Vec<u64>,
38    /// Pairs: decision, packed vector.
39    vectors: Vec<u64>,
40}
41
42/// What publishing a server's run needs from its frontend.
43pub struct OwnedServer<'a> {
44    /// `Go`, `Java/Kotlin`: said in the line the run prints.
45    pub language: &'static str,
46    /// The frontend's own declaration; its first runner becomes the browser
47    /// runner the tests came from.
48    pub declaration: FrontendRunDeclaration,
49    pub environment: &'a str,
50    pub coverage_model: CoverageModelDeclaration,
51    pub manifest: &'a CoverageManifest,
52    pub probes: &'a BTreeMap<u64, GoProbe>,
53    pub widths: &'a [u8],
54    pub integrity: &'a RunIntegrity,
55    pub source_inputs: &'a crate::source_manifest::Inputs,
56    pub source_roots: Option<Vec<String>>,
57    /// `.supercov/work/<run>/<language>`, holding `evidence/`.
58    pub directory: &'a Path,
59    pub source_files: usize,
60}
61
62fn read_windows(directory: &Path) -> Result<Vec<ServerWindow>, String> {
63    let mut files = fs::read_dir(directory)
64        .map(|entries| {
65            entries
66                .filter_map(Result::ok)
67                .map(|entry| entry.path())
68                .filter(|path| {
69                    path.extension()
70                        .is_some_and(|extension| extension == "windows")
71                })
72                .collect::<Vec<_>>()
73        })
74        .unwrap_or_default();
75    files.sort();
76    let mut windows = Vec::new();
77    for file in &files {
78        let text =
79            fs::read_to_string(file).map_err(|error| format!("{}: {error}", file.display()))?;
80        // A line cut short by the server being killed is the one window it
81        // was writing.
82        windows.extend(
83            text.lines()
84                .filter_map(|line| serde_json::from_str::<ServerWindow>(line).ok()),
85        );
86    }
87    Ok(windows)
88}
89
90/// The run's evidence from its windows: run-wide totals from every window,
91/// and each browser test's from the windows that overlap its span and no
92/// other test's. Returns the tests that were credited any, and those whose
93/// server work was all done while another test ran too.
94fn credit(
95    windows: &[ServerWindow],
96    served: &[ServedTest],
97    probe_count: usize,
98    widths: &[u8],
99) -> (
100    OwnedEvidence,
101    Vec<OwnedTestOutcome>,
102    BTreeSet<usize>,
103    BTreeSet<usize>,
104) {
105    let mut global = vec![0_u32; probe_count];
106    let mut decision_vectors = vec![Vec::<u64>::new(); widths.len()];
107    let mut by_test = BTreeMap::<usize, OwnedTestEvidence>::new();
108    let mut overlapped = BTreeSet::new();
109    for window in windows {
110        let running = served
111            .iter()
112            .enumerate()
113            .filter(|(_, test)| {
114                test.window
115                    .is_some_and(|(start, end)| start <= window.end && window.start <= end)
116            })
117            .map(|(index, _)| index)
118            .collect::<Vec<_>>();
119        let owner = match running.as_slice() {
120            [index] => Some(*index),
121            _ => None,
122        };
123        if running.len() > 1 && !window.probes.is_empty() {
124            overlapped.extend(running);
125        }
126        for pair in window.probes.chunks_exact(2) {
127            let (index, mask) = (pair[0] as usize, pair[1] as u32);
128            if let Some(total) = global.get_mut(index) {
129                *total |= mask;
130            }
131            if let Some(owner) = owner {
132                *by_test
133                    .entry(owner)
134                    .or_default()
135                    .probes
136                    .entry(index as u32)
137                    .or_default() |= mask;
138            }
139        }
140        for pair in window.vectors.chunks_exact(2) {
141            let (decision, key) = (pair[0] as u32, pair[1]);
142            if let Some(seen) = decision_vectors.get_mut(decision as usize)
143                && !seen.contains(&key)
144            {
145                seen.push(key);
146            }
147            if let Some(owner) = owner {
148                let test = by_test.entry(owner).or_default();
149                let vector = PackedVector { decision, key };
150                if !test.vectors.contains(&vector) {
151                    test.vectors.push(vector);
152                }
153            }
154        }
155    }
156    let credited = by_test.keys().copied().collect::<BTreeSet<_>>();
157    let overlapped = overlapped.difference(&credited).copied().collect();
158    let mut tests = Vec::new();
159    let mut outcomes = Vec::new();
160    for (index, test) in served.iter().enumerate() {
161        let status = match test.status.as_str() {
162            "passed" => "passed",
163            "skipped" => "skipped",
164            _ => "failed",
165        };
166        let mut evidence = by_test.remove(&index).unwrap_or_default();
167        evidence.name = test.name.clone();
168        evidence.status = status.into();
169        evidence.runner = test.provenance.runner.clone();
170        tests.push(evidence);
171        outcomes.push(OwnedTestOutcome {
172            name: test.name.clone(),
173            runner: test.provenance.runner.clone(),
174            package: test.worker.clone(),
175            file: Some(test.file.clone()),
176            status: status.into(),
177            attributed: true,
178        });
179    }
180    (
181        OwnedEvidence {
182            global,
183            tests,
184            widths: widths.to_vec(),
185            decision_vectors,
186        },
187        outcomes,
188        credited,
189        overlapped,
190    )
191}
192
193/// Publish what the server ran as a run of its own, credited by time. None
194/// when no instrumented server ran, or no browser test reported.
195pub fn publish(
196    root: &Path,
197    server: OwnedServer,
198    metadata: RunMetadata,
199    served: &[ServedTest],
200    diagnostics: &mut dyn Write,
201) -> Result<Option<(PathBuf, RunMetadata)>, String> {
202    let evidence_directory = server.directory.join("evidence");
203    let result = (|| {
204        let windows = read_windows(&evidence_directory)?;
205        if served.is_empty() || windows.is_empty() {
206            return Ok(None);
207        }
208        let probe_count = server
209            .probes
210            .keys()
211            .max()
212            .map_or(0, |highest| *highest as usize + 1);
213        let (evidence, outcomes, credited, overlapped) =
214            credit(&windows, served, probe_count, server.widths);
215        // The tests came from the browser runner, whichever runner the
216        // frontend declares for its own suites.
217        let mut declaration = server.declaration;
218        declaration.runners.truncate(1);
219        for runner in &mut declaration.runners {
220            runner.runner = "playwright".into();
221            runner.execution_model = supercov_contracts::ExecutionModel::ParallelContextPropagated;
222        }
223        let mut run = build_frontend_run(OwnedRunInputs {
224            declaration,
225            environment: server.environment,
226            manifest: server.manifest,
227            probes: server.probes,
228            evidence: &evidence,
229            outcomes: &outcomes,
230            run_id: &metadata.id,
231            generated_at: &metadata.started_at,
232            test_exit_code: metadata.test_exit_code.unwrap_or_default(),
233            coverage_model: server.coverage_model,
234        })
235        .map_err(|error| error.to_string())?;
236        for (raw, (index, test)) in run
237            .request
238            .raw_results
239            .iter_mut()
240            .zip(served.iter().enumerate())
241        {
242            raw.title = Some(test.title.clone());
243            raw.expected_status = Some(test.expected_status.clone());
244            raw.status = Some(test.status.clone());
245            raw.provenance = crate::coverage_report::TestProvenance {
246                source: raw.provenance.source.clone(),
247                ..test.provenance.clone()
248            };
249            if credited.contains(&index) {
250                raw.attribution = crate::coverage_report::ATTRIBUTION_BY_TIME.into();
251            }
252        }
253        validate_frontend_report_request(&run.declaration, &run.request)
254            .map_err(|error| error.to_string())?;
255        let archive_path = root
256            .join(".supercov/work")
257            .join(&metadata.id)
258            .join(format!("{}-evidence.raw.gz", server.environment));
259        if let Some(parent) = archive_path.parent() {
260            fs::create_dir_all(parent).map_err(|error| error.to_string())?;
261        }
262        let raw = write_archive(
263            crate::source_capture::append(
264                run.archive_entries().map_err(|error| error.to_string())?,
265                server.source_inputs,
266            )?,
267            &archive_path,
268        )
269        .map_err(|error| error.to_string())?;
270        let metadata = RunMetadata {
271            integrity: server.integrity.clone(),
272            raw_evidence: RawEvidenceMetadata {
273                schema_version: raw.schema_version,
274                format: raw.format.into(),
275                file: raw.file.into(),
276                files: raw.files,
277                uncompressed_bytes: raw.uncompressed_bytes,
278                compressed_bytes: raw.compressed_bytes,
279            },
280            source_roots: server.source_roots.clone(),
281            ..metadata
282        };
283        let run_directory =
284            publish_run(root, &metadata, &archive_path).map_err(|error| error.to_string())?;
285        writeln!(
286            diagnostics,
287            "[supercov] {} server coverage: {} test(s) across {} source file(s), credited by time",
288            server.language,
289            served.len(),
290            server.source_files
291        )
292        .map_err(|error| error.to_string())?;
293        // Parallel workers share one server, and its probes cannot tell
294        // their requests apart. Say what it cost, and how to have it all.
295        if !overlapped.is_empty() {
296            writeln!(
297                diagnostics,
298                "[supercov] {} of those test(s) had the server's work only while another test ran too, so it is the run's and none of theirs; with `--workers=1` each test's is its own",
299                overlapped.len()
300            )
301            .map_err(|error| error.to_string())?;
302        }
303        Ok(Some((run_directory, metadata)))
304    })();
305    let result = result.map_err(|error| {
306        let run = server
307            .directory
308            .parent()
309            .and_then(Path::file_name)
310            .map(|name| name.to_string_lossy().into_owned())
311            .unwrap_or_default();
312        note_kept_evidence(
313            root,
314            &evidence_directory,
315            &format!("{run}-{}", server.environment),
316            error,
317        )
318    });
319    let _ = remove_stored_tree_deferred(root, server.directory);
320    result
321}