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}
73
74#[derive(Debug)]
75pub enum DirectJavascriptRunError {
76 Interrupted {
77 signal: ForwardedSignal,
78 exit_code: i32,
79 timings: RunTimings,
80 total_ms: f64,
81 },
82 Failed(String),
83}
84
85impl std::fmt::Display for DirectJavascriptRunError {
86 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::Interrupted { signal, .. } => {
89 write!(formatter, "interrupted by {}", signal_name(*signal))
90 }
91 Self::Failed(message) => formatter.write_str(message),
92 }
93 }
94}
95
96impl std::error::Error for DirectJavascriptRunError {}
97
98impl From<String> for DirectJavascriptRunError {
99 fn from(value: String) -> Self {
100 Self::Failed(value)
101 }
102}
103
104fn signal_name(signal: ForwardedSignal) -> &'static str {
105 match signal {
106 ForwardedSignal::Sighup => "SIGHUP",
107 ForwardedSignal::Sigint => "SIGINT",
108 ForwardedSignal::Sigterm => "SIGTERM",
109 }
110}
111
112fn javascript_runner_declaration(
113 runner: String,
114 results: &[&RawTestResult],
115) -> FrontendRunnerDeclaration {
116 let has_observations = |raw: &&RawTestResult| {
117 !raw.phases.is_empty()
118 || !raw.server.is_empty()
119 || raw.runtime.iter().chain(&raw.browser).any(|snapshot| {
120 !snapshot.decisions.is_empty()
121 || !snapshot.hits.is_empty()
122 || !snapshot.events.is_empty()
123 })
124 };
125 let exact_test = results.iter().all(|raw| {
126 raw.test_id
127 .as_deref()
128 .is_some_and(|value| !value.is_empty())
129 && raw
130 .scope
131 .as_ref()
132 .is_none_or(|scope| raw.test_id.as_deref() == Some(scope.test_id.as_str()))
133 });
134 let exact_worker = results.iter().all(|raw| {
135 !has_observations(raw)
136 || raw
137 .scope
138 .as_ref()
139 .is_some_and(|scope| !scope.worker_id.is_empty())
140 });
141 let exact_retry = results.iter().all(|raw| {
142 raw.retry.is_some()
143 && raw
144 .scope
145 .as_ref()
146 .is_none_or(|scope| raw.retry == Some(scope.retry))
147 });
148 let contextual = !results.is_empty() && exact_test && exact_worker && exact_retry;
149 let precision = |exact| {
150 if exact {
151 AttributionPrecision::Exact
152 } else {
153 AttributionPrecision::Unavailable
154 }
155 };
156 let attribution = FrontendAttribution {
157 run: AttributionPrecision::Exact,
158 worker: precision(contextual),
159 test: precision(contextual),
160 retry: precision(contextual),
161 phase: precision(contextual),
162 action: precision(contextual),
163 assertion: precision(contextual),
164 };
165 let mut limitations = Vec::new();
166 if !contextual {
167 for (suffix, scope) in [
168 ("worker", FrontendLimitationScope::Worker),
169 ("test", FrontendLimitationScope::Test),
170 ("retry", FrontendLimitationScope::Retry),
171 ("phase", FrontendLimitationScope::Phase),
172 ("action", FrontendLimitationScope::Action),
173 ("assertion", FrontendLimitationScope::Assertion),
174 ] {
175 limitations.push(FrontendLimitation {
176 id: format!("{runner}-no-{suffix}").replace(':', "-"),
177 scopes: vec![scope],
178 reason: format!(
179 "Runner {runner} did not expose exact {suffix} identity for every result"
180 ),
181 });
182 }
183 }
184 FrontendRunnerDeclaration {
185 runner,
186 execution_model: if contextual {
187 ExecutionModel::ParallelContextPropagated
188 } else {
189 ExecutionModel::ParallelUnattributed
190 },
191 attribution,
192 limitations,
193 }
194}
195
196fn javascript_archive_entries(
197 mut entries: Vec<EvidenceArchiveEntry>,
198 manifest: &crate::javascript_frontend::JavascriptManifest,
199 run_id: &str,
200 exit_code: i32,
201) -> Result<Vec<EvidenceArchiveEntry>, String> {
202 let mut results = Vec::new();
203 for entry in &entries {
204 if entry.path == "mcdc.json" || entry.path.ends_with("/mcdc.json") {
205 results.push(
206 serde_json::from_slice::<RawTestResult>(&entry.contents)
207 .map_err(|error| format!("invalid {}: {error}", entry.path))?,
208 );
209 } else if entry.path == "mcdc.jsonl" || entry.path.ends_with(".mcdc.jsonl") {
210 let contents = entry
211 .contents
212 .strip_suffix(b"\n")
213 .ok_or_else(|| format!("{} does not end with a newline", entry.path))?;
214 for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
215 if line.is_empty() {
216 return Err(format!(
217 "{} contains a blank record at line {}",
218 entry.path,
219 index + 1
220 ));
221 }
222 results.push(
223 serde_json::from_slice::<RawTestResult>(line).map_err(|error| {
224 format!(
225 "invalid {} record at line {}: {error}",
226 entry.path,
227 index + 1
228 )
229 })?,
230 );
231 }
232 }
233 }
234 if results.is_empty() {
235 let status = match exit_code {
236 0 => "passed",
237 supercov_contracts::COMMAND_TIMEOUT_EXIT_CODE => "timedOut",
238 _ => "failed",
239 };
240 let command_result = RawTestResult {
241 test_id: Some(format!("command:{run_id}")),
242 scope: None,
243 test: "Test command".into(),
244 test_file: None,
245 title: Some("Test command".into()),
246 retry: Some(0),
247 status: Some(status.into()),
248 expected_status: None,
249 flaky: false,
250 provenance: crate::coverage_report::TestProvenance {
251 runner: "command".into(),
252 kind: "setup".into(),
253 project: None,
254 source: "engine".into(),
255 },
256 role: "setup".into(),
257 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
258 phases: vec![],
259 runtime: vec![],
260 browser: vec![],
261 server: vec![],
262 };
263 entries.push(EvidenceArchiveEntry {
264 path: "results/command/mcdc.json".into(),
265 contents: serde_json::to_vec(&command_result).map_err(|error| error.to_string())?,
266 });
267 results.push(command_result);
268 }
269 let mut by_runner = BTreeMap::<String, Vec<&RawTestResult>>::new();
270 for result in &results {
271 by_runner
272 .entry(result.provenance.runner.clone())
273 .or_default()
274 .push(result);
275 }
276 if entries
277 .iter()
278 .any(|entry| entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl"))
279 {
280 by_runner.entry("background".into()).or_default();
281 }
282 let declaration = FrontendRunDeclaration {
283 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
284 frontend_id: "javascript".into(),
285 frontend_version: "javascript-owned-v1".into(),
286 language: "javascript".into(),
287 structural_source: StructuralSource::OwnedProbes,
288 runners: by_runner
289 .into_iter()
290 .map(|(runner, results)| javascript_runner_declaration(runner, &results))
291 .collect(),
292 structural_limitations: manifest
293 .limitations
294 .iter()
295 .map(|limitation| limitation.id.clone())
296 .collect(),
297 };
298 entries.push(EvidenceArchiveEntry {
299 path: "coverage-model.json".into(),
300 contents: serde_json::to_vec(
301 &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
302 .expect("JavaScript coverage model is contract-valid"),
303 )
304 .map_err(|error| error.to_string())?,
305 });
306 entries.push(EvidenceArchiveEntry {
307 path: "frontend.json".into(),
308 contents: serde_json::to_vec(&declaration).map_err(|error| error.to_string())?,
309 });
310 Ok(entries)
311}
312
313struct RunCleanup {
314 root: PathBuf,
315 run_id: String,
316 started_at: String,
317 lock: ProjectLock,
318 workspace: Option<PathBuf>,
319 state_written: bool,
320 terminal_recorded: bool,
321 keep_workspace: bool,
324}
325
326impl RunCleanup {
327 fn lock(&self) -> &ProjectLock {
328 &self.lock
329 }
330
331 fn set_workspace(&mut self, workspace: PathBuf) {
332 self.workspace = Some(workspace);
333 }
334
335 fn mark_state_written(&mut self) {
336 self.state_written = true;
337 }
338
339 fn mark_terminal(&mut self) {
340 self.terminal_recorded = true;
341 }
342}
343
344impl Drop for RunCleanup {
345 fn drop(&mut self) {
346 if self.state_written && !self.terminal_recorded {
347 let _ = update_run_state(
348 &self.root,
349 &self.run_id,
350 RunStateStatus::Failed,
351 &self.started_at,
352 Some("Rust run exited before reaching a terminal lifecycle state".into()),
353 );
354 }
355 if let Some(workspace) = &self.workspace {
356 let _ = remove_stored_tree_deferred(
357 &self.root,
358 &workspace.join(".supercov/evidence").join(&self.run_id),
359 );
360 let _ = remove_stored_tree_deferred(
361 &self.root,
362 &workspace
363 .join(".supercov/server-evidence")
364 .join(&self.run_id),
365 );
366 let keep_workspace = self.keep_workspace
367 || std::env::var("SUPERCOV_KEEP_WORKSPACE").is_ok_and(|value| !value.is_empty());
368 if !keep_workspace {
369 let _ = prune_cached_workspace_sources(&self.root, &self.lock);
370 }
371 }
372 let _ = self.lock.release();
373 }
374}
375
376fn now_nonce() -> u128 {
377 SystemTime::now()
378 .duration_since(UNIX_EPOCH)
379 .unwrap_or_default()
380 .as_millis()
381}
382
383fn elapsed_ms(started: Instant) -> f64 {
384 started.elapsed().as_secs_f64() * 1000.0
385}
386
387fn rounded_millisecond(value: f64) -> f64 {
388 (value * 10.0).round() / 10.0
389}
390
391fn supervision_options() -> Result<SupervisionOptions, String> {
392 let defaults = SupervisionOptions::default();
393 Ok(SupervisionOptions {
394 diagnostic_interval: positive_milliseconds(
395 std::env::var("SUPERCOV_DIAGNOSTIC_INTERVAL_MS")
396 .ok()
397 .as_deref(),
398 "SUPERCOV_DIAGNOSTIC_INTERVAL_MS",
399 )
400 .map_err(|error| error.to_string())?
401 .unwrap_or(defaults.diagnostic_interval),
402 timeout: positive_milliseconds(
403 std::env::var("SUPERCOV_COMMAND_TIMEOUT_MS").ok().as_deref(),
404 "SUPERCOV_COMMAND_TIMEOUT_MS",
405 )
406 .map_err(|error| error.to_string())?,
407 termination_grace: defaults.termination_grace,
408 })
409}
410
411fn remove_derived_pnpm_config(environment: &mut BTreeMap<OsString, OsString>) {
412 const PNPM_ONLY_NPM_CONFIG: &[&str] = &[
418 "npm_config_auto_install_peers",
419 "npm_config_enable_pre_post_scripts",
420 "npm_config_shamefully_hoist",
421 ];
422 environment.retain(|key, _| {
423 let key = key.to_string_lossy().to_ascii_lowercase();
424 !PNPM_ONLY_NPM_CONFIG.contains(&key.as_str())
425 });
426}
427
428fn environment_with(values: BTreeMap<String, String>) -> Vec<(OsString, OsString)> {
429 let mut environment = std::env::vars_os().collect::<BTreeMap<_, _>>();
430 remove_derived_pnpm_config(&mut environment);
431 for (key, value) in values {
432 environment.retain(|existing, _| !existing.to_string_lossy().eq_ignore_ascii_case(&key));
433 environment.insert(key.into(), value.into());
434 }
435 environment.into_iter().collect()
436}
437
438const PLAYWRIGHT_CACHE_VARIABLE: &str = "PWTEST_CACHE_DIR";
440
441pub fn playwright_transform_cache(root: &Path) -> PathBuf {
443 crate::workspace::workspace_container(root).join("caches/playwright-transform")
444}
445
446fn node_options(preload: &Path) -> String {
447 let preload = std::path::absolute(preload).unwrap_or_else(|_| preload.to_path_buf());
453 [
459 std::env::var("NODE_OPTIONS").ok(),
460 Some("--enable-source-maps".into()),
461 Some(format!("--import={}", crate::workspace::file_url(&preload))),
462 ]
463 .into_iter()
464 .flatten()
465 .filter(|value| !value.is_empty())
466 .collect::<Vec<_>>()
467 .join(" ")
468}
469
470pub fn current_javascript_integrity(
474 root: &Path,
475 command: &[String],
476 source_roots: Option<&[String]>,
477) -> Result<RunIntegrity, String> {
478 let mut environment = std::env::vars().collect::<BTreeMap<_, _>>();
479 match source_roots.filter(|roots| !roots.is_empty()) {
484 Some(roots) => {
485 environment.insert(
486 crate::source_discovery::SOURCE_ROOTS_VARIABLE.into(),
487 roots.join(","),
488 );
489 }
490 None => {
491 environment.remove(crate::source_discovery::SOURCE_ROOTS_VARIABLE);
492 }
493 }
494 let project = discover_coverage_project(root, &environment, command)
495 .map_err(|error| error.to_string())?;
496 javascript_integrity_for_project(root, &project)
497}
498
499fn javascript_integrity_for_project(
500 root: &Path,
501 project: &crate::project_discovery::CoverageProject,
502) -> Result<RunIntegrity, String> {
503 let frontend = FrontendIntegrityInputs::embedded_javascript();
504 create_run_integrity(root, project, &frontend).map_err(|error| error.to_string())
505}
506
507pub fn run_direct_javascript(
510 request: &DirectJavascriptRunRequest,
511 diagnostics: &mut dyn std::io::Write,
512) -> Result<DirectJavascriptRunResult, DirectJavascriptRunError> {
513 if request.command.is_empty() {
514 return Err(DirectJavascriptRunError::Failed(
515 "test command must not be empty".into(),
516 ));
517 }
518 let total_started = Instant::now();
519 let initialization_started = Instant::now();
520 let root = crate::workspace::canonicalize_simplified(&request.root)
521 .map_err(|error| format!("{}: {error}", request.root.display()))?;
522 let nonce = now_nonce();
523 let run_id = request
524 .run_id
525 .clone()
526 .unwrap_or_else(|| format!("rust-{nonce}"));
527 let started_at = request
528 .started_at
529 .clone()
530 .unwrap_or_else(|| format!("unix-ms-{nonce}"));
531 let lock =
532 ProjectLock::acquire(&root, &run_id, &started_at).map_err(|error| error.to_string())?;
533 let mut cleanup = RunCleanup {
534 root: root.clone(),
535 run_id: run_id.clone(),
536 started_at: started_at.clone(),
537 lock,
538 workspace: None,
539 state_written: false,
540 terminal_recorded: false,
541 keep_workspace: false,
542 };
543 let recovered_runs =
544 recover_abandoned_runs(&root, &started_at).map_err(|error| error.to_string())?;
545 if !recovered_runs.is_empty() {
546 writeln!(
547 diagnostics,
548 "[supercov] recovered abandoned run(s): {}",
549 recovered_runs.join(", ")
550 )
551 .map_err(|error| error.to_string())?;
552 }
553 let environment = std::env::vars().collect::<BTreeMap<_, _>>();
554 let source_roots = crate::source_discovery::configured_source_roots(&environment);
555 let project = crate::project_discovery::discover_coverage_project_of_any_size(
556 &root,
557 &environment,
558 &request.command,
559 )
560 .map_err(|error| error.to_string())?;
561 let browser_suite = project.playwright_config.is_some();
565 let serves_python = browser_suite
566 && crate::python_project::discover_python_files(&root)
567 .is_ok_and(|files| !files.sources.is_empty());
568 let serves_ruby = browser_suite
569 && crate::ruby_project::discover_ruby_files(&root)
570 .is_ok_and(|files| !files.sources.is_empty());
571 let serves_go = browser_suite
572 && (root.join("go.mod").is_file()
573 || !crate::go_project::workspace_modules(&root).is_empty());
574 let serves_jvm = browser_suite
575 && [
576 "pom.xml",
577 "build.gradle",
578 "build.gradle.kts",
579 "src/main/java",
580 "src/main/kotlin",
581 ]
582 .iter()
583 .any(|name| root.join(name).exists());
584 if project.source_files.is_empty()
585 && !serves_python
586 && !serves_ruby
587 && !serves_go
588 && !serves_jvm
589 {
590 return Err(
591 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
592 .to_string()
593 .into(),
594 );
595 }
596 let integrity = javascript_integrity_for_project(&root, &project)?;
597 let source_inputs = crate::source_capture::capture(
598 &root,
599 "javascript",
600 crate::integrity::javascript_assertion_paths(&root, &project).map_err(|e| e.to_string())?,
601 )?;
602 let frontend_cache_key = format!(
608 "{}:{}:{}:{}:{}",
609 integrity.fingerprint.combined,
610 integrity.fingerprint.execution,
611 crate::source_manifest::digest(&request.command),
612 crate::source_manifest::digest(&project.root.to_string_lossy()),
613 crate::source_manifest::digest(&(&project.source_files, &source_roots))
614 );
615 let prior_workspace =
616 cached_workspace_path(&root, cleanup.lock()).map_err(|error| error.to_string())?;
617 let reusable_frontend = read_javascript_frontend_cache(&prior_workspace, &frontend_cache_key);
618 let cached_paths = reusable_frontend
619 .as_ref()
620 .map(javascript_frontend_reuse_paths)
621 .unwrap_or_default();
622 let initialization_ms = elapsed_ms(initialization_started);
623 let prepare_servers = |workspace: PathBuf| {
627 let (root, run_id, command) = (root.clone(), run_id.clone(), request.command.clone());
628 move || -> Result<Vec<Server>, String> {
629 let mut servers = Vec::new();
630 if serves_python
631 && let Some(server) =
632 crate::python_run::PythonServer::prepare(&root, &workspace, &run_id, &command)?
633 {
634 servers.push(Server::Python(server));
635 }
636 if serves_ruby
637 && let Some(server) =
638 crate::ruby_run::RubyServer::prepare(&root, &workspace, &run_id, &command)?
639 {
640 servers.push(Server::Ruby(server));
641 }
642 Ok(servers)
643 }
644 };
645 let preparing_servers = std::thread::spawn(prepare_servers(prior_workspace.clone()));
646
647 let workspace_started = Instant::now();
648 let workspace_progress = crate::progress::ProgressLine::start("preparing isolated workspace");
649 let workspace = prepare_cached_workspace(&root, cleanup.lock(), &cached_paths)
650 .map_err(|error| error.to_string())?;
651 drop(workspace_progress);
652 cleanup.set_workspace(workspace.clone());
653 crate::lifecycle::sweep_trash_in_background(&root);
655 if let Err(error) = crate::workspace::mirror_git_repository(&root, &workspace)
656 && std::env::var_os("SUPERCOV_DEBUG").is_some()
657 {
658 eprintln!("[supercov] the workspace has no git repository of its own: {error}");
659 }
660 let workspace_preparation_ms = elapsed_ms(workspace_started);
661 writeln!(
662 diagnostics,
663 "[supercov] instrumenting isolated workspace {}",
664 workspace.display()
665 )
666 .map_err(|error| error.to_string())?;
667 cleanup.mark_state_written();
668 write_run_state(
669 &root,
670 &RunState {
671 id: run_id.clone(),
672 pid: std::process::id(),
673 root: root.display().to_string(),
674 workspace: workspace.display().to_string(),
675 started_at: started_at.clone(),
676 updated_at: started_at.clone(),
677 status: RunStateStatus::Preparing,
678 signal: None,
679 error: None,
680 },
681 )
682 .map_err(|error| error.to_string())?;
683
684 let adapter_started = Instant::now();
685 let instrumentation_progress = crate::progress::ProgressLine::start("instrumenting sources");
686 let collector_id = format!("collector-{}", integrity.fingerprint.execution);
687 let frontend = if let Some(cache) = &reusable_frontend {
688 load_cached_javascript_frontend(&workspace, cache)
689 } else {
690 prepare_javascript_frontend(
691 &workspace,
692 &project,
693 &collector_id,
694 &frontend_cache_key,
695 &request.command,
696 )
697 }
698 .map_err(|error| error.to_string())?;
699 crate::workspace::hide_rewritten_files(
700 &workspace,
701 &crate::javascript_frontend::rewritten_files(&workspace),
702 );
703 drop(instrumentation_progress);
704 let adapter_setup_ms = elapsed_ms(adapter_started);
705 if let Some(detail) = crate::javascript_frontend::setup_timing_detail() {
706 writeln!(diagnostics, "[supercov] {detail}").map_err(|error| error.to_string())?;
707 }
708
709 let mut servers = preparing_servers
710 .join()
711 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))?;
712 if workspace != prior_workspace && !servers.is_empty() {
715 servers = prepare_servers(workspace.clone())()?;
716 }
717 if serves_go
719 && let Some(server) =
720 crate::go_run::GoServer::prepare(&root, &workspace, &run_id, &request.command)?
721 {
722 servers.push(Server::Go(server));
723 }
724 if serves_jvm
725 && let Some(server) =
726 crate::jvm_run::JvmServer::prepare(&root, &workspace, &run_id, &request.command)?
727 {
728 servers.push(Server::Jvm(server));
729 }
730 if project.source_files.is_empty() && servers.is_empty() {
731 return Err(
732 crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
733 .to_string()
734 .into(),
735 );
736 }
737 for server in &servers {
738 writeln!(
739 diagnostics,
740 "[supercov] measuring {} {language} source file(s) in any {language} server the tests start",
741 server.source_files(),
742 language = server.language(),
743 )
744 .map_err(|error| error.to_string())?;
745 }
746
747 let evidence_relative = format!(".supercov/evidence/{run_id}");
748 let evidence_directory = workspace.join(&evidence_relative);
749 let server_evidence_root = workspace.join(".supercov/server-evidence");
750 let diagnostic_owner = workspace.join(format!(".supercov/diagnostic-owner-{run_id}"));
751 let mut overrides: BTreeMap<String, String> = BTreeMap::from([
752 ("NODE_OPTIONS".into(), node_options(&frontend.preload_path)),
753 ("SUPERCOV_CJS_INTERCEPT".into(), "1".into()),
754 ("SUPERCOV_DIRECT_INSTRUMENTATION".into(), "1".into()),
755 ("TAP_ALLOW_INCOMPLETE_COVERAGE".into(), "1".into()),
760 (
764 "SUPERCOV_EVIDENCE_DIR".into(),
765 evidence_directory.display().to_string(),
766 ),
767 (
768 "SUPERCOV_DIAGNOSTIC_OWNER_FILE".into(),
769 diagnostic_owner.display().to_string(),
770 ),
771 (
772 "SUPERCOV_EXECUTION_FINGERPRINT".into(),
773 integrity.fingerprint.execution.clone(),
774 ),
775 (
776 "SUPERCOV_EXECUTION_LOG".into(),
777 evidence_directory
778 .join("execution.jsonl")
779 .display()
780 .to_string(),
781 ),
782 (
783 "SUPERCOV_MANIFEST".into(),
784 frontend.manifest_path.display().to_string(),
785 ),
786 (
787 "SUPERCOV_PROJECT_ROOT".into(),
788 workspace.display().to_string(),
789 ),
790 ("SUPERCOV_RUN_ID".into(), run_id.clone()),
791 (
792 "SUPERCOV_SERVER_EVIDENCE_ROOT".into(),
793 server_evidence_root.display().to_string(),
794 ),
795 (
796 "SUPERCOV_SOURCE_PROJECT_ROOT".into(),
797 root.display().to_string(),
798 ),
799 ]);
800 if std::env::var_os(PLAYWRIGHT_CACHE_VARIABLE).is_none_or(|value| value.is_empty()) {
810 overrides.insert(
811 PLAYWRIGHT_CACHE_VARIABLE.into(),
812 playwright_transform_cache(&root).display().to_string(),
813 );
814 }
815 overrides.insert(
819 "SUPERCOV_GENERATED_VITEST_CONFIG".into(),
820 frontend.vitest_config_path.display().to_string(),
821 );
822 overrides.insert(
823 "SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG".into(),
824 frontend.playwright_config_path.display().to_string(),
825 );
826 overrides.insert(
827 "SUPERCOV_PLAYWRIGHT_MODULE".into(),
828 project.playwright_module.clone(),
829 );
830 overrides.insert(
831 "SUPERCOV_PLAYWRIGHT_TEST_EXPORT".into(),
832 project.playwright_test_export.clone(),
833 );
834 overrides.insert(
835 "SUPERCOV_PLAYWRIGHT_WRAPPER".into(),
836 "./.supercov/node_modules/playwright.mjs".into(),
837 );
838 if let Some(original) = project
839 .playwright_config
840 .as_ref()
841 .and_then(|path| path.strip_prefix(&root).ok())
842 .map(|path| workspace.join(path))
843 {
844 overrides.insert(
845 "SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG".into(),
846 original.display().to_string(),
847 );
848 }
849 for server in &servers {
850 overrides.extend(server.environment());
851 }
852 if crate::workspace::links_a_virtual_environment(&workspace)
858 && std::env::var_os("UV_NO_SYNC").is_none()
859 {
860 overrides.insert("UV_NO_SYNC".into(), "1".into());
861 }
862 if let Some(path) = crate::source_tools::path_with_launchers(&workspace)
865 && let Some(path) = path.to_str()
866 {
867 overrides.insert("PATH".into(), path.to_owned());
868 }
869 let docker_state = root.join(".supercov/work").join(&run_id).join("docker");
876 let mut docker_environment = overrides
877 .keys()
878 .filter(|name| name.starts_with("SUPERCOV_"))
879 .cloned()
880 .collect::<std::collections::BTreeSet<_>>();
881 docker_environment.extend(
882 servers
883 .iter()
884 .flat_map(|server| server.environment().into_keys()),
885 );
886 let docker_environment = docker_environment.into_iter().collect::<Vec<_>>();
887 let docker_servers = !project.source_files.is_empty() || !servers.is_empty();
888 if cfg!(unix) && docker_servers {
889 let launchers = workspace.join(".supercov/node_modules/.docker");
890 let path = overrides
891 .get("PATH")
892 .map(std::ffi::OsString::from)
893 .or_else(|| std::env::var_os("PATH"));
894 if let Some(path) = path
895 && crate::docker_servers::write_launchers(&launchers, &path).is_some()
896 && let Ok(joined) =
897 std::env::join_paths(std::iter::once(launchers).chain(std::env::split_paths(&path)))
898 && let Some(joined) = joined.to_str()
899 {
900 overrides.insert("PATH".into(), joined.to_owned());
901 overrides.insert(
902 crate::docker_servers::STATE_VARIABLE.into(),
903 docker_state.display().to_string(),
904 );
905 overrides.insert(
906 crate::docker_servers::MOUNT_VARIABLE.into(),
907 root.join(".supercov").display().to_string(),
908 );
909 overrides.insert(
910 crate::docker_servers::COPY_VARIABLE.into(),
911 workspace.display().to_string(),
912 );
913 overrides.insert(
914 crate::docker_servers::ENVIRONMENT_VARIABLE.into(),
915 docker_environment.join(","),
916 );
917 }
918 }
919 let settings = serde_json::json!({
923 "root": workspace.display().to_string(),
924 "environment": overrides
925 .iter()
926 .filter(|(name, _)| name.starts_with("SUPERCOV_"))
927 .collect::<BTreeMap<_, _>>(),
928 });
929 let settings_path = frontend.preload_path.with_file_name("run.json");
930 std::fs::write(
931 &settings_path,
932 serde_json::to_vec(&settings).map_err(|error| error.to_string())?,
933 )
934 .map_err(|error| format!("{}: {error}", settings_path.display()))?;
935 let _ = crate::source_tools::clear_source_changes(&workspace);
938 let plan = ExecutionPlan {
940 preparation: Vec::new(),
941 test: ExecutionPhase {
942 name: "test".into(),
943 kind: PhaseKind::Test,
944 command: CommandSpec {
945 program: request.command[0].clone().into(),
946 arguments: request.command[1..].iter().map(OsString::from).collect(),
947 cwd: workspace.clone(),
948 environment: Some(environment_with(overrides)),
949 captured_output: None,
950 console: None,
951 },
952 },
953 };
954 let options = supervision_options()?;
955 let watchdog_program = request.watchdog_program.as_ref().ok_or_else(|| {
956 DirectJavascriptRunError::Failed(
957 "the JavaScript run is missing its crash-containment executable".into(),
958 )
959 })?;
960 let supervisor = ProcessSupervisor::new_crash_safe(watchdog_program)
961 .map_err(|error| DirectJavascriptRunError::Failed(error.to_string()))?;
962 let output_baseline = std::cell::RefCell::new(None);
963 let execution = match execute_plan_with_supervisor(
964 &supervisor,
965 &plan,
966 options,
967 diagnostics,
968 |phase, diagnostics| {
969 {
970 *output_baseline.borrow_mut() =
974 Some(workspace_output_baseline(&workspace).map_err(|error| {
975 OrchestrationError::PhaseSetup {
976 phase: phase.name.clone(),
977 reason: error.to_string(),
978 }
979 })?);
980 if frontend.assertion_calls > 0 {
981 writeln!(
982 diagnostics,
983 "[supercov] attributed {} native node:assert call(s)",
984 frontend.assertion_calls
985 )
986 .map_err(|error| OrchestrationError::PhaseSetup {
987 phase: phase.name.clone(),
988 reason: error.to_string(),
989 })?;
990 }
991 writeln!(
992 diagnostics,
993 "[supercov] running in isolated workspace: {}",
994 request.command.join(" ")
995 )
996 .map_err(|error| OrchestrationError::PhaseSetup {
997 phase: phase.name.clone(),
998 reason: error.to_string(),
999 })?;
1000 }
1001 update_run_state(&root, &run_id, RunStateStatus::Testing, &started_at, None).map_err(
1002 |error| OrchestrationError::PhaseSetup {
1003 phase: phase.name.clone(),
1004 reason: error.to_string(),
1005 },
1006 )?;
1007 Ok(())
1008 },
1009 ) {
1010 Ok(execution) => execution,
1011 Err(error) => {
1012 let message = error.to_string();
1013 let state_updated = update_run_state(
1014 &root,
1015 &run_id,
1016 RunStateStatus::Failed,
1017 &started_at,
1018 Some(message.clone()),
1019 )
1020 .is_ok();
1021 if state_updated {
1022 cleanup.mark_terminal();
1023 }
1024 return Err(message.into());
1025 }
1026 };
1027 crate::docker_servers::stop_started(&docker_state);
1029 let test_command_ms = execution
1030 .phases
1031 .iter()
1032 .find(|phase| phase.kind == PhaseKind::Test)
1033 .map_or(0.0, |phase| phase.duration_ms as f64);
1034 if execution.exit_code != 0
1039 && execution.interrupted_signal.is_none()
1040 && !project.unrun_build.is_empty()
1041 {
1042 let build = project.unrun_build.join(" ");
1043 let _ = writeln!(
1044 diagnostics,
1045 "[supercov] the command failed. It ran in a copy of the project that starts without build output, and Supercov runs only the command it is given: if these tests need `{build}` first, put it in the command, as in `supercov -- sh -c \"{build} && {}\"`",
1046 request.command.join(" ")
1047 );
1048 }
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() {
1146 Vec::new()
1147 } else {
1148 crate::served_tests::read(&evidence_directory)
1149 };
1150 if project.source_files.is_empty() {
1151 let metadata = RunMetadata {
1154 id: run_id.clone(),
1155 started_at: started_at.clone(),
1156 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1157 command: request.command.clone(),
1158 test_exit_code: Some(execution.exit_code),
1159 integrity: integrity.clone(),
1160 raw_evidence: RawEvidenceMetadata::default(),
1161 isolated_build: Some(true),
1162 instrumented_build_cache: None,
1163 timings: Some(RunTimings {
1164 initialization_ms: rounded_millisecond(initialization_ms),
1165 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1166 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1167 instrumented_build_ms: 0.0,
1168 test_command_ms: rounded_millisecond(test_command_ms),
1169 evidence_publication_ms: 0.0,
1170 }),
1171 merged: None,
1172 parents: None,
1173 source_roots: source_roots.clone(),
1174 copy: Some(cleanup.lock().copy()),
1175 merged_across: None,
1176 recorded_by: None,
1177 companions: None,
1178 };
1179 let mut published = None;
1180 let mut server_runs = Vec::new();
1181 let mut failure = None;
1182 for server in servers {
1183 let id = if published.is_none() {
1184 run_id.clone()
1185 } else {
1186 companion_run_id(&run_id, server_runs.len() + 1)
1187 };
1188 let language = server.language();
1189 match server.publish(
1190 &root,
1191 &run_id,
1192 RunMetadata {
1193 id: id.clone(),
1194 ..metadata.clone()
1195 },
1196 &served,
1197 diagnostics,
1198 ) {
1199 Ok(Some(run)) if published.is_none() => published = Some(run),
1200 Ok(Some((directory, _))) => {
1201 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1202 server_runs.push((language.to_owned(), directory));
1203 }
1204 Ok(None) => {}
1205 Err(error) => {
1206 let _ = writeln!(
1207 diagnostics,
1208 "[supercov] the {language} server's run could not be published: {error}"
1209 );
1210 failure.get_or_insert(error);
1211 }
1212 }
1213 }
1214 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1215 .map_err(|error| error.to_string())?;
1216 remove_stored_tree_deferred(&root, &server_evidence_root)
1217 .map_err(|error| error.to_string())?;
1218 if published.is_none()
1219 && let Some(error) = failure
1220 {
1221 return Err(error.into());
1222 }
1223 let Some((run_directory, metadata)) = published else {
1224 let message = if execution.exit_code != 0 && served.is_empty() {
1225 format!(
1226 "the test command exited {} before any test reported an outcome; its own output above says why, and no run was published",
1227 execution.exit_code
1228 )
1229 } else {
1230 "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()
1231 };
1232 let _ = update_run_state(
1233 &root,
1234 &run_id,
1235 RunStateStatus::Failed,
1236 &started_at,
1237 Some(message.clone()),
1238 );
1239 cleanup.mark_terminal();
1240 return Err(message.into());
1241 };
1242 update_run_state(
1243 &root,
1244 &run_id,
1245 if execution.exit_code == 0 {
1246 RunStateStatus::Complete
1247 } else {
1248 RunStateStatus::Failed
1249 },
1250 &started_at,
1251 None,
1252 )
1253 .map_err(|error| error.to_string())?;
1254 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1255 cleanup.mark_terminal();
1256 return Ok(DirectJavascriptRunResult {
1257 run_id,
1258 run_directory,
1259 workspace,
1260 exit_code: execution.exit_code,
1261 assertion_calls: frontend.assertion_calls,
1262 recovered_runs,
1263 metadata,
1264 server_runs,
1265 });
1266 }
1267 let archive_path = root
1268 .join(".supercov/work")
1269 .join(&run_id)
1270 .join("evidence.raw.gz");
1271 let entries = collect_sources(&[
1272 EvidenceArchiveSource::File {
1273 file: frontend.manifest_path,
1274 path: "manifest.json".into(),
1275 },
1276 EvidenceArchiveSource::File {
1277 file: workspace.join(".supercov/statement-exclusions.json"),
1278 path: "statement-exclusions.json".into(),
1279 },
1280 EvidenceArchiveSource::Directory {
1281 directory: evidence_directory,
1282 prefix: None,
1283 },
1284 EvidenceArchiveSource::Directory {
1285 directory: server_evidence_root.join(&run_id),
1286 prefix: Some("server".into()),
1287 },
1288 ])
1289 .map_err(|error| error.to_string())?;
1290 let entries =
1291 javascript_archive_entries(entries, &frontend.manifest, &run_id, execution.exit_code)?;
1292 let entries = crate::source_capture::append(entries, &source_inputs)?;
1293 let raw = write_archive(entries, &archive_path).map_err(|error| error.to_string())?;
1294 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1295 .map_err(|error| error.to_string())?;
1296 remove_stored_tree_deferred(&root, &server_evidence_root).map_err(|error| error.to_string())?;
1297 let evidence_publication_ms = elapsed_ms(publication_started);
1298 let timings = RunTimings {
1299 initialization_ms: rounded_millisecond(initialization_ms),
1300 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1301 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1302 instrumented_build_ms: 0.0,
1303 test_command_ms: rounded_millisecond(test_command_ms),
1304 evidence_publication_ms: rounded_millisecond(evidence_publication_ms),
1305 };
1306 let metadata = RunMetadata {
1307 id: run_id.clone(),
1308 started_at: started_at.clone(),
1309 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1310 command: request.command.clone(),
1311 test_exit_code: Some(execution.exit_code),
1312 integrity,
1313 raw_evidence: RawEvidenceMetadata {
1314 schema_version: raw.schema_version,
1315 format: raw.format.into(),
1316 file: raw.file.into(),
1317 files: raw.files,
1318 uncompressed_bytes: raw.uncompressed_bytes,
1319 compressed_bytes: raw.compressed_bytes,
1320 },
1321 isolated_build: Some(true),
1322 instrumented_build_cache: None,
1323 timings: Some(timings),
1324 merged: None,
1325 parents: None,
1326 source_roots: source_roots.clone(),
1327 copy: Some(cleanup.lock().copy()),
1328 merged_across: None,
1329 recorded_by: None,
1330 companions: None,
1331 };
1332 let mut server_runs = Vec::new();
1336 let mut companions = Vec::new();
1337 for server in servers {
1338 let id = companion_run_id(&run_id, server_runs.len() + 1);
1339 let language = server.language();
1340 let server_metadata = RunMetadata {
1341 id: id.clone(),
1342 companions: Some(vec![crate::run_store::CompanionRun {
1343 id: run_id.clone(),
1344 language: "JavaScript".into(),
1345 role: "browser".into(),
1346 }]),
1347 ..metadata.clone()
1348 };
1349 match server.publish(&root, &run_id, server_metadata, &served, diagnostics) {
1350 Ok(Some((directory, _))) => {
1351 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1352 server_runs.push((language.to_owned(), directory));
1353 companions.push(crate::run_store::CompanionRun {
1354 id,
1355 language: language.into(),
1356 role: "server".into(),
1357 });
1358 }
1359 Ok(None) => {}
1360 Err(error) => {
1362 let _ = writeln!(
1363 diagnostics,
1364 "[supercov] the {language} server's run could not be published: {error}"
1365 );
1366 }
1367 }
1368 }
1369 let metadata = RunMetadata {
1370 companions: (!companions.is_empty()).then_some(companions),
1371 ..metadata
1372 };
1373 let run_directory =
1374 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1375 let terminal_status = if execution.exit_code == 0 {
1376 RunStateStatus::Complete
1377 } else {
1378 RunStateStatus::Failed
1379 };
1380 update_run_state(&root, &run_id, terminal_status, &started_at, None)
1381 .map_err(|error| error.to_string())?;
1382 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1383 cleanup.mark_terminal();
1384 Ok(DirectJavascriptRunResult {
1385 run_id,
1386 run_directory,
1387 workspace,
1388 exit_code: execution.exit_code,
1389 assertion_calls: frontend.assertion_calls,
1390 recovered_runs,
1391 metadata,
1392 server_runs,
1393 })
1394}
1395
1396fn companion_run_id(run_id: &str, nth: usize) -> String {
1400 match run_id
1401 .strip_prefix("run_")
1402 .filter(|hex| hex.len() == 16)
1403 .and_then(|hex| u64::from_str_radix(hex, 16).ok())
1404 {
1405 Some(number) => format!("run_{:016x}", number.wrapping_sub(nth as u64)),
1406 None => format!("{run_id}-server-{nth}"),
1407 }
1408}
1409
1410enum Server {
1412 Python(crate::python_run::PythonServer),
1413 Ruby(crate::ruby_run::RubyServer),
1414 Go(crate::go_run::GoServer),
1415 Jvm(crate::jvm_run::JvmServer),
1416}
1417
1418impl Server {
1419 fn language(&self) -> &'static str {
1420 match self {
1421 Self::Python(_) => "Python",
1422 Self::Ruby(_) => "Ruby",
1423 Self::Go(_) => "Go",
1424 Self::Jvm(_) => "Java/Kotlin",
1425 }
1426 }
1427
1428 fn source_files(&self) -> usize {
1429 match self {
1430 Self::Python(server) => server.source_files(),
1431 Self::Ruby(server) => server.source_files(),
1432 Self::Go(server) => server.source_files(),
1433 Self::Jvm(server) => server.source_files(),
1434 }
1435 }
1436
1437 fn environment(&self) -> BTreeMap<String, String> {
1438 match self {
1439 Self::Python(server) => server.environment(),
1440 Self::Ruby(server) => server.environment(),
1441 Self::Go(server) => server.environment(),
1442 Self::Jvm(server) => server.environment(),
1443 }
1444 }
1445
1446 fn publish(
1447 self,
1448 root: &Path,
1449 evidence_run_id: &str,
1450 metadata: RunMetadata,
1451 served: &[crate::served_tests::ServedTest],
1452 diagnostics: &mut dyn std::io::Write,
1453 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1454 match self {
1455 Self::Python(server) => {
1456 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1457 }
1458 Self::Ruby(server) => {
1459 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1460 }
1461 Self::Go(server) => {
1462 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1463 }
1464 Self::Jvm(server) => {
1465 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1466 }
1467 }
1468 }
1469}
1470
1471#[cfg(test)]
1472mod environment_tests {
1473 use super::*;
1474
1475 #[test]
1478 fn a_companion_run_id_is_public_and_sorts_after_its_run() {
1479 for run in [
1480 "run_0123456789abcdef",
1481 "run_ffffffffffffffff",
1482 "run_8000000000000000",
1483 ] {
1484 let companion = companion_run_id(run, 1);
1485 assert_eq!(companion.len(), 20);
1486 assert!(companion.starts_with("run_"));
1487 assert!(companion.as_str() < run, "{companion} >= {run}");
1488 assert_eq!(companion, companion_run_id(run, 1));
1489 assert!(companion_run_id(run, 2) < companion);
1490 }
1491 assert_eq!(companion_run_id("rust-1", 1), "rust-1-server-1");
1492 }
1493
1494 #[test]
1495 fn nested_npm_drops_only_pnpm_derived_environment_aliases() {
1496 let mut environment = BTreeMap::from([
1497 ("npm_config_auto_install_peers".into(), "true".into()),
1498 ("NPM_CONFIG_SHAMEFULLY_HOIST".into(), "true".into()),
1499 (
1500 "npm_config_registry".into(),
1501 "https://registry.npmjs.org".into(),
1502 ),
1503 ("USER_VALUE".into(), "kept".into()),
1504 ]);
1505 remove_derived_pnpm_config(&mut environment);
1506 assert_eq!(
1507 environment,
1508 BTreeMap::from([
1509 ("USER_VALUE".into(), "kept".into()),
1510 (
1511 "npm_config_registry".into(),
1512 "https://registry.npmjs.org".into()
1513 ),
1514 ])
1515 );
1516 }
1517}