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    #[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        // The toolchain has to be really installed for `+1.95.0` to resolve to
1453        // a real rustc, which is what this test is about.
1454        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        // The target comes from `model_inputs`, which pins the host the way
1464        // every other test in this file does. Reading it from the machine
1465        // instead made the test pass only on an Apple silicon Mac: everywhere
1466        // else the plan honoured the pinned host and the assertion compared it
1467        // against the real one.
1468        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}