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