use std::ffi::{OsStr, OsString};
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use anyhow::{Context, Result, anyhow};
use fslock::LockFile;
use sysinfo::Pid;
use crate::{NumberedDir, NumberedDirBuilder};
const CARGO_PID_FILE_NAME: &str = "cargo-pid";
const CARGO_PID_LOCKFILE: &str = "cargo-pid.lock";
static CARGO_PID: LazyLock<Option<Pid>> = LazyLock::new(cargo_pid);
#[cfg(target_family = "unix")]
const CARGO_NAME: &str = "cargo";
#[cfg(target_family = "unix")]
const NEXTEST_NAME: &str = "cargo-nextest";
#[cfg(target_family = "unix")]
const RUSTDOC_NAME: &str = "rustdoc";
#[cfg(target_family = "unix")]
const RUST_OUT_NAME: &str = "rust_out";
#[cfg(target_family = "windows")]
const CARGO_NAME: &str = "cargo.exe";
#[cfg(target_family = "windows")]
const NEXTEST_NAME: &str = "cargo-nextest.exe";
#[cfg(target_family = "windows")]
const RUSTDOC_NAME: &str = "rustdoc.exe";
#[cfg(target_family = "windows")]
const RUST_OUT_NAME: &str = "rust_out.exe";
pub fn init_testdir() -> NumberedDir {
let parent = cargo_target_dir();
let pkg_name = "testdir";
let mut builder = NumberedDirBuilder::new(pkg_name.to_string());
builder.set_parent(parent);
builder.reusefn(reuse_cargo);
let mut testdir = builder.create().expect("Failed to create testdir");
let mut count = 0;
while create_cargo_pid_file(testdir.path()).is_err() {
count += 1;
if count > 20 {
break;
}
testdir = builder.create().expect("Failed to create testdir");
}
testdir
}
fn cargo_target_dir() -> PathBuf {
match cargo_metadata::MetadataCommand::new().exec() {
Ok(metadata) => metadata.target_directory.into(),
Err(_) => {
let current_exe = ::std::env::current_exe().expect("no current exe");
current_exe
.parent()
.expect("no parent dir for current exe")
.into()
}
}
}
pub(crate) fn reuse_cargo(dir: &Path) -> bool {
let file_name = dir.join(CARGO_PID_FILE_NAME);
if let Some(read_cargo_pid) = fs::read_to_string(&file_name)
.ok()
.and_then(|content| content.parse::<Pid>().ok())
{
return Some(read_cargo_pid) == *CARGO_PID;
}
create_cargo_pid_file(dir).is_ok()
}
pub(crate) fn create_cargo_pid_file(dir: &Path) -> Result<()> {
let cargo_pid = CARGO_PID
.map(|pid| pid.to_string())
.unwrap_or("failed to get cargo PID".to_string());
let mut lockfile = LockFile::open(&dir.join(CARGO_PID_LOCKFILE))?;
lockfile.lock()?;
let file_name = dir.join(CARGO_PID_FILE_NAME);
match File::create_new(&file_name) {
Ok(_) => {
fs::write(&file_name, cargo_pid).context("failed to write cargo-pid")?;
Ok(())
}
Err(_) => {
let read_pid = fs::read_to_string(&file_name)
.context("failed to read cargo-pid")?
.parse::<Pid>()?;
if Some(read_pid) == *CARGO_PID {
Ok(())
} else {
Err(anyhow::Error::msg("cargo PID does not match"))
}
}
}
}
fn cargo_pid() -> Option<Pid> {
let pid = sysinfo::get_current_pid().ok()?;
let (ppid, parent_name) = parent_process(pid).ok()?;
if parent_name == OsStr::new(CARGO_NAME) || parent_name == OsStr::new(NEXTEST_NAME) {
Some(ppid)
} else if parent_name == OsStr::new(RUST_OUT_NAME) {
let (doctest_pid, doctest_name) = parent_process(ppid).ok()?;
if doctest_name == OsStr::new(RUSTDOC_NAME) {
let (cargo_pid, cargo_name) = parent_process(doctest_pid).ok()?;
if cargo_name == OsStr::new(CARGO_NAME) {
Some(cargo_pid)
} else {
None
}
} else {
None
}
} else if parent_name == OsStr::new(RUSTDOC_NAME) {
let (cargo_pid, cargo_name) = parent_process(ppid).ok()?;
if cargo_name == OsStr::new(CARGO_NAME) {
Some(cargo_pid)
} else {
None
}
} else {
None
}
}
fn parent_process(pid: Pid) -> Result<(Pid, OsString)> {
let mut sys = sysinfo::System::new();
let what = sysinfo::ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::Always);
sys.refresh_processes_specifics(sysinfo::ProcessesToUpdate::Some(&[pid]), false, what);
let process = sys.process(pid).ok_or(anyhow!("failed process fetch"))?;
let ppid = process.parent().ok_or(anyhow!("no parent process"))?;
sys.refresh_processes_specifics(sysinfo::ProcessesToUpdate::Some(&[ppid]), false, what);
let parent = sys.process(ppid).ok_or(anyhow!("failed parent fetch"))?;
let parent_name = parent
.exe()
.and_then(|exe| exe.file_name())
.unwrap_or_else(|| parent.name());
Ok((ppid, parent_name.to_os_string()))
}
pub fn extract_test_name(module_path: &str) -> String {
let mut name = std::thread::current()
.name()
.unwrap_or_default()
.to_string();
if name == "main" {
name = extract_test_name_from_backtrace(module_path);
}
if let Some((_head, tail)) = name.split_once("::") {
name = tail.to_string();
}
name.replace("::", ::std::path::MAIN_SEPARATOR_STR)
}
fn extract_test_name_from_backtrace(module_path: &str) -> String {
for symbol in backtrace::Backtrace::new()
.frames()
.iter()
.rev()
.flat_map(|x| x.symbols())
.filter_map(|x| x.name())
.map(|x| x.to_string())
{
if let Some(symbol) = symbol.strip_prefix(module_path) {
if let Some(symbol) = symbol.strip_suffix("::{{closure}}") {
return symbol.to_string();
} else {
return symbol.to_string();
}
}
}
String::from("unknown_test")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cargo_pid() {
let val = cargo_pid();
assert!(val.is_some());
}
}