use std::env;
use std::ffi::{OsStr, OsString};
use std::process::Command;
use std::str::FromStr;
use cargo_platform::{Cfg, Platform};
use crate::CoverageGateError;
use crate::error::{ExecuteRustcError, InvalidRustcCfgError, MissingRustcHostTargetError, ResolveTargetError, RustcCommandFailedError};
struct RustcRun {
success: bool,
status: String,
stdout: String,
stderr: String,
}
#[derive(Debug, Clone)]
pub(crate) struct TargetContext {
pub(crate) triple: String,
cfg: Vec<Cfg>,
}
impl TargetContext {
pub(crate) fn resolve(target: Option<&str>) -> Result<Self, CoverageGateError> {
let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
Self::resolve_with_rustc(target, &rustc).map_err(Into::into)
}
fn resolve_with_rustc(target: Option<&str>, rustc: &OsStr) -> Result<Self, ResolveTargetError> {
let rustc_display = rustc.to_string_lossy().into_owned();
Self::resolve_with_runner(target, &rustc_display, |args| {
let output = Command::new(rustc).args(args).output()?;
Ok(RustcRun {
success: output.status.success(),
status: output.status.to_string(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
})
})
}
fn resolve_with_runner(
target: Option<&str>,
rustc_display: &str,
mut run: impl FnMut(&[&str]) -> Result<RustcRun, std::io::Error>,
) -> Result<Self, ResolveTargetError> {
let triple = if let Some(target) = target {
target.to_owned()
} else {
let command = format!("{rustc_display} -vV");
let output = run(&["-vV"]).map_err(|error| ExecuteRustcError::caused_by(command.clone(), error))?;
if !output.success {
return Err(RustcCommandFailedError::new(command, output.status, output.stderr).into());
}
output
.stdout
.lines()
.find_map(|line| line.strip_prefix("host: "))
.map(str::to_owned)
.ok_or_else(|| MissingRustcHostTargetError::new(command))?
};
let command = format!("{rustc_display} --print cfg --target {triple}");
let output = run(&["--print", "cfg", "--target", &triple]).map_err(|error| ExecuteRustcError::caused_by(command.clone(), error))?;
if !output.success {
return Err(RustcCommandFailedError::new(command, output.status, output.stderr).into());
}
let cfg = output
.stdout
.lines()
.filter(|line| !line.is_empty())
.map(|line| Cfg::from_str(line).map_err(|error| InvalidRustcCfgError::caused_by(line.to_owned(), triple.clone(), error).into()))
.collect::<Result<Vec<_>, ResolveTargetError>>()?;
Ok(Self { triple, cfg })
}
pub(crate) fn matches(&self, platform: &Platform) -> bool {
platform.matches(&self.triple, &self.cfg)
}
#[cfg(test)]
pub(crate) fn from_parts(triple: &str, cfg: &[&str]) -> Self {
Self {
triple: triple.to_owned(),
cfg: cfg
.iter()
.map(|value| Cfg::from_str(value).expect("test cfg must use rustc --print cfg syntax"))
.collect(),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::error::Error as _;
use std::io::{Error as IoError, ErrorKind};
use super::*;
fn ok(stdout: &str) -> RustcRun {
RustcRun {
success: true,
status: "exit status: 0".to_owned(),
stdout: stdout.to_owned(),
stderr: String::new(),
}
}
fn failed(status: &str, stderr: &str) -> RustcRun {
RustcRun {
success: false,
status: status.to_owned(),
stdout: String::new(),
stderr: stderr.to_owned(),
}
}
fn replaying(results: Vec<Result<RustcRun, IoError>>) -> impl FnMut(&[&str]) -> Result<RustcRun, IoError> {
let mut remaining = results.into_iter();
move |_| {
remaining
.next()
.expect("runner called more times than the test supplied results for")
}
}
#[test]
fn matches_exact_and_cfg_selectors() {
let target = TargetContext::from_parts(
"x86_64-pc-windows-msvc",
&["windows", "target_arch=\"x86_64\"", "target_os=\"windows\""],
);
assert!(target.matches(&Platform::from_str("x86_64-pc-windows-msvc").expect("exact target")));
assert!(target.matches(&Platform::from_str("cfg(windows)").expect("windows cfg")));
assert!(target.matches(&Platform::from_str("cfg(target_os = \"windows\")").expect("target_os cfg")));
assert!(!target.matches(&Platform::from_str("cfg(unix)").expect("unix cfg")));
}
#[test]
#[cfg_attr(miri, ignore = "spawns rustc; miri isolation forbids process execution")]
fn resolves_host_and_cfg_from_real_rustc() {
let target = TargetContext::resolve(None).expect("resolving the host target must succeed");
assert!(!target.triple.is_empty(), "rustc must report a host triple");
assert!(target.matches(&Platform::from_str(&target.triple).expect("reported triple must parse")));
#[cfg(windows)]
assert!(target.matches(&Platform::from_str("cfg(windows)").expect("windows cfg")));
#[cfg(unix)]
assert!(target.matches(&Platform::from_str("cfg(unix)").expect("unix cfg")));
}
#[test]
#[cfg_attr(miri, ignore = "spawns rustc; miri isolation forbids process execution")]
fn resolves_explicit_target_from_real_rustc() {
let target = TargetContext::resolve(Some("x86_64-unknown-linux-gnu")).expect("resolving an explicit target must succeed");
assert_eq!(target.triple, "x86_64-unknown-linux-gnu");
assert!(target.matches(&Platform::from_str("cfg(unix)").expect("unix cfg")));
assert!(!target.matches(&Platform::from_str("cfg(windows)").expect("windows cfg")));
}
#[test]
#[cfg_attr(miri, ignore = "spawns a process; miri isolation forbids that")]
fn reports_a_rustc_that_cannot_be_launched() {
let error =
TargetContext::resolve_with_rustc(None, "cargo-coverage-gate-no-such-rustc".as_ref()).expect_err("a missing rustc must fail");
assert!(error.to_string().contains("failed to resolve"));
assert!(error.source().is_some(), "resolve error must preserve its typed cause");
}
#[test]
fn reports_a_failed_host_query() {
let error = TargetContext::resolve_with_runner(None, "rustc", replaying(vec![Ok(failed("exit status: 7", "boom"))]))
.expect_err("a failed host query must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("RustcCommandFailedError"), "unexpected cause: {rendered}");
assert!(rendered.contains("boom"), "the stderr must reach the error: {rendered}");
}
#[test]
fn reports_version_output_without_a_host_line() {
let error = TargetContext::resolve_with_runner(None, "rustc", replaying(vec![Ok(ok("rustc 1.97.0"))]))
.expect_err("version output with no host line must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("MissingRustcHostTargetError"), "unexpected cause: {rendered}");
}
#[test]
fn reports_a_host_query_that_cannot_be_launched() {
let error = TargetContext::resolve_with_runner(None, "rustc", replaying(vec![Err(IoError::other("launch failed"))]))
.expect_err("a host query that cannot spawn must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("ExecuteRustcError"), "unexpected cause: {rendered}");
}
#[test]
fn reports_a_failed_cfg_query() {
let error = TargetContext::resolve_with_runner(
Some("x86_64-unknown-linux-gnu"),
"rustc",
replaying(vec![Ok(failed("exit status: 8", "no such target"))]),
)
.expect_err("a failed cfg query must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("RustcCommandFailedError"), "unexpected cause: {rendered}");
assert!(rendered.contains("no such target"), "the stderr must reach the error: {rendered}");
}
#[test]
fn reports_a_cfg_query_that_cannot_be_launched() {
let error = TargetContext::resolve_with_runner(
None,
"rustc",
replaying(vec![
Ok(ok("host: x86_64-unknown-linux-gnu")),
Err(IoError::from(ErrorKind::NotFound)),
]),
)
.expect_err("a cfg query that cannot spawn must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("ExecuteRustcError"), "unexpected cause: {rendered}");
}
#[test]
fn reports_unparsable_cfg_output() {
let error = TargetContext::resolve_with_runner(
Some("x86_64-unknown-linux-gnu"),
"rustc",
replaying(vec![Ok(ok("unix\nnot a cfg"))]),
)
.expect_err("unparsable cfg output must fail");
let rendered = format!("{error:?}");
assert!(rendered.contains("InvalidRustcCfgError"), "unexpected cause: {rendered}");
assert!(
rendered.contains("not a cfg"),
"the offending value must reach the error: {rendered}"
);
}
#[test]
fn ignores_blank_cfg_lines() {
let target = TargetContext::resolve_with_runner(
Some("x86_64-unknown-linux-gnu"),
"rustc",
replaying(vec![Ok(ok("unix\n\ntarget_os=\"linux\"\n"))]),
)
.expect("blank lines must be skipped rather than parsed");
assert!(target.matches(&Platform::from_str("cfg(unix)").expect("unix cfg")));
assert!(target.matches(&Platform::from_str("cfg(target_os = \"linux\")").expect("target_os cfg")));
}
}