use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use lazy_static::lazy_static;
use sysinfo::{Pid, PidExt, Process, ProcessExt, ProcessRefreshKind, SystemExt};
use crate::utils::DELTA_ATOMIC_ORDERING;
pub type DeltaPid = u32;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CallingProcess {
GitDiff(CommandLine),
GitShow(CommandLine, Option<String>), GitLog(CommandLine),
GitReflog(CommandLine),
GitBlame(CommandLine),
GitGrep(CommandLine),
OtherGrep, None, Pending, }
static CALLER_INFO_SOURCE: AtomicUsize = AtomicUsize::new(CALLER_GUESSED);
const CALLER_GUESSED: usize = 1;
const CALLER_KNOWN: usize = 2;
impl CallingProcess {
pub fn paths_in_input_are_relative_to_cwd(&self) -> bool {
match self {
CallingProcess::GitDiff(cmd) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitShow(cmd, _) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitLog(cmd) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitBlame(_)
| CallingProcess::GitGrep(_)
| CallingProcess::OtherGrep => true,
_ => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommandLine {
pub long_options: HashSet<String>,
pub short_options: HashSet<String>,
pub last_arg: Option<String>,
}
lazy_static! {
static ref CALLER: Arc<(Mutex<CallingProcess>, Condvar)> =
Arc::new((Mutex::new(CallingProcess::Pending), Condvar::new()));
}
pub fn start_determining_calling_process_in_thread() {
std::thread::Builder::new()
.name("find_calling_process".into())
.spawn(move || {
let calling_process = determine_calling_process();
let (caller_mutex, determine_done) = &**CALLER;
let mut caller = caller_mutex.lock().unwrap();
if CALLER_INFO_SOURCE.load(DELTA_ATOMIC_ORDERING) <= CALLER_GUESSED {
*caller = calling_process;
}
determine_done.notify_all();
})
.unwrap();
}
pub fn set_calling_process(args: &[String]) {
if let ProcessArgs::Args(result) = describe_calling_process(args) {
let (caller_mutex, determine_done) = &**CALLER;
let mut caller = caller_mutex.lock().unwrap();
*caller = result;
CALLER_INFO_SOURCE.store(CALLER_KNOWN, DELTA_ATOMIC_ORDERING);
determine_done.notify_all();
}
}
#[cfg(not(test))]
pub fn calling_process() -> MutexGuard<'static, CallingProcess> {
let (caller_mutex, determine_done) = &**CALLER;
determine_done
.wait_while(caller_mutex.lock().unwrap(), |caller| {
*caller == CallingProcess::Pending
})
.unwrap()
}
#[cfg(test)]
pub fn calling_process() -> Box<CallingProcess> {
type _UnusedImport = MutexGuard<'static, i8>;
if crate::utils::process::tests::FakeParentArgs::are_set() {
Box::new(determine_calling_process())
} else {
let (caller_mutex, _) = &**CALLER;
let mut caller = caller_mutex.lock().unwrap();
if *caller == CallingProcess::Pending {
*caller = determine_calling_process();
}
Box::new(caller.clone())
}
}
fn determine_calling_process() -> CallingProcess {
calling_process_cmdline(ProcInfo::new(), describe_calling_process)
.unwrap_or(CallingProcess::None)
}
#[derive(Debug, PartialEq, Eq)]
pub enum ProcessArgs<T> {
Args(T),
ArgError,
OtherProcess,
}
pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess> {
let mut args = args.iter().map(|s| s.as_str());
fn is_any_of<'a, I>(cmd: Option<&str>, others: I) -> bool
where
I: IntoIterator<Item = &'a str>,
{
cmd.map(|cmd| others.into_iter().any(|o| o.eq_ignore_ascii_case(cmd)))
.unwrap_or(false)
}
match args.next() {
Some(command) => match Path::new(command).file_stem() {
Some(s) if s.to_str().map(is_git_binary).unwrap_or(false) => {
let mut args = args.skip_while(|s| {
*s != "diff"
&& *s != "show"
&& *s != "log"
&& *s != "reflog"
&& *s != "grep"
&& *s != "blame"
});
match args.next() {
Some("diff") => {
ProcessArgs::Args(CallingProcess::GitDiff(parse_command_line(args)))
}
Some("show") => {
let command_line = parse_command_line(args);
let filename = if let Some(last_arg) = &command_line.last_arg {
match last_arg.split_once(':') {
Some((_, filename)) => Path::new(filename)
.file_name()
.map(|f| f.to_string_lossy().to_string()),
None => None,
}
} else {
None
};
ProcessArgs::Args(CallingProcess::GitShow(command_line, filename))
}
Some("log") => {
ProcessArgs::Args(CallingProcess::GitLog(parse_command_line(args)))
}
Some("reflog") => {
ProcessArgs::Args(CallingProcess::GitReflog(parse_command_line(args)))
}
Some("grep") => {
ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
}
Some("blame") => {
ProcessArgs::Args(CallingProcess::GitBlame(parse_command_line(args)))
}
_ => {
ProcessArgs::ArgError
}
}
}
Some(s) if is_any_of(s.to_str(), ["rg", "ack", "sift"]) => {
ProcessArgs::Args(CallingProcess::OtherGrep)
}
Some(_) => {
ProcessArgs::OtherProcess
}
_ => {
ProcessArgs::OtherProcess
}
},
_ => {
ProcessArgs::OtherProcess
}
}
}
fn is_git_binary(git: &str) -> bool {
Path::new(git)
.file_stem()
.and_then(|os_str| os_str.to_str())
.map(|s| s.eq_ignore_ascii_case("git"))
.unwrap_or(false)
}
fn parse_command_line<'a>(args: impl Iterator<Item = &'a str>) -> CommandLine {
let mut long_options = HashSet::new();
let mut short_options = HashSet::new();
let mut last_arg = None;
let mut after_double_dash = false;
for s in args {
if after_double_dash {
last_arg = Some(s);
} else if s == "--" {
after_double_dash = true;
} else if s.starts_with("--") {
long_options.insert(s.split('=').next().unwrap().to_owned());
} else if let Some(suffix) = s.strip_prefix('-') {
short_options.extend(suffix.chars().map(|c| format!("-{c}")));
} else {
last_arg = Some(s);
}
}
CommandLine {
long_options,
short_options,
last_arg: last_arg.map(|s| s.to_string()),
}
}
struct ProcInfo {
info: sysinfo::System,
}
impl ProcInfo {
fn new() -> Self {
sysinfo::set_open_files_limit(0);
ProcInfo {
info: sysinfo::System::new(),
}
}
}
trait ProcActions {
fn cmd(&self) -> &[String];
fn parent(&self) -> Option<DeltaPid>;
fn pid(&self) -> DeltaPid;
fn start_time(&self) -> u64;
}
impl<T> ProcActions for T
where
T: ProcessExt,
{
fn cmd(&self) -> &[String] {
ProcessExt::cmd(self)
}
fn parent(&self) -> Option<DeltaPid> {
ProcessExt::parent(self).map(|p| p.as_u32())
}
fn pid(&self) -> DeltaPid {
ProcessExt::pid(self).as_u32()
}
fn start_time(&self) -> u64 {
ProcessExt::start_time(self)
}
}
trait ProcessInterface {
type Out: ProcActions;
fn my_pid(&self) -> DeltaPid;
fn process(&self, pid: DeltaPid) -> Option<&Self::Out>;
fn processes(&self) -> &HashMap<Pid, Self::Out>;
fn refresh_process(&mut self, pid: DeltaPid) -> bool;
fn refresh_processes(&mut self);
fn parent_process(&mut self, pid: DeltaPid) -> Option<&Self::Out> {
self.refresh_process(pid).then_some(())?;
let parent_pid = self.process(pid)?.parent()?;
self.refresh_process(parent_pid).then_some(())?;
self.process(parent_pid)
}
fn naive_sibling_process(&mut self, pid: DeltaPid) -> Option<&Self::Out> {
let sibling_pid = pid - 1;
self.refresh_process(sibling_pid).then_some(())?;
self.process(sibling_pid)
}
fn find_sibling_in_refreshed_processes<F, T>(
&mut self,
pid: DeltaPid,
extract_args: &F,
) -> Option<T>
where
F: Fn(&[String]) -> ProcessArgs<T>,
Self: Sized,
{
let this_start_time = self.process(pid)?.start_time();
let mut pid_distances = HashMap::<DeltaPid, usize>::new();
let mut collect_parent_pids = |pid, distance| {
pid_distances.insert(pid, distance);
};
iter_parents(self, pid, &mut collect_parent_pids);
let process_start_time_difference_less_than_3s = |a, b| (a as i64 - b as i64).abs() < 3;
let cmdline_of_closest_matching_process = self
.processes()
.iter()
.filter(|(_, proc)| {
process_start_time_difference_less_than_3s(this_start_time, proc.start_time())
})
.filter_map(|(&pid, proc)| match extract_args(proc.cmd()) {
ProcessArgs::Args(args) => {
let mut length_of_process_chain = usize::MAX;
let mut sum_distance = |pid, distance| {
if length_of_process_chain == usize::MAX {
if let Some(distance_to_first_common_parent) = pid_distances.get(&pid) {
length_of_process_chain =
distance_to_first_common_parent + distance;
}
}
};
iter_parents(self, pid.as_u32(), &mut sum_distance);
if length_of_process_chain == usize::MAX {
None
} else {
Some((length_of_process_chain, args))
}
}
_ => None,
})
.min_by_key(|(distance, _)| *distance)
.map(|(_, result)| result);
cmdline_of_closest_matching_process
}
}
impl ProcessInterface for ProcInfo {
type Out = Process;
fn my_pid(&self) -> DeltaPid {
std::process::id()
}
fn refresh_process(&mut self, pid: DeltaPid) -> bool {
self.info
.refresh_process_specifics(Pid::from_u32(pid), ProcessRefreshKind::new())
}
fn process(&self, pid: DeltaPid) -> Option<&Self::Out> {
self.info.process(Pid::from_u32(pid))
}
fn processes(&self) -> &HashMap<Pid, Self::Out> {
self.info.processes()
}
fn refresh_processes(&mut self) {
self.info
.refresh_processes_specifics(ProcessRefreshKind::new())
}
}
fn calling_process_cmdline<P, F, T>(mut info: P, extract_args: F) -> Option<T>
where
P: ProcessInterface,
F: Fn(&[String]) -> ProcessArgs<T>,
{
#[cfg(test)]
{
if let Some(args) = tests::FakeParentArgs::get() {
match extract_args(&args) {
ProcessArgs::Args(result) => return Some(result),
_ => return None,
}
}
}
let my_pid = info.my_pid();
let mut current_pid = my_pid;
'parent_iter: for depth in [1, 2, 3] {
let parent = match info.parent_process(current_pid) {
None => {
break 'parent_iter;
}
Some(parent) => parent,
};
let parent_pid = parent.pid();
match extract_args(parent.cmd()) {
ProcessArgs::Args(result) => return Some(result),
ProcessArgs::ArgError => return None,
ProcessArgs::OtherProcess if depth == 1 => {
let sibling = info.naive_sibling_process(current_pid);
if let Some(proc) = sibling {
if let ProcessArgs::Args(result) = extract_args(proc.cmd()) {
return Some(result);
}
}
}
ProcessArgs::OtherProcess => {}
}
current_pid = parent_pid;
}
let pid_range = my_pid.saturating_sub(10)..my_pid.saturating_add(10);
for p in pid_range {
if info.process(p).is_none() {
info.refresh_process(p);
}
}
match info.find_sibling_in_refreshed_processes(my_pid, &extract_args) {
None => {
#[cfg(not(target_os = "linux"))]
let full_scan = true;
#[cfg(target_os = "linux")]
let full_scan = std::env::var("DELTA_CALLING_PROCESS_QUERY_ALL")
.is_ok_and(|v| !["0", "false", "no"].iter().any(|&n| n == v));
if full_scan {
info.refresh_processes();
info.find_sibling_in_refreshed_processes(my_pid, &extract_args)
} else {
None
}
}
some => some,
}
}
fn iter_parents<P, F>(info: &P, starting_pid: DeltaPid, f: F)
where
P: ProcessInterface,
F: FnMut(DeltaPid, usize),
{
fn inner_iter_parents<P, F>(info: &P, pid: DeltaPid, mut f: F, distance: usize)
where
P: ProcessInterface,
F: FnMut(u32, usize),
{
if distance > 2000 {
return;
}
if let Some(proc) = info.process(pid) {
if let Some(pid) = proc.parent() {
f(pid, distance);
inner_iter_parents(info, pid, f, distance + 1)
}
}
}
inner_iter_parents(info, starting_pid, f, 1)
}
#[cfg(test)]
pub mod tests {
use super::*;
use itertools::Itertools;
use std::cell::RefCell;
use std::rc::Rc;
thread_local! {
static FAKE_ARGS: RefCell<TlsState<Vec<String>>> = const { RefCell::new(TlsState::None) };
}
#[derive(Debug, PartialEq)]
enum TlsState<T> {
Once(T),
Scope(T),
With(usize, Rc<Vec<T>>),
None,
Invalid,
ErrorAlreadyHandled,
}
pub struct FakeParentArgs {}
impl FakeParentArgs {
pub fn once(args: &str) -> Self {
Self::new(args, TlsState::Once, "once")
}
pub fn for_scope(args: &str) -> Self {
Self::new(args, TlsState::Scope, "for_scope")
}
fn new<F>(args: &str, initial: F, from_: &str) -> Self
where
F: Fn(Vec<String>) -> TlsState<Vec<String>>,
{
let string_vec = args.split(' ').map(str::to_owned).collect();
if FAKE_ARGS.with(|a| a.replace(initial(string_vec))) != TlsState::None {
Self::error(from_);
}
FakeParentArgs {}
}
pub fn with(args: &[&str]) -> Self {
let with = TlsState::With(
0,
Rc::new(
args.iter()
.map(|a| a.split(' ').map(str::to_owned).collect())
.collect(),
),
);
if FAKE_ARGS.with(|a| a.replace(with)) != TlsState::None || args.is_empty() {
Self::error("with creation");
}
FakeParentArgs {}
}
pub fn get() -> Option<Vec<String>> {
FAKE_ARGS.with(|a| {
let old_value = a.replace_with(|old_value| match old_value {
TlsState::Once(_) => TlsState::Invalid,
TlsState::Scope(args) => TlsState::Scope(args.clone()),
TlsState::With(n, args) => TlsState::With(*n + 1, Rc::clone(args)),
TlsState::None => TlsState::None,
TlsState::Invalid => TlsState::Invalid,
TlsState::ErrorAlreadyHandled => TlsState::ErrorAlreadyHandled,
});
match old_value {
TlsState::Once(args) | TlsState::Scope(args) => Some(args),
TlsState::With(n, args) if n < args.len() => Some(args[n].clone()),
TlsState::None => None,
TlsState::Invalid | TlsState::With(_, _) | TlsState::ErrorAlreadyHandled => {
Self::error("get");
None
}
}
})
}
pub fn are_set() -> bool {
FAKE_ARGS.with(|a| {
*a.borrow() != TlsState::None && *a.borrow() != TlsState::ErrorAlreadyHandled
})
}
fn error(where_: &str) {
FAKE_ARGS.with(|a| {
let old_value = a.replace(TlsState::ErrorAlreadyHandled);
match old_value {
TlsState::ErrorAlreadyHandled => (),
_ => {
panic!(
"test logic error (in {}): wrong FakeParentArgs scope?",
where_
);
}
}
});
}
}
impl Drop for FakeParentArgs {
fn drop(&mut self) {
FAKE_ARGS.with(|a| {
let old_value = a.replace(TlsState::None);
match old_value {
TlsState::With(n, args) => {
if n != args.len() {
Self::error("drop with")
}
}
TlsState::Once(_) | TlsState::None => Self::error("drop"),
TlsState::Scope(_) | TlsState::Invalid | TlsState::ErrorAlreadyHandled => {}
}
});
}
}
#[derive(Debug, Default)]
struct FakeProc {
#[allow(dead_code)]
pid: DeltaPid,
start_time: u64,
cmd: Vec<String>,
ppid: Option<DeltaPid>,
}
impl FakeProc {
fn new(pid: DeltaPid, start_time: u64, cmd: Vec<String>, ppid: Option<DeltaPid>) -> Self {
FakeProc {
pid,
start_time,
cmd,
ppid,
}
}
}
impl ProcActions for FakeProc {
fn cmd(&self) -> &[String] {
&self.cmd
}
fn parent(&self) -> Option<DeltaPid> {
self.ppid
}
fn pid(&self) -> DeltaPid {
self.pid
}
fn start_time(&self) -> u64 {
self.start_time
}
}
#[derive(Debug, Default)]
struct MockProcInfo {
delta_pid: DeltaPid,
info: HashMap<Pid, FakeProc>,
}
impl MockProcInfo {
fn with(processes: &[(DeltaPid, u64, &str, Option<DeltaPid>)]) -> Self {
MockProcInfo {
delta_pid: processes.last().map(|p| p.0).unwrap_or(1),
info: processes
.iter()
.map(|(pid, start_time, cmd, ppid)| {
let cmd_vec = cmd.split(' ').map(str::to_owned).collect();
(
Pid::from_u32(*pid),
FakeProc::new(*pid, *start_time, cmd_vec, *ppid),
)
})
.collect(),
}
}
}
impl ProcessInterface for MockProcInfo {
type Out = FakeProc;
fn my_pid(&self) -> DeltaPid {
self.delta_pid
}
fn process(&self, pid: DeltaPid) -> Option<&Self::Out> {
self.info.get(&Pid::from_u32(pid))
}
fn processes(&self) -> &HashMap<Pid, Self::Out> {
&self.info
}
fn refresh_processes(&mut self) {}
fn refresh_process(&mut self, _pid: DeltaPid) -> bool {
true
}
}
fn set(arg1: &[&str]) -> HashSet<String> {
arg1.iter().map(|&s| s.to_owned()).collect()
}
#[test]
fn test_process_testing() {
{
let _args = FakeParentArgs::once("git blame hello");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("hello".into())
}))
);
}
{
let _args = FakeParentArgs::once("git blame world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("world.txt".into())
}))
);
}
{
let _args = FakeParentArgs::for_scope("git blame hello world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("world.txt".into())
}))
);
}
}
#[test]
#[should_panic(expected = "test logic error (in get): wrong FakeParentArgs scope?")]
fn test_process_testing_assert() {
let _args = FakeParentArgs::once("git blame do.not.panic");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("do.not.panic".into())
}))
);
calling_process_cmdline(ProcInfo::new(), describe_calling_process);
}
#[test]
#[should_panic(expected = "test logic error (in drop): wrong FakeParentArgs scope?")]
fn test_process_testing_assert_once_never_used() {
let _args = FakeParentArgs::once("never used");
}
#[test]
#[should_panic(expected = "test logic error (in once): wrong FakeParentArgs scope?")]
fn test_process_testing_assert_for_scope_never_used() {
let _args = FakeParentArgs::for_scope(&"never used");
let _args = FakeParentArgs::once(&"never used");
}
#[test]
#[should_panic(expected = "test logic error (in for_scope): wrong FakeParentArgs scope?")]
fn test_process_testing_assert_once_never_used2() {
let _args = FakeParentArgs::once(&"never used");
let _args = FakeParentArgs::for_scope(&"never used");
}
#[test]
fn test_process_testing_scope_can_remain_unused() {
let _args = FakeParentArgs::for_scope("never used");
}
#[test]
fn test_process_testing_n_times() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("twice".into())
}))
);
}
#[test]
#[should_panic(expected = "test logic error (in drop with): wrong FakeParentArgs scope?")]
fn test_process_testing_n_times_unused() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
}
#[test]
#[should_panic(expected = "test logic error (in drop with): wrong FakeParentArgs scope?")]
fn test_process_testing_n_times_underused() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
}
#[test]
#[should_panic(expected = "test logic error (in get): wrong FakeParentArgs scope?")]
fn test_process_testing_n_times_overused() {
let _args = FakeParentArgs::with(&["git blame once"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
calling_process_cmdline(ProcInfo::new(), describe_calling_process);
}
#[test]
fn test_describe_calling_process_blame() {
let no_processes = MockProcInfo::with(&[]);
assert_eq!(
calling_process_cmdline(no_processes, describe_calling_process),
None
);
let two_trees = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame src/main.rs", Some(2)),
(4, 100, "call_delta.sh", None),
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(two_trees, describe_calling_process),
None
);
let no_options_command_line = CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("hello.txt".to_string()),
};
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(no_options_command_line.clone()))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame -- hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(no_options_command_line.clone()))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame -- --not.an.argument", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("--not.an.argument".to_string()),
}))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame --help.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: ["--help.txt".into()].into(),
short_options: [].into(),
last_arg: None,
}))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame --", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: None,
}))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "Git.exe blame hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(no_options_command_line.clone()))
);
let git_blame_command =
"git -c a=b blame -fnb --incremental -t --color-by-age -M --since=3.weeks --contents annotation.txt -C -C2 hello.txt";
let expected_result = Some(CallingProcess::GitBlame(CommandLine {
long_options: set(&["--incremental", "--color-by-age", "--since", "--contents"]),
short_options: set(&["-f", "-n", "-b", "-t", "-M", "-C", "-2"]),
last_arg: Some("hello.txt".to_string()),
}));
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, git_blame_command, Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
expected_result
);
let grandparent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, git_blame_command, Some(2)),
(4, 100, "call_delta.sh", Some(3)),
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(grandparent, describe_calling_process),
expected_result
);
let sibling = MockProcInfo::with(&[
(2, 100, "-xterm", None),
(3, 100, "-shell", Some(2)),
(4, 100, "git blame src/main.rs", Some(3)),
(5, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(sibling, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.rs".into())
}))
);
let indirect_sibling = MockProcInfo::with(&[
(2, 100, "-xterm", None),
(3, 100, "-shell", Some(2)),
(4, 100, "Git.exe blame --correct src/main.abc", Some(3)),
(
10,
100,
"Git.exe blame --ignored-child src/main.def",
Some(4),
),
(5, 100, "delta.sh", Some(3)),
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: set(&["--correct"]),
short_options: [].into(),
last_arg: Some("src/main.abc".into())
}))
);
let indirect_sibling2 = MockProcInfo::with(&[
(2, 100, "-xterm", None),
(3, 100, "-shell", Some(2)),
(4, 100, "git wrap src/main.abc", Some(3)),
(10, 100, "git blame src/main.def", Some(4)),
(5, 100, "delta.sh", Some(3)),
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling2, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.def".into())
}))
);
let indirect_sibling_start_times = MockProcInfo::with(&[
(2, 100, "-xterm", None),
(3, 100, "-shell", Some(2)),
(4, 109, "git wrap src/main.abc", Some(3)),
(10, 109, "git blame src/main.def", Some(4)),
(20, 100, "git wrap1 src/main.abc", Some(3)),
(21, 100, "git wrap2 src/main.def", Some(20)),
(22, 101, "git blame src/main.not", Some(21)),
(23, 102, "git blame src/main.this", Some(20)),
(5, 100, "delta.sh", Some(3)),
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling_start_times, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.this".into())
}))
);
}
#[test]
fn test_describe_calling_process_grep() {
let no_processes = MockProcInfo::with(&[]);
assert_eq!(
calling_process_cmdline(no_processes, describe_calling_process),
None
);
let empty_command_line = CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("hello.txt".to_string()),
};
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git grep pattern hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitGrep(empty_command_line.clone()))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "Git.exe grep pattern hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitGrep(empty_command_line))
);
for grep_command in &[
"/usr/local/bin/rg pattern hello.txt",
"RG.exe pattern hello.txt",
"/usr/local/bin/ack pattern hello.txt",
"ack.exe pattern hello.txt",
] {
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, grep_command, Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::OtherGrep)
);
}
let git_grep_command =
"git grep -ab --function-context -n --show-function -W --foo=val pattern hello.txt";
let expected_result = Some(CallingProcess::GitGrep(CommandLine {
long_options: set(&["--function-context", "--show-function", "--foo"]),
short_options: set(&["-a", "-b", "-n", "-W"]),
last_arg: Some("hello.txt".to_string()),
}));
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, git_grep_command, Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
expected_result
);
let grandparent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, git_grep_command, Some(2)),
(4, 100, "call_delta.sh", Some(3)),
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(grandparent, describe_calling_process),
expected_result
);
}
#[test]
fn test_describe_calling_process_git_show() {
for (command, expected_extension) in [
(
"/usr/local/bin/git show --abbrev-commit -w 775c3b84:./src/hello.rs",
"hello.rs",
),
(
"/usr/local/bin/git show --abbrev-commit -w HEAD~1:Makefile",
"Makefile",
),
(
"git -c x.y=z show --abbrev-commit -w 775c3b84:./src/hello.bye.R",
"hello.bye.R",
),
] {
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, command, Some(2)),
(4, 100, "delta", Some(3)),
]);
if let Some(CallingProcess::GitShow(cmd_line, filename)) =
calling_process_cmdline(parent, describe_calling_process)
{
assert_eq!(cmd_line.long_options, set(&["--abbrev-commit"]));
assert_eq!(cmd_line.short_options, set(&["-w"]));
assert_eq!(filename, Some(expected_extension.to_string()));
} else {
unreachable!();
}
}
}
#[test]
fn test_process_calling_cmdline() {
if std::env::vars().any(|(key, _)| key == "CROSS_RUNNER" || key == "QEMU_LD_PREFIX") {
return;
}
let mut info = ProcInfo::new();
info.refresh_processes();
let mut ppid_distance = Vec::new();
iter_parents(&info, std::process::id(), |pid, distance| {
ppid_distance.push(pid as i32);
ppid_distance.push(distance as i32)
});
assert!(ppid_distance[1] == 1);
fn find_calling_process(args: &[String], want: &[&str]) -> ProcessArgs<()> {
if args.iter().any(|have| want.iter().any(|want| want == have)) {
ProcessArgs::Args(())
} else {
ProcessArgs::ArgError
}
}
let find_test = |args: &[String]| find_calling_process(args, &["t", "test", "tarpaulin"]);
assert_eq!(calling_process_cmdline(info, find_test), Some(()));
let nonsense = ppid_distance
.iter()
.map(|i| i.to_string())
.join("Y40ii4RihK6lHiK4BDsGSx");
let find_nothing = |args: &[String]| find_calling_process(args, &[&nonsense]);
assert_eq!(calling_process_cmdline(ProcInfo::new(), find_nothing), None);
}
}