Files
delta-mirror/src/utils/process.rs
T
2021-11-29 08:41:24 -05:00

1036 lines
34 KiB
Rust

use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use sysinfo::{Pid, Process, ProcessExt, SystemExt};
use lazy_static::lazy_static;
#[derive(Clone, Debug, PartialEq)]
pub enum CallingProcess {
GitShow(CommandLine, Option<String>), // element 2 is file extension
GitGrep(CommandLine),
OtherGrep, // rg, grep, ag, ack, etc
}
#[derive(Clone, Debug, PartialEq)]
pub struct CommandLine {
pub long_options: HashSet<String>,
pub short_options: HashSet<String>,
last_arg: Option<String>,
}
pub fn calling_process() -> Option<Cow<'static, CallingProcess>> {
#[cfg(not(test))]
{
CACHED_CALLING_PROCESS
.as_ref()
.map(|proc| Cow::Borrowed(proc))
}
#[cfg(test)]
{
determine_calling_process().map(|proc| Cow::Owned(proc))
}
}
lazy_static! {
static ref CACHED_CALLING_PROCESS: Option<CallingProcess> = determine_calling_process();
}
fn determine_calling_process() -> Option<CallingProcess> {
calling_process_cmdline(ProcInfo::new(), describe_calling_process)
}
// Return value of `extract_args(args: &[String]) -> ProcessArgs<T>` function which is
// passed to `calling_process_cmdline()`.
#[derive(Debug, PartialEq)]
pub enum ProcessArgs<T> {
// A result has been successfully extracted from args.
Args(T),
// The extraction has failed.
ArgError,
// The process does not match, others may be inspected.
OtherProcess,
}
pub fn git_blame_filename_extension() -> Option<String> {
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension)
}
pub fn guess_git_blame_filename_extension(args: &[String]) -> ProcessArgs<String> {
let all_args = args.iter().map(|s| s.as_str());
// See git(1) and git-blame(1). Some arguments separate their parameter with space or '=', e.g.
// --date 2015 or --date=2015.
let git_blame_options_with_parameter =
"-C -c -L --since --ignore-rev --ignore-revs-file --contents --reverse --date";
let selected_args =
skip_uninteresting_args(all_args, git_blame_options_with_parameter.split(' '));
match selected_args.as_slice() {
[git, "blame", .., last_arg] if is_git_binary(git) => match last_arg.split('.').last() {
Some(arg) => ProcessArgs::Args(arg.to_string()),
None => ProcessArgs::ArgError,
},
[git, "blame"] if is_git_binary(git) => ProcessArgs::ArgError,
_ => ProcessArgs::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(|s| is_git_binary(s)).unwrap_or(false) => {
let mut args = args.skip_while(|s| *s != "grep" && *s != "show");
match args.next() {
Some("grep") => {
ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
}
Some("show") => {
let command_line = parse_command_line(args);
let extension = if let Some(last_arg) = &command_line.last_arg {
match last_arg.split_once(':') {
Some((_, suffix)) => {
suffix.split('.').last().map(|s| s.to_string())
}
None => None,
}
} else {
None
};
ProcessArgs::Args(CallingProcess::GitShow(command_line, extension))
}
_ => {
// It's git, but not a subcommand that we parse. Don't
// look at any more processes.
ProcessArgs::ArgError
}
}
}
// TODO: parse_style_sections is failing to parse ANSI escape sequences emitted by
// grep (BSD and GNU), ag, pt. See #794
Some(s) if is_any_of(s.to_str(), ["rg", "ack", "sift"]) => {
ProcessArgs::Args(CallingProcess::OtherGrep)
}
Some(_) => {
// It's not git, and it's not another grep tool. Keep
// looking at other processes.
ProcessArgs::OtherProcess
}
_ => {
// Could not parse file stem (not expected); keep looking at
// other processes.
ProcessArgs::OtherProcess
}
},
_ => {
// Empty arguments (not expected); keep looking.
ProcessArgs::OtherProcess
}
}
}
fn is_git_binary(git: &str) -> bool {
// Ignore case, for e.g. NTFS or APFS file systems
Path::new(git)
.file_stem()
.and_then(|os_str| os_str.to_str())
.map(|s| s.eq_ignore_ascii_case("git"))
.unwrap_or(false)
}
// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
// `skip_this_plus_parameter` plus their respective next argument.
// Keep all arguments once a '--' is encountered.
// (Note that some arguments work with and without '=': '--foo' 'bar' / '--foo=bar')
fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
mut args_it: ArgsI,
skip_this_plus_parameter: SkipI,
) -> Vec<&'a str>
where
ArgsI: Iterator<Item = &'a str>,
SkipI: Iterator<Item = &'b str>,
{
let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
let mut result = Vec::new();
loop {
match args_it.next() {
None => break result,
Some("--") => {
result.extend(args_it);
break result;
}
Some(arg) if arg_follows_space.contains(arg) => {
let _skip_parameter = args_it.next();
}
Some(arg) if !arg.starts_with('-') => {
result.push(arg);
}
Some(_) => { /* skip: --these -and --also=this */ }
}
}
}
// Given `--aa val -bc -d val e f -- ...` return
// ({"--aa"}, {"-b", "-c", "-d"})
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;
for s in args {
if s == "--" {
break;
} 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 {
ProcInfo {
info: sysinfo::System::new(),
}
}
}
trait ProcActions {
fn cmd(&self) -> &[String];
fn parent(&self) -> Option<Pid>;
fn start_time(&self) -> u64;
}
impl<T> ProcActions for T
where
T: ProcessExt,
{
fn cmd(&self) -> &[String] {
ProcessExt::cmd(self)
}
fn parent(&self) -> Option<Pid> {
ProcessExt::parent(self)
}
fn start_time(&self) -> u64 {
ProcessExt::start_time(self)
}
}
trait ProcessInterface {
type Out: ProcActions;
fn my_pid(&self) -> Pid;
fn process(&self, pid: Pid) -> Option<&Self::Out>;
fn processes(&self) -> &HashMap<Pid, Self::Out>;
fn refresh_process(&mut self, pid: Pid) -> bool;
fn refresh_processes(&mut self);
fn parent_process(&mut self, pid: Pid) -> Option<&Self::Out> {
self.refresh_process(pid).then(|| ())?;
let parent_pid = self.process(pid)?.parent()?;
self.refresh_process(parent_pid).then(|| ())?;
self.process(parent_pid)
}
fn naive_sibling_process(&mut self, pid: Pid) -> Option<&Self::Out> {
let sibling_pid = pid - 1;
self.refresh_process(sibling_pid).then(|| ())?;
self.process(sibling_pid)
}
fn find_sibling_process<F, T>(&mut self, pid: Pid, extract_args: F) -> Option<T>
where
F: Fn(&[String]) -> ProcessArgs<T>,
Self: Sized,
{
self.refresh_processes();
let this_start_time = self.process(pid)?.start_time();
/*
$ start_blame_of.sh src/main.rs | delta
\_ /usr/bin/some-terminal-emulator
| \_ common_git_and_delta_ancestor
| \_ /bin/sh /opt/git/start_blame_of.sh src/main.rs
| | \_ /bin/sh /opt/some/wrapper git blame src/main.rs
| | \_ /usr/bin/git blame src/main.rs
| \_ /bin/sh /opt/some/wrapper delta
| \_ delta
Walk up the process tree of delta and of every matching other process, counting the steps
along the way.
Find the common ancestor processes, calculate the distance, and select the one with the shortest.
*/
let mut pid_distances = HashMap::<Pid, 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, &mut sum_distance);
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) -> Pid {
std::process::id() as Pid
}
fn refresh_process(&mut self, pid: Pid) -> bool {
self.info.refresh_process(pid)
}
fn process(&self, pid: Pid) -> Option<&Self::Out> {
self.info.process(pid)
}
fn processes(&self) -> &HashMap<Pid, Self::Out> {
self.info.processes()
}
fn refresh_processes(&mut self) {
self.info.refresh_processes()
}
}
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();
// 1) Try the parent process. If delta is set as the pager in git, then git is the parent process.
let parent = info.parent_process(my_pid)?;
match extract_args(parent.cmd()) {
ProcessArgs::Args(result) => return Some(result),
ProcessArgs::ArgError => return None,
// 2) The parent process was something else, this can happen if git output is piped into delta, e.g.
// `git blame foo.txt | delta`. When the shell sets up the pipe it creates the two processes, the pids
// are usually consecutive, so check if the process with `my_pid - 1` matches.
ProcessArgs::OtherProcess => {
let sibling = info.naive_sibling_process(my_pid);
if let Some(proc) = sibling {
if let ProcessArgs::Args(result) = extract_args(proc.cmd()) {
return Some(result);
}
}
// else try the fallback
}
}
/*
3) Neither parent nor direct sibling were a match.
The most likely case is that the input program of the pipe wrote all its data and exited before delta
started, so no command line can be parsed. Same if the data was piped from an input file.
There might also be intermediary scripts in between or piped input with a gap in pids or (rarely)
randomized pids, so check all processes for the closest match in the process tree.
100 /usr/bin/some-terminal-emulator
124 \_ -shell
301 | \_ /usr/bin/git blame src/main.rs
302 | \_ wraps_delta.sh
303 | \_ delta
304 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
125 \_ -shell
800 | \_ /usr/bin/git blame src/main.rs
200 | \_ delta
400 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
126 \_ -shell
501 | \_ /bin/sh /wrapper/for/git blame src/main.rs
555 | | \_ /usr/bin/git blame src/main.rs
502 | \_ delta
567 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
*/
info.find_sibling_process(my_pid, extract_args)
}
// Walk up the process tree, calling `f` with the pid and the distance to `starting_pid`.
// Prerequisite: `info.refresh_processes()` has been called.
fn iter_parents<P, F>(info: &P, starting_pid: Pid, f: F)
where
P: ProcessInterface,
F: FnMut(Pid, usize),
{
fn inner_iter_parents<P, F>(info: &P, pid: Pid, mut f: F, distance: usize)
where
P: ProcessInterface,
F: FnMut(Pid, usize),
{
// Probably bad input, not a tree:
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>>> = RefCell::new(TlsState::None);
}
#[derive(Debug, PartialEq)]
enum TlsState<T> {
Once(T),
Scope(T),
With(usize, Rc<Vec<T>>),
None,
Invalid,
}
// When calling `FakeParentArgs::get()`, it can return `Some(values)` which were set earlier
// during in the #[test]. Otherwise returns None.
// This value can be valid once: `FakeParentArgs::once(val)`, for the entire scope:
// `FakeParentArgs::for_scope(val)`, or can be different values everytime `get()` is called:
// `FakeParentArgs::with([val1, val2, val3])`.
// It is an error if `once` or `with` values remain unused, or are overused.
// Note: The values are stored per-thread, so the expectation is that no thread boundaries are
// crossed.
pub struct FakeParentArgs {}
impl FakeParentArgs {
pub fn once(args: &str) -> Self {
Self::new(args, |v| TlsState::Once(v), "once")
}
pub fn for_scope(args: &str) -> Self {
Self::new(args, |v| TlsState::Scope(v), "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,
});
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(_, _) => Self::error("get"),
}
})
}
fn error(where_: &str) -> ! {
panic!(
"test logic error (in {}): wrong FakeParentArgs scope?",
where_
);
}
}
impl Drop for FakeParentArgs {
fn drop(&mut self) {
// Clears an Invalid state and tests if a Once or With value has been used.
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 => {}
}
});
}
}
#[test]
fn test_guess_git_blame_filename_extension() {
use ProcessArgs::Args;
fn make_string_vec(args: &[&str]) -> Vec<String> {
args.iter().map(|&x| x.to_owned()).collect::<Vec<String>>()
}
let args = make_string_vec(&["git", "blame", "hello", "world.txt"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
Args("txt".into())
);
let args = make_string_vec(&[
"git",
"blame",
"-s",
"-f",
"hello.txt",
"--date=2015",
"--date",
"now",
]);
assert_eq!(
guess_git_blame_filename_extension(&args),
Args("txt".into())
);
let args = make_string_vec(&["git", "blame", "-s", "-f", "--", "hello.txt"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
Args("txt".into())
);
let args = make_string_vec(&["git", "blame", "--", "--not.an.argument"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
Args("argument".into())
);
let args = make_string_vec(&["foo", "bar", "-a", "--123", "not.git"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
ProcessArgs::OtherProcess
);
let args = make_string_vec(&["git", "blame", "--help.txt"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
ProcessArgs::ArgError
);
let args = make_string_vec(&["git", "-c", "a=b", "blame", "main.rs"]);
assert_eq!(guess_git_blame_filename_extension(&args), Args("rs".into()));
let args = make_string_vec(&["git", "blame", "README"]);
assert_eq!(
guess_git_blame_filename_extension(&args),
Args("README".into())
);
let args = make_string_vec(&["git", "blame", ""]);
assert_eq!(guess_git_blame_filename_extension(&args), Args("".into()));
}
#[derive(Debug, Default)]
struct FakeProc {
pid: Pid,
start_time: u64,
cmd: Vec<String>,
ppid: Option<Pid>,
}
impl FakeProc {
fn new(pid: Pid, start_time: u64, cmd: Vec<String>, ppid: Option<Pid>) -> Self {
FakeProc {
pid,
start_time,
cmd,
ppid,
}
}
}
impl ProcActions for FakeProc {
fn cmd(&self) -> &[String] {
&self.cmd
}
fn parent(&self) -> Option<Pid> {
self.ppid
}
fn start_time(&self) -> u64 {
self.start_time
}
}
#[derive(Debug, Default)]
struct MockProcInfo {
delta_pid: Pid,
info: HashMap<Pid, FakeProc>,
}
impl MockProcInfo {
fn with(processes: &[(Pid, u64, &str, Option<Pid>)]) -> Self {
MockProcInfo {
delta_pid: processes.last().map(|p| p.0).unwrap_or(1),
info: processes
.into_iter()
.map(|(pid, start_time, cmd, ppid)| {
let cmd_vec = cmd.split(' ').map(str::to_owned).collect();
(*pid, FakeProc::new(*pid, *start_time, cmd_vec, *ppid))
})
.collect(),
}
}
}
impl ProcessInterface for MockProcInfo {
type Out = FakeProc;
fn my_pid(&self) -> Pid {
self.delta_pid
}
fn process(&self, pid: Pid) -> Option<&Self::Out> {
self.info.get(&pid)
}
fn processes(&self) -> &HashMap<Pid, Self::Out> {
&self.info
}
fn refresh_processes(&mut self) {}
fn refresh_process(&mut self, _pid: Pid) -> 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(), guess_git_blame_filename_extension),
Some("hello".into())
);
}
{
let _args = FakeParentArgs::once(&"git blame world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("txt".into())
);
}
{
let _args = FakeParentArgs::for_scope(&"git blame hello world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("txt".into())
);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("txt".into())
);
}
}
#[test]
#[should_panic]
fn test_process_testing_assert() {
let _args = FakeParentArgs::once(&"git blame do.not.panic");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("panic".into())
);
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
}
#[test]
#[should_panic]
fn test_process_testing_assert_never_used() {
let _args = FakeParentArgs::once(&"never used");
// causes a panic while panicing, so can't test:
// let _args = FakeParentArgs::for_scope(&"never used");
// let _args = FakeParentArgs::once(&"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_panic() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("once".into())
);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("twice".into())
);
}
#[test]
#[should_panic]
fn test_process_testing_n_times_unused() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
}
#[test]
#[should_panic]
fn test_process_testing_n_times_underused() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("once".into())
);
}
#[test]
#[should_panic]
#[ignore]
fn test_process_testing_n_times_overused() {
let _args = FakeParentArgs::with(&["git blame once"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
Some("once".into())
);
// ignored: dropping causes a panic while panicing, so can't test
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
}
#[test]
fn test_process_blame_info_with_parent() {
let no_processes = MockProcInfo::with(&[]);
assert_eq!(
calling_process_cmdline(no_processes, guess_git_blame_filename_extension),
None
);
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, guess_git_blame_filename_extension),
Some("txt".into())
);
let grandparent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame src/main.rs", Some(2)),
(4, 100, "call_delta.sh", Some(3)),
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(grandparent, guess_git_blame_filename_extension),
Some("rs".into())
);
}
#[test]
fn test_process_blame_info_with_sibling() {
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, guess_git_blame_filename_extension),
Some("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, guess_git_blame_filename_extension),
Some("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, guess_git_blame_filename_extension),
Some("def".into())
);
// 3 blame processes, 2 with matching start times, pick the one with lower
// distance but larger start time difference.
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,
guess_git_blame_filename_extension
),
Some("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.clone()))
);
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",
"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",
"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, ext)) =
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!(ext, Some(expected_extension.to_string()));
} else {
assert!(false);
}
}
}
#[test]
fn test_process_calling_cmdline() {
// Github runs CI tests for arm under qemu where where sysinfo can not find the parent process.
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() as Pid, |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
}
}
// Tests that caller is something like "cargo test" or "cargo tarpaulin"
let find_test = |args: &[String]| find_calling_process(args, &["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);
}
}