1use std::{
4 collections::BTreeMap,
5 ffi::OsString,
6 path::{Path, PathBuf},
7 time::{Instant, SystemTime, UNIX_EPOCH},
8};
9
10use serde::{Deserialize, Serialize};
11use supercov_contracts::{
12 AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
13 FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
14 LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
15};
16
17use crate::{
18 coverage_report::{PersistedCoverageModel, RawTestResult, javascript_coverage_model},
19 evidence_archive::{
20 EvidenceArchiveEntry, EvidenceArchiveSource, collect_sources, write_archive,
21 },
22 integrity::{FrontendIntegrityInputs, create_run_integrity},
23 javascript_frontend::{
24 javascript_frontend_reuse_paths, load_cached_javascript_frontend,
25 prepare_javascript_frontend, read_javascript_frontend_cache,
26 },
27 lifecycle::{
28 ProjectLock, RunState, RunStateStatus, finalize_published_run, interrupt_run_state,
29 publish_run, recover_abandoned_runs, remove_stored_tree_deferred, update_run_state,
30 write_run_state,
31 },
32 orchestration::{
33 ExecutionPhase, ExecutionPlan, OrchestrationError, PhaseKind, execute_plan_with_supervisor,
34 },
35 process_supervision::{
36 CommandSpec, ForwardedSignal, ProcessSupervisor, SupervisionOptions, positive_milliseconds,
37 },
38 project_discovery::discover_coverage_project,
39 run_store::{RawEvidenceMetadata, RunIntegrity, RunMetadata, RunTimings},
40 workspace::{
41 cached_workspace_path, prepare_cached_workspace, prune_cached_workspace_sources,
42 sync_command_outputs, workspace_output_baseline,
43 },
44};
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48pub struct DirectJavascriptRunRequest {
49 pub root: PathBuf,
50 pub command: Vec<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub run_id: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub started_at: Option<String>,
55 #[serde(skip)]
56 pub watchdog_program: Option<PathBuf>,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase", deny_unknown_fields)]
61pub struct DirectJavascriptRunResult {
62 pub run_id: String,
63 pub run_directory: PathBuf,
64 pub workspace: PathBuf,
65 pub exit_code: i32,
66 pub assertion_calls: usize,
67 pub recovered_runs: Vec<String>,
68 pub metadata: RunMetadata,
69 pub server_runs: Vec<(String, PathBuf)>,
72 #[serde(skip)]
76 pub unrun_build: Vec<String>,
77}
78
79#[derive(Debug)]
80pub enum DirectJavascriptRunError {
81 Interrupted {
82 signal: ForwardedSignal,
83 exit_code: i32,
84 timings: RunTimings,
85 total_ms: f64,
86 },
87 Failed(String),
88}
89
90impl std::fmt::Display for DirectJavascriptRunError {
91 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::Interrupted { signal, .. } => {
94 write!(formatter, "interrupted by {}", signal_name(*signal))
95 }
96 Self::Failed(message) => formatter.write_str(message),
97 }
98 }
99}
100
101impl std::error::Error for DirectJavascriptRunError {}
102
103impl From<String> for DirectJavascriptRunError {
104 fn from(value: String) -> Self {
105 Self::Failed(value)
106 }
107}
108
109fn signal_name(signal: ForwardedSignal) -> &'static str {
110 match signal {
111 ForwardedSignal::Sighup => "SIGHUP",
112 ForwardedSignal::Sigint => "SIGINT",
113 ForwardedSignal::Sigterm => "SIGTERM",
114 }
115}
116
117fn javascript_runner_declaration(
118 runner: String,
119 results: &[&RawTestResult],
120) -> FrontendRunnerDeclaration {
121 let has_observations = |raw: &&RawTestResult| {
122 !raw.phases.is_empty()
123 || !raw.server.is_empty()
124 || raw.runtime.iter().chain(&raw.browser).any(|snapshot| {
125 !snapshot.decisions.is_empty()
126 || !snapshot.hits.is_empty()
127 || !snapshot.events.is_empty()
128 })
129 };
130 let exact_test = results.iter().all(|raw| {
131 raw.test_id
132 .as_deref()
133 .is_some_and(|value| !value.is_empty())
134 && raw
135 .scope
136 .as_ref()
137 .is_none_or(|scope| raw.test_id.as_deref() == Some(scope.test_id.as_str()))
138 });
139 let exact_worker = results.iter().all(|raw| {
140 !has_observations(raw)
141 || raw
142 .scope
143 .as_ref()
144 .is_some_and(|scope| !scope.worker_id.is_empty())
145 });
146 let exact_retry = results.iter().all(|raw| {
147 raw.retry.is_some()
148 && raw
149 .scope
150 .as_ref()
151 .is_none_or(|scope| raw.retry == Some(scope.retry))
152 });
153 let contextual = !results.is_empty() && exact_test && exact_worker && exact_retry;
154 let precision = |exact| {
155 if exact {
156 AttributionPrecision::Exact
157 } else {
158 AttributionPrecision::Unavailable
159 }
160 };
161 let attribution = FrontendAttribution {
162 run: AttributionPrecision::Exact,
163 worker: precision(contextual),
164 test: precision(contextual),
165 retry: precision(contextual),
166 phase: precision(contextual),
167 action: precision(contextual),
168 assertion: precision(contextual),
169 };
170 let mut limitations = Vec::new();
171 if !contextual {
172 for (suffix, scope) in [
173 ("worker", FrontendLimitationScope::Worker),
174 ("test", FrontendLimitationScope::Test),
175 ("retry", FrontendLimitationScope::Retry),
176 ("phase", FrontendLimitationScope::Phase),
177 ("action", FrontendLimitationScope::Action),
178 ("assertion", FrontendLimitationScope::Assertion),
179 ] {
180 limitations.push(FrontendLimitation {
181 id: format!("{runner}-no-{suffix}").replace(':', "-"),
182 scopes: vec![scope],
183 reason: format!(
184 "Runner {runner} did not expose exact {suffix} identity for every result"
185 ),
186 });
187 }
188 }
189 FrontendRunnerDeclaration {
190 runner,
191 execution_model: if contextual {
192 ExecutionModel::ParallelContextPropagated
193 } else {
194 ExecutionModel::ParallelUnattributed
195 },
196 attribution,
197 limitations,
198 }
199}
200
201fn javascript_archive_entries(
202 mut entries: Vec<EvidenceArchiveEntry>,
203 manifest: &crate::javascript_frontend::JavascriptManifest,
204 run_id: &str,
205 exit_code: i32,
206) -> Result<Vec<EvidenceArchiveEntry>, String> {
207 let mut results = Vec::new();
208 for entry in &entries {
209 if entry.path == "mcdc.json" || entry.path.ends_with("/mcdc.json") {
210 results.push(
211 serde_json::from_slice::<RawTestResult>(&entry.contents)
212 .map_err(|error| format!("invalid {}: {error}", entry.path))?,
213 );
214 } else if entry.path == "mcdc.jsonl" || entry.path.ends_with(".mcdc.jsonl") {
215 let contents = entry
216 .contents
217 .strip_suffix(b"\n")
218 .ok_or_else(|| format!("{} does not end with a newline", entry.path))?;
219 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
220 if line.is_empty() {
221 return Err(format!(
222 "{} contains a blank record at line {}",
223 entry.path,
224 index + 1
225 ));
226 }
227 results.push(
228 serde_json::from_slice::<RawTestResult>(line).map_err(|error| {
229 format!(
230 "invalid {} record at line {}: {error}",
231 entry.path,
232 index + 1
233 )
234 })?,
235 );
236 }
237 }
238 }
239 if results.is_empty() {
240 let status = match exit_code {
241 0 => "passed",
242 supercov_contracts::COMMAND_TIMEOUT_EXIT_CODE => "timedOut",
243 _ => "failed",
244 };
245 let command_result = RawTestResult {
246 span: None,
247 test_id: Some(format!("command:{run_id}")),
248 scope: None,
249 test: "Test command".into(),
250 test_file: None,
251 title: Some("Test command".into()),
252 retry: Some(0),
253 status: Some(status.into()),
254 expected_status: None,
255 flaky: false,
256 provenance: crate::coverage_report::TestProvenance {
257 runner: "command".into(),
258 kind: "setup".into(),
259 project: None,
260 source: "engine".into(),
261 tags: Vec::new(),
262 },
263 role: "setup".into(),
264 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
265 phases: vec![],
266 runtime: vec![],
267 browser: vec![],
268 server: vec![],
269 };
270 entries.push(EvidenceArchiveEntry {
271 path: "results/command/mcdc.json".into(),
272 contents: serde_json::to_vec(&command_result).map_err(|error| error.to_string())?,
273 });
274 results.push(command_result);
275 }
276 let mut by_runner = BTreeMap::<String, Vec<&RawTestResult>>::new();
277 for result in &results {
278 by_runner
279 .entry(result.provenance.runner.clone())
280 .or_default()
281 .push(result);
282 }
283 if entries
284 .iter()
285 .any(|entry| entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl"))
286 {
287 by_runner.entry("background".into()).or_default();
288 }
289 let declaration = FrontendRunDeclaration {
290 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
291 frontend_id: "javascript".into(),
292 frontend_version: "javascript-owned-v1".into(),
293 language: "javascript".into(),
294 structural_source: StructuralSource::OwnedProbes,
295 runners: by_runner
296 .into_iter()
297 .map(|(runner, results)| javascript_runner_declaration(runner, &results))
298 .collect(),
299 structural_limitations: manifest
300 .limitations
301 .iter()
302 .map(|limitation| limitation.id.clone())
303 .collect(),
304 };
305 entries.push(EvidenceArchiveEntry {
306 path: "coverage-model.json".into(),
307 contents: serde_json::to_vec(
308 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
309 .expect("JavaScript coverage model is contract-valid"),
310 )
311 .map_err(|error| error.to_string())?,
312 });
313 entries.push(EvidenceArchiveEntry {
314 path: "frontend.json".into(),
315 contents: serde_json::to_vec(&declaration).map_err(|error| error.to_string())?,
316 });
317 Ok(entries)
318}
319
320struct RunCleanup {
321 root: PathBuf,
322 run_id: String,
323 started_at: String,
324 lock: ProjectLock,
325 workspace: Option<PathBuf>,
326 state_written: bool,
327 terminal_recorded: bool,
328 keep_workspace: bool,
331}
332
333impl RunCleanup {
334 fn lock(&self) -> &ProjectLock {
335 &self.lock
336 }
337
338 fn set_workspace(&mut self, workspace: PathBuf) {
339 self.workspace = Some(workspace);
340 }
341
342 fn mark_state_written(&mut self) {
343 self.state_written = true;
344 }
345
346 fn mark_terminal(&mut self) {
347 self.terminal_recorded = true;
348 }
349}
350
351impl Drop for RunCleanup {
352 fn drop(&mut self) {
353 if self.state_written && !self.terminal_recorded {
354 let _ = update_run_state(
355 &self.root,
356 &self.run_id,
357 RunStateStatus::Failed,
358 &self.started_at,
359 Some("Rust run exited before reaching a terminal lifecycle state".into()),
360 );
361 }
362 if let Some(workspace) = &self.workspace {
363 let _ = remove_stored_tree_deferred(
364 &self.root,
365 &workspace.join(".supercov/evidence").join(&self.run_id),
366 );
367 let _ = remove_stored_tree_deferred(
368 &self.root,
369 &workspace
370 .join(".supercov/server-evidence")
371 .join(&self.run_id),
372 );
373 let keep_workspace = self.keep_workspace
374 || std::env::var("SUPERCOV_KEEP_WORKSPACE").is_ok_and(|value| !value.is_empty());
375 if !keep_workspace {
376 let _ = prune_cached_workspace_sources(&self.root, &self.lock);
377 }
378 }
379 let _ = self.lock.release();
380 }
381}
382
383fn now_nonce() -> u128 {
384 SystemTime::now()
385 .duration_since(UNIX_EPOCH)
386 .unwrap_or_default()
387 .as_millis()
388}
389
390fn elapsed_ms(started: Instant) -> f64 {
391 started.elapsed().as_secs_f64() * 1000.0
392}
393
394fn rounded_millisecond(value: f64) -> f64 {
395 (value * 10.0).round() / 10.0
396}
397
398fn supervision_options() -> Result<SupervisionOptions, String> {
399 let defaults = SupervisionOptions::default();
400 Ok(SupervisionOptions {
401 diagnostic_interval: positive_milliseconds(
402 std::env::var("SUPERCOV_DIAGNOSTIC_INTERVAL_MS")
403 .ok()
404 .as_deref(),
405 "SUPERCOV_DIAGNOSTIC_INTERVAL_MS",
406 )
407 .map_err(|error| error.to_string())?
408 .unwrap_or(defaults.diagnostic_interval),
409 timeout: positive_milliseconds(
410 std::env::var("SUPERCOV_COMMAND_TIMEOUT_MS").ok().as_deref(),
411 "SUPERCOV_COMMAND_TIMEOUT_MS",
412 )
413 .map_err(|error| error.to_string())?,
414 termination_grace: defaults.termination_grace,
415 })
416}
417
418fn remove_derived_pnpm_config(environment: &mut BTreeMap<OsString, OsString>) {
419 const PNPM_ONLY_NPM_CONFIG: &[&str] = &[
425 "npm_config_auto_install_peers",
426 "npm_config_enable_pre_post_scripts",
427 "npm_config_shamefully_hoist",
428 ];
429 environment.retain(|key, _| {
430 let key = key.to_string_lossy().to_ascii_lowercase();
431 !PNPM_ONLY_NPM_CONFIG.contains(&key.as_str())
432 });
433}
434
435fn environment_with(values: BTreeMap<String, String>) -> Vec<(OsString, OsString)> {
436 let mut environment = std::env::vars_os().collect::<BTreeMap<_, _>>();
437 remove_derived_pnpm_config(&mut environment);
438 for (key, value) in values {
439 environment.retain(|existing, _| !existing.to_string_lossy().eq_ignore_ascii_case(&key));
440 environment.insert(key.into(), value.into());
441 }
442 environment.into_iter().collect()
443}
444
445const PLAYWRIGHT_CACHE_VARIABLE: &str = "PWTEST_CACHE_DIR";
447
448pub fn playwright_transform_cache(root: &Path) -> PathBuf {
450 crate::workspace::workspace_container(root).join("caches/playwright-transform")
451}
452
453fn node_options(preload: &Path) -> String {
454 let preload = std::path::absolute(preload).unwrap_or_else(|_| preload.to_path_buf());
460 [
466 std::env::var("NODE_OPTIONS").ok(),
467 Some("--enable-source-maps".into()),
468 Some(format!("--import={}", crate::workspace::file_url(&preload))),
469 ]
470 .into_iter()
471 .flatten()
472 .filter(|value| !value.is_empty())
473 .collect::<Vec<_>>()
474 .join(" ")
475}
476
477pub fn current_javascript_integrity(
481 root: &Path,
482 command: &[String],
483 source_roots: Option<&[String]>,
484) -> Result<RunIntegrity, String> {
485 let mut environment = std::env::vars().collect::<BTreeMap<_, _>>();
486 match source_roots.filter(|roots| !roots.is_empty()) {
491 Some(roots) => {
492 environment.insert(
493 crate::source_discovery::SOURCE_ROOTS_VARIABLE.into(),
494 roots.join(","),
495 );
496 }
497 None => {
498 environment.remove(crate::source_discovery::SOURCE_ROOTS_VARIABLE);
499 }
500 }
501 let project = discover_coverage_project(root, &environment, command)
502 .map_err(|error| error.to_string())?;
503 javascript_integrity_for_project(root, &project)
504}
505
506fn javascript_integrity_for_project(
507 root: &Path,
508 project: &crate::project_discovery::CoverageProject,
509) -> Result<RunIntegrity, String> {
510 let frontend = FrontendIntegrityInputs::embedded_javascript();
511 create_run_integrity(root, project, &frontend).map_err(|error| error.to_string())
512}
513
514pub fn run_direct_javascript(
517 request: &DirectJavascriptRunRequest,
518 diagnostics: &mut dyn std::io::Write,
519) -> Result<DirectJavascriptRunResult, DirectJavascriptRunError> {
520 if request.command.is_empty() {
521 return Err(DirectJavascriptRunError::Failed(
522 "test command must not be empty".into(),
523 ));
524 }
525 let total_started = Instant::now();
526 let initialization_started = Instant::now();
527 let root = crate::workspace::canonicalize_simplified(&request.root)
528 .map_err(|error| format!("{}: {error}", request.root.display()))?;
529 let nonce = now_nonce();
530 let run_id = request
531 .run_id
532 .clone()
533 .unwrap_or_else(|| format!("rust-{nonce}"));
534 let started_at = request
535 .started_at
536 .clone()
537 .unwrap_or_else(|| format!("unix-ms-{nonce}"));
538 let lock =
539 ProjectLock::acquire(&root, &run_id, &started_at).map_err(|error| error.to_string())?;
540 let mut cleanup = RunCleanup {
541 root: root.clone(),
542 run_id: run_id.clone(),
543 started_at: started_at.clone(),
544 lock,
545 workspace: None,
546 state_written: false,
547 terminal_recorded: false,
548 keep_workspace: false,
549 };
550 let recovered_runs =
551 recover_abandoned_runs(&root, &started_at).map_err(|error| error.to_string())?;
552 if !recovered_runs.is_empty() {
553 writeln!(
554 diagnostics,
555 "[supercov] recovered abandoned run(s): {}",
556 recovered_runs.join(", ")
557 )
558 .map_err(|error| error.to_string())?;
559 }
560 let environment = std::env::vars().collect::<BTreeMap<_, _>>();
561 let source_roots = crate::source_discovery::configured_source_roots(&environment);
562 let project = crate::project_discovery::discover_coverage_project_of_any_size(
563 &root,
564 &environment,
565 &request.command,
566 )
567 .map_err(|error| error.to_string())?;
568 let browser_suite = project.playwright_config.is_some();
573 let serves_python = browser_suite
574 && crate::python_project::discover_python_files(&root)
575 .is_ok_and(|files| !files.sources.is_empty());
576 let serves_ruby = browser_suite
577 && crate::ruby_project::discover_ruby_files(&root)
578 .is_ok_and(|files| !files.sources.is_empty());
579 let serves_go = browser_suite
580 && (root.join("go.mod").is_file()
581 || !crate::go_project::workspace_modules(&root).is_empty());
582 let serves_rust_worker = browser_suite && crate::rust_worker::RustWorkerServer::present(&root);
583 let serves_rust_wasm = crate::rust_wasm::RustWasmServer::present(&root);
585 let serves_jvm = browser_suite
586 && [
587 "pom.xml",
588 "build.gradle",
589 "build.gradle.kts",
590 "src/main/java",
591 "src/main/kotlin",
592 ]
593 .iter()
594 .any(|name| root.join(name).exists());
595 if project.source_files.is_empty()
596 && !serves_python
597 && !serves_ruby
598 && !serves_go
599 && !serves_jvm
600 && !serves_rust_worker
601 && !serves_rust_wasm
602 {
603 return Err(
604 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
605 .to_string()
606 .into(),
607 );
608 }
609 let integrity = javascript_integrity_for_project(&root, &project)?;
610 let source_inputs = crate::source_capture::capture(
611 &root,
612 "javascript",
613 crate::integrity::javascript_assertion_paths(&root, &project).map_err(|e| e.to_string())?,
614 )?;
615 let frontend_cache_key = format!(
621 "{}:{}:{}:{}:{}",
622 integrity.fingerprint.combined,
623 integrity.fingerprint.execution,
624 crate::source_manifest::digest(&request.command),
625 crate::source_manifest::digest(&project.root.to_string_lossy()),
626 crate::source_manifest::digest(&(&project.source_files, &source_roots))
627 );
628 let prior_workspace =
629 cached_workspace_path(&root, cleanup.lock()).map_err(|error| error.to_string())?;
630 let reusable_frontend = read_javascript_frontend_cache(&prior_workspace, &frontend_cache_key);
631 let cached_paths = reusable_frontend
632 .as_ref()
633 .map(javascript_frontend_reuse_paths)
634 .unwrap_or_default();
635 let initialization_ms = elapsed_ms(initialization_started);
636 let prepare_servers = |workspace: PathBuf| {
640 let (root, run_id, command) = (root.clone(), run_id.clone(), request.command.clone());
641 move || -> Result<Vec<Server>, String> {
642 let mut servers = Vec::new();
643 if serves_python
644 && let Some(server) =
645 crate::python_run::PythonServer::prepare(&root, &workspace, &run_id, &command)?
646 {
647 servers.push(Server::Python(server));
648 }
649 if serves_ruby
650 && let Some(server) =
651 crate::ruby_run::RubyServer::prepare(&root, &workspace, &run_id, &command)?
652 {
653 servers.push(Server::Ruby(server));
654 }
655 Ok(servers)
656 }
657 };
658 let preparing_servers = std::thread::spawn(prepare_servers(prior_workspace.clone()));
659
660 let workspace_started = Instant::now();
661 let workspace_progress = crate::progress::ProgressLine::start("preparing isolated workspace");
662 let workspace = prepare_cached_workspace(&root, cleanup.lock(), &cached_paths)
663 .map_err(|error| error.to_string())?;
664 drop(workspace_progress);
665 cleanup.set_workspace(workspace.clone());
666 crate::lifecycle::sweep_trash_in_background(&root);
668 if let Err(error) = crate::workspace::mirror_git_repository(&root, &workspace)
669 && std::env::var_os("SUPERCOV_DEBUG").is_some()
670 {
671 eprintln!("[supercov] the workspace has no git repository of its own: {error}");
672 }
673 let workspace_preparation_ms = elapsed_ms(workspace_started);
674 writeln!(
675 diagnostics,
676 "[supercov] instrumenting isolated workspace {}",
677 workspace.display()
678 )
679 .map_err(|error| error.to_string())?;
680 cleanup.mark_state_written();
681 write_run_state(
682 &root,
683 &RunState {
684 id: run_id.clone(),
685 pid: std::process::id(),
686 root: root.display().to_string(),
687 workspace: workspace.display().to_string(),
688 started_at: started_at.clone(),
689 updated_at: started_at.clone(),
690 status: RunStateStatus::Preparing,
691 signal: None,
692 error: None,
693 },
694 )
695 .map_err(|error| error.to_string())?;
696
697 let adapter_started = Instant::now();
698 let instrumentation_progress = crate::progress::ProgressLine::start("instrumenting sources");
699 let collector_id = format!("collector-{}", integrity.fingerprint.execution);
700 let frontend = if let Some(cache) = &reusable_frontend {
701 load_cached_javascript_frontend(&workspace, cache)
702 } else {
703 prepare_javascript_frontend(
704 &workspace,
705 &project,
706 &collector_id,
707 &frontend_cache_key,
708 &request.command,
709 )
710 }
711 .map_err(|error| error.to_string())?;
712 crate::workspace::hide_rewritten_files(
713 &workspace,
714 &crate::javascript_frontend::rewritten_files(&workspace),
715 );
716 drop(instrumentation_progress);
717 let adapter_setup_ms = elapsed_ms(adapter_started);
718 if let Some(detail) = crate::javascript_frontend::setup_timing_detail() {
719 writeln!(diagnostics, "[supercov] {detail}").map_err(|error| error.to_string())?;
720 }
721
722 let mut servers = preparing_servers
723 .join()
724 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))?;
725 if workspace != prior_workspace && !servers.is_empty() {
728 servers = prepare_servers(workspace.clone())()?;
729 }
730 if serves_go
732 && let Some(server) =
733 crate::go_run::GoServer::prepare(&root, &workspace, &run_id, &request.command)?
734 {
735 servers.push(Server::Go(server));
736 }
737 if serves_jvm
738 && let Some(server) =
739 crate::jvm_run::JvmServer::prepare(&root, &workspace, &run_id, &request.command)?
740 {
741 servers.push(Server::Jvm(server));
742 }
743 if serves_rust_worker {
746 servers.extend(
747 crate::rust_worker::RustWorkerServer::prepare(
748 &root,
749 &workspace,
750 &run_id,
751 &request.command,
752 diagnostics,
753 )?
754 .into_iter()
755 .map(Server::RustWorker),
756 );
757 }
758 if serves_rust_wasm {
759 servers.extend(
760 crate::rust_wasm::RustWasmServer::prepare(
761 &root,
762 &workspace,
763 &workspace.join(".supercov/server-evidence"),
764 &run_id,
765 &request.command,
766 )?
767 .into_iter()
768 .map(Server::RustWasm),
769 );
770 }
771 if project.source_files.is_empty() && servers.is_empty() {
772 return Err(
773 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
774 .to_string()
775 .into(),
776 );
777 }
778 for server in &servers {
779 writeln!(
780 diagnostics,
781 "[supercov] measuring {} {language} source file(s) in any {language} server the tests start",
782 server.source_files(),
783 language = server.language(),
784 )
785 .map_err(|error| error.to_string())?;
786 }
787
788 let evidence_relative = format!(".supercov/evidence/{run_id}");
789 let evidence_directory = workspace.join(&evidence_relative);
790 let server_evidence_root = workspace.join(".supercov/server-evidence");
791 let diagnostic_owner = workspace.join(format!(".supercov/diagnostic-owner-{run_id}"));
792 let mut overrides: BTreeMap<String, String> = BTreeMap::from([
793 ("NODE_OPTIONS".into(), node_options(&frontend.preload_path)),
794 ("SUPERCOV_CJS_INTERCEPT".into(), "1".into()),
795 ("SUPERCOV_DIRECT_INSTRUMENTATION".into(), "1".into()),
796 ("TAP_ALLOW_INCOMPLETE_COVERAGE".into(), "1".into()),
801 (
805 "SUPERCOV_EVIDENCE_DIR".into(),
806 evidence_directory.display().to_string(),
807 ),
808 (
809 "SUPERCOV_DIAGNOSTIC_OWNER_FILE".into(),
810 diagnostic_owner.display().to_string(),
811 ),
812 (
813 "SUPERCOV_EXECUTION_FINGERPRINT".into(),
814 integrity.fingerprint.execution.clone(),
815 ),
816 (
817 "SUPERCOV_EXECUTION_LOG".into(),
818 evidence_directory
819 .join("execution.jsonl")
820 .display()
821 .to_string(),
822 ),
823 (
824 "SUPERCOV_MANIFEST".into(),
825 frontend.manifest_path.display().to_string(),
826 ),
827 (
828 "SUPERCOV_PROJECT_ROOT".into(),
829 workspace.display().to_string(),
830 ),
831 ("SUPERCOV_RUN_ID".into(), run_id.clone()),
832 (
833 "SUPERCOV_SERVER_EVIDENCE_ROOT".into(),
834 server_evidence_root.display().to_string(),
835 ),
836 (
837 "SUPERCOV_SOURCE_PROJECT_ROOT".into(),
838 root.display().to_string(),
839 ),
840 ]);
841 if std::env::var_os(PLAYWRIGHT_CACHE_VARIABLE).is_none_or(|value| value.is_empty()) {
851 overrides.insert(
852 PLAYWRIGHT_CACHE_VARIABLE.into(),
853 playwright_transform_cache(&root).display().to_string(),
854 );
855 }
856 overrides.insert(
860 "SUPERCOV_GENERATED_VITEST_CONFIG".into(),
861 frontend.vitest_config_path.display().to_string(),
862 );
863 overrides.insert(
864 "SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG".into(),
865 frontend.playwright_config_path.display().to_string(),
866 );
867 overrides.insert(
868 "SUPERCOV_PLAYWRIGHT_MODULE".into(),
869 project.playwright_module.clone(),
870 );
871 overrides.insert(
872 "SUPERCOV_PLAYWRIGHT_TEST_EXPORT".into(),
873 project.playwright_test_export.clone(),
874 );
875 overrides.insert(
876 "SUPERCOV_PLAYWRIGHT_WRAPPER".into(),
877 "./.supercov/node_modules/playwright.mjs".into(),
878 );
879 if let Some(original) = project
880 .playwright_config
881 .as_ref()
882 .and_then(|path| path.strip_prefix(&root).ok())
883 .map(|path| workspace.join(path))
884 {
885 overrides.insert(
886 "SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG".into(),
887 original.display().to_string(),
888 );
889 }
890 for server in &servers {
891 overrides.extend(server.environment());
892 }
893 if crate::workspace::links_a_virtual_environment(&workspace)
899 && std::env::var_os("UV_NO_SYNC").is_none()
900 {
901 overrides.insert("UV_NO_SYNC".into(), "1".into());
902 }
903 let support_directory = workspace.join(".supercov/support").join(&run_id);
907 if project.playwright_config.is_some() {
908 overrides.insert(
909 crate::support_reach::DIRECTORY_VARIABLE.into(),
910 support_directory.display().to_string(),
911 );
912 }
913 if let Some(path) = crate::source_tools::path_with_launchers(&workspace)
916 && let Some(path) = path.to_str()
917 {
918 overrides.insert("PATH".into(), path.to_owned());
919 }
920 #[cfg(unix)]
924 let docker_state = root.join(".supercov/work").join(&run_id).join("docker");
925 #[cfg(unix)]
926 let mut docker_proxy = if !project.source_files.is_empty() || !servers.is_empty() {
927 start_docker_relay(
928 &root,
929 &workspace,
930 &docker_state,
931 &servers,
932 &environment,
933 &mut overrides,
934 diagnostics,
935 )?
936 } else {
937 None
938 };
939 let settings = serde_json::json!({
943 "root": workspace.display().to_string(),
944 "environment": overrides
945 .iter()
946 .filter(|(name, _)| name.starts_with("SUPERCOV_"))
947 .collect::<BTreeMap<_, _>>(),
948 });
949 let settings_path = frontend.preload_path.with_file_name("run.json");
950 std::fs::write(
951 &settings_path,
952 serde_json::to_vec(&settings).map_err(|error| error.to_string())?,
953 )
954 .map_err(|error| format!("{}: {error}", settings_path.display()))?;
955 let _ = crate::source_tools::clear_source_changes(&workspace);
958 let plan = ExecutionPlan {
960 preparation: Vec::new(),
961 test: ExecutionPhase {
962 name: "test".into(),
963 kind: PhaseKind::Test,
964 command: CommandSpec {
965 program: request.command[0].clone().into(),
966 arguments: request.command[1..].iter().map(OsString::from).collect(),
967 cwd: workspace.clone(),
968 environment: Some(environment_with(overrides)),
969 captured_output: None,
970 console: None,
971 },
972 },
973 };
974 let options = supervision_options()?;
975 let watchdog_program = request.watchdog_program.as_ref().ok_or_else(|| {
976 DirectJavascriptRunError::Failed(
977 "the JavaScript run is missing its crash-containment executable".into(),
978 )
979 })?;
980 let supervisor = ProcessSupervisor::new_crash_safe(watchdog_program)
981 .map_err(|error| DirectJavascriptRunError::Failed(error.to_string()))?;
982 let output_baseline = std::cell::RefCell::new(None);
983 let execution = match execute_plan_with_supervisor(
984 &supervisor,
985 &plan,
986 options,
987 diagnostics,
988 |phase, diagnostics| {
989 {
990 *output_baseline.borrow_mut() =
994 Some(workspace_output_baseline(&workspace).map_err(|error| {
995 OrchestrationError::PhaseSetup {
996 phase: phase.name.clone(),
997 reason: error.to_string(),
998 }
999 })?);
1000 if frontend.assertion_calls > 0 {
1001 writeln!(
1002 diagnostics,
1003 "[supercov] attributed {} native node:assert call(s)",
1004 frontend.assertion_calls
1005 )
1006 .map_err(|error| OrchestrationError::PhaseSetup {
1007 phase: phase.name.clone(),
1008 reason: error.to_string(),
1009 })?;
1010 }
1011 writeln!(
1012 diagnostics,
1013 "[supercov] running in isolated workspace: {}",
1014 request.command.join(" ")
1015 )
1016 .map_err(|error| OrchestrationError::PhaseSetup {
1017 phase: phase.name.clone(),
1018 reason: error.to_string(),
1019 })?;
1020 }
1021 update_run_state(&root, &run_id, RunStateStatus::Testing, &started_at, None).map_err(
1022 |error| OrchestrationError::PhaseSetup {
1023 phase: phase.name.clone(),
1024 reason: error.to_string(),
1025 },
1026 )?;
1027 Ok(())
1028 },
1029 ) {
1030 Ok(execution) => execution,
1031 Err(error) => {
1032 let message = error.to_string();
1033 let state_updated = update_run_state(
1034 &root,
1035 &run_id,
1036 RunStateStatus::Failed,
1037 &started_at,
1038 Some(message.clone()),
1039 )
1040 .is_ok();
1041 if state_updated {
1042 cleanup.mark_terminal();
1043 }
1044 return Err(message.into());
1045 }
1046 };
1047 #[cfg(unix)]
1049 if let Some(proxy) = docker_proxy.take() {
1050 proxy.finish();
1051 for note in crate::docker_proxy::notes(&docker_state.join("containers.jsonl")) {
1052 writeln!(diagnostics, "[supercov] {note}").map_err(|error| error.to_string())?;
1053 }
1054 }
1055 let test_command_ms = execution
1056 .phases
1057 .iter()
1058 .find(|phase| phase.kind == PhaseKind::Test)
1059 .map_or(0.0, |phase| phase.duration_ms as f64);
1060 if let Some(signal) = execution.interrupted_signal {
1061 interrupt_run_state(&root, &run_id, &started_at, signal_name(signal))
1062 .map_err(|error| error.to_string())?;
1063 cleanup.mark_terminal();
1064 return Err(DirectJavascriptRunError::Interrupted {
1065 signal,
1066 exit_code: execution.exit_code,
1067 timings: RunTimings {
1068 initialization_ms: rounded_millisecond(initialization_ms),
1069 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1070 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1071 instrumented_build_ms: 0.0,
1072 test_command_ms: rounded_millisecond(test_command_ms),
1073 evidence_publication_ms: 0.0,
1074 },
1075 total_ms: rounded_millisecond(elapsed_ms(total_started)),
1076 });
1077 }
1078 if let Some(baseline) = output_baseline.borrow().as_ref() {
1083 let protected = project
1084 .source_files
1085 .iter()
1086 .map(PathBuf::from)
1087 .collect::<std::collections::BTreeSet<_>>();
1088 let outputs = sync_command_outputs(&root, &workspace, baseline, &protected)
1089 .map_err(|error| error.to_string())?;
1090 if outputs.synced > 0 {
1091 let _ = writeln!(
1092 diagnostics,
1093 "[supercov] synced {} file(s) the command created or changed back to the project: {}",
1094 outputs.synced,
1095 crate::workspace::summarize_paths(&outputs.synced_paths)
1096 );
1097 }
1098 if !outputs.skipped_instrumented.is_empty() {
1099 let _ = writeln!(
1100 diagnostics,
1101 "[supercov] {} file(s) stayed in the isolated workspace: they are instrumented copies, or were built from them, and must not overwrite your project: {}",
1102 outputs.skipped_instrumented.len(),
1103 crate::workspace::summarize_paths(&outputs.skipped_instrumented)
1104 );
1105 }
1106 match crate::source_tools::apply_source_changes(&root, &workspace) {
1111 Ok(changes) => {
1112 if !changes.applied.is_empty() {
1113 let _ = writeln!(
1114 diagnostics,
1115 "[supercov] the command's tools changed {} source file(s), now changed in the project too: {}; this run measured them as they were before",
1116 changes.applied.len(),
1117 crate::workspace::summarize_paths(&changes.applied)
1118 );
1119 }
1120 if !changes.kept_back.is_empty() {
1121 let _ = writeln!(
1122 diagnostics,
1123 "[supercov] the command's tools changed {} source file(s) that were edited in the project while it ran; those changes were not applied: {}",
1124 changes.kept_back.len(),
1125 crate::workspace::summarize_paths(&changes.kept_back)
1126 );
1127 }
1128 }
1129 Err(error) => {
1130 let _ = writeln!(
1131 diagnostics,
1132 "[supercov] the source changes the command's tools made could not be applied to the project: {error}"
1133 );
1134 }
1135 }
1136 if !outputs.deleted_in_workspace.is_empty() {
1137 let _ = writeln!(
1138 diagnostics,
1139 "[supercov] the command deleted {} file(s) in the isolated workspace; deletions are not propagated to the project: {}",
1140 outputs.deleted_in_workspace.len(),
1141 crate::workspace::summarize_paths(&outputs.deleted_in_workspace)
1142 );
1143 }
1144 }
1145 update_run_state(
1146 &root,
1147 &run_id,
1148 RunStateStatus::Publishing,
1149 &started_at,
1150 None,
1151 )
1152 .map_err(|error| error.to_string())?;
1153
1154 let publication_started = Instant::now();
1155 let served = if servers.is_empty() && !support_directory.is_dir() {
1157 Vec::new()
1158 } else {
1159 crate::served_tests::read(&evidence_directory)
1160 };
1161 let support = crate::support_reach::read(&support_directory, &workspace, &served);
1162 if project.source_files.is_empty() {
1163 let metadata = RunMetadata {
1166 id: run_id.clone(),
1167 started_at: started_at.clone(),
1168 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1169 command: request.command.clone(),
1170 test_exit_code: Some(execution.exit_code),
1171 integrity: integrity.clone(),
1172 raw_evidence: RawEvidenceMetadata::default(),
1173 isolated_build: Some(true),
1174 instrumented_build_cache: None,
1175 timings: Some(RunTimings {
1176 initialization_ms: rounded_millisecond(initialization_ms),
1177 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1178 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1179 instrumented_build_ms: 0.0,
1180 test_command_ms: rounded_millisecond(test_command_ms),
1181 evidence_publication_ms: 0.0,
1182 }),
1183 merged: None,
1184 parents: None,
1185 source_roots: source_roots.clone(),
1186 copy: Some(cleanup.lock().copy()),
1187 merged_across: None,
1188 recorded_by: None,
1189 companions: None,
1190 };
1191 let mut published = None;
1192 let mut server_runs = Vec::new();
1193 let mut failure = None;
1194 for server in servers {
1195 let id = if published.is_none() {
1196 run_id.clone()
1197 } else {
1198 companion_run_id(&run_id, server_runs.len() + 1)
1199 };
1200 let language = server.language();
1201 match server.publish(
1202 &root,
1203 &run_id,
1204 RunMetadata {
1205 id: id.clone(),
1206 ..metadata.clone()
1207 },
1208 &served,
1209 diagnostics,
1210 ) {
1211 Ok(Some(run)) if published.is_none() => published = Some(run),
1212 Ok(Some((directory, _))) => {
1213 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1214 server_runs.push((language.to_owned(), directory));
1215 }
1216 Ok(None) => {}
1217 Err(error) => {
1218 let _ = writeln!(
1219 diagnostics,
1220 "[supercov] the {language} server's run could not be published: {error}"
1221 );
1222 failure.get_or_insert(error);
1223 }
1224 }
1225 }
1226 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1227 .map_err(|error| error.to_string())?;
1228 remove_stored_tree_deferred(&root, &server_evidence_root)
1229 .map_err(|error| error.to_string())?;
1230 if published.is_none()
1231 && let Some(error) = failure
1232 {
1233 return Err(error.into());
1234 }
1235 let Some((run_directory, metadata)) = published else {
1236 let message = if execution.exit_code != 0 && served.is_empty() {
1237 format!(
1238 "the test command exited {} before any test reported an outcome; its own output above says why, and no run was published",
1239 execution.exit_code
1240 )
1241 } else {
1242 "the project has no JavaScript source, and no test's request reached measured server code: there was nothing to measure, and no run was published".to_owned()
1243 };
1244 let _ = update_run_state(
1245 &root,
1246 &run_id,
1247 RunStateStatus::Failed,
1248 &started_at,
1249 Some(message.clone()),
1250 );
1251 cleanup.mark_terminal();
1252 return Err(message.into());
1253 };
1254 update_run_state(
1255 &root,
1256 &run_id,
1257 if execution.exit_code == 0 {
1258 RunStateStatus::Complete
1259 } else {
1260 RunStateStatus::Failed
1261 },
1262 &started_at,
1263 None,
1264 )
1265 .map_err(|error| error.to_string())?;
1266 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1267 cleanup.mark_terminal();
1268 return Ok(DirectJavascriptRunResult {
1269 run_id,
1270 run_directory,
1271 workspace,
1272 exit_code: execution.exit_code,
1273 assertion_calls: frontend.assertion_calls,
1274 recovered_runs,
1275 metadata,
1276 server_runs,
1277 unrun_build: project.unrun_build.clone(),
1278 });
1279 }
1280 let archive_path = root
1281 .join(".supercov/work")
1282 .join(&run_id)
1283 .join("evidence.raw.gz");
1284 let entries = collect_sources(&[
1285 EvidenceArchiveSource::File {
1286 file: frontend.manifest_path,
1287 path: "manifest.json".into(),
1288 },
1289 EvidenceArchiveSource::File {
1290 file: workspace.join(".supercov/statement-exclusions.json"),
1291 path: "statement-exclusions.json".into(),
1292 },
1293 EvidenceArchiveSource::Directory {
1294 directory: evidence_directory,
1295 prefix: None,
1296 },
1297 EvidenceArchiveSource::Directory {
1298 directory: server_evidence_root.join(&run_id),
1299 prefix: Some("server".into()),
1300 },
1301 ])
1302 .map_err(|error| error.to_string())?;
1303 let mut entries =
1304 javascript_archive_entries(entries, &frontend.manifest, &run_id, execution.exit_code)?;
1305 if let Some(support) = &support {
1306 entries.push(EvidenceArchiveEntry {
1307 path: crate::support_reach::ARCHIVE_PATH.into(),
1308 contents: serde_json::to_vec(support).map_err(|error| error.to_string())?,
1309 });
1310 }
1311 let mut recorded_inputs = source_inputs.clone();
1315 if let Some(support) = &support
1316 && let Ok(reached) = crate::source_capture::capture(
1317 &root,
1318 "javascript",
1319 support.files.keys().map(PathBuf::from),
1320 )
1321 {
1322 for (file, text) in reached.files {
1323 recorded_inputs.files.entry(file).or_insert(text);
1324 }
1325 }
1326 let entries = crate::source_capture::append(entries, &recorded_inputs)?;
1327 let raw = write_archive(entries, &archive_path).map_err(|error| error.to_string())?;
1328 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1329 .map_err(|error| error.to_string())?;
1330 remove_stored_tree_deferred(&root, &server_evidence_root).map_err(|error| error.to_string())?;
1331 let evidence_publication_ms = elapsed_ms(publication_started);
1332 let timings = RunTimings {
1333 initialization_ms: rounded_millisecond(initialization_ms),
1334 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1335 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1336 instrumented_build_ms: 0.0,
1337 test_command_ms: rounded_millisecond(test_command_ms),
1338 evidence_publication_ms: rounded_millisecond(evidence_publication_ms),
1339 };
1340 let metadata = RunMetadata {
1341 id: run_id.clone(),
1342 started_at: started_at.clone(),
1343 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1344 command: request.command.clone(),
1345 test_exit_code: Some(execution.exit_code),
1346 integrity,
1347 raw_evidence: RawEvidenceMetadata {
1348 schema_version: raw.schema_version,
1349 format: raw.format.into(),
1350 file: raw.file.into(),
1351 files: raw.files,
1352 uncompressed_bytes: raw.uncompressed_bytes,
1353 compressed_bytes: raw.compressed_bytes,
1354 },
1355 isolated_build: Some(true),
1356 instrumented_build_cache: None,
1357 timings: Some(timings),
1358 merged: None,
1359 parents: None,
1360 source_roots: source_roots.clone(),
1361 copy: Some(cleanup.lock().copy()),
1362 merged_across: None,
1363 recorded_by: None,
1364 companions: None,
1365 };
1366 let mut server_runs = Vec::new();
1370 let mut companions = Vec::new();
1371 for server in servers {
1372 let id = companion_run_id(&run_id, server_runs.len() + 1);
1373 let language = server.language();
1374 let server_metadata = RunMetadata {
1375 id: id.clone(),
1376 companions: Some(vec![crate::run_store::CompanionRun {
1377 id: run_id.clone(),
1378 language: "JavaScript".into(),
1379 role: "browser".into(),
1380 }]),
1381 ..metadata.clone()
1382 };
1383 match server.publish(&root, &run_id, server_metadata, &served, diagnostics) {
1384 Ok(Some((directory, _))) => {
1385 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1386 server_runs.push((language.to_owned(), directory));
1387 companions.push(crate::run_store::CompanionRun {
1388 id,
1389 language: language.into(),
1390 role: "server".into(),
1391 });
1392 }
1393 Ok(None) => {}
1394 Err(error) => {
1396 let _ = writeln!(
1397 diagnostics,
1398 "[supercov] the {language} server's run could not be published: {error}"
1399 );
1400 }
1401 }
1402 }
1403 let metadata = RunMetadata {
1404 companions: (!companions.is_empty()).then_some(companions),
1405 ..metadata
1406 };
1407 let run_directory =
1408 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1409 let terminal_status = if execution.exit_code == 0 {
1410 RunStateStatus::Complete
1411 } else {
1412 RunStateStatus::Failed
1413 };
1414 update_run_state(&root, &run_id, terminal_status, &started_at, None)
1415 .map_err(|error| error.to_string())?;
1416 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1417 cleanup.mark_terminal();
1418 Ok(DirectJavascriptRunResult {
1419 run_id,
1420 run_directory,
1421 workspace,
1422 exit_code: execution.exit_code,
1423 assertion_calls: frontend.assertion_calls,
1424 recovered_runs,
1425 metadata,
1426 server_runs,
1427 unrun_build: project.unrun_build.clone(),
1428 })
1429}
1430
1431fn companion_run_id(run_id: &str, nth: usize) -> String {
1435 match run_id
1436 .strip_prefix("run_")
1437 .filter(|hex| hex.len() == 16)
1438 .and_then(|hex| u64::from_str_radix(hex, 16).ok())
1439 {
1440 Some(number) => format!("run_{:016x}", number.wrapping_sub(nth as u64)),
1441 None => format!("{run_id}-server-{nth}"),
1442 }
1443}
1444
1445#[cfg(unix)]
1451fn start_docker_relay(
1452 root: &Path,
1453 workspace: &Path,
1454 docker_state: &Path,
1455 servers: &[Server],
1456 environment: &BTreeMap<String, String>,
1457 overrides: &mut BTreeMap<String, String>,
1458 diagnostics: &mut dyn std::io::Write,
1459) -> Result<Option<crate::docker_proxy::DockerProxy>, String> {
1460 let Some(upstream) = crate::docker_proxy::upstream_socket(environment) else {
1461 return Ok(None);
1462 };
1463 let mut names = overrides
1464 .keys()
1465 .filter(|name| name.starts_with("SUPERCOV_"))
1466 .cloned()
1467 .collect::<std::collections::BTreeSet<_>>();
1468 names.extend(
1469 servers
1470 .iter()
1471 .flat_map(|server| server.environment().into_keys()),
1472 );
1473 let settings = crate::docker_proxy::ProxySettings {
1474 copy: workspace.to_owned(),
1475 mount: root.join(".supercov"),
1476 environment: names
1477 .iter()
1478 .filter_map(|name| Some((name.clone(), overrides.get(name)?.clone())))
1479 .collect(),
1480 fingerprint: servers
1481 .iter()
1482 .map(Server::source_fingerprint)
1483 .collect::<Vec<_>>()
1484 .join(","),
1485 registry: root.join(".supercov/cache/docker-images.json"),
1486 state: docker_state.join("containers.jsonl"),
1487 };
1488 match crate::docker_proxy::socket_path(docker_state)
1489 .and_then(|socket| crate::docker_proxy::DockerProxy::start(&socket, &upstream, settings))
1490 {
1491 Ok(proxy) => {
1492 overrides.extend(proxy.environment(environment));
1493 Ok(Some(proxy))
1494 }
1495 Err(error) => {
1496 writeln!(
1497 diagnostics,
1498 "[supercov] containers the tests start are not measured: the Docker relay could not start ({error})"
1499 )
1500 .map_err(|error| error.to_string())?;
1501 Ok(None)
1502 }
1503 }
1504}
1505
1506enum Server {
1508 Python(crate::python_run::PythonServer),
1509 Ruby(crate::ruby_run::RubyServer),
1510 Go(crate::go_run::GoServer),
1511 Jvm(crate::jvm_run::JvmServer),
1512 RustWorker(crate::rust_worker::RustWorkerServer),
1513 RustWasm(crate::rust_wasm::RustWasmServer),
1514}
1515
1516impl Server {
1517 fn language(&self) -> &'static str {
1518 match self {
1519 Self::Python(_) => "Python",
1520 Self::Ruby(_) => "Ruby",
1521 Self::Go(_) => "Go",
1522 Self::Jvm(_) => "Java/Kotlin",
1523 Self::RustWorker(_) => "Rust Worker",
1524 Self::RustWasm(_) => "Rust wasm",
1525 }
1526 }
1527
1528 fn source_files(&self) -> usize {
1529 match self {
1530 Self::Python(server) => server.source_files(),
1531 Self::Ruby(server) => server.source_files(),
1532 Self::Go(server) => server.source_files(),
1533 Self::Jvm(server) => server.source_files(),
1534 Self::RustWorker(server) => server.source_files(),
1535 Self::RustWasm(server) => server.source_files(),
1536 }
1537 }
1538
1539 #[cfg(unix)]
1542 fn source_fingerprint(&self) -> String {
1543 match self {
1544 Self::Python(server) => server.source_fingerprint(),
1545 Self::Ruby(server) => server.source_fingerprint(),
1546 Self::Go(server) => server.source_fingerprint(),
1547 Self::Jvm(server) => server.source_fingerprint(),
1548 Self::RustWorker(server) => server.source_fingerprint(),
1549 Self::RustWasm(server) => server.source_fingerprint(),
1550 }
1551 }
1552
1553 fn environment(&self) -> BTreeMap<String, String> {
1554 match self {
1555 Self::Python(server) => server.environment(),
1556 Self::Ruby(server) => server.environment(),
1557 Self::Go(server) => server.environment(),
1558 Self::Jvm(server) => server.environment(),
1559 Self::RustWorker(server) => server.environment(),
1560 Self::RustWasm(server) => server.environment(),
1561 }
1562 }
1563
1564 fn publish(
1565 self,
1566 root: &Path,
1567 evidence_run_id: &str,
1568 metadata: RunMetadata,
1569 served: &[crate::served_tests::ServedTest],
1570 diagnostics: &mut dyn std::io::Write,
1571 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1572 match self {
1573 Self::Python(server) => {
1574 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1575 }
1576 Self::Ruby(server) => {
1577 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1578 }
1579 Self::Go(server) => {
1580 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1581 }
1582 Self::Jvm(server) => {
1583 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1584 }
1585 Self::RustWorker(server) => {
1586 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1587 }
1588 Self::RustWasm(server) => {
1589 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1590 }
1591 }
1592 }
1593}
1594
1595#[cfg(test)]
1596mod environment_tests {
1597 use super::*;
1598
1599 #[test]
1602 fn a_companion_run_id_is_public_and_sorts_after_its_run() {
1603 for run in [
1604 "run_0123456789abcdef",
1605 "run_ffffffffffffffff",
1606 "run_8000000000000000",
1607 ] {
1608 let companion = companion_run_id(run, 1);
1609 assert_eq!(companion.len(), 20);
1610 assert!(companion.starts_with("run_"));
1611 assert!(companion.as_str() < run, "{companion} >= {run}");
1612 assert_eq!(companion, companion_run_id(run, 1));
1613 assert!(companion_run_id(run, 2) < companion);
1614 }
1615 assert_eq!(companion_run_id("rust-1", 1), "rust-1-server-1");
1616 }
1617
1618 #[test]
1619 fn nested_npm_drops_only_pnpm_derived_environment_aliases() {
1620 let mut environment = BTreeMap::from([
1621 ("npm_config_auto_install_peers".into(), "true".into()),
1622 ("NPM_CONFIG_SHAMEFULLY_HOIST".into(), "true".into()),
1623 (
1624 "npm_config_registry".into(),
1625 "https://registry.npmjs.org".into(),
1626 ),
1627 ("USER_VALUE".into(), "kept".into()),
1628 ]);
1629 remove_derived_pnpm_config(&mut environment);
1630 assert_eq!(
1631 environment,
1632 BTreeMap::from([
1633 ("USER_VALUE".into(), "kept".into()),
1634 (
1635 "npm_config_registry".into(),
1636 "https://registry.npmjs.org".into()
1637 ),
1638 ])
1639 );
1640 }
1641}