Skip to main content

supercov_engine/
rust_cargo_configuration.rs

1//! Exact pre-execution resolution for Cargo target runners.
2//!
3//! Cargo remains the authority for building and ordering test artifacts, but
4//! Supercov must compose an already-configured target runner inside its
5//! authenticated Cargo-runner boundary without changing Cargo or libtest
6//! semantics. This module resolves the user's original Cargo configuration
7//! before the repository is copied into the isolated workspace. Unsupported
8//! configuration surfaces fail before user code executes.
9
10use 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
849/// What a test command builds with: its resolved configuration, compiler
850/// command and targets.
851struct 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
891/// The cfg values of the one target a test command builds for: what decides
892/// which `#[cfg(..)]` items the compiler leaves out. None when the command
893/// builds for several targets or they cannot be read, so nothing is assumed.
894pub(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    /// Write an executable script by way of another process. A file this
1187    /// process has open for writing is inherited by a child another test
1188    /// thread forks, and held until that child execs; running the script
1189    /// meanwhile fails with "Text file busy". Written by `cp`, the script
1190    /// never has a descriptor here.
1191    #[cfg(unix)]
1192    fn write_script(path: &Path, text: &str) {
1193        use std::os::unix::fs::PermissionsExt;
1194
1195        let draft = path.with_extension("draft");
1196        fs::write(&draft, text).unwrap();
1197        let copied = std::process::Command::new("cp")
1198            .arg(&draft)
1199            .arg(path)
1200            .status()
1201            .unwrap();
1202        assert!(copied.success(), "cp {} failed", draft.display());
1203        fs::remove_file(&draft).unwrap();
1204        fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1205    }
1206
1207    #[cfg(unix)]
1208    #[test]
1209    fn included_build_rustc_drives_host_and_cfg_runner_selection() {
1210        let root = fixture();
1211        let rustc = which::which("rustc").unwrap();
1212        let proxy = root.join("compiler-proxy");
1213        let log = root.join("compiler.log");
1214        write_script(
1215            &proxy,
1216            &format!(
1217                "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"{}\"\nexec \"{}\" \"$@\"\n",
1218                log.display(),
1219                rustc.display()
1220            ),
1221        );
1222        fs::write(
1223            root.join(".cargo/compiler.toml"),
1224            concat!(
1225                "[build]\nrustc=\"./compiler-proxy\"\n",
1226                "[target.'cfg(unix)']\nrunner=[\"cfg-runner\",\"--cfg\"]\n",
1227            ),
1228        )
1229        .unwrap();
1230        fs::write(
1231            root.join(".cargo/config.toml"),
1232            "include=[\"compiler.toml\"]\n",
1233        )
1234        .unwrap();
1235        let mut inputs = model_inputs();
1236        inputs.host_override = None;
1237        let plan = resolve_with_inputs(&root, &root, &invocation(&["test"]), inputs).unwrap();
1238        assert_eq!(
1239            plan.compiler.rustc,
1240            RustCargoRunnerProgram::WorkspaceRelative {
1241                value: "compiler-proxy".into()
1242            }
1243        );
1244        assert_eq!(
1245            plan.targets[0].underlying_runner,
1246            Some(RustCargoUnderlyingRunner {
1247                program: RustCargoRunnerProgram::SearchPath {
1248                    value: "cfg-runner".into()
1249                },
1250                arguments: vec!["--cfg".into()]
1251            })
1252        );
1253        let invocations = fs::read_to_string(&log).unwrap();
1254        assert!(invocations.lines().any(|line| line == "-vV"));
1255        assert!(
1256            invocations
1257                .lines()
1258                .any(|line| line.contains("--print cfg --target"))
1259        );
1260        fs::remove_dir_all(root).unwrap();
1261    }
1262
1263    #[test]
1264    fn exact_target_beats_cfg_and_environment_beats_both() {
1265        let root = fixture();
1266        fs::write(
1267            root.join(".cargo/config.toml"),
1268            concat!(
1269                "[target.'cfg(target_vendor = \"apple\")']\n",
1270                "runner=[\"cfg-runner\",\"--cfg\"]\n",
1271                "[target.aarch64-apple-darwin]\n",
1272                "runner=\"exact-runner --exact\"\n",
1273            ),
1274        )
1275        .unwrap();
1276        let plan =
1277            resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1278        assert_eq!(
1279            plan.targets[0].underlying_runner,
1280            Some(RustCargoUnderlyingRunner {
1281                program: RustCargoRunnerProgram::SearchPath {
1282                    value: "exact-runner".into(),
1283                },
1284                arguments: vec!["--exact".into()],
1285            })
1286        );
1287        let plan = resolve_with_inputs(
1288            &root,
1289            &root,
1290            &invocation(&["test"]),
1291            model_inputs_with([(
1292                OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1293                OsString::from("environment-runner --environment"),
1294            )]),
1295        )
1296        .unwrap();
1297        assert_eq!(
1298            plan.targets[0].underlying_runner,
1299            Some(RustCargoUnderlyingRunner {
1300                program: RustCargoRunnerProgram::SearchPath {
1301                    value: "environment-runner".into(),
1302                },
1303                arguments: vec!["--environment".into()],
1304            })
1305        );
1306        fs::remove_dir_all(root).unwrap();
1307    }
1308
1309    #[test]
1310    fn resolves_exact_array_runner_and_relocates_workspace_program() {
1311        let root = fixture();
1312        fs::write(
1313            root.join(".cargo/config.toml"),
1314            "[target.aarch64-apple-darwin]\nrunner=[\"bin with spaces/runner\",\"--fixed\"]\n",
1315        )
1316        .unwrap();
1317        let plan =
1318            resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1319        assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1320        assert_eq!(
1321            plan.targets[0].underlying_runner,
1322            Some(RustCargoUnderlyingRunner {
1323                program: RustCargoRunnerProgram::WorkspaceRelative {
1324                    value: PathBuf::from("bin with spaces/runner")
1325                },
1326                arguments: vec!["--fixed".into()],
1327            })
1328        );
1329        fs::remove_dir_all(root).unwrap();
1330    }
1331
1332    #[test]
1333    fn ordinary_files_use_cargo_195_runner_parsing_and_duplicate_cfg_rules() {
1334        let root = fixture();
1335        fs::write(
1336            root.join(".cargo/config.toml"),
1337            "[target.aarch64-apple-darwin]\nrunner=\"runner\\t--one\\n--two\"\n",
1338        )
1339        .unwrap();
1340        let plan =
1341            resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1342        assert_eq!(
1343            plan.targets[0].underlying_runner,
1344            Some(RustCargoUnderlyingRunner {
1345                program: RustCargoRunnerProgram::SearchPath {
1346                    value: "runner".into()
1347                },
1348                arguments: vec!["--one".into(), "--two".into()]
1349            })
1350        );
1351        fs::write(
1352            root.join(".cargo/config.toml"),
1353            concat!(
1354                "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1355                "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1356            ),
1357        )
1358        .unwrap();
1359        let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1360            .unwrap_err()
1361            .to_string();
1362        assert!(error.contains("several matching instances"), "{error}");
1363        fs::remove_dir_all(root).unwrap();
1364    }
1365
1366    #[test]
1367    fn cargo_duplicate_and_host_tuple_targets_collapse_before_runner_selection() {
1368        let root = fixture();
1369        fs::write(
1370            root.join(".cargo/config.toml"),
1371            "[target.aarch64-apple-darwin]\nrunner=\"runner\"\n",
1372        )
1373        .unwrap();
1374        let plan = resolve_with_inputs(
1375            &root,
1376            &root,
1377            &invocation(&[
1378                "test",
1379                "--target=host-tuple",
1380                "--target=aarch64-apple-darwin",
1381                "--target=aarch64-apple-darwin",
1382            ]),
1383            model_inputs(),
1384        )
1385        .unwrap();
1386        assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1387        assert_eq!(
1388            plan.targets[0].underlying_runner,
1389            Some(RustCargoUnderlyingRunner {
1390                program: RustCargoRunnerProgram::SearchPath {
1391                    value: "runner".into()
1392                },
1393                arguments: Vec::new()
1394            })
1395        );
1396        fs::remove_dir_all(root).unwrap();
1397    }
1398
1399    #[test]
1400    fn custom_target_paths_are_project_relative_and_name_collisions_fail_closed() {
1401        let root = fixture();
1402        fs::create_dir_all(root.join("targets/first")).unwrap();
1403        fs::create_dir_all(root.join("targets/second")).unwrap();
1404        fs::write(root.join("targets/first/custom.json"), "{}").unwrap();
1405        fs::write(root.join("targets/second/custom.json"), "{}").unwrap();
1406        let model = CargoConfigValue {
1407            kind: CargoConfigKind::Table(std::collections::BTreeMap::new()),
1408            definition: CargoConfigDefinition::BuiltIn,
1409        };
1410        let inputs = model_inputs();
1411        let target = model_targets(
1412            &root,
1413            &model,
1414            vec!["targets/first/custom.json".into()],
1415            "aarch64-apple-darwin",
1416            &inputs,
1417        )
1418        .unwrap();
1419        assert_eq!(target[0].name, "custom");
1420        assert_eq!(
1421            Path::new(&target[0].cargo_argument),
1422            fs::canonicalize(root.join("targets/first/custom.json")).unwrap()
1423        );
1424        let error = model_targets(
1425            &root,
1426            &model,
1427            vec![
1428                "targets/first/custom.json".into(),
1429                "targets/second/custom.json".into(),
1430            ],
1431            "aarch64-apple-darwin",
1432            &inputs,
1433        )
1434        .unwrap_err()
1435        .to_string();
1436        assert!(error.contains("same configuration identity"), "{error}");
1437        fs::remove_dir_all(root).unwrap();
1438    }
1439
1440    #[test]
1441    fn direct_cargo_path_keeps_cargos_default_rustc_search_semantics() {
1442        let root = fixture();
1443        fs::write(root.join(".cargo/config.toml"), "").unwrap();
1444        let cargo = which::which("cargo").unwrap();
1445        let plan = resolve_with_inputs(
1446            &root,
1447            &root,
1448            &CargoTestInvocation {
1449                program: cargo.to_string_lossy().into_owned(),
1450                kind: crate::rust_test_runner::RustCargoCommandKind::CargoTest,
1451                arguments: vec!["test".into()],
1452                runner_arguments: Vec::new(),
1453            },
1454            model_inputs(),
1455        )
1456        .unwrap();
1457        assert_eq!(
1458            plan.compiler.rustc,
1459            RustCargoRunnerProgram::SearchPath {
1460                value: format!("rustc{}", std::env::consts::EXE_SUFFIX)
1461            }
1462        );
1463        fs::remove_dir_all(root).unwrap();
1464    }
1465
1466    #[test]
1467    fn resolves_an_explicit_installed_rustup_toolchain_before_target_configuration() {
1468        let root = fixture();
1469        // The toolchain has to be really installed for `+1.95.0` to resolve to
1470        // a real rustc, which is what this test is about.
1471        let rustc = Command::new("rustc")
1472            .args(["+1.95.0", "-vV"])
1473            .output()
1474            .unwrap();
1475        assert!(
1476            rustc.status.success(),
1477            "{}",
1478            String::from_utf8_lossy(&rustc.stderr)
1479        );
1480        // The target comes from `model_inputs`, which pins the host the way
1481        // every other test in this file does. Reading it from the machine
1482        // instead made the test pass only on an Apple silicon Mac: everywhere
1483        // else the plan honoured the pinned host and the assertion compared it
1484        // against the real one.
1485        let host = "aarch64-apple-darwin";
1486        fs::write(
1487            root.join(".cargo/config.toml"),
1488            format!("[target.{host}]\nrunner=[\"selected-runner\",\"--selected\"]\n"),
1489        )
1490        .unwrap();
1491        let plan = resolve_with_inputs(
1492            &root,
1493            &root,
1494            &invocation(&["+1.95.0", "test"]),
1495            model_inputs(),
1496        )
1497        .unwrap();
1498        let rustc_executable = format!("rustc{}", std::env::consts::EXE_SUFFIX);
1499        assert!(matches!(
1500            &plan.compiler.rustc,
1501            RustCargoRunnerProgram::Absolute { value }
1502                if value.file_name().and_then(|name| name.to_str())
1503                    == Some(rustc_executable.as_str())
1504        ));
1505        assert_eq!(plan.targets[0].target, host);
1506        assert_eq!(
1507            plan.targets[0].underlying_runner,
1508            Some(RustCargoUnderlyingRunner {
1509                program: RustCargoRunnerProgram::SearchPath {
1510                    value: "selected-runner".into(),
1511                },
1512                arguments: vec!["--selected".into()],
1513            })
1514        );
1515        fs::remove_dir_all(root).unwrap();
1516    }
1517
1518    #[test]
1519    fn resolves_include_cli_cfg_and_multiple_target_runners_but_rejects_open_surfaces() {
1520        let root = fixture();
1521        fs::write(
1522            root.join(".cargo/extra.toml"),
1523            "[target.aarch64-apple-darwin]\nrunner=[\"included\",\"--included\"]\n",
1524        )
1525        .unwrap();
1526        fs::write(
1527            root.join(".cargo/config.toml"),
1528            "include=[\"extra.toml\"]\n",
1529        )
1530        .unwrap();
1531        assert_eq!(
1532            resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1533                .unwrap()
1534                .targets[0]
1535                .underlying_runner,
1536            Some(RustCargoUnderlyingRunner {
1537                program: RustCargoRunnerProgram::SearchPath {
1538                    value: "included".into()
1539                },
1540                arguments: vec!["--included".into()]
1541            })
1542        );
1543        assert_eq!(
1544            resolve_with_inputs(
1545                &root,
1546                &root,
1547                &invocation(&[
1548                    "test",
1549                    "--config",
1550                    "target.aarch64-apple-darwin.runner=[\"cli\",\"--cli\"]",
1551                ]),
1552                model_inputs_with([(
1553                    OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1554                    OsString::from("environment"),
1555                )]),
1556            )
1557            .unwrap()
1558            .targets[0]
1559                .underlying_runner,
1560            Some(RustCargoUnderlyingRunner {
1561                program: RustCargoRunnerProgram::SearchPath {
1562                    value: "cli".into()
1563                },
1564                arguments: vec!["--cli".into()]
1565            })
1566        );
1567        let multi_target_environment = [
1568            (
1569                OsString::from("CARGO_TARGET_A_RUNNER"),
1570                OsString::from("runner-a"),
1571            ),
1572            (
1573                OsString::from("CARGO_TARGET_B_RUNNER"),
1574                OsString::from("runner-b"),
1575            ),
1576        ];
1577        let plan = resolve_with_inputs(
1578            &root,
1579            &root,
1580            &invocation(&["test", "--target=a", "--target=b"]),
1581            model_inputs_with(multi_target_environment),
1582        );
1583        let plan = plan.unwrap();
1584        assert_eq!(
1585            plan.targets
1586                .iter()
1587                .map(|target| target.target.as_str())
1588                .collect::<Vec<_>>(),
1589            ["a", "b"]
1590        );
1591        assert_eq!(
1592            plan.targets
1593                .iter()
1594                .map(|target| {
1595                    target
1596                        .underlying_runner
1597                        .as_ref()
1598                        .map(|runner| &runner.program)
1599                })
1600                .collect::<Vec<_>>(),
1601            [
1602                Some(&RustCargoRunnerProgram::SearchPath {
1603                    value: "runner-a".into()
1604                }),
1605                Some(&RustCargoRunnerProgram::SearchPath {
1606                    value: "runner-b".into()
1607                })
1608            ]
1609        );
1610        fs::write(
1611            root.join(".cargo/extra.toml"),
1612            "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"included-cfg\"\n",
1613        )
1614        .unwrap();
1615        assert_eq!(
1616            resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1617                .unwrap()
1618                .targets[0]
1619                .underlying_runner,
1620            Some(RustCargoUnderlyingRunner {
1621                program: RustCargoRunnerProgram::SearchPath {
1622                    value: "included-cfg".into()
1623                },
1624                arguments: Vec::new()
1625            })
1626        );
1627        fs::write(
1628            root.join(".cargo/extra.toml"),
1629            concat!(
1630                "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1631                "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1632            ),
1633        )
1634        .unwrap();
1635        let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1636            .unwrap_err()
1637            .to_string();
1638        assert!(error.contains("several matching instances"), "{error}");
1639        assert!(
1640            command_targets(&invocation(&["--quiet", "+nightly", "test"]))
1641                .unwrap_err()
1642                .to_string()
1643                .contains("must be the first")
1644        );
1645        assert!(
1646            resolve_with_inputs(
1647                &root,
1648                &root,
1649                &invocation(&["-Ztarget-applies-to-host", "test"]),
1650                model_inputs(),
1651            )
1652            .unwrap_err()
1653            .to_string()
1654            .contains("-Z configuration semantics")
1655        );
1656        fs::remove_dir_all(root).unwrap();
1657    }
1658}