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 test_id: Some(format!("command:{run_id}")),
247 scope: None,
248 test: "Test command".into(),
249 test_file: None,
250 title: Some("Test command".into()),
251 retry: Some(0),
252 status: Some(status.into()),
253 expected_status: None,
254 flaky: false,
255 provenance: crate::coverage_report::TestProvenance {
256 runner: "command".into(),
257 kind: "setup".into(),
258 project: None,
259 source: "engine".into(),
260 },
261 role: "setup".into(),
262 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
263 phases: vec![],
264 runtime: vec![],
265 browser: vec![],
266 server: vec![],
267 };
268 entries.push(EvidenceArchiveEntry {
269 path: "results/command/mcdc.json".into(),
270 contents: serde_json::to_vec(&command_result).map_err(|error| error.to_string())?,
271 });
272 results.push(command_result);
273 }
274 let mut by_runner = BTreeMap::<String, Vec<&RawTestResult>>::new();
275 for result in &results {
276 by_runner
277 .entry(result.provenance.runner.clone())
278 .or_default()
279 .push(result);
280 }
281 if entries
282 .iter()
283 .any(|entry| entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl"))
284 {
285 by_runner.entry("background".into()).or_default();
286 }
287 let declaration = FrontendRunDeclaration {
288 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
289 frontend_id: "javascript".into(),
290 frontend_version: "javascript-owned-v1".into(),
291 language: "javascript".into(),
292 structural_source: StructuralSource::OwnedProbes,
293 runners: by_runner
294 .into_iter()
295 .map(|(runner, results)| javascript_runner_declaration(runner, &results))
296 .collect(),
297 structural_limitations: manifest
298 .limitations
299 .iter()
300 .map(|limitation| limitation.id.clone())
301 .collect(),
302 };
303 entries.push(EvidenceArchiveEntry {
304 path: "coverage-model.json".into(),
305 contents: serde_json::to_vec(
306 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
307 .expect("JavaScript coverage model is contract-valid"),
308 )
309 .map_err(|error| error.to_string())?,
310 });
311 entries.push(EvidenceArchiveEntry {
312 path: "frontend.json".into(),
313 contents: serde_json::to_vec(&declaration).map_err(|error| error.to_string())?,
314 });
315 Ok(entries)
316}
317
318struct RunCleanup {
319 root: PathBuf,
320 run_id: String,
321 started_at: String,
322 lock: ProjectLock,
323 workspace: Option<PathBuf>,
324 state_written: bool,
325 terminal_recorded: bool,
326 keep_workspace: bool,
329}
330
331impl RunCleanup {
332 fn lock(&self) -> &ProjectLock {
333 &self.lock
334 }
335
336 fn set_workspace(&mut self, workspace: PathBuf) {
337 self.workspace = Some(workspace);
338 }
339
340 fn mark_state_written(&mut self) {
341 self.state_written = true;
342 }
343
344 fn mark_terminal(&mut self) {
345 self.terminal_recorded = true;
346 }
347}
348
349impl Drop for RunCleanup {
350 fn drop(&mut self) {
351 if self.state_written && !self.terminal_recorded {
352 let _ = update_run_state(
353 &self.root,
354 &self.run_id,
355 RunStateStatus::Failed,
356 &self.started_at,
357 Some("Rust run exited before reaching a terminal lifecycle state".into()),
358 );
359 }
360 if let Some(workspace) = &self.workspace {
361 let _ = remove_stored_tree_deferred(
362 &self.root,
363 &workspace.join(".supercov/evidence").join(&self.run_id),
364 );
365 let _ = remove_stored_tree_deferred(
366 &self.root,
367 &workspace
368 .join(".supercov/server-evidence")
369 .join(&self.run_id),
370 );
371 let keep_workspace = self.keep_workspace
372 || std::env::var("SUPERCOV_KEEP_WORKSPACE").is_ok_and(|value| !value.is_empty());
373 if !keep_workspace {
374 let _ = prune_cached_workspace_sources(&self.root, &self.lock);
375 }
376 }
377 let _ = self.lock.release();
378 }
379}
380
381fn now_nonce() -> u128 {
382 SystemTime::now()
383 .duration_since(UNIX_EPOCH)
384 .unwrap_or_default()
385 .as_millis()
386}
387
388fn elapsed_ms(started: Instant) -> f64 {
389 started.elapsed().as_secs_f64() * 1000.0
390}
391
392fn rounded_millisecond(value: f64) -> f64 {
393 (value * 10.0).round() / 10.0
394}
395
396fn supervision_options() -> Result<SupervisionOptions, String> {
397 let defaults = SupervisionOptions::default();
398 Ok(SupervisionOptions {
399 diagnostic_interval: positive_milliseconds(
400 std::env::var("SUPERCOV_DIAGNOSTIC_INTERVAL_MS")
401 .ok()
402 .as_deref(),
403 "SUPERCOV_DIAGNOSTIC_INTERVAL_MS",
404 )
405 .map_err(|error| error.to_string())?
406 .unwrap_or(defaults.diagnostic_interval),
407 timeout: positive_milliseconds(
408 std::env::var("SUPERCOV_COMMAND_TIMEOUT_MS").ok().as_deref(),
409 "SUPERCOV_COMMAND_TIMEOUT_MS",
410 )
411 .map_err(|error| error.to_string())?,
412 termination_grace: defaults.termination_grace,
413 })
414}
415
416fn remove_derived_pnpm_config(environment: &mut BTreeMap<OsString, OsString>) {
417 const PNPM_ONLY_NPM_CONFIG: &[&str] = &[
423 "npm_config_auto_install_peers",
424 "npm_config_enable_pre_post_scripts",
425 "npm_config_shamefully_hoist",
426 ];
427 environment.retain(|key, _| {
428 let key = key.to_string_lossy().to_ascii_lowercase();
429 !PNPM_ONLY_NPM_CONFIG.contains(&key.as_str())
430 });
431}
432
433fn environment_with(values: BTreeMap<String, String>) -> Vec<(OsString, OsString)> {
434 let mut environment = std::env::vars_os().collect::<BTreeMap<_, _>>();
435 remove_derived_pnpm_config(&mut environment);
436 for (key, value) in values {
437 environment.retain(|existing, _| !existing.to_string_lossy().eq_ignore_ascii_case(&key));
438 environment.insert(key.into(), value.into());
439 }
440 environment.into_iter().collect()
441}
442
443const PLAYWRIGHT_CACHE_VARIABLE: &str = "PWTEST_CACHE_DIR";
445
446pub fn playwright_transform_cache(root: &Path) -> PathBuf {
448 crate::workspace::workspace_container(root).join("caches/playwright-transform")
449}
450
451fn node_options(preload: &Path) -> String {
452 let preload = std::path::absolute(preload).unwrap_or_else(|_| preload.to_path_buf());
458 [
464 std::env::var("NODE_OPTIONS").ok(),
465 Some("--enable-source-maps".into()),
466 Some(format!("--import={}", crate::workspace::file_url(&preload))),
467 ]
468 .into_iter()
469 .flatten()
470 .filter(|value| !value.is_empty())
471 .collect::<Vec<_>>()
472 .join(" ")
473}
474
475pub fn current_javascript_integrity(
479 root: &Path,
480 command: &[String],
481 source_roots: Option<&[String]>,
482) -> Result<RunIntegrity, String> {
483 let mut environment = std::env::vars().collect::<BTreeMap<_, _>>();
484 match source_roots.filter(|roots| !roots.is_empty()) {
489 Some(roots) => {
490 environment.insert(
491 crate::source_discovery::SOURCE_ROOTS_VARIABLE.into(),
492 roots.join(","),
493 );
494 }
495 None => {
496 environment.remove(crate::source_discovery::SOURCE_ROOTS_VARIABLE);
497 }
498 }
499 let project = discover_coverage_project(root, &environment, command)
500 .map_err(|error| error.to_string())?;
501 javascript_integrity_for_project(root, &project)
502}
503
504fn javascript_integrity_for_project(
505 root: &Path,
506 project: &crate::project_discovery::CoverageProject,
507) -> Result<RunIntegrity, String> {
508 let frontend = FrontendIntegrityInputs::embedded_javascript();
509 create_run_integrity(root, project, &frontend).map_err(|error| error.to_string())
510}
511
512pub fn run_direct_javascript(
515 request: &DirectJavascriptRunRequest,
516 diagnostics: &mut dyn std::io::Write,
517) -> Result<DirectJavascriptRunResult, DirectJavascriptRunError> {
518 if request.command.is_empty() {
519 return Err(DirectJavascriptRunError::Failed(
520 "test command must not be empty".into(),
521 ));
522 }
523 let total_started = Instant::now();
524 let initialization_started = Instant::now();
525 let root = crate::workspace::canonicalize_simplified(&request.root)
526 .map_err(|error| format!("{}: {error}", request.root.display()))?;
527 let nonce = now_nonce();
528 let run_id = request
529 .run_id
530 .clone()
531 .unwrap_or_else(|| format!("rust-{nonce}"));
532 let started_at = request
533 .started_at
534 .clone()
535 .unwrap_or_else(|| format!("unix-ms-{nonce}"));
536 let lock =
537 ProjectLock::acquire(&root, &run_id, &started_at).map_err(|error| error.to_string())?;
538 let mut cleanup = RunCleanup {
539 root: root.clone(),
540 run_id: run_id.clone(),
541 started_at: started_at.clone(),
542 lock,
543 workspace: None,
544 state_written: false,
545 terminal_recorded: false,
546 keep_workspace: false,
547 };
548 let recovered_runs =
549 recover_abandoned_runs(&root, &started_at).map_err(|error| error.to_string())?;
550 if !recovered_runs.is_empty() {
551 writeln!(
552 diagnostics,
553 "[supercov] recovered abandoned run(s): {}",
554 recovered_runs.join(", ")
555 )
556 .map_err(|error| error.to_string())?;
557 }
558 let environment = std::env::vars().collect::<BTreeMap<_, _>>();
559 let source_roots = crate::source_discovery::configured_source_roots(&environment);
560 let project = crate::project_discovery::discover_coverage_project_of_any_size(
561 &root,
562 &environment,
563 &request.command,
564 )
565 .map_err(|error| error.to_string())?;
566 let browser_suite = project.playwright_config.is_some();
570 let serves_python = browser_suite
571 && crate::python_project::discover_python_files(&root)
572 .is_ok_and(|files| !files.sources.is_empty());
573 let serves_ruby = browser_suite
574 && crate::ruby_project::discover_ruby_files(&root)
575 .is_ok_and(|files| !files.sources.is_empty());
576 let serves_go = browser_suite
577 && (root.join("go.mod").is_file()
578 || !crate::go_project::workspace_modules(&root).is_empty());
579 let serves_jvm = browser_suite
580 && [
581 "pom.xml",
582 "build.gradle",
583 "build.gradle.kts",
584 "src/main/java",
585 "src/main/kotlin",
586 ]
587 .iter()
588 .any(|name| root.join(name).exists());
589 if project.source_files.is_empty()
590 && !serves_python
591 && !serves_ruby
592 && !serves_go
593 && !serves_jvm
594 {
595 return Err(
596 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
597 .to_string()
598 .into(),
599 );
600 }
601 let integrity = javascript_integrity_for_project(&root, &project)?;
602 let source_inputs = crate::source_capture::capture(
603 &root,
604 "javascript",
605 crate::integrity::javascript_assertion_paths(&root, &project).map_err(|e| e.to_string())?,
606 )?;
607 let frontend_cache_key = format!(
613 "{}:{}:{}:{}:{}",
614 integrity.fingerprint.combined,
615 integrity.fingerprint.execution,
616 crate::source_manifest::digest(&request.command),
617 crate::source_manifest::digest(&project.root.to_string_lossy()),
618 crate::source_manifest::digest(&(&project.source_files, &source_roots))
619 );
620 let prior_workspace =
621 cached_workspace_path(&root, cleanup.lock()).map_err(|error| error.to_string())?;
622 let reusable_frontend = read_javascript_frontend_cache(&prior_workspace, &frontend_cache_key);
623 let cached_paths = reusable_frontend
624 .as_ref()
625 .map(javascript_frontend_reuse_paths)
626 .unwrap_or_default();
627 let initialization_ms = elapsed_ms(initialization_started);
628 let prepare_servers = |workspace: PathBuf| {
632 let (root, run_id, command) = (root.clone(), run_id.clone(), request.command.clone());
633 move || -> Result<Vec<Server>, String> {
634 let mut servers = Vec::new();
635 if serves_python
636 && let Some(server) =
637 crate::python_run::PythonServer::prepare(&root, &workspace, &run_id, &command)?
638 {
639 servers.push(Server::Python(server));
640 }
641 if serves_ruby
642 && let Some(server) =
643 crate::ruby_run::RubyServer::prepare(&root, &workspace, &run_id, &command)?
644 {
645 servers.push(Server::Ruby(server));
646 }
647 Ok(servers)
648 }
649 };
650 let preparing_servers = std::thread::spawn(prepare_servers(prior_workspace.clone()));
651
652 let workspace_started = Instant::now();
653 let workspace_progress = crate::progress::ProgressLine::start("preparing isolated workspace");
654 let workspace = prepare_cached_workspace(&root, cleanup.lock(), &cached_paths)
655 .map_err(|error| error.to_string())?;
656 drop(workspace_progress);
657 cleanup.set_workspace(workspace.clone());
658 crate::lifecycle::sweep_trash_in_background(&root);
660 if let Err(error) = crate::workspace::mirror_git_repository(&root, &workspace)
661 && std::env::var_os("SUPERCOV_DEBUG").is_some()
662 {
663 eprintln!("[supercov] the workspace has no git repository of its own: {error}");
664 }
665 let workspace_preparation_ms = elapsed_ms(workspace_started);
666 writeln!(
667 diagnostics,
668 "[supercov] instrumenting isolated workspace {}",
669 workspace.display()
670 )
671 .map_err(|error| error.to_string())?;
672 cleanup.mark_state_written();
673 write_run_state(
674 &root,
675 &RunState {
676 id: run_id.clone(),
677 pid: std::process::id(),
678 root: root.display().to_string(),
679 workspace: workspace.display().to_string(),
680 started_at: started_at.clone(),
681 updated_at: started_at.clone(),
682 status: RunStateStatus::Preparing,
683 signal: None,
684 error: None,
685 },
686 )
687 .map_err(|error| error.to_string())?;
688
689 let adapter_started = Instant::now();
690 let instrumentation_progress = crate::progress::ProgressLine::start("instrumenting sources");
691 let collector_id = format!("collector-{}", integrity.fingerprint.execution);
692 let frontend = if let Some(cache) = &reusable_frontend {
693 load_cached_javascript_frontend(&workspace, cache)
694 } else {
695 prepare_javascript_frontend(
696 &workspace,
697 &project,
698 &collector_id,
699 &frontend_cache_key,
700 &request.command,
701 )
702 }
703 .map_err(|error| error.to_string())?;
704 crate::workspace::hide_rewritten_files(
705 &workspace,
706 &crate::javascript_frontend::rewritten_files(&workspace),
707 );
708 drop(instrumentation_progress);
709 let adapter_setup_ms = elapsed_ms(adapter_started);
710 if let Some(detail) = crate::javascript_frontend::setup_timing_detail() {
711 writeln!(diagnostics, "[supercov] {detail}").map_err(|error| error.to_string())?;
712 }
713
714 let mut servers = preparing_servers
715 .join()
716 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))?;
717 if workspace != prior_workspace && !servers.is_empty() {
720 servers = prepare_servers(workspace.clone())()?;
721 }
722 if serves_go
724 && let Some(server) =
725 crate::go_run::GoServer::prepare(&root, &workspace, &run_id, &request.command)?
726 {
727 servers.push(Server::Go(server));
728 }
729 if serves_jvm
730 && let Some(server) =
731 crate::jvm_run::JvmServer::prepare(&root, &workspace, &run_id, &request.command)?
732 {
733 servers.push(Server::Jvm(server));
734 }
735 if project.source_files.is_empty() && servers.is_empty() {
736 return Err(
737 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
738 .to_string()
739 .into(),
740 );
741 }
742 for server in &servers {
743 writeln!(
744 diagnostics,
745 "[supercov] measuring {} {language} source file(s) in any {language} server the tests start",
746 server.source_files(),
747 language = server.language(),
748 )
749 .map_err(|error| error.to_string())?;
750 }
751
752 let evidence_relative = format!(".supercov/evidence/{run_id}");
753 let evidence_directory = workspace.join(&evidence_relative);
754 let server_evidence_root = workspace.join(".supercov/server-evidence");
755 let diagnostic_owner = workspace.join(format!(".supercov/diagnostic-owner-{run_id}"));
756 let mut overrides: BTreeMap<String, String> = BTreeMap::from([
757 ("NODE_OPTIONS".into(), node_options(&frontend.preload_path)),
758 ("SUPERCOV_CJS_INTERCEPT".into(), "1".into()),
759 ("SUPERCOV_DIRECT_INSTRUMENTATION".into(), "1".into()),
760 ("TAP_ALLOW_INCOMPLETE_COVERAGE".into(), "1".into()),
765 (
769 "SUPERCOV_EVIDENCE_DIR".into(),
770 evidence_directory.display().to_string(),
771 ),
772 (
773 "SUPERCOV_DIAGNOSTIC_OWNER_FILE".into(),
774 diagnostic_owner.display().to_string(),
775 ),
776 (
777 "SUPERCOV_EXECUTION_FINGERPRINT".into(),
778 integrity.fingerprint.execution.clone(),
779 ),
780 (
781 "SUPERCOV_EXECUTION_LOG".into(),
782 evidence_directory
783 .join("execution.jsonl")
784 .display()
785 .to_string(),
786 ),
787 (
788 "SUPERCOV_MANIFEST".into(),
789 frontend.manifest_path.display().to_string(),
790 ),
791 (
792 "SUPERCOV_PROJECT_ROOT".into(),
793 workspace.display().to_string(),
794 ),
795 ("SUPERCOV_RUN_ID".into(), run_id.clone()),
796 (
797 "SUPERCOV_SERVER_EVIDENCE_ROOT".into(),
798 server_evidence_root.display().to_string(),
799 ),
800 (
801 "SUPERCOV_SOURCE_PROJECT_ROOT".into(),
802 root.display().to_string(),
803 ),
804 ]);
805 if std::env::var_os(PLAYWRIGHT_CACHE_VARIABLE).is_none_or(|value| value.is_empty()) {
815 overrides.insert(
816 PLAYWRIGHT_CACHE_VARIABLE.into(),
817 playwright_transform_cache(&root).display().to_string(),
818 );
819 }
820 overrides.insert(
824 "SUPERCOV_GENERATED_VITEST_CONFIG".into(),
825 frontend.vitest_config_path.display().to_string(),
826 );
827 overrides.insert(
828 "SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG".into(),
829 frontend.playwright_config_path.display().to_string(),
830 );
831 overrides.insert(
832 "SUPERCOV_PLAYWRIGHT_MODULE".into(),
833 project.playwright_module.clone(),
834 );
835 overrides.insert(
836 "SUPERCOV_PLAYWRIGHT_TEST_EXPORT".into(),
837 project.playwright_test_export.clone(),
838 );
839 overrides.insert(
840 "SUPERCOV_PLAYWRIGHT_WRAPPER".into(),
841 "./.supercov/node_modules/playwright.mjs".into(),
842 );
843 if let Some(original) = project
844 .playwright_config
845 .as_ref()
846 .and_then(|path| path.strip_prefix(&root).ok())
847 .map(|path| workspace.join(path))
848 {
849 overrides.insert(
850 "SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG".into(),
851 original.display().to_string(),
852 );
853 }
854 for server in &servers {
855 overrides.extend(server.environment());
856 }
857 if crate::workspace::links_a_virtual_environment(&workspace)
863 && std::env::var_os("UV_NO_SYNC").is_none()
864 {
865 overrides.insert("UV_NO_SYNC".into(), "1".into());
866 }
867 let support_directory = workspace.join(".supercov/support").join(&run_id);
871 if project.playwright_config.is_some() {
872 overrides.insert(
873 crate::support_reach::DIRECTORY_VARIABLE.into(),
874 support_directory.display().to_string(),
875 );
876 }
877 if let Some(path) = crate::source_tools::path_with_launchers(&workspace)
880 && let Some(path) = path.to_str()
881 {
882 overrides.insert("PATH".into(), path.to_owned());
883 }
884 let docker_state = root.join(".supercov/work").join(&run_id).join("docker");
891 let mut docker_environment = overrides
892 .keys()
893 .filter(|name| name.starts_with("SUPERCOV_"))
894 .cloned()
895 .collect::<std::collections::BTreeSet<_>>();
896 docker_environment.extend(
897 servers
898 .iter()
899 .flat_map(|server| server.environment().into_keys()),
900 );
901 let docker_environment = docker_environment.into_iter().collect::<Vec<_>>();
902 let docker_servers = !project.source_files.is_empty() || !servers.is_empty();
903 if cfg!(unix) && docker_servers {
904 let launchers = workspace.join(".supercov/node_modules/.docker");
905 let path = overrides
906 .get("PATH")
907 .map(std::ffi::OsString::from)
908 .or_else(|| std::env::var_os("PATH"));
909 if let Some(path) = path
910 && crate::docker_servers::write_launchers(&launchers, &path).is_some()
911 && let Ok(joined) =
912 std::env::join_paths(std::iter::once(launchers).chain(std::env::split_paths(&path)))
913 && let Some(joined) = joined.to_str()
914 {
915 overrides.insert("PATH".into(), joined.to_owned());
916 overrides.insert(
917 crate::docker_servers::STATE_VARIABLE.into(),
918 docker_state.display().to_string(),
919 );
920 overrides.insert(
921 crate::docker_servers::MOUNT_VARIABLE.into(),
922 root.join(".supercov").display().to_string(),
923 );
924 overrides.insert(
925 crate::docker_servers::COPY_VARIABLE.into(),
926 workspace.display().to_string(),
927 );
928 overrides.insert(
929 crate::docker_servers::ENVIRONMENT_VARIABLE.into(),
930 docker_environment.join(","),
931 );
932 }
933 }
934 let settings = serde_json::json!({
938 "root": workspace.display().to_string(),
939 "environment": overrides
940 .iter()
941 .filter(|(name, _)| name.starts_with("SUPERCOV_"))
942 .collect::<BTreeMap<_, _>>(),
943 });
944 let settings_path = frontend.preload_path.with_file_name("run.json");
945 std::fs::write(
946 &settings_path,
947 serde_json::to_vec(&settings).map_err(|error| error.to_string())?,
948 )
949 .map_err(|error| format!("{}: {error}", settings_path.display()))?;
950 let _ = crate::source_tools::clear_source_changes(&workspace);
953 let plan = ExecutionPlan {
955 preparation: Vec::new(),
956 test: ExecutionPhase {
957 name: "test".into(),
958 kind: PhaseKind::Test,
959 command: CommandSpec {
960 program: request.command[0].clone().into(),
961 arguments: request.command[1..].iter().map(OsString::from).collect(),
962 cwd: workspace.clone(),
963 environment: Some(environment_with(overrides)),
964 captured_output: None,
965 console: None,
966 },
967 },
968 };
969 let options = supervision_options()?;
970 let watchdog_program = request.watchdog_program.as_ref().ok_or_else(|| {
971 DirectJavascriptRunError::Failed(
972 "the JavaScript run is missing its crash-containment executable".into(),
973 )
974 })?;
975 let supervisor = ProcessSupervisor::new_crash_safe(watchdog_program)
976 .map_err(|error| DirectJavascriptRunError::Failed(error.to_string()))?;
977 let output_baseline = std::cell::RefCell::new(None);
978 let execution = match execute_plan_with_supervisor(
979 &supervisor,
980 &plan,
981 options,
982 diagnostics,
983 |phase, diagnostics| {
984 {
985 *output_baseline.borrow_mut() =
989 Some(workspace_output_baseline(&workspace).map_err(|error| {
990 OrchestrationError::PhaseSetup {
991 phase: phase.name.clone(),
992 reason: error.to_string(),
993 }
994 })?);
995 if frontend.assertion_calls > 0 {
996 writeln!(
997 diagnostics,
998 "[supercov] attributed {} native node:assert call(s)",
999 frontend.assertion_calls
1000 )
1001 .map_err(|error| OrchestrationError::PhaseSetup {
1002 phase: phase.name.clone(),
1003 reason: error.to_string(),
1004 })?;
1005 }
1006 writeln!(
1007 diagnostics,
1008 "[supercov] running in isolated workspace: {}",
1009 request.command.join(" ")
1010 )
1011 .map_err(|error| OrchestrationError::PhaseSetup {
1012 phase: phase.name.clone(),
1013 reason: error.to_string(),
1014 })?;
1015 }
1016 update_run_state(&root, &run_id, RunStateStatus::Testing, &started_at, None).map_err(
1017 |error| OrchestrationError::PhaseSetup {
1018 phase: phase.name.clone(),
1019 reason: error.to_string(),
1020 },
1021 )?;
1022 Ok(())
1023 },
1024 ) {
1025 Ok(execution) => execution,
1026 Err(error) => {
1027 let message = error.to_string();
1028 let state_updated = update_run_state(
1029 &root,
1030 &run_id,
1031 RunStateStatus::Failed,
1032 &started_at,
1033 Some(message.clone()),
1034 )
1035 .is_ok();
1036 if state_updated {
1037 cleanup.mark_terminal();
1038 }
1039 return Err(message.into());
1040 }
1041 };
1042 crate::docker_servers::stop_started(&docker_state);
1044 let test_command_ms = execution
1045 .phases
1046 .iter()
1047 .find(|phase| phase.kind == PhaseKind::Test)
1048 .map_or(0.0, |phase| phase.duration_ms as f64);
1049 if let Some(signal) = execution.interrupted_signal {
1050 interrupt_run_state(&root, &run_id, &started_at, signal_name(signal))
1051 .map_err(|error| error.to_string())?;
1052 cleanup.mark_terminal();
1053 return Err(DirectJavascriptRunError::Interrupted {
1054 signal,
1055 exit_code: execution.exit_code,
1056 timings: RunTimings {
1057 initialization_ms: rounded_millisecond(initialization_ms),
1058 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1059 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1060 instrumented_build_ms: 0.0,
1061 test_command_ms: rounded_millisecond(test_command_ms),
1062 evidence_publication_ms: 0.0,
1063 },
1064 total_ms: rounded_millisecond(elapsed_ms(total_started)),
1065 });
1066 }
1067 if let Some(baseline) = output_baseline.borrow().as_ref() {
1072 let protected = project
1073 .source_files
1074 .iter()
1075 .map(PathBuf::from)
1076 .collect::<std::collections::BTreeSet<_>>();
1077 let outputs = sync_command_outputs(&root, &workspace, baseline, &protected)
1078 .map_err(|error| error.to_string())?;
1079 if outputs.synced > 0 {
1080 let _ = writeln!(
1081 diagnostics,
1082 "[supercov] synced {} file(s) the command created or changed back to the project: {}",
1083 outputs.synced,
1084 crate::workspace::summarize_paths(&outputs.synced_paths)
1085 );
1086 }
1087 if !outputs.skipped_instrumented.is_empty() {
1088 let _ = writeln!(
1089 diagnostics,
1090 "[supercov] {} file(s) stayed in the isolated workspace: they are instrumented copies, or were built from them, and must not overwrite your project: {}",
1091 outputs.skipped_instrumented.len(),
1092 crate::workspace::summarize_paths(&outputs.skipped_instrumented)
1093 );
1094 }
1095 match crate::source_tools::apply_source_changes(&root, &workspace) {
1100 Ok(changes) => {
1101 if !changes.applied.is_empty() {
1102 let _ = writeln!(
1103 diagnostics,
1104 "[supercov] the command's tools changed {} source file(s), now changed in the project too: {}; this run measured them as they were before",
1105 changes.applied.len(),
1106 crate::workspace::summarize_paths(&changes.applied)
1107 );
1108 }
1109 if !changes.kept_back.is_empty() {
1110 let _ = writeln!(
1111 diagnostics,
1112 "[supercov] the command's tools changed {} source file(s) that were edited in the project while it ran; those changes were not applied: {}",
1113 changes.kept_back.len(),
1114 crate::workspace::summarize_paths(&changes.kept_back)
1115 );
1116 }
1117 }
1118 Err(error) => {
1119 let _ = writeln!(
1120 diagnostics,
1121 "[supercov] the source changes the command's tools made could not be applied to the project: {error}"
1122 );
1123 }
1124 }
1125 if !outputs.deleted_in_workspace.is_empty() {
1126 let _ = writeln!(
1127 diagnostics,
1128 "[supercov] the command deleted {} file(s) in the isolated workspace; deletions are not propagated to the project: {}",
1129 outputs.deleted_in_workspace.len(),
1130 crate::workspace::summarize_paths(&outputs.deleted_in_workspace)
1131 );
1132 }
1133 }
1134 update_run_state(
1135 &root,
1136 &run_id,
1137 RunStateStatus::Publishing,
1138 &started_at,
1139 None,
1140 )
1141 .map_err(|error| error.to_string())?;
1142
1143 let publication_started = Instant::now();
1144 let served = if servers.is_empty() && !support_directory.is_dir() {
1146 Vec::new()
1147 } else {
1148 crate::served_tests::read(&evidence_directory)
1149 };
1150 let support = crate::support_reach::read(&support_directory, &workspace, &served);
1151 if project.source_files.is_empty() {
1152 let metadata = RunMetadata {
1155 id: run_id.clone(),
1156 started_at: started_at.clone(),
1157 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1158 command: request.command.clone(),
1159 test_exit_code: Some(execution.exit_code),
1160 integrity: integrity.clone(),
1161 raw_evidence: RawEvidenceMetadata::default(),
1162 isolated_build: Some(true),
1163 instrumented_build_cache: None,
1164 timings: Some(RunTimings {
1165 initialization_ms: rounded_millisecond(initialization_ms),
1166 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1167 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1168 instrumented_build_ms: 0.0,
1169 test_command_ms: rounded_millisecond(test_command_ms),
1170 evidence_publication_ms: 0.0,
1171 }),
1172 merged: None,
1173 parents: None,
1174 source_roots: source_roots.clone(),
1175 copy: Some(cleanup.lock().copy()),
1176 merged_across: None,
1177 recorded_by: None,
1178 companions: None,
1179 };
1180 let mut published = None;
1181 let mut server_runs = Vec::new();
1182 let mut failure = None;
1183 for server in servers {
1184 let id = if published.is_none() {
1185 run_id.clone()
1186 } else {
1187 companion_run_id(&run_id, server_runs.len() + 1)
1188 };
1189 let language = server.language();
1190 match server.publish(
1191 &root,
1192 &run_id,
1193 RunMetadata {
1194 id: id.clone(),
1195 ..metadata.clone()
1196 },
1197 &served,
1198 diagnostics,
1199 ) {
1200 Ok(Some(run)) if published.is_none() => published = Some(run),
1201 Ok(Some((directory, _))) => {
1202 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1203 server_runs.push((language.to_owned(), directory));
1204 }
1205 Ok(None) => {}
1206 Err(error) => {
1207 let _ = writeln!(
1208 diagnostics,
1209 "[supercov] the {language} server's run could not be published: {error}"
1210 );
1211 failure.get_or_insert(error);
1212 }
1213 }
1214 }
1215 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1216 .map_err(|error| error.to_string())?;
1217 remove_stored_tree_deferred(&root, &server_evidence_root)
1218 .map_err(|error| error.to_string())?;
1219 if published.is_none()
1220 && let Some(error) = failure
1221 {
1222 return Err(error.into());
1223 }
1224 let Some((run_directory, metadata)) = published else {
1225 let message = if execution.exit_code != 0 && served.is_empty() {
1226 format!(
1227 "the test command exited {} before any test reported an outcome; its own output above says why, and no run was published",
1228 execution.exit_code
1229 )
1230 } else {
1231 "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()
1232 };
1233 let _ = update_run_state(
1234 &root,
1235 &run_id,
1236 RunStateStatus::Failed,
1237 &started_at,
1238 Some(message.clone()),
1239 );
1240 cleanup.mark_terminal();
1241 return Err(message.into());
1242 };
1243 update_run_state(
1244 &root,
1245 &run_id,
1246 if execution.exit_code == 0 {
1247 RunStateStatus::Complete
1248 } else {
1249 RunStateStatus::Failed
1250 },
1251 &started_at,
1252 None,
1253 )
1254 .map_err(|error| error.to_string())?;
1255 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1256 cleanup.mark_terminal();
1257 return Ok(DirectJavascriptRunResult {
1258 run_id,
1259 run_directory,
1260 workspace,
1261 exit_code: execution.exit_code,
1262 assertion_calls: frontend.assertion_calls,
1263 recovered_runs,
1264 metadata,
1265 server_runs,
1266 unrun_build: project.unrun_build.clone(),
1267 });
1268 }
1269 let archive_path = root
1270 .join(".supercov/work")
1271 .join(&run_id)
1272 .join("evidence.raw.gz");
1273 let entries = collect_sources(&[
1274 EvidenceArchiveSource::File {
1275 file: frontend.manifest_path,
1276 path: "manifest.json".into(),
1277 },
1278 EvidenceArchiveSource::File {
1279 file: workspace.join(".supercov/statement-exclusions.json"),
1280 path: "statement-exclusions.json".into(),
1281 },
1282 EvidenceArchiveSource::Directory {
1283 directory: evidence_directory,
1284 prefix: None,
1285 },
1286 EvidenceArchiveSource::Directory {
1287 directory: server_evidence_root.join(&run_id),
1288 prefix: Some("server".into()),
1289 },
1290 ])
1291 .map_err(|error| error.to_string())?;
1292 let mut entries =
1293 javascript_archive_entries(entries, &frontend.manifest, &run_id, execution.exit_code)?;
1294 if let Some(support) = &support {
1295 entries.push(EvidenceArchiveEntry {
1296 path: crate::support_reach::ARCHIVE_PATH.into(),
1297 contents: serde_json::to_vec(support).map_err(|error| error.to_string())?,
1298 });
1299 }
1300 let mut recorded_inputs = source_inputs.clone();
1304 if let Some(support) = &support
1305 && let Ok(reached) = crate::source_capture::capture(
1306 &root,
1307 "javascript",
1308 support.files.keys().map(PathBuf::from),
1309 )
1310 {
1311 for (file, text) in reached.files {
1312 recorded_inputs.files.entry(file).or_insert(text);
1313 }
1314 }
1315 let entries = crate::source_capture::append(entries, &recorded_inputs)?;
1316 let raw = write_archive(entries, &archive_path).map_err(|error| error.to_string())?;
1317 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1318 .map_err(|error| error.to_string())?;
1319 remove_stored_tree_deferred(&root, &server_evidence_root).map_err(|error| error.to_string())?;
1320 let evidence_publication_ms = elapsed_ms(publication_started);
1321 let timings = RunTimings {
1322 initialization_ms: rounded_millisecond(initialization_ms),
1323 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1324 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1325 instrumented_build_ms: 0.0,
1326 test_command_ms: rounded_millisecond(test_command_ms),
1327 evidence_publication_ms: rounded_millisecond(evidence_publication_ms),
1328 };
1329 let metadata = RunMetadata {
1330 id: run_id.clone(),
1331 started_at: started_at.clone(),
1332 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1333 command: request.command.clone(),
1334 test_exit_code: Some(execution.exit_code),
1335 integrity,
1336 raw_evidence: RawEvidenceMetadata {
1337 schema_version: raw.schema_version,
1338 format: raw.format.into(),
1339 file: raw.file.into(),
1340 files: raw.files,
1341 uncompressed_bytes: raw.uncompressed_bytes,
1342 compressed_bytes: raw.compressed_bytes,
1343 },
1344 isolated_build: Some(true),
1345 instrumented_build_cache: None,
1346 timings: Some(timings),
1347 merged: None,
1348 parents: None,
1349 source_roots: source_roots.clone(),
1350 copy: Some(cleanup.lock().copy()),
1351 merged_across: None,
1352 recorded_by: None,
1353 companions: None,
1354 };
1355 let mut server_runs = Vec::new();
1359 let mut companions = Vec::new();
1360 for server in servers {
1361 let id = companion_run_id(&run_id, server_runs.len() + 1);
1362 let language = server.language();
1363 let server_metadata = RunMetadata {
1364 id: id.clone(),
1365 companions: Some(vec![crate::run_store::CompanionRun {
1366 id: run_id.clone(),
1367 language: "JavaScript".into(),
1368 role: "browser".into(),
1369 }]),
1370 ..metadata.clone()
1371 };
1372 match server.publish(&root, &run_id, server_metadata, &served, diagnostics) {
1373 Ok(Some((directory, _))) => {
1374 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1375 server_runs.push((language.to_owned(), directory));
1376 companions.push(crate::run_store::CompanionRun {
1377 id,
1378 language: language.into(),
1379 role: "server".into(),
1380 });
1381 }
1382 Ok(None) => {}
1383 Err(error) => {
1385 let _ = writeln!(
1386 diagnostics,
1387 "[supercov] the {language} server's run could not be published: {error}"
1388 );
1389 }
1390 }
1391 }
1392 let metadata = RunMetadata {
1393 companions: (!companions.is_empty()).then_some(companions),
1394 ..metadata
1395 };
1396 let run_directory =
1397 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1398 let terminal_status = if execution.exit_code == 0 {
1399 RunStateStatus::Complete
1400 } else {
1401 RunStateStatus::Failed
1402 };
1403 update_run_state(&root, &run_id, terminal_status, &started_at, None)
1404 .map_err(|error| error.to_string())?;
1405 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1406 cleanup.mark_terminal();
1407 Ok(DirectJavascriptRunResult {
1408 run_id,
1409 run_directory,
1410 workspace,
1411 exit_code: execution.exit_code,
1412 assertion_calls: frontend.assertion_calls,
1413 recovered_runs,
1414 metadata,
1415 server_runs,
1416 unrun_build: project.unrun_build.clone(),
1417 })
1418}
1419
1420fn companion_run_id(run_id: &str, nth: usize) -> String {
1424 match run_id
1425 .strip_prefix("run_")
1426 .filter(|hex| hex.len() == 16)
1427 .and_then(|hex| u64::from_str_radix(hex, 16).ok())
1428 {
1429 Some(number) => format!("run_{:016x}", number.wrapping_sub(nth as u64)),
1430 None => format!("{run_id}-server-{nth}"),
1431 }
1432}
1433
1434enum Server {
1436 Python(crate::python_run::PythonServer),
1437 Ruby(crate::ruby_run::RubyServer),
1438 Go(crate::go_run::GoServer),
1439 Jvm(crate::jvm_run::JvmServer),
1440}
1441
1442impl Server {
1443 fn language(&self) -> &'static str {
1444 match self {
1445 Self::Python(_) => "Python",
1446 Self::Ruby(_) => "Ruby",
1447 Self::Go(_) => "Go",
1448 Self::Jvm(_) => "Java/Kotlin",
1449 }
1450 }
1451
1452 fn source_files(&self) -> usize {
1453 match self {
1454 Self::Python(server) => server.source_files(),
1455 Self::Ruby(server) => server.source_files(),
1456 Self::Go(server) => server.source_files(),
1457 Self::Jvm(server) => server.source_files(),
1458 }
1459 }
1460
1461 fn environment(&self) -> BTreeMap<String, String> {
1462 match self {
1463 Self::Python(server) => server.environment(),
1464 Self::Ruby(server) => server.environment(),
1465 Self::Go(server) => server.environment(),
1466 Self::Jvm(server) => server.environment(),
1467 }
1468 }
1469
1470 fn publish(
1471 self,
1472 root: &Path,
1473 evidence_run_id: &str,
1474 metadata: RunMetadata,
1475 served: &[crate::served_tests::ServedTest],
1476 diagnostics: &mut dyn std::io::Write,
1477 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1478 match self {
1479 Self::Python(server) => {
1480 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1481 }
1482 Self::Ruby(server) => {
1483 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1484 }
1485 Self::Go(server) => {
1486 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1487 }
1488 Self::Jvm(server) => {
1489 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1490 }
1491 }
1492 }
1493}
1494
1495#[cfg(test)]
1496mod environment_tests {
1497 use super::*;
1498
1499 #[test]
1502 fn a_companion_run_id_is_public_and_sorts_after_its_run() {
1503 for run in [
1504 "run_0123456789abcdef",
1505 "run_ffffffffffffffff",
1506 "run_8000000000000000",
1507 ] {
1508 let companion = companion_run_id(run, 1);
1509 assert_eq!(companion.len(), 20);
1510 assert!(companion.starts_with("run_"));
1511 assert!(companion.as_str() < run, "{companion} >= {run}");
1512 assert_eq!(companion, companion_run_id(run, 1));
1513 assert!(companion_run_id(run, 2) < companion);
1514 }
1515 assert_eq!(companion_run_id("rust-1", 1), "rust-1-server-1");
1516 }
1517
1518 #[test]
1519 fn nested_npm_drops_only_pnpm_derived_environment_aliases() {
1520 let mut environment = BTreeMap::from([
1521 ("npm_config_auto_install_peers".into(), "true".into()),
1522 ("NPM_CONFIG_SHAMEFULLY_HOIST".into(), "true".into()),
1523 (
1524 "npm_config_registry".into(),
1525 "https://registry.npmjs.org".into(),
1526 ),
1527 ("USER_VALUE".into(), "kept".into()),
1528 ]);
1529 remove_derived_pnpm_config(&mut environment);
1530 assert_eq!(
1531 environment,
1532 BTreeMap::from([
1533 ("USER_VALUE".into(), "kept".into()),
1534 (
1535 "npm_config_registry".into(),
1536 "https://registry.npmjs.org".into()
1537 ),
1538 ])
1539 );
1540 }
1541}