use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::SystemTime;
use cargo_metadata::Edition;
use error_chain::error_chain;
use walkdir::WalkDir;
use crate::skeptic::Test;
#[derive(Debug)]
pub enum TestResult {
Successful(Output),
CompileFailed(Output),
RunFailed(Output),
Cached,
}
impl TestResult {
pub fn met_test_expectations(&self, test: &Test) -> bool {
match self {
TestResult::CompileFailed(_) if test.compile_fail => true,
TestResult::Successful(_) if !test.should_panic => true,
TestResult::RunFailed(_) if test.should_panic => true,
TestResult::Cached => true,
_ => false,
}
}
}
pub fn handle_test(
manifest_dir: Option<&Path>,
target_dir: &Path,
target_triple: &str,
testcase_path: &Path,
compile_type: CompileType,
terminal_colors: bool,
externs: &Vec<String>,
) -> TestResult {
let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
let mut cmd = Command::new(rustc);
cmd.arg(testcase_path)
.arg("--verbose")
.arg(if terminal_colors {
"--color=always"
} else {
"--color=never"
});
match compile_type {
CompileType::Full => cmd.arg("--crate-type=bin"),
CompileType::Check => cmd.arg("--crate-type=lib"),
};
if let Some(manifest_dir) = manifest_dir {
let mut cargo_toml_path = PathBuf::from(manifest_dir);
cargo_toml_path.push("Cargo.toml");
let mut deps_dir = PathBuf::from(target_dir);
deps_dir.push("debug/deps");
let metadata = get_cargo_meta(&cargo_toml_path).expect("failed to read Cargo.toml");
let edition = metadata
.packages
.iter()
.filter_map(|package| edition_str(&package.edition))
.max()
.unwrap();
if edition != "2015" {
cmd.arg(format!("--edition={}", edition));
}
cmd.arg("-L")
.arg(target_dir)
.arg("-L")
.arg(&deps_dir)
.arg("--target")
.arg(target_triple);
for dep in externs {
cmd.arg("--extern");
cmd.arg(dep);
}
for dep in get_rlib_dependencies(manifest_dir.to_path_buf(), target_dir.to_path_buf())
.expect("failed to read dependencies")
{
cmd.arg("--extern");
cmd.arg(format!(
"{}={}",
dep.libname,
dep.rlib.to_str().expect("filename not utf8"),
));
}
}
let mut binary_path = PathBuf::from(testcase_path);
binary_path.set_extension("exe");
match compile_type {
CompileType::Full => cmd.arg("-o").arg(&binary_path),
CompileType::Check => cmd.arg(format!(
"--emit=dep-info={0}.d,metadata={0}.m",
binary_path.display()
)),
};
let command_result = cmd.output().unwrap();
return if !command_result.status.success() {
TestResult::CompileFailed(command_result)
} else if CompileType::Check == compile_type {
TestResult::Successful(command_result)
} else {
let cmd_current_dir = testcase_path
.parent()
.expect("File must live in a directory.");
let mut cmd = Command::new(binary_path);
cmd.current_dir(cmd_current_dir);
let command_output = cmd.output().unwrap();
if command_output.status.success() {
TestResult::Successful(command_result)
} else {
TestResult::RunFailed(command_result)
}
};
}
fn get_rlib_dependencies(manifest_dir: PathBuf, target_dir: PathBuf) -> Result<Vec<Fingerprint>> {
let lock = LockedDeps::from_path(manifest_dir)?;
let fingerprint_dir = target_dir.join(".fingerprint/");
let locked_deps: HashMap<String, String> = lock.collect();
let mut found_deps: HashMap<String, Fingerprint> = HashMap::new();
for finger in WalkDir::new(fingerprint_dir)
.into_iter()
.filter_map(|v| Fingerprint::from_path(v.ok()?.path()).ok())
{
let locked_ver = match locked_deps.get(&finger.name()) {
Some(ver) => ver,
None => continue,
};
match (found_deps.entry(finger.name()), finger.version()) {
(Entry::Occupied(mut e), Some(ver)) => {
if *locked_ver == ver && e.get().mtime < finger.mtime {
e.insert(finger);
}
}
(Entry::Vacant(e), ver) => {
if ver.unwrap_or_else(|| locked_ver.clone()) == *locked_ver {
e.insert(finger);
}
}
_ => (),
}
}
Ok(found_deps
.into_iter()
.filter_map(|(_, val)| if val.rlib.exists() { Some(val) } else { None })
.collect())
}
#[derive(Debug)]
struct LockedDeps {
dependencies: Vec<String>,
}
fn get_cargo_meta<P: AsRef<Path> + std::convert::AsRef<std::ffi::OsStr>>(
path: P,
) -> Result<cargo_metadata::Metadata> {
Ok(cargo_metadata::MetadataCommand::new()
.manifest_path(&path)
.exec()?)
}
impl LockedDeps {
fn from_path<P: AsRef<Path>>(path: P) -> Result<LockedDeps> {
let path = path.as_ref().join("Cargo.toml");
let metadata = get_cargo_meta(path)?;
let workspace_members = metadata.workspace_members;
let deps = metadata
.resolve
.ok_or("Missing dependency metadata")?
.nodes
.into_iter()
.filter(|node| workspace_members.contains(&node.id))
.flat_map(|node| node.dependencies.into_iter())
.chain(workspace_members.clone());
Ok(LockedDeps {
dependencies: deps.map(|node| node.repr).collect(),
})
}
}
impl Iterator for LockedDeps {
type Item = (String, String);
fn next(&mut self) -> Option<(String, String)> {
let dep = self.dependencies.pop()?;
let mut parts = dep.split_whitespace();
let name = parts.next()?;
let val = parts.next()?;
Some((name.replace('-', "_"), val.to_owned()))
}
}
#[derive(Debug)]
struct Fingerprint {
libname: String,
version: Option<String>, rlib: PathBuf,
mtime: SystemTime,
}
fn guess_ext(mut path: PathBuf, exts: &[&str]) -> Result<PathBuf> {
for ext in exts {
path.set_extension(ext);
if path.exists() {
return Ok(path);
}
}
Err(ErrorKind::Fingerprint.into())
}
impl Fingerprint {
fn from_path<P: AsRef<Path>>(path: P) -> Result<Fingerprint> {
let path = path.as_ref();
let mut captures = path
.parent()
.and_then(Path::file_stem)
.and_then(OsStr::to_str)
.ok_or(ErrorKind::Fingerprint)?
.rsplit('-');
let hash = captures.next().ok_or(ErrorKind::Fingerprint)?;
let mut libname_parts = captures.collect::<Vec<_>>();
libname_parts.reverse();
let libname = libname_parts.join("_");
path.extension()
.and_then(|e| if e == "json" { Some(e) } else { None })
.ok_or(ErrorKind::Fingerprint)?;
let mut rlib = PathBuf::from(path);
rlib.pop();
rlib.pop();
rlib.pop();
let mut dll = rlib.clone();
rlib.push(format!("deps/lib{}-{}", libname, hash));
dll.push(format!("deps/{}-{}", libname, hash));
rlib = guess_ext(rlib, &["rlib", "so", "dylib"]).or_else(|_| guess_ext(dll, &["dll"]))?;
Ok(Fingerprint {
libname,
version: None,
rlib,
mtime: fs::metadata(path)?.modified()?,
})
}
fn name(&self) -> String {
self.libname.clone()
}
fn version(&self) -> Option<String> {
self.version.clone()
}
}
error_chain! {
errors { Fingerprint }
foreign_links {
Io(std::io::Error);
Metadata(cargo_metadata::Error);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CompileType {
Full,
Check,
}
fn edition_str(edition: &Edition) -> Option<&'static str> {
Some(match edition {
Edition::E2015 => "2015",
Edition::E2018 => "2018",
Edition::E2021 => "2021",
_ => return None,
})
}