1use std::{
11 collections::{BTreeSet, HashMap},
12 ffi::OsString,
13 fs,
14 path::{Component, Path, PathBuf},
15 process::Command,
16 str::FromStr,
17};
18
19use cargo_config2::cargo_home_with_cwd;
20use cargo_platform::{Cfg, CfgExpr};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 rust_cargo_config_model::{
25 CargoConfigDefinition, CargoConfigKind, CargoConfigValue, load_cargo_configuration,
26 },
27 rust_test_runner::CargoTestInvocation,
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
31#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
32pub enum RustCargoRunnerProgram {
33 SearchPath { value: String },
34 Absolute { value: PathBuf },
35 WorkspaceRelative { value: PathBuf },
36}
37
38impl RustCargoRunnerProgram {
39 pub fn resolve(&self, workspace: &Path) -> PathBuf {
40 match self {
41 Self::SearchPath { value } => PathBuf::from(value),
42 Self::Absolute { value } => value.clone(),
43 Self::WorkspaceRelative { value } => workspace.join(value),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct RustCargoUnderlyingRunner {
51 pub program: RustCargoRunnerProgram,
52 pub arguments: Vec<String>,
53}
54
55impl RustCargoUnderlyingRunner {
56 pub fn resolve(&self, workspace: &Path) -> RustCargoResolvedRunner {
57 RustCargoResolvedRunner {
58 program: self.program.resolve(workspace),
59 arguments: self.arguments.clone(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct RustCargoResolvedRunner {
67 pub program: PathBuf,
68 pub arguments: Vec<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct RustCargoTargetRunnerPlan {
74 pub target: String,
75 pub underlying_runner: Option<RustCargoUnderlyingRunner>,
76}
77
78impl RustCargoTargetRunnerPlan {
79 pub fn resolve(&self, workspace: &Path) -> RustCargoResolvedTargetRunner {
80 RustCargoResolvedTargetRunner {
81 target: self.target.clone(),
82 underlying_runner: self
83 .underlying_runner
84 .as_ref()
85 .map(|runner| runner.resolve(workspace)),
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct RustCargoResolvedTargetRunner {
93 pub target: String,
94 pub underlying_runner: Option<RustCargoResolvedRunner>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99pub struct RustCargoRunnerPlan {
100 pub compiler: RustCargoCompilerCommandPlan,
101 pub targets: Vec<RustCargoTargetRunnerPlan>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct RustCargoCompilerCommandPlan {
107 pub rustc: RustCargoRunnerProgram,
108 pub rustc_wrapper: Option<RustCargoRunnerProgram>,
109 pub rustc_workspace_wrapper: Option<RustCargoRunnerProgram>,
110}
111
112impl RustCargoCompilerCommandPlan {
113 fn resolved_programs(&self, workspace: &Path) -> Vec<PathBuf> {
114 self.rustc_wrapper
115 .iter()
116 .chain(self.rustc_workspace_wrapper.iter())
117 .chain(std::iter::once(&self.rustc))
118 .map(|program| program.resolve(workspace))
119 .collect()
120 }
121
122 fn command(&self, workspace: &Path) -> Command {
123 let programs = self.resolved_programs(workspace);
124 let mut command = Command::new(&programs[0]);
125 command.args(&programs[1..]);
126 command
127 }
128}
129
130#[derive(Debug, Clone)]
131struct CargoModelInputs {
132 cargo_home: Option<PathBuf>,
133 environment: HashMap<String, OsString>,
134 host_override: Option<String>,
135}
136
137impl CargoModelInputs {
138 fn ambient(root: &Path) -> Self {
139 Self {
140 cargo_home: cargo_home_with_cwd(root),
141 environment: std::env::vars_os()
142 .filter_map(|(key, value)| key.into_string().ok().map(|key| (key, value)))
143 .collect(),
144 host_override: None,
145 }
146 }
147
148 fn environment(&self, key: &str) -> Option<&OsString> {
149 self.environment.get(key)
150 }
151}
152
153#[derive(Debug)]
154pub enum RustCargoConfigurationError {
155 Io { path: PathBuf, reason: String },
156 Invalid(String),
157 Unsupported(String),
158}
159
160impl std::fmt::Display for RustCargoConfigurationError {
161 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
164 Self::Invalid(reason) => write!(formatter, "invalid Cargo configuration: {reason}"),
165 Self::Unsupported(reason) => formatter.write_str(reason),
166 }
167 }
168}
169
170impl std::error::Error for RustCargoConfigurationError {}
171
172fn io_error(path: &Path, error: impl std::fmt::Display) -> RustCargoConfigurationError {
173 RustCargoConfigurationError::Io {
174 path: path.to_owned(),
175 reason: error.to_string(),
176 }
177}
178
179fn command_targets(
180 invocation: &CargoTestInvocation,
181) -> Result<Vec<String>, RustCargoConfigurationError> {
182 let command_position = invocation.command_position();
183 toolchain_selector(invocation, command_position)?;
184 if invocation.arguments[..command_position]
185 .iter()
186 .any(|argument| argument == "-Z" || argument.starts_with("-Z"))
187 {
188 return Err(RustCargoConfigurationError::Unsupported(
189 "Cargo runner composition does not yet resolve pre-subcommand -Z configuration semantics exactly"
190 .into(),
191 ));
192 }
193 let mut targets = Vec::new();
194 let mut index = 0;
195 while index < invocation.arguments.len() {
196 let argument = &invocation.arguments[index];
197 if argument == "--config" {
198 if invocation.arguments.get(index + 1).is_none() {
199 return Err(RustCargoConfigurationError::Invalid(
200 "--config has no value".into(),
201 ));
202 }
203 index += 2;
204 continue;
205 }
206 if let Some(value) = argument.strip_prefix("--config=") {
207 if value.is_empty() {
208 return Err(RustCargoConfigurationError::Invalid(
209 "--config has no value".into(),
210 ));
211 }
212 index += 1;
213 continue;
214 }
215 if argument == "--target" {
216 let target = invocation.arguments.get(index + 1).ok_or_else(|| {
217 RustCargoConfigurationError::Invalid("--target has no value".into())
218 })?;
219 targets.push(target.clone());
220 index += 2;
221 continue;
222 }
223 if let Some(target) = argument.strip_prefix("--target=") {
224 if target.is_empty() {
225 return Err(RustCargoConfigurationError::Invalid(
226 "--target has no value".into(),
227 ));
228 }
229 targets.push(target.to_owned());
230 }
231 index += 1;
232 }
233 Ok(targets)
234}
235
236fn command_config_arguments(
237 invocation: &CargoTestInvocation,
238) -> Result<Vec<String>, RustCargoConfigurationError> {
239 let mut values = Vec::new();
240 let mut index = 0;
241 while index < invocation.arguments.len() {
242 let argument = &invocation.arguments[index];
243 if argument == "--config" {
244 let value = invocation.arguments.get(index + 1).ok_or_else(|| {
245 RustCargoConfigurationError::Invalid("--config has no value".into())
246 })?;
247 values.push(value.clone());
248 index += 2;
249 continue;
250 }
251 if let Some(value) = argument.strip_prefix("--config=") {
252 if value.is_empty() {
253 return Err(RustCargoConfigurationError::Invalid(
254 "--config has no value".into(),
255 ));
256 }
257 values.push(value.to_owned());
258 }
259 index += 1;
260 }
261 Ok(values)
262}
263
264fn toolchain_selector(
265 invocation: &CargoTestInvocation,
266 test_position: usize,
267) -> Result<Option<&str>, RustCargoConfigurationError> {
268 let prefix = &invocation.arguments[..test_position];
269 let selectors = prefix
270 .iter()
271 .enumerate()
272 .filter(|(_, argument)| argument.starts_with('+'))
273 .collect::<Vec<_>>();
274 match selectors.as_slice() {
275 [] => Ok(None),
276 [(0, selector)] if selector.len() > 1 => Ok(Some(&selector[1..])),
277 [(0, _)] => Err(RustCargoConfigurationError::Invalid(
278 "the rustup toolchain selector is empty".into(),
279 )),
280 _ => Err(RustCargoConfigurationError::Invalid(
281 "the rustup +toolchain selector must be the first and only selector before the Cargo subcommand"
282 .into(),
283 )),
284 }
285}
286
287fn command_stdout(
288 program: &Path,
289 arguments: &[&str],
290 operation: &str,
291) -> Result<String, RustCargoConfigurationError> {
292 let output = Command::new(program)
293 .args(arguments)
294 .output()
295 .map_err(|error| io_error(program, error))?;
296 if !output.status.success() {
297 return Err(RustCargoConfigurationError::Invalid(format!(
298 "{} failed while {operation} with status {}: {}",
299 program.display(),
300 output
301 .status
302 .code()
303 .map_or_else(|| "signal".into(), |value| value.to_string()),
304 String::from_utf8_lossy(&output.stderr).trim()
305 )));
306 }
307 if !output.stderr.is_empty() {
308 return Err(RustCargoConfigurationError::Invalid(format!(
309 "{} wrote unexpected stderr while {operation}: {}",
310 program.display(),
311 String::from_utf8_lossy(&output.stderr).trim()
312 )));
313 }
314 String::from_utf8(output.stdout).map_err(|_| {
315 RustCargoConfigurationError::Invalid(format!(
316 "{} produced non-UTF-8 output while {operation}",
317 program.display()
318 ))
319 })
320}
321
322fn rustup_program(cargo: &Path) -> PathBuf {
323 cargo
324 .parent()
325 .filter(|parent| !parent.as_os_str().is_empty())
326 .map_or_else(
327 || PathBuf::from(format!("rustup{}", std::env::consts::EXE_SUFFIX)),
328 |parent| parent.join(format!("rustup{}", std::env::consts::EXE_SUFFIX)),
329 )
330}
331
332fn selected_cargo_program(
333 invocation: &CargoTestInvocation,
334) -> Result<OsString, RustCargoConfigurationError> {
335 let command_position = invocation.command_position();
336 let Some(selector) = toolchain_selector(invocation, command_position)? else {
337 return Ok(invocation.program.clone().into());
338 };
339 let cargo_proxy = which::which(&invocation.program).map_err(|error| {
340 RustCargoConfigurationError::Invalid(format!(
341 "could not resolve the Cargo proxy {}: {error}",
342 invocation.program
343 ))
344 })?;
345 let rustup = rustup_program(&cargo_proxy);
346 let selected = command_stdout(
347 &rustup,
348 &["which", "--toolchain", selector, "cargo"],
349 "resolving the explicit rustup toolchain's Cargo",
350 )?;
351 let selected = PathBuf::from(selected.trim());
352 if !selected.is_absolute() {
353 return Err(RustCargoConfigurationError::Invalid(format!(
354 "rustup returned a non-absolute Cargo path for +{selector}: {}",
355 selected.display()
356 )));
357 }
358 let selected = fs::canonicalize(&selected).map_err(|error| io_error(&selected, error))?;
359 let metadata = fs::symlink_metadata(&selected).map_err(|error| io_error(&selected, error))?;
360 if !metadata.file_type().is_file() {
361 return Err(RustCargoConfigurationError::Invalid(format!(
362 "rustup returned a non-regular Cargo path for +{selector}: {}",
363 selected.display()
364 )));
365 }
366 let proxy_version = command_stdout(
367 &cargo_proxy,
368 &[&format!("+{selector}"), "-Vv"],
369 "verifying the explicit rustup Cargo selection",
370 )?;
371 let selected_version = command_stdout(
372 &selected,
373 &["-Vv"],
374 "verifying the resolved Cargo executable",
375 )?;
376 if proxy_version != selected_version {
377 return Err(RustCargoConfigurationError::Invalid(format!(
378 "the +{selector} Cargo proxy and rustup's selected Cargo executable disagree"
379 )));
380 }
381 Ok(selected.into_os_string())
382}
383
384fn model_target_config<'a>(
385 model: &'a CargoConfigValue,
386 target: &str,
387) -> Option<&'a CargoConfigValue> {
388 let target_table = model.at(&["target"])?.table()?;
389 if let Some(value) = target_table.get(target) {
390 return Some(value);
391 }
392 let mut value = model.at(&["target"])?;
393 for component in target.split('.') {
394 value = value.table()?.get(component)?;
395 }
396 Some(value)
397}
398
399fn model_exact_runner<'a>(
400 model: &'a CargoConfigValue,
401 target: &str,
402) -> Option<&'a CargoConfigValue> {
403 model_target_config(model, target)?.at(&["runner"])
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
407enum ModelTargetKind {
408 Tuple,
409 Json,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
413struct ModelTarget {
414 kind: ModelTargetKind,
415 name: String,
416 cargo_argument: String,
417}
418
419fn model_targets(
420 root: &Path,
421 model: &CargoConfigValue,
422 command_targets: Vec<String>,
423 host: &str,
424 inputs: &CargoModelInputs,
425) -> Result<Vec<ModelTarget>, RustCargoConfigurationError> {
426 let convert = |target: &str| {
427 let target = target.trim();
428 if target.is_empty() {
429 return Err(RustCargoConfigurationError::Invalid(
430 "Cargo target was empty".into(),
431 ));
432 }
433 let target = if target == "host-tuple" { host } else { target };
434 if target.ends_with(".json") {
435 let requested = Path::new(target);
436 let requested = if requested.is_absolute() {
437 requested.to_owned()
438 } else {
439 root.join(requested)
440 };
441 let path = fs::canonicalize(&requested).map_err(|error| io_error(&requested, error))?;
442 let name = path
443 .file_stem()
444 .and_then(|stem| stem.to_str())
445 .filter(|stem| !stem.is_empty())
446 .ok_or_else(|| {
447 RustCargoConfigurationError::Invalid(format!(
448 "Cargo target specification has no UTF-8 file stem: {}",
449 path.display()
450 ))
451 })?;
452 let cargo_argument = path.to_str().ok_or_else(|| {
453 RustCargoConfigurationError::Invalid(format!(
454 "Cargo target specification path is not UTF-8: {}",
455 path.display()
456 ))
457 })?;
458 return Ok(ModelTarget {
459 kind: ModelTargetKind::Json,
460 name: name.to_owned(),
461 cargo_argument: cargo_argument.to_owned(),
462 });
463 }
464 Ok(ModelTarget {
465 kind: ModelTargetKind::Tuple,
466 name: target.to_owned(),
467 cargo_argument: target.to_owned(),
468 })
469 };
470 let targets = if !command_targets.is_empty() {
471 command_targets
472 } else if let Some(target) = inputs.environment("CARGO_BUILD_TARGET") {
473 let target = target.clone().into_string().map_err(|_| {
474 RustCargoConfigurationError::Invalid("CARGO_BUILD_TARGET is not UTF-8".into())
475 })?;
476 vec![target]
477 } else if let Some(targets) = model.at(&["build", "target"]) {
478 targets
479 .string_list()
480 .ok_or_else(|| {
481 RustCargoConfigurationError::Invalid(
482 "build.target must be a string or string array".into(),
483 )
484 })?
485 .into_iter()
486 .map(str::to_owned)
487 .collect()
488 } else {
489 vec![host.to_owned()]
490 };
491 let targets = targets
492 .iter()
493 .map(|target| convert(target))
494 .collect::<Result<BTreeSet<_>, _>>()
495 .map(BTreeSet::into_iter)
496 .map(Iterator::collect::<Vec<_>>)?;
497 let mut names = HashMap::new();
498 for target in &targets {
499 if let Some(first) = names.insert(&target.name, &target.cargo_argument) {
500 return Err(RustCargoConfigurationError::Invalid(format!(
501 "Cargo targets {first} and {} have the same configuration identity {:?}",
502 target.cargo_argument, target.name
503 )));
504 }
505 }
506 Ok(targets)
507}
508
509fn environment_runner(
510 target: &str,
511 inputs: &CargoModelInputs,
512) -> Result<Option<CargoConfigValue>, RustCargoConfigurationError> {
513 let mut key = target.replace(['-', '.'], "_");
514 key.make_ascii_uppercase();
515 let key = format!("CARGO_TARGET_{key}_RUNNER");
516 let Some(value) = inputs.environment(&key) else {
517 return Ok(None);
518 };
519 let value = value
520 .clone()
521 .into_string()
522 .map_err(|_| RustCargoConfigurationError::Invalid(format!("{key} is not UTF-8")))?;
523 Ok(Some(CargoConfigValue {
524 kind: CargoConfigKind::String(value),
525 definition: CargoConfigDefinition::Environment(key),
526 }))
527}
528
529fn environment_tool(
530 root: &Path,
531 key: &str,
532 inputs: &CargoModelInputs,
533 empty_disables: bool,
534 non_utf8_is_absent: bool,
535) -> Result<Option<Option<RustCargoRunnerProgram>>, RustCargoConfigurationError> {
536 let Some(value) = inputs.environment(key) else {
537 return Ok(None);
538 };
539 let value = match value.clone().into_string() {
540 Ok(value) => value,
541 Err(_) if non_utf8_is_absent => return Ok(None),
542 Err(_) => {
543 return Err(RustCargoConfigurationError::Invalid(format!(
544 "{key} is not UTF-8"
545 )));
546 }
547 };
548 if empty_disables && value.is_empty() {
549 return Ok(Some(None));
550 }
551 let path = if value.contains('/') || value.contains('\\') {
552 root.join(value)
553 } else {
554 PathBuf::from(value)
555 };
556 Ok(Some(Some(runner_program(root, path)?)))
557}
558
559fn model_tool(
560 root: &Path,
561 value: &CargoConfigValue,
562 empty_disables: bool,
563) -> Result<Option<RustCargoRunnerProgram>, RustCargoConfigurationError> {
564 let path = value
565 .program_path(root)
566 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
567 if empty_disables && path.as_os_str().is_empty() {
568 return Ok(None);
569 }
570 runner_program(root, path).map(Some)
571}
572
573fn selected_compiler_tool(
574 root: &Path,
575 model: &CargoConfigValue,
576 inputs: &CargoModelInputs,
577 model_key: &str,
578 direct_environment: &str,
579 cargo_environment: &str,
580 empty_disables: bool,
581) -> Result<Option<RustCargoRunnerProgram>, RustCargoConfigurationError> {
582 let configured = model.at(&["build", model_key]);
583 if let Some(selected) =
584 environment_tool(root, direct_environment, inputs, empty_disables, true)?
585 {
586 return Ok(selected);
587 }
588 if configured.is_some_and(|value| {
589 matches!(
590 value.definition,
591 CargoConfigDefinition::CliFile(_) | CargoConfigDefinition::CliValue
592 )
593 }) {
594 return model_tool(root, configured.expect("checked above"), empty_disables);
595 }
596 if let Some(selected) =
597 environment_tool(root, cargo_environment, inputs, empty_disables, false)?
598 {
599 return Ok(selected);
600 }
601 configured
602 .map(|value| model_tool(root, value, empty_disables))
603 .transpose()
604 .map(Option::flatten)
605}
606
607fn compiler_command_plan(
608 root: &Path,
609 model: &CargoConfigValue,
610 default_rustc: RustCargoRunnerProgram,
611 inputs: &CargoModelInputs,
612) -> Result<RustCargoCompilerCommandPlan, RustCargoConfigurationError> {
613 let rustc = selected_compiler_tool(
614 root,
615 model,
616 inputs,
617 "rustc",
618 "RUSTC",
619 "CARGO_BUILD_RUSTC",
620 false,
621 )?
622 .unwrap_or(default_rustc);
623 let rustc_wrapper = selected_compiler_tool(
624 root,
625 model,
626 inputs,
627 "rustc-wrapper",
628 "RUSTC_WRAPPER",
629 "CARGO_BUILD_RUSTC_WRAPPER",
630 true,
631 )?;
632 let rustc_workspace_wrapper = selected_compiler_tool(
633 root,
634 model,
635 inputs,
636 "rustc-workspace-wrapper",
637 "RUSTC_WORKSPACE_WRAPPER",
638 "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
639 true,
640 )?;
641 Ok(RustCargoCompilerCommandPlan {
642 rustc,
643 rustc_wrapper,
644 rustc_workspace_wrapper,
645 })
646}
647
648fn compiler_host(
649 root: &Path,
650 compiler: &RustCargoCompilerCommandPlan,
651) -> Result<String, RustCargoConfigurationError> {
652 let output = compiler
653 .command(root)
654 .arg("-vV")
655 .output()
656 .map_err(|error| io_error(&compiler.rustc.resolve(root), error))?;
657 if !output.status.success() {
658 return Err(RustCargoConfigurationError::Invalid(format!(
659 "Cargo's configured compiler command failed while resolving its host with status {}: {}",
660 output
661 .status
662 .code()
663 .map_or_else(|| "signal".into(), |value| value.to_string()),
664 String::from_utf8_lossy(&output.stderr).trim()
665 )));
666 }
667 let stdout = String::from_utf8(output.stdout).map_err(|_| {
668 RustCargoConfigurationError::Invalid(
669 "Cargo's configured compiler command produced non-UTF-8 verbose version output".into(),
670 )
671 })?;
672 stdout
673 .lines()
674 .find_map(|line| line.strip_prefix("host: "))
675 .filter(|host| !host.is_empty())
676 .map(str::to_owned)
677 .ok_or_else(|| {
678 RustCargoConfigurationError::Invalid(
679 "Cargo's configured compiler command did not report a host in rustc -vV output"
680 .into(),
681 )
682 })
683}
684
685fn parsed_model_runner(
686 root: &Path,
687 runner: &CargoConfigValue,
688) -> Result<RustCargoUnderlyingRunner, RustCargoConfigurationError> {
689 let (program, arguments) = runner
690 .program_and_arguments(root)
691 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
692 Ok(RustCargoUnderlyingRunner {
693 program: runner_program(root, program)?,
694 arguments,
695 })
696}
697
698fn target_cfg(
699 configuration_root: &Path,
700 execution_root: &Path,
701 compiler: &RustCargoCompilerCommandPlan,
702 target: &ModelTarget,
703) -> Result<Vec<Cfg>, RustCargoConfigurationError> {
704 let target_argument = match target.kind {
705 ModelTargetKind::Tuple => PathBuf::from(&target.cargo_argument),
706 ModelTargetKind::Json => {
707 let source = Path::new(&target.cargo_argument);
708 source.strip_prefix(configuration_root).map_or_else(
709 |_| source.to_owned(),
710 |relative| execution_root.join(relative),
711 )
712 }
713 };
714 let output = compiler
715 .command(execution_root)
716 .args(["--print", "cfg", "--target"])
717 .arg(&target_argument)
718 .output()
719 .map_err(|error| io_error(&compiler.rustc.resolve(execution_root), error))?;
720 if !output.status.success() {
721 return Err(RustCargoConfigurationError::Invalid(format!(
722 "Cargo's configured compiler command failed while resolving cfg values for target {} with status {}: {}",
723 target.cargo_argument,
724 output
725 .status
726 .code()
727 .map_or_else(|| "signal".into(), |value| value.to_string()),
728 String::from_utf8_lossy(&output.stderr).trim()
729 )));
730 }
731 let stdout = String::from_utf8(output.stdout).map_err(|_| {
732 RustCargoConfigurationError::Invalid(format!(
733 "Cargo's configured compiler command produced non-UTF-8 cfg output for target {}",
734 target.cargo_argument
735 ))
736 })?;
737 stdout
738 .lines()
739 .filter(|line| !line.trim().is_empty())
740 .map(|line| {
741 Cfg::from_str(line.trim()).map_err(|error| {
742 RustCargoConfigurationError::Invalid(format!(
743 "rustc emitted an invalid cfg value {line:?}: {error}"
744 ))
745 })
746 })
747 .collect()
748}
749
750fn cfg_runner<'a>(
751 model: &'a CargoConfigValue,
752 cfg: &[Cfg],
753) -> Result<Option<&'a CargoConfigValue>, RustCargoConfigurationError> {
754 let Some(targets) = model.at(&["target"]).and_then(CargoConfigValue::table) else {
755 return Ok(None);
756 };
757 let mut matches = Vec::new();
758 for (key, target) in targets {
759 let Some(expression) = key
760 .strip_prefix("cfg(")
761 .and_then(|key| key.strip_suffix(')'))
762 else {
763 continue;
764 };
765 let expression = CfgExpr::from_str(expression).map_err(|error| {
766 RustCargoConfigurationError::Invalid(format!(
767 "invalid Cargo target cfg key {key:?}: {error}"
768 ))
769 })?;
770 if expression.matches(cfg)
771 && let Some(runner) = target.at(&["runner"])
772 {
773 matches.push((key, runner));
774 }
775 }
776 match matches.as_slice() {
777 [] => Ok(None),
778 [(_, runner)] => Ok(Some(*runner)),
779 [(first, _), (second, _), ..] => Err(RustCargoConfigurationError::Invalid(format!(
780 "several matching instances of target.'cfg(..)'.runner: {first} and {second}"
781 ))),
782 }
783}
784
785fn model_runner(
786 configuration_root: &Path,
787 execution_root: &Path,
788 model: &CargoConfigValue,
789 compiler: &RustCargoCompilerCommandPlan,
790 target: &ModelTarget,
791 inputs: &CargoModelInputs,
792) -> Result<Option<RustCargoUnderlyingRunner>, RustCargoConfigurationError> {
793 let exact = model_exact_runner(model, &target.name);
794 let environment = environment_runner(&target.name, inputs)?;
795 let selected = match exact {
796 Some(value)
797 if matches!(
798 value.definition,
799 CargoConfigDefinition::CliFile(_) | CargoConfigDefinition::CliValue
800 ) =>
801 {
802 Some(value)
803 }
804 _ => environment.as_ref().or(exact),
805 };
806 if let Some(runner) = selected {
807 return parsed_model_runner(configuration_root, runner).map(Some);
808 }
809 let cfg = target_cfg(configuration_root, execution_root, compiler, target)?;
810 cfg_runner(model, &cfg)?
811 .map(|runner| parsed_model_runner(configuration_root, runner))
812 .transpose()
813}
814
815fn runner_program(
816 root: &Path,
817 path: PathBuf,
818) -> Result<RustCargoRunnerProgram, RustCargoConfigurationError> {
819 if path.is_absolute() {
820 if let Ok(relative) = path.strip_prefix(root) {
821 if relative.as_os_str().is_empty()
822 || relative
823 .components()
824 .any(|component| !matches!(component, Component::Normal(_)))
825 {
826 return Err(RustCargoConfigurationError::Invalid(format!(
827 "runner path is not a regular workspace-relative path: {}",
828 path.display()
829 )));
830 }
831 return Ok(RustCargoRunnerProgram::WorkspaceRelative {
832 value: relative.to_owned(),
833 });
834 }
835 return Ok(RustCargoRunnerProgram::Absolute { value: path });
836 }
837 if path.components().count() == 1 {
838 let value = path.into_os_string().into_string().map_err(|_| {
839 RustCargoConfigurationError::Invalid("runner search path is not UTF-8".into())
840 })?;
841 return Ok(RustCargoRunnerProgram::SearchPath { value });
842 }
843 Err(RustCargoConfigurationError::Invalid(format!(
844 "Cargo returned an unresolved relative runner path: {}",
845 path.display()
846 )))
847}
848
849struct ResolvedTargets {
852 root: PathBuf,
853 execution_root: PathBuf,
854 model: CargoConfigValue,
855 compiler: RustCargoCompilerCommandPlan,
856 targets: Vec<ModelTarget>,
857}
858
859fn resolve_with_inputs(
860 root: &Path,
861 execution_root: &Path,
862 invocation: &CargoTestInvocation,
863 model_inputs: CargoModelInputs,
864) -> Result<RustCargoRunnerPlan, RustCargoConfigurationError> {
865 let ResolvedTargets {
866 root,
867 execution_root,
868 model,
869 compiler,
870 targets,
871 } = resolve_targets(root, execution_root, invocation, &model_inputs)?;
872 let targets = targets
873 .iter()
874 .map(|target| {
875 Ok(RustCargoTargetRunnerPlan {
876 target: target.name.clone(),
877 underlying_runner: model_runner(
878 &root,
879 &execution_root,
880 &model,
881 &compiler,
882 target,
883 &model_inputs,
884 )?,
885 })
886 })
887 .collect::<Result<Vec<_>, RustCargoConfigurationError>>()?;
888 Ok(RustCargoRunnerPlan { compiler, targets })
889}
890
891pub(crate) fn active_target_cfg(root: &Path, invocation: &CargoTestInvocation) -> Option<Vec<Cfg>> {
895 let inputs = CargoModelInputs::ambient(root);
896 let resolved = resolve_targets(root, root, invocation, &inputs).ok()?;
897 let [target] = resolved.targets.as_slice() else {
898 return None;
899 };
900 target_cfg(
901 &resolved.root,
902 &resolved.execution_root,
903 &resolved.compiler,
904 target,
905 )
906 .ok()
907}
908
909fn resolve_targets(
910 root: &Path,
911 execution_root: &Path,
912 invocation: &CargoTestInvocation,
913 model_inputs: &CargoModelInputs,
914) -> Result<ResolvedTargets, RustCargoConfigurationError> {
915 let root = fs::canonicalize(root).map_err(|error| io_error(root, error))?;
916 let execution_root =
917 fs::canonicalize(execution_root).map_err(|error| io_error(execution_root, error))?;
918 let command_targets = command_targets(invocation)?;
919 let command_config = command_config_arguments(invocation)?;
920 let command_position = invocation.command_position();
921 let explicit_toolchain = toolchain_selector(invocation, command_position)?.is_some();
922 let selected_cargo = selected_cargo_program(invocation)?;
923 let model = load_cargo_configuration(&root, model_inputs.cargo_home.clone(), &command_config)
924 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
925 let selected_cargo_path = PathBuf::from(&selected_cargo);
926 let default_rustc = if explicit_toolchain {
927 selected_cargo_path
928 .parent()
929 .filter(|parent| !parent.as_os_str().is_empty())
930 .map(|parent| parent.join(format!("rustc{}", std::env::consts::EXE_SUFFIX)))
931 .filter(|candidate| candidate.is_file())
932 .map(|candidate| runner_program(&root, candidate))
933 .transpose()?
934 .unwrap_or_else(|| RustCargoRunnerProgram::SearchPath {
935 value: format!("rustc{}", std::env::consts::EXE_SUFFIX),
936 })
937 } else {
938 RustCargoRunnerProgram::SearchPath {
939 value: format!("rustc{}", std::env::consts::EXE_SUFFIX),
940 }
941 };
942 let compiler = compiler_command_plan(&root, &model, default_rustc, model_inputs)?;
943 let host = model_inputs
944 .host_override
945 .clone()
946 .map(Ok)
947 .unwrap_or_else(|| compiler_host(&execution_root, &compiler))?;
948 let targets = model_targets(&root, &model, command_targets, &host, model_inputs)?;
949 Ok(ResolvedTargets {
950 root,
951 execution_root,
952 model,
953 compiler,
954 targets,
955 })
956}
957
958pub(crate) fn resolve_cargo_runner_plan(
959 root: &Path,
960 execution_root: &Path,
961 invocation: &CargoTestInvocation,
962) -> Result<RustCargoRunnerPlan, RustCargoConfigurationError> {
963 resolve_with_inputs(
964 root,
965 execution_root,
966 invocation,
967 CargoModelInputs::ambient(root),
968 )
969}
970
971#[cfg(test)]
972mod tests {
973 use std::{
974 sync::atomic::{AtomicU64, Ordering},
975 time::{SystemTime, UNIX_EPOCH},
976 };
977
978 use super::*;
979
980 static FIXTURE_ID: AtomicU64 = AtomicU64::new(0);
981
982 fn fixture() -> PathBuf {
983 let root = std::env::temp_dir().join(format!(
984 "supercov-cargo-configuration-{}-{}-{}",
985 std::process::id(),
986 SystemTime::now()
987 .duration_since(UNIX_EPOCH)
988 .unwrap()
989 .as_nanos(),
990 FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
991 ));
992 fs::create_dir_all(root.join(".cargo/bin with spaces")).unwrap();
993 root
994 }
995
996 fn invocation(arguments: &[&str]) -> CargoTestInvocation {
997 CargoTestInvocation {
998 program: "cargo".into(),
999 kind: crate::rust_test_runner::RustCargoCommandKind::CargoTest,
1000 arguments: arguments.iter().map(|value| (*value).into()).collect(),
1001 runner_arguments: Vec::new(),
1002 }
1003 }
1004
1005 fn model_inputs() -> CargoModelInputs {
1006 model_inputs_with([])
1007 }
1008
1009 fn model_inputs_with<const N: usize>(
1010 additional: [(OsString, OsString); N],
1011 ) -> CargoModelInputs {
1012 CargoModelInputs {
1013 cargo_home: None,
1014 environment: additional
1015 .into_iter()
1016 .filter_map(|(key, value)| key.into_string().ok().map(|key| (key, value)))
1017 .collect(),
1018 host_override: Some("aarch64-apple-darwin".into()),
1019 }
1020 }
1021
1022 #[test]
1023 fn compiler_tools_follow_cargo_cli_and_environment_precedence() {
1024 let root = fixture();
1025 fs::write(
1026 root.join(".cargo/config.toml"),
1027 concat!(
1028 "[build]\n",
1029 "rustc=\"./file-rustc\"\n",
1030 "rustc-wrapper=\"./file-wrapper\"\n",
1031 "rustc-workspace-wrapper=\"./file-workspace-wrapper\"\n",
1032 ),
1033 )
1034 .unwrap();
1035 let cli = [
1036 "build.rustc=\"./cli-rustc\"".into(),
1037 "build.rustc-wrapper=\"./cli-wrapper\"".into(),
1038 "build.rustc-workspace-wrapper=\"./cli-workspace-wrapper\"".into(),
1039 ];
1040 let model = load_cargo_configuration(&root, None, &cli).unwrap();
1041 let plan = compiler_command_plan(
1042 &root,
1043 &model,
1044 RustCargoRunnerProgram::SearchPath {
1045 value: "rustc".into(),
1046 },
1047 &model_inputs_with([
1048 (OsString::from("RUSTC"), OsString::from("./direct-rustc")),
1049 (
1050 OsString::from("CARGO_BUILD_RUSTC_WRAPPER"),
1051 OsString::from("./cargo-wrapper"),
1052 ),
1053 (OsString::from("RUSTC_WORKSPACE_WRAPPER"), OsString::new()),
1054 ]),
1055 )
1056 .unwrap();
1057 assert_eq!(
1058 plan.rustc,
1059 RustCargoRunnerProgram::WorkspaceRelative {
1060 value: "direct-rustc".into()
1061 }
1062 );
1063 assert_eq!(
1064 plan.rustc_wrapper,
1065 Some(RustCargoRunnerProgram::WorkspaceRelative {
1066 value: "cli-wrapper".into()
1067 })
1068 );
1069 assert_eq!(plan.rustc_workspace_wrapper, None);
1070 fs::remove_dir_all(root).unwrap();
1071 }
1072
1073 #[cfg(unix)]
1074 #[test]
1075 fn direct_non_utf8_tool_environment_falls_back_but_config_environment_is_invalid() {
1076 use std::os::unix::ffi::OsStringExt;
1077
1078 let root = fixture();
1079 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1080 let model =
1081 load_cargo_configuration(&root, None, &["build.rustc=\"./cli-rustc\"".into()]).unwrap();
1082 let plan = compiler_command_plan(
1083 &root,
1084 &model,
1085 RustCargoRunnerProgram::SearchPath {
1086 value: "rustc".into(),
1087 },
1088 &model_inputs_with([(OsString::from("RUSTC"), OsString::from_vec(vec![0xff]))]),
1089 )
1090 .unwrap();
1091 assert_eq!(
1092 plan.rustc,
1093 RustCargoRunnerProgram::WorkspaceRelative {
1094 value: "cli-rustc".into()
1095 }
1096 );
1097
1098 let empty_model = load_cargo_configuration(&root, None, &[]).unwrap();
1099 let error = compiler_command_plan(
1100 &root,
1101 &empty_model,
1102 RustCargoRunnerProgram::SearchPath {
1103 value: "rustc".into(),
1104 },
1105 &model_inputs_with([(
1106 OsString::from("CARGO_BUILD_RUSTC"),
1107 OsString::from_vec(vec![0xff]),
1108 )]),
1109 )
1110 .unwrap_err()
1111 .to_string();
1112 assert!(error.contains("CARGO_BUILD_RUSTC is not UTF-8"), "{error}");
1113 fs::remove_dir_all(root).unwrap();
1114 }
1115
1116 #[test]
1117 fn every_compiler_wrapper_layer_is_preserved_in_the_compiler_plan() {
1118 let root = fixture();
1119 fs::write(
1120 root.join(".cargo/config.toml"),
1121 "[build]\nrustc-wrapper=\"wrapper\"\n",
1122 )
1123 .unwrap();
1124 let model = load_cargo_configuration(&root, None, &[]).unwrap();
1125 let plan = compiler_command_plan(
1126 &root,
1127 &model,
1128 RustCargoRunnerProgram::SearchPath {
1129 value: "rustc".into(),
1130 },
1131 &model_inputs(),
1132 )
1133 .unwrap();
1134 assert_eq!(
1135 plan.rustc_wrapper,
1136 Some(RustCargoRunnerProgram::SearchPath {
1137 value: "wrapper".into()
1138 })
1139 );
1140
1141 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1142 let model = load_cargo_configuration(
1143 &root,
1144 None,
1145 &["build.rustc-workspace-wrapper=\"workspace-wrapper\"".into()],
1146 )
1147 .unwrap();
1148 let plan = compiler_command_plan(
1149 &root,
1150 &model,
1151 RustCargoRunnerProgram::SearchPath {
1152 value: "rustc".into(),
1153 },
1154 &model_inputs(),
1155 )
1156 .unwrap();
1157 assert_eq!(
1158 plan.rustc_workspace_wrapper,
1159 Some(RustCargoRunnerProgram::SearchPath {
1160 value: "workspace-wrapper".into()
1161 })
1162 );
1163
1164 let model = load_cargo_configuration(&root, None, &[]).unwrap();
1165 let plan = compiler_command_plan(
1166 &root,
1167 &model,
1168 RustCargoRunnerProgram::SearchPath {
1169 value: "rustc".into(),
1170 },
1171 &model_inputs_with([(
1172 OsString::from("RUSTC_WRAPPER"),
1173 OsString::from("environment-wrapper"),
1174 )]),
1175 )
1176 .unwrap();
1177 assert_eq!(
1178 plan.rustc_wrapper,
1179 Some(RustCargoRunnerProgram::SearchPath {
1180 value: "environment-wrapper".into()
1181 })
1182 );
1183 fs::remove_dir_all(root).unwrap();
1184 }
1185
1186 #[cfg(unix)]
1187 #[test]
1188 fn included_build_rustc_drives_host_and_cfg_runner_selection() {
1189 use std::os::unix::fs::PermissionsExt;
1190
1191 let root = fixture();
1192 let rustc = which::which("rustc").unwrap();
1193 let proxy = root.join("compiler-proxy");
1194 let log = root.join("compiler.log");
1195 fs::write(
1196 &proxy,
1197 format!(
1198 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"{}\"\nexec \"{}\" \"$@\"\n",
1199 log.display(),
1200 rustc.display()
1201 ),
1202 )
1203 .unwrap();
1204 fs::set_permissions(&proxy, fs::Permissions::from_mode(0o755)).unwrap();
1205 fs::write(
1206 root.join(".cargo/compiler.toml"),
1207 concat!(
1208 "[build]\nrustc=\"./compiler-proxy\"\n",
1209 "[target.'cfg(unix)']\nrunner=[\"cfg-runner\",\"--cfg\"]\n",
1210 ),
1211 )
1212 .unwrap();
1213 fs::write(
1214 root.join(".cargo/config.toml"),
1215 "include=[\"compiler.toml\"]\n",
1216 )
1217 .unwrap();
1218 let mut inputs = model_inputs();
1219 inputs.host_override = None;
1220 let plan = resolve_with_inputs(&root, &root, &invocation(&["test"]), inputs).unwrap();
1221 assert_eq!(
1222 plan.compiler.rustc,
1223 RustCargoRunnerProgram::WorkspaceRelative {
1224 value: "compiler-proxy".into()
1225 }
1226 );
1227 assert_eq!(
1228 plan.targets[0].underlying_runner,
1229 Some(RustCargoUnderlyingRunner {
1230 program: RustCargoRunnerProgram::SearchPath {
1231 value: "cfg-runner".into()
1232 },
1233 arguments: vec!["--cfg".into()]
1234 })
1235 );
1236 let invocations = fs::read_to_string(&log).unwrap();
1237 assert!(invocations.lines().any(|line| line == "-vV"));
1238 assert!(
1239 invocations
1240 .lines()
1241 .any(|line| line.contains("--print cfg --target"))
1242 );
1243 fs::remove_dir_all(root).unwrap();
1244 }
1245
1246 #[test]
1247 fn exact_target_beats_cfg_and_environment_beats_both() {
1248 let root = fixture();
1249 fs::write(
1250 root.join(".cargo/config.toml"),
1251 concat!(
1252 "[target.'cfg(target_vendor = \"apple\")']\n",
1253 "runner=[\"cfg-runner\",\"--cfg\"]\n",
1254 "[target.aarch64-apple-darwin]\n",
1255 "runner=\"exact-runner --exact\"\n",
1256 ),
1257 )
1258 .unwrap();
1259 let plan =
1260 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1261 assert_eq!(
1262 plan.targets[0].underlying_runner,
1263 Some(RustCargoUnderlyingRunner {
1264 program: RustCargoRunnerProgram::SearchPath {
1265 value: "exact-runner".into(),
1266 },
1267 arguments: vec!["--exact".into()],
1268 })
1269 );
1270 let plan = resolve_with_inputs(
1271 &root,
1272 &root,
1273 &invocation(&["test"]),
1274 model_inputs_with([(
1275 OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1276 OsString::from("environment-runner --environment"),
1277 )]),
1278 )
1279 .unwrap();
1280 assert_eq!(
1281 plan.targets[0].underlying_runner,
1282 Some(RustCargoUnderlyingRunner {
1283 program: RustCargoRunnerProgram::SearchPath {
1284 value: "environment-runner".into(),
1285 },
1286 arguments: vec!["--environment".into()],
1287 })
1288 );
1289 fs::remove_dir_all(root).unwrap();
1290 }
1291
1292 #[test]
1293 fn resolves_exact_array_runner_and_relocates_workspace_program() {
1294 let root = fixture();
1295 fs::write(
1296 root.join(".cargo/config.toml"),
1297 "[target.aarch64-apple-darwin]\nrunner=[\"bin with spaces/runner\",\"--fixed\"]\n",
1298 )
1299 .unwrap();
1300 let plan =
1301 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1302 assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1303 assert_eq!(
1304 plan.targets[0].underlying_runner,
1305 Some(RustCargoUnderlyingRunner {
1306 program: RustCargoRunnerProgram::WorkspaceRelative {
1307 value: PathBuf::from("bin with spaces/runner")
1308 },
1309 arguments: vec!["--fixed".into()],
1310 })
1311 );
1312 fs::remove_dir_all(root).unwrap();
1313 }
1314
1315 #[test]
1316 fn ordinary_files_use_cargo_195_runner_parsing_and_duplicate_cfg_rules() {
1317 let root = fixture();
1318 fs::write(
1319 root.join(".cargo/config.toml"),
1320 "[target.aarch64-apple-darwin]\nrunner=\"runner\\t--one\\n--two\"\n",
1321 )
1322 .unwrap();
1323 let plan =
1324 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1325 assert_eq!(
1326 plan.targets[0].underlying_runner,
1327 Some(RustCargoUnderlyingRunner {
1328 program: RustCargoRunnerProgram::SearchPath {
1329 value: "runner".into()
1330 },
1331 arguments: vec!["--one".into(), "--two".into()]
1332 })
1333 );
1334 fs::write(
1335 root.join(".cargo/config.toml"),
1336 concat!(
1337 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1338 "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1339 ),
1340 )
1341 .unwrap();
1342 let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1343 .unwrap_err()
1344 .to_string();
1345 assert!(error.contains("several matching instances"), "{error}");
1346 fs::remove_dir_all(root).unwrap();
1347 }
1348
1349 #[test]
1350 fn cargo_duplicate_and_host_tuple_targets_collapse_before_runner_selection() {
1351 let root = fixture();
1352 fs::write(
1353 root.join(".cargo/config.toml"),
1354 "[target.aarch64-apple-darwin]\nrunner=\"runner\"\n",
1355 )
1356 .unwrap();
1357 let plan = resolve_with_inputs(
1358 &root,
1359 &root,
1360 &invocation(&[
1361 "test",
1362 "--target=host-tuple",
1363 "--target=aarch64-apple-darwin",
1364 "--target=aarch64-apple-darwin",
1365 ]),
1366 model_inputs(),
1367 )
1368 .unwrap();
1369 assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1370 assert_eq!(
1371 plan.targets[0].underlying_runner,
1372 Some(RustCargoUnderlyingRunner {
1373 program: RustCargoRunnerProgram::SearchPath {
1374 value: "runner".into()
1375 },
1376 arguments: Vec::new()
1377 })
1378 );
1379 fs::remove_dir_all(root).unwrap();
1380 }
1381
1382 #[test]
1383 fn custom_target_paths_are_project_relative_and_name_collisions_fail_closed() {
1384 let root = fixture();
1385 fs::create_dir_all(root.join("targets/first")).unwrap();
1386 fs::create_dir_all(root.join("targets/second")).unwrap();
1387 fs::write(root.join("targets/first/custom.json"), "{}").unwrap();
1388 fs::write(root.join("targets/second/custom.json"), "{}").unwrap();
1389 let model = CargoConfigValue {
1390 kind: CargoConfigKind::Table(std::collections::BTreeMap::new()),
1391 definition: CargoConfigDefinition::BuiltIn,
1392 };
1393 let inputs = model_inputs();
1394 let target = model_targets(
1395 &root,
1396 &model,
1397 vec!["targets/first/custom.json".into()],
1398 "aarch64-apple-darwin",
1399 &inputs,
1400 )
1401 .unwrap();
1402 assert_eq!(target[0].name, "custom");
1403 assert_eq!(
1404 Path::new(&target[0].cargo_argument),
1405 fs::canonicalize(root.join("targets/first/custom.json")).unwrap()
1406 );
1407 let error = model_targets(
1408 &root,
1409 &model,
1410 vec![
1411 "targets/first/custom.json".into(),
1412 "targets/second/custom.json".into(),
1413 ],
1414 "aarch64-apple-darwin",
1415 &inputs,
1416 )
1417 .unwrap_err()
1418 .to_string();
1419 assert!(error.contains("same configuration identity"), "{error}");
1420 fs::remove_dir_all(root).unwrap();
1421 }
1422
1423 #[test]
1424 fn direct_cargo_path_keeps_cargos_default_rustc_search_semantics() {
1425 let root = fixture();
1426 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1427 let cargo = which::which("cargo").unwrap();
1428 let plan = resolve_with_inputs(
1429 &root,
1430 &root,
1431 &CargoTestInvocation {
1432 program: cargo.to_string_lossy().into_owned(),
1433 kind: crate::rust_test_runner::RustCargoCommandKind::CargoTest,
1434 arguments: vec!["test".into()],
1435 runner_arguments: Vec::new(),
1436 },
1437 model_inputs(),
1438 )
1439 .unwrap();
1440 assert_eq!(
1441 plan.compiler.rustc,
1442 RustCargoRunnerProgram::SearchPath {
1443 value: format!("rustc{}", std::env::consts::EXE_SUFFIX)
1444 }
1445 );
1446 fs::remove_dir_all(root).unwrap();
1447 }
1448
1449 #[test]
1450 fn resolves_an_explicit_installed_rustup_toolchain_before_target_configuration() {
1451 let root = fixture();
1452 let rustc = Command::new("rustc")
1455 .args(["+1.95.0", "-vV"])
1456 .output()
1457 .unwrap();
1458 assert!(
1459 rustc.status.success(),
1460 "{}",
1461 String::from_utf8_lossy(&rustc.stderr)
1462 );
1463 let host = "aarch64-apple-darwin";
1469 fs::write(
1470 root.join(".cargo/config.toml"),
1471 format!("[target.{host}]\nrunner=[\"selected-runner\",\"--selected\"]\n"),
1472 )
1473 .unwrap();
1474 let plan = resolve_with_inputs(
1475 &root,
1476 &root,
1477 &invocation(&["+1.95.0", "test"]),
1478 model_inputs(),
1479 )
1480 .unwrap();
1481 let rustc_executable = format!("rustc{}", std::env::consts::EXE_SUFFIX);
1482 assert!(matches!(
1483 &plan.compiler.rustc,
1484 RustCargoRunnerProgram::Absolute { value }
1485 if value.file_name().and_then(|name| name.to_str())
1486 == Some(rustc_executable.as_str())
1487 ));
1488 assert_eq!(plan.targets[0].target, host);
1489 assert_eq!(
1490 plan.targets[0].underlying_runner,
1491 Some(RustCargoUnderlyingRunner {
1492 program: RustCargoRunnerProgram::SearchPath {
1493 value: "selected-runner".into(),
1494 },
1495 arguments: vec!["--selected".into()],
1496 })
1497 );
1498 fs::remove_dir_all(root).unwrap();
1499 }
1500
1501 #[test]
1502 fn resolves_include_cli_cfg_and_multiple_target_runners_but_rejects_open_surfaces() {
1503 let root = fixture();
1504 fs::write(
1505 root.join(".cargo/extra.toml"),
1506 "[target.aarch64-apple-darwin]\nrunner=[\"included\",\"--included\"]\n",
1507 )
1508 .unwrap();
1509 fs::write(
1510 root.join(".cargo/config.toml"),
1511 "include=[\"extra.toml\"]\n",
1512 )
1513 .unwrap();
1514 assert_eq!(
1515 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1516 .unwrap()
1517 .targets[0]
1518 .underlying_runner,
1519 Some(RustCargoUnderlyingRunner {
1520 program: RustCargoRunnerProgram::SearchPath {
1521 value: "included".into()
1522 },
1523 arguments: vec!["--included".into()]
1524 })
1525 );
1526 assert_eq!(
1527 resolve_with_inputs(
1528 &root,
1529 &root,
1530 &invocation(&[
1531 "test",
1532 "--config",
1533 "target.aarch64-apple-darwin.runner=[\"cli\",\"--cli\"]",
1534 ]),
1535 model_inputs_with([(
1536 OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1537 OsString::from("environment"),
1538 )]),
1539 )
1540 .unwrap()
1541 .targets[0]
1542 .underlying_runner,
1543 Some(RustCargoUnderlyingRunner {
1544 program: RustCargoRunnerProgram::SearchPath {
1545 value: "cli".into()
1546 },
1547 arguments: vec!["--cli".into()]
1548 })
1549 );
1550 let multi_target_environment = [
1551 (
1552 OsString::from("CARGO_TARGET_A_RUNNER"),
1553 OsString::from("runner-a"),
1554 ),
1555 (
1556 OsString::from("CARGO_TARGET_B_RUNNER"),
1557 OsString::from("runner-b"),
1558 ),
1559 ];
1560 let plan = resolve_with_inputs(
1561 &root,
1562 &root,
1563 &invocation(&["test", "--target=a", "--target=b"]),
1564 model_inputs_with(multi_target_environment),
1565 );
1566 let plan = plan.unwrap();
1567 assert_eq!(
1568 plan.targets
1569 .iter()
1570 .map(|target| target.target.as_str())
1571 .collect::<Vec<_>>(),
1572 ["a", "b"]
1573 );
1574 assert_eq!(
1575 plan.targets
1576 .iter()
1577 .map(|target| {
1578 target
1579 .underlying_runner
1580 .as_ref()
1581 .map(|runner| &runner.program)
1582 })
1583 .collect::<Vec<_>>(),
1584 [
1585 Some(&RustCargoRunnerProgram::SearchPath {
1586 value: "runner-a".into()
1587 }),
1588 Some(&RustCargoRunnerProgram::SearchPath {
1589 value: "runner-b".into()
1590 })
1591 ]
1592 );
1593 fs::write(
1594 root.join(".cargo/extra.toml"),
1595 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"included-cfg\"\n",
1596 )
1597 .unwrap();
1598 assert_eq!(
1599 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1600 .unwrap()
1601 .targets[0]
1602 .underlying_runner,
1603 Some(RustCargoUnderlyingRunner {
1604 program: RustCargoRunnerProgram::SearchPath {
1605 value: "included-cfg".into()
1606 },
1607 arguments: Vec::new()
1608 })
1609 );
1610 fs::write(
1611 root.join(".cargo/extra.toml"),
1612 concat!(
1613 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1614 "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1615 ),
1616 )
1617 .unwrap();
1618 let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1619 .unwrap_err()
1620 .to_string();
1621 assert!(error.contains("several matching instances"), "{error}");
1622 assert!(
1623 command_targets(&invocation(&["--quiet", "+nightly", "test"]))
1624 .unwrap_err()
1625 .to_string()
1626 .contains("must be the first")
1627 );
1628 assert!(
1629 resolve_with_inputs(
1630 &root,
1631 &root,
1632 &invocation(&["-Ztarget-applies-to-host", "test"]),
1633 model_inputs(),
1634 )
1635 .unwrap_err()
1636 .to_string()
1637 .contains("-Z configuration semantics")
1638 );
1639 fs::remove_dir_all(root).unwrap();
1640 }
1641}