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 let support_directory = workspace.join(".supercov/support").join(&run_id);
866 if project.playwright_config.is_some() {
867 overrides.insert(
868 crate::support_reach::DIRECTORY_VARIABLE.into(),
869 support_directory.display().to_string(),
870 );
871 }
872 if let Some(path) = crate::source_tools::path_with_launchers(&workspace)
875 && let Some(path) = path.to_str()
876 {
877 overrides.insert("PATH".into(), path.to_owned());
878 }
879 let docker_state = root.join(".supercov/work").join(&run_id).join("docker");
886 let mut docker_environment = overrides
887 .keys()
888 .filter(|name| name.starts_with("SUPERCOV_"))
889 .cloned()
890 .collect::<std::collections::BTreeSet<_>>();
891 docker_environment.extend(
892 servers
893 .iter()
894 .flat_map(|server| server.environment().into_keys()),
895 );
896 let docker_environment = docker_environment.into_iter().collect::<Vec<_>>();
897 let docker_servers = !project.source_files.is_empty() || !servers.is_empty();
898 if cfg!(unix) && docker_servers {
899 let launchers = workspace.join(".supercov/node_modules/.docker");
900 let path = overrides
901 .get("PATH")
902 .map(std::ffi::OsString::from)
903 .or_else(|| std::env::var_os("PATH"));
904 if let Some(path) = path
905 && crate::docker_servers::write_launchers(&launchers, &path).is_some()
906 && let Ok(joined) =
907 std::env::join_paths(std::iter::once(launchers).chain(std::env::split_paths(&path)))
908 && let Some(joined) = joined.to_str()
909 {
910 overrides.insert("PATH".into(), joined.to_owned());
911 overrides.insert(
912 crate::docker_servers::STATE_VARIABLE.into(),
913 docker_state.display().to_string(),
914 );
915 overrides.insert(
916 crate::docker_servers::MOUNT_VARIABLE.into(),
917 root.join(".supercov").display().to_string(),
918 );
919 overrides.insert(
920 crate::docker_servers::COPY_VARIABLE.into(),
921 workspace.display().to_string(),
922 );
923 overrides.insert(
924 crate::docker_servers::ENVIRONMENT_VARIABLE.into(),
925 docker_environment.join(","),
926 );
927 }
928 }
929 let settings = serde_json::json!({
933 "root": workspace.display().to_string(),
934 "environment": overrides
935 .iter()
936 .filter(|(name, _)| name.starts_with("SUPERCOV_"))
937 .collect::<BTreeMap<_, _>>(),
938 });
939 let settings_path = frontend.preload_path.with_file_name("run.json");
940 std::fs::write(
941 &settings_path,
942 serde_json::to_vec(&settings).map_err(|error| error.to_string())?,
943 )
944 .map_err(|error| format!("{}: {error}", settings_path.display()))?;
945 let _ = crate::source_tools::clear_source_changes(&workspace);
948 let plan = ExecutionPlan {
950 preparation: Vec::new(),
951 test: ExecutionPhase {
952 name: "test".into(),
953 kind: PhaseKind::Test,
954 command: CommandSpec {
955 program: request.command[0].clone().into(),
956 arguments: request.command[1..].iter().map(OsString::from).collect(),
957 cwd: workspace.clone(),
958 environment: Some(environment_with(overrides)),
959 captured_output: None,
960 console: None,
961 },
962 },
963 };
964 let options = supervision_options()?;
965 let watchdog_program = request.watchdog_program.as_ref().ok_or_else(|| {
966 DirectJavascriptRunError::Failed(
967 "the JavaScript run is missing its crash-containment executable".into(),
968 )
969 })?;
970 let supervisor = ProcessSupervisor::new_crash_safe(watchdog_program)
971 .map_err(|error| DirectJavascriptRunError::Failed(error.to_string()))?;
972 let output_baseline = std::cell::RefCell::new(None);
973 let execution = match execute_plan_with_supervisor(
974 &supervisor,
975 &plan,
976 options,
977 diagnostics,
978 |phase, diagnostics| {
979 {
980 *output_baseline.borrow_mut() =
984 Some(workspace_output_baseline(&workspace).map_err(|error| {
985 OrchestrationError::PhaseSetup {
986 phase: phase.name.clone(),
987 reason: error.to_string(),
988 }
989 })?);
990 if frontend.assertion_calls > 0 {
991 writeln!(
992 diagnostics,
993 "[supercov] attributed {} native node:assert call(s)",
994 frontend.assertion_calls
995 )
996 .map_err(|error| OrchestrationError::PhaseSetup {
997 phase: phase.name.clone(),
998 reason: error.to_string(),
999 })?;
1000 }
1001 writeln!(
1002 diagnostics,
1003 "[supercov] running in isolated workspace: {}",
1004 request.command.join(" ")
1005 )
1006 .map_err(|error| OrchestrationError::PhaseSetup {
1007 phase: phase.name.clone(),
1008 reason: error.to_string(),
1009 })?;
1010 }
1011 update_run_state(&root, &run_id, RunStateStatus::Testing, &started_at, None).map_err(
1012 |error| OrchestrationError::PhaseSetup {
1013 phase: phase.name.clone(),
1014 reason: error.to_string(),
1015 },
1016 )?;
1017 Ok(())
1018 },
1019 ) {
1020 Ok(execution) => execution,
1021 Err(error) => {
1022 let message = error.to_string();
1023 let state_updated = update_run_state(
1024 &root,
1025 &run_id,
1026 RunStateStatus::Failed,
1027 &started_at,
1028 Some(message.clone()),
1029 )
1030 .is_ok();
1031 if state_updated {
1032 cleanup.mark_terminal();
1033 }
1034 return Err(message.into());
1035 }
1036 };
1037 crate::docker_servers::stop_started(&docker_state);
1039 let test_command_ms = execution
1040 .phases
1041 .iter()
1042 .find(|phase| phase.kind == PhaseKind::Test)
1043 .map_or(0.0, |phase| phase.duration_ms as f64);
1044 if execution.exit_code != 0
1049 && execution.interrupted_signal.is_none()
1050 && !project.unrun_build.is_empty()
1051 {
1052 let build = project.unrun_build.join(" ");
1053 let _ = writeln!(
1054 diagnostics,
1055 "[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} && {}\"`",
1056 request.command.join(" ")
1057 );
1058 }
1059 if let Some(signal) = execution.interrupted_signal {
1060 interrupt_run_state(&root, &run_id, &started_at, signal_name(signal))
1061 .map_err(|error| error.to_string())?;
1062 cleanup.mark_terminal();
1063 return Err(DirectJavascriptRunError::Interrupted {
1064 signal,
1065 exit_code: execution.exit_code,
1066 timings: RunTimings {
1067 initialization_ms: rounded_millisecond(initialization_ms),
1068 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1069 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1070 instrumented_build_ms: 0.0,
1071 test_command_ms: rounded_millisecond(test_command_ms),
1072 evidence_publication_ms: 0.0,
1073 },
1074 total_ms: rounded_millisecond(elapsed_ms(total_started)),
1075 });
1076 }
1077 if let Some(baseline) = output_baseline.borrow().as_ref() {
1082 let protected = project
1083 .source_files
1084 .iter()
1085 .map(PathBuf::from)
1086 .collect::<std::collections::BTreeSet<_>>();
1087 let outputs = sync_command_outputs(&root, &workspace, baseline, &protected)
1088 .map_err(|error| error.to_string())?;
1089 if outputs.synced > 0 {
1090 let _ = writeln!(
1091 diagnostics,
1092 "[supercov] synced {} file(s) the command created or changed back to the project: {}",
1093 outputs.synced,
1094 crate::workspace::summarize_paths(&outputs.synced_paths)
1095 );
1096 }
1097 if !outputs.skipped_instrumented.is_empty() {
1098 let _ = writeln!(
1099 diagnostics,
1100 "[supercov] {} file(s) stayed in the isolated workspace: they are instrumented copies, or were built from them, and must not overwrite your project: {}",
1101 outputs.skipped_instrumented.len(),
1102 crate::workspace::summarize_paths(&outputs.skipped_instrumented)
1103 );
1104 }
1105 match crate::source_tools::apply_source_changes(&root, &workspace) {
1110 Ok(changes) => {
1111 if !changes.applied.is_empty() {
1112 let _ = writeln!(
1113 diagnostics,
1114 "[supercov] the command's tools changed {} source file(s), now changed in the project too: {}; this run measured them as they were before",
1115 changes.applied.len(),
1116 crate::workspace::summarize_paths(&changes.applied)
1117 );
1118 }
1119 if !changes.kept_back.is_empty() {
1120 let _ = writeln!(
1121 diagnostics,
1122 "[supercov] the command's tools changed {} source file(s) that were edited in the project while it ran; those changes were not applied: {}",
1123 changes.kept_back.len(),
1124 crate::workspace::summarize_paths(&changes.kept_back)
1125 );
1126 }
1127 }
1128 Err(error) => {
1129 let _ = writeln!(
1130 diagnostics,
1131 "[supercov] the source changes the command's tools made could not be applied to the project: {error}"
1132 );
1133 }
1134 }
1135 if !outputs.deleted_in_workspace.is_empty() {
1136 let _ = writeln!(
1137 diagnostics,
1138 "[supercov] the command deleted {} file(s) in the isolated workspace; deletions are not propagated to the project: {}",
1139 outputs.deleted_in_workspace.len(),
1140 crate::workspace::summarize_paths(&outputs.deleted_in_workspace)
1141 );
1142 }
1143 }
1144 update_run_state(
1145 &root,
1146 &run_id,
1147 RunStateStatus::Publishing,
1148 &started_at,
1149 None,
1150 )
1151 .map_err(|error| error.to_string())?;
1152
1153 let publication_started = Instant::now();
1154 let served = if servers.is_empty() && !support_directory.is_dir() {
1156 Vec::new()
1157 } else {
1158 crate::served_tests::read(&evidence_directory)
1159 };
1160 let support = crate::support_reach::read(&support_directory, &workspace, &served);
1161 if project.source_files.is_empty() {
1162 let metadata = RunMetadata {
1165 id: run_id.clone(),
1166 started_at: started_at.clone(),
1167 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1168 command: request.command.clone(),
1169 test_exit_code: Some(execution.exit_code),
1170 integrity: integrity.clone(),
1171 raw_evidence: RawEvidenceMetadata::default(),
1172 isolated_build: Some(true),
1173 instrumented_build_cache: None,
1174 timings: Some(RunTimings {
1175 initialization_ms: rounded_millisecond(initialization_ms),
1176 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1177 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1178 instrumented_build_ms: 0.0,
1179 test_command_ms: rounded_millisecond(test_command_ms),
1180 evidence_publication_ms: 0.0,
1181 }),
1182 merged: None,
1183 parents: None,
1184 source_roots: source_roots.clone(),
1185 copy: Some(cleanup.lock().copy()),
1186 merged_across: None,
1187 recorded_by: None,
1188 companions: None,
1189 };
1190 let mut published = None;
1191 let mut server_runs = Vec::new();
1192 let mut failure = None;
1193 for server in servers {
1194 let id = if published.is_none() {
1195 run_id.clone()
1196 } else {
1197 companion_run_id(&run_id, server_runs.len() + 1)
1198 };
1199 let language = server.language();
1200 match server.publish(
1201 &root,
1202 &run_id,
1203 RunMetadata {
1204 id: id.clone(),
1205 ..metadata.clone()
1206 },
1207 &served,
1208 diagnostics,
1209 ) {
1210 Ok(Some(run)) if published.is_none() => published = Some(run),
1211 Ok(Some((directory, _))) => {
1212 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1213 server_runs.push((language.to_owned(), directory));
1214 }
1215 Ok(None) => {}
1216 Err(error) => {
1217 let _ = writeln!(
1218 diagnostics,
1219 "[supercov] the {language} server's run could not be published: {error}"
1220 );
1221 failure.get_or_insert(error);
1222 }
1223 }
1224 }
1225 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1226 .map_err(|error| error.to_string())?;
1227 remove_stored_tree_deferred(&root, &server_evidence_root)
1228 .map_err(|error| error.to_string())?;
1229 if published.is_none()
1230 && let Some(error) = failure
1231 {
1232 return Err(error.into());
1233 }
1234 let Some((run_directory, metadata)) = published else {
1235 let message = if execution.exit_code != 0 && served.is_empty() {
1236 format!(
1237 "the test command exited {} before any test reported an outcome; its own output above says why, and no run was published",
1238 execution.exit_code
1239 )
1240 } else {
1241 "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()
1242 };
1243 let _ = update_run_state(
1244 &root,
1245 &run_id,
1246 RunStateStatus::Failed,
1247 &started_at,
1248 Some(message.clone()),
1249 );
1250 cleanup.mark_terminal();
1251 return Err(message.into());
1252 };
1253 update_run_state(
1254 &root,
1255 &run_id,
1256 if execution.exit_code == 0 {
1257 RunStateStatus::Complete
1258 } else {
1259 RunStateStatus::Failed
1260 },
1261 &started_at,
1262 None,
1263 )
1264 .map_err(|error| error.to_string())?;
1265 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1266 cleanup.mark_terminal();
1267 return Ok(DirectJavascriptRunResult {
1268 run_id,
1269 run_directory,
1270 workspace,
1271 exit_code: execution.exit_code,
1272 assertion_calls: frontend.assertion_calls,
1273 recovered_runs,
1274 metadata,
1275 server_runs,
1276 });
1277 }
1278 let archive_path = root
1279 .join(".supercov/work")
1280 .join(&run_id)
1281 .join("evidence.raw.gz");
1282 let entries = collect_sources(&[
1283 EvidenceArchiveSource::File {
1284 file: frontend.manifest_path,
1285 path: "manifest.json".into(),
1286 },
1287 EvidenceArchiveSource::File {
1288 file: workspace.join(".supercov/statement-exclusions.json"),
1289 path: "statement-exclusions.json".into(),
1290 },
1291 EvidenceArchiveSource::Directory {
1292 directory: evidence_directory,
1293 prefix: None,
1294 },
1295 EvidenceArchiveSource::Directory {
1296 directory: server_evidence_root.join(&run_id),
1297 prefix: Some("server".into()),
1298 },
1299 ])
1300 .map_err(|error| error.to_string())?;
1301 let mut entries =
1302 javascript_archive_entries(entries, &frontend.manifest, &run_id, execution.exit_code)?;
1303 if let Some(support) = &support {
1304 entries.push(EvidenceArchiveEntry {
1305 path: crate::support_reach::ARCHIVE_PATH.into(),
1306 contents: serde_json::to_vec(support).map_err(|error| error.to_string())?,
1307 });
1308 }
1309 let mut recorded_inputs = source_inputs.clone();
1313 if let Some(support) = &support
1314 && let Ok(reached) = crate::source_capture::capture(
1315 &root,
1316 "javascript",
1317 support.files.keys().map(PathBuf::from),
1318 )
1319 {
1320 for (file, text) in reached.files {
1321 recorded_inputs.files.entry(file).or_insert(text);
1322 }
1323 }
1324 let entries = crate::source_capture::append(entries, &recorded_inputs)?;
1325 let raw = write_archive(entries, &archive_path).map_err(|error| error.to_string())?;
1326 remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1327 .map_err(|error| error.to_string())?;
1328 remove_stored_tree_deferred(&root, &server_evidence_root).map_err(|error| error.to_string())?;
1329 let evidence_publication_ms = elapsed_ms(publication_started);
1330 let timings = RunTimings {
1331 initialization_ms: rounded_millisecond(initialization_ms),
1332 workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1333 adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1334 instrumented_build_ms: 0.0,
1335 test_command_ms: rounded_millisecond(test_command_ms),
1336 evidence_publication_ms: rounded_millisecond(evidence_publication_ms),
1337 };
1338 let metadata = RunMetadata {
1339 id: run_id.clone(),
1340 started_at: started_at.clone(),
1341 duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1342 command: request.command.clone(),
1343 test_exit_code: Some(execution.exit_code),
1344 integrity,
1345 raw_evidence: RawEvidenceMetadata {
1346 schema_version: raw.schema_version,
1347 format: raw.format.into(),
1348 file: raw.file.into(),
1349 files: raw.files,
1350 uncompressed_bytes: raw.uncompressed_bytes,
1351 compressed_bytes: raw.compressed_bytes,
1352 },
1353 isolated_build: Some(true),
1354 instrumented_build_cache: None,
1355 timings: Some(timings),
1356 merged: None,
1357 parents: None,
1358 source_roots: source_roots.clone(),
1359 copy: Some(cleanup.lock().copy()),
1360 merged_across: None,
1361 recorded_by: None,
1362 companions: None,
1363 };
1364 let mut server_runs = Vec::new();
1368 let mut companions = Vec::new();
1369 for server in servers {
1370 let id = companion_run_id(&run_id, server_runs.len() + 1);
1371 let language = server.language();
1372 let server_metadata = RunMetadata {
1373 id: id.clone(),
1374 companions: Some(vec![crate::run_store::CompanionRun {
1375 id: run_id.clone(),
1376 language: "JavaScript".into(),
1377 role: "browser".into(),
1378 }]),
1379 ..metadata.clone()
1380 };
1381 match server.publish(&root, &run_id, server_metadata, &served, diagnostics) {
1382 Ok(Some((directory, _))) => {
1383 finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1384 server_runs.push((language.to_owned(), directory));
1385 companions.push(crate::run_store::CompanionRun {
1386 id,
1387 language: language.into(),
1388 role: "server".into(),
1389 });
1390 }
1391 Ok(None) => {}
1392 Err(error) => {
1394 let _ = writeln!(
1395 diagnostics,
1396 "[supercov] the {language} server's run could not be published: {error}"
1397 );
1398 }
1399 }
1400 }
1401 let metadata = RunMetadata {
1402 companions: (!companions.is_empty()).then_some(companions),
1403 ..metadata
1404 };
1405 let run_directory =
1406 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1407 let terminal_status = if execution.exit_code == 0 {
1408 RunStateStatus::Complete
1409 } else {
1410 RunStateStatus::Failed
1411 };
1412 update_run_state(&root, &run_id, terminal_status, &started_at, None)
1413 .map_err(|error| error.to_string())?;
1414 finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1415 cleanup.mark_terminal();
1416 Ok(DirectJavascriptRunResult {
1417 run_id,
1418 run_directory,
1419 workspace,
1420 exit_code: execution.exit_code,
1421 assertion_calls: frontend.assertion_calls,
1422 recovered_runs,
1423 metadata,
1424 server_runs,
1425 })
1426}
1427
1428fn companion_run_id(run_id: &str, nth: usize) -> String {
1432 match run_id
1433 .strip_prefix("run_")
1434 .filter(|hex| hex.len() == 16)
1435 .and_then(|hex| u64::from_str_radix(hex, 16).ok())
1436 {
1437 Some(number) => format!("run_{:016x}", number.wrapping_sub(nth as u64)),
1438 None => format!("{run_id}-server-{nth}"),
1439 }
1440}
1441
1442enum Server {
1444 Python(crate::python_run::PythonServer),
1445 Ruby(crate::ruby_run::RubyServer),
1446 Go(crate::go_run::GoServer),
1447 Jvm(crate::jvm_run::JvmServer),
1448}
1449
1450impl Server {
1451 fn language(&self) -> &'static str {
1452 match self {
1453 Self::Python(_) => "Python",
1454 Self::Ruby(_) => "Ruby",
1455 Self::Go(_) => "Go",
1456 Self::Jvm(_) => "Java/Kotlin",
1457 }
1458 }
1459
1460 fn source_files(&self) -> usize {
1461 match self {
1462 Self::Python(server) => server.source_files(),
1463 Self::Ruby(server) => server.source_files(),
1464 Self::Go(server) => server.source_files(),
1465 Self::Jvm(server) => server.source_files(),
1466 }
1467 }
1468
1469 fn environment(&self) -> BTreeMap<String, String> {
1470 match self {
1471 Self::Python(server) => server.environment(),
1472 Self::Ruby(server) => server.environment(),
1473 Self::Go(server) => server.environment(),
1474 Self::Jvm(server) => server.environment(),
1475 }
1476 }
1477
1478 fn publish(
1479 self,
1480 root: &Path,
1481 evidence_run_id: &str,
1482 metadata: RunMetadata,
1483 served: &[crate::served_tests::ServedTest],
1484 diagnostics: &mut dyn std::io::Write,
1485 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1486 match self {
1487 Self::Python(server) => {
1488 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1489 }
1490 Self::Ruby(server) => {
1491 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1492 }
1493 Self::Go(server) => {
1494 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1495 }
1496 Self::Jvm(server) => {
1497 server.publish(root, evidence_run_id, metadata, served, diagnostics)
1498 }
1499 }
1500 }
1501}
1502
1503#[cfg(test)]
1504mod environment_tests {
1505 use super::*;
1506
1507 #[test]
1510 fn a_companion_run_id_is_public_and_sorts_after_its_run() {
1511 for run in [
1512 "run_0123456789abcdef",
1513 "run_ffffffffffffffff",
1514 "run_8000000000000000",
1515 ] {
1516 let companion = companion_run_id(run, 1);
1517 assert_eq!(companion.len(), 20);
1518 assert!(companion.starts_with("run_"));
1519 assert!(companion.as_str() < run, "{companion} >= {run}");
1520 assert_eq!(companion, companion_run_id(run, 1));
1521 assert!(companion_run_id(run, 2) < companion);
1522 }
1523 assert_eq!(companion_run_id("rust-1", 1), "rust-1-server-1");
1524 }
1525
1526 #[test]
1527 fn nested_npm_drops_only_pnpm_derived_environment_aliases() {
1528 let mut environment = BTreeMap::from([
1529 ("npm_config_auto_install_peers".into(), "true".into()),
1530 ("NPM_CONFIG_SHAMEFULLY_HOIST".into(), "true".into()),
1531 (
1532 "npm_config_registry".into(),
1533 "https://registry.npmjs.org".into(),
1534 ),
1535 ("USER_VALUE".into(), "kept".into()),
1536 ]);
1537 remove_derived_pnpm_config(&mut environment);
1538 assert_eq!(
1539 environment,
1540 BTreeMap::from([
1541 ("USER_VALUE".into(), "kept".into()),
1542 (
1543 "npm_config_registry".into(),
1544 "https://registry.npmjs.org".into()
1545 ),
1546 ])
1547 );
1548 }
1549}