1use 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#[derive(Deserialize)]
33struct ServerWindow {
34 start: i64,
35 end: i64,
36 probes: Vec<u64>,
38 vectors: Vec<u64>,
40}
41
42pub struct OwnedServer<'a> {
44 pub language: &'static str,
46 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 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 windows.extend(
83 text.lines()
84 .filter_map(|line| serde_json::from_str::<ServerWindow>(line).ok()),
85 );
86 }
87 Ok(windows)
88}
89
90fn 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
193pub 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 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 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}