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