delta_lib/utils/
process.rs

1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3use std::sync::{Arc, Condvar, Mutex, MutexGuard};
4
5use lazy_static::lazy_static;
6use sysinfo::{Pid, PidExt, Process, ProcessExt, ProcessRefreshKind, SystemExt};
7
8pub type DeltaPid = u32;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum CallingProcess {
12    GitDiff(CommandLine),
13    GitShow(CommandLine, Option<String>), // element 2 is file extension
14    GitLog(CommandLine),
15    GitReflog(CommandLine),
16    GitGrep(CommandLine),
17    OtherGrep, // rg, grep, ag, ack, etc
18    None,      // no matching process could be found
19    Pending,   // calling process is currently being determined
20}
21// TODO: Git blame is currently handled differently
22
23impl CallingProcess {
24    pub fn paths_in_input_are_relative_to_cwd(&self) -> bool {
25        match self {
26            CallingProcess::GitDiff(cmd) if cmd.long_options.contains("--relative") => true,
27            CallingProcess::GitShow(cmd, _) if cmd.long_options.contains("--relative") => true,
28            CallingProcess::GitLog(cmd) if cmd.long_options.contains("--relative") => true,
29            CallingProcess::GitGrep(_) | CallingProcess::OtherGrep => true,
30            _ => false,
31        }
32    }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct CommandLine {
37    pub long_options: HashSet<String>,
38    pub short_options: HashSet<String>,
39    last_arg: Option<String>,
40}
41
42lazy_static! {
43    static ref CALLER: Arc<(Mutex<CallingProcess>, Condvar)> =
44        Arc::new((Mutex::new(CallingProcess::Pending), Condvar::new()));
45}
46
47pub fn start_determining_calling_process_in_thread() {
48    // The handle is neither kept nor returned nor joined but dropped, so the main
49    // thread can exit early if it does not need to know its parent process.
50    std::thread::Builder::new()
51        .name("find_calling_process".into())
52        .spawn(move || {
53            let calling_process = determine_calling_process();
54
55            let (caller_mutex, determine_done) = &**CALLER;
56
57            let mut caller = caller_mutex.lock().unwrap();
58            *caller = calling_process;
59            determine_done.notify_all();
60        })
61        .unwrap();
62}
63
64#[cfg(not(test))]
65pub fn calling_process() -> MutexGuard<'static, CallingProcess> {
66    let (caller_mutex, determine_done) = &**CALLER;
67
68    determine_done
69        .wait_while(caller_mutex.lock().unwrap(), |caller| {
70            *caller == CallingProcess::Pending
71        })
72        .unwrap()
73}
74
75// The return value is duck-typed to work in place of a MutexGuard when testing.
76#[cfg(test)]
77pub fn calling_process() -> Box<CallingProcess> {
78    type _UnusedImport = MutexGuard<'static, i8>;
79
80    if crate::utils::process::tests::FakeParentArgs::are_set() {
81        // If the (thread-local) FakeParentArgs are set, then the following command returns
82        // these, so the cached global real ones can not be used.
83        Box::new(determine_calling_process())
84    } else {
85        let (caller_mutex, _) = &**CALLER;
86
87        let mut caller = caller_mutex.lock().unwrap();
88        if *caller == CallingProcess::Pending {
89            *caller = determine_calling_process();
90        }
91
92        Box::new(caller.clone())
93    }
94}
95
96fn determine_calling_process() -> CallingProcess {
97    calling_process_cmdline(ProcInfo::new(), describe_calling_process)
98        .unwrap_or(CallingProcess::None)
99}
100
101// Return value of `extract_args(args: &[String]) -> ProcessArgs<T>` function which is
102// passed to `calling_process_cmdline()`.
103#[derive(Debug, PartialEq, Eq)]
104pub enum ProcessArgs<T> {
105    // A result has been successfully extracted from args.
106    Args(T),
107    // The extraction has failed.
108    ArgError,
109    // The process does not match, others may be inspected.
110    OtherProcess,
111}
112
113pub fn git_blame_filename_extension() -> Option<String> {
114    calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension)
115}
116
117pub fn guess_git_blame_filename_extension(args: &[String]) -> ProcessArgs<String> {
118    let all_args = args.iter().map(|s| s.as_str());
119
120    // See git(1) and git-blame(1). Some arguments separate their parameter with space or '=', e.g.
121    // --date 2015 or --date=2015.
122    let git_blame_options_with_parameter =
123        "-C -c -L --since --ignore-rev --ignore-revs-file --contents --reverse --date";
124
125    let selected_args =
126        skip_uninteresting_args(all_args, git_blame_options_with_parameter.split(' '));
127
128    match selected_args.as_slice() {
129        [git, "blame", .., last_arg] if is_git_binary(git) => match last_arg.split('.').last() {
130            Some(arg) => ProcessArgs::Args(arg.to_string()),
131            None => ProcessArgs::ArgError,
132        },
133        [git, "blame"] if is_git_binary(git) => ProcessArgs::ArgError,
134        _ => ProcessArgs::OtherProcess,
135    }
136}
137
138pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess> {
139    let mut args = args.iter().map(|s| s.as_str());
140
141    fn is_any_of<'a, I>(cmd: Option<&str>, others: I) -> bool
142    where
143        I: IntoIterator<Item = &'a str>,
144    {
145        cmd.map(|cmd| others.into_iter().any(|o| o.eq_ignore_ascii_case(cmd)))
146            .unwrap_or(false)
147    }
148
149    match args.next() {
150        Some(command) => match Path::new(command).file_stem() {
151            Some(s) if s.to_str().map(is_git_binary).unwrap_or(false) => {
152                let mut args = args.skip_while(|s| {
153                    *s != "diff" && *s != "show" && *s != "log" && *s != "reflog" && *s != "grep"
154                });
155                match args.next() {
156                    Some("diff") => {
157                        ProcessArgs::Args(CallingProcess::GitDiff(parse_command_line(args)))
158                    }
159                    Some("show") => {
160                        let command_line = parse_command_line(args);
161                        let extension = if let Some(last_arg) = &command_line.last_arg {
162                            match last_arg.split_once(':') {
163                                Some((_, suffix)) => {
164                                    suffix.split('.').last().map(|s| s.to_string())
165                                }
166                                None => None,
167                            }
168                        } else {
169                            None
170                        };
171                        ProcessArgs::Args(CallingProcess::GitShow(command_line, extension))
172                    }
173                    Some("log") => {
174                        ProcessArgs::Args(CallingProcess::GitLog(parse_command_line(args)))
175                    }
176                    Some("reflog") => {
177                        ProcessArgs::Args(CallingProcess::GitReflog(parse_command_line(args)))
178                    }
179                    Some("grep") => {
180                        ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
181                    }
182                    _ => {
183                        // It's git, but not a subcommand that we parse. Don't
184                        // look at any more processes.
185                        ProcessArgs::ArgError
186                    }
187                }
188            }
189            // TODO: parse_style_sections is failing to parse ANSI escape sequences emitted by
190            // grep (BSD and GNU), ag, pt. See #794
191            Some(s) if is_any_of(s.to_str(), ["rg", "ack", "sift"]) => {
192                ProcessArgs::Args(CallingProcess::OtherGrep)
193            }
194            Some(_) => {
195                // It's not git, and it's not another grep tool. Keep
196                // looking at other processes.
197                ProcessArgs::OtherProcess
198            }
199            _ => {
200                // Could not parse file stem (not expected); keep looking at
201                // other processes.
202                ProcessArgs::OtherProcess
203            }
204        },
205        _ => {
206            // Empty arguments (not expected); keep looking.
207            ProcessArgs::OtherProcess
208        }
209    }
210}
211
212fn is_git_binary(git: &str) -> bool {
213    // Ignore case, for e.g. NTFS or APFS file systems
214    Path::new(git)
215        .file_stem()
216        .and_then(|os_str| os_str.to_str())
217        .map(|s| s.eq_ignore_ascii_case("git"))
218        .unwrap_or(false)
219}
220
221// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
222// `skip_this_plus_parameter` plus their respective next argument.
223// Keep all arguments once a '--' is encountered.
224// (Note that some arguments work with and without '=': '--foo' 'bar' / '--foo=bar')
225fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
226    mut args_it: ArgsI,
227    skip_this_plus_parameter: SkipI,
228) -> Vec<&'a str>
229where
230    ArgsI: Iterator<Item = &'a str>,
231    SkipI: Iterator<Item = &'b str>,
232{
233    let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
234
235    let mut result = Vec::new();
236    loop {
237        match args_it.next() {
238            None => break result,
239            Some("--") => {
240                result.extend(args_it);
241                break result;
242            }
243            Some(arg) if arg_follows_space.contains(arg) => {
244                let _skip_parameter = args_it.next();
245            }
246            Some(arg) if !arg.starts_with('-') => {
247                result.push(arg);
248            }
249            Some(_) => { /* skip: --these -and --also=this */ }
250        }
251    }
252}
253
254// Given `--aa val -bc -d val e f -- ...` return
255// ({"--aa"}, {"-b", "-c", "-d"})
256fn parse_command_line<'a>(args: impl Iterator<Item = &'a str>) -> CommandLine {
257    let mut long_options = HashSet::new();
258    let mut short_options = HashSet::new();
259    let mut last_arg = None;
260
261    for s in args {
262        if s == "--" {
263            break;
264        } else if s.starts_with("--") {
265            long_options.insert(s.split('=').next().unwrap().to_owned());
266        } else if let Some(suffix) = s.strip_prefix('-') {
267            short_options.extend(suffix.chars().map(|c| format!("-{}", c)));
268        } else {
269            last_arg = Some(s);
270        }
271    }
272
273    CommandLine {
274        long_options,
275        short_options,
276        last_arg: last_arg.map(|s| s.to_string()),
277    }
278}
279
280struct ProcInfo {
281    info: sysinfo::System,
282}
283impl ProcInfo {
284    fn new() -> Self {
285        // On Linux sysinfo optimizes for repeated process queries and keeps per-process
286        // /proc file descriptors open. This caching is not needed here, so
287        // set this to zero (this does nothing on other platforms).
288        // Also, there is currently a kernel bug which slows down syscalls when threads are
289        // involved (here: the ctrlc handler) and a lot of files are kept open.
290        sysinfo::set_open_files_limit(0);
291
292        ProcInfo {
293            info: sysinfo::System::new(),
294        }
295    }
296}
297
298trait ProcActions {
299    fn cmd(&self) -> &[String];
300    fn parent(&self) -> Option<DeltaPid>;
301    fn pid(&self) -> DeltaPid;
302    fn start_time(&self) -> u64;
303}
304
305impl<T> ProcActions for T
306where
307    T: ProcessExt,
308{
309    fn cmd(&self) -> &[String] {
310        ProcessExt::cmd(self)
311    }
312    fn parent(&self) -> Option<DeltaPid> {
313        ProcessExt::parent(self).map(|p| p.as_u32())
314    }
315    fn pid(&self) -> DeltaPid {
316        ProcessExt::pid(self).as_u32()
317    }
318    fn start_time(&self) -> u64 {
319        ProcessExt::start_time(self)
320    }
321}
322
323trait ProcessInterface {
324    type Out: ProcActions;
325
326    fn my_pid(&self) -> DeltaPid;
327
328    fn process(&self, pid: DeltaPid) -> Option<&Self::Out>;
329    fn processes(&self) -> &HashMap<Pid, Self::Out>;
330
331    fn refresh_process(&mut self, pid: DeltaPid) -> bool;
332    fn refresh_processes(&mut self);
333
334    fn parent_process(&mut self, pid: DeltaPid) -> Option<&Self::Out> {
335        self.refresh_process(pid).then(|| ())?;
336        let parent_pid = self.process(pid)?.parent()?;
337        self.refresh_process(parent_pid).then(|| ())?;
338        self.process(parent_pid)
339    }
340    fn naive_sibling_process(&mut self, pid: DeltaPid) -> Option<&Self::Out> {
341        let sibling_pid = pid - 1;
342        self.refresh_process(sibling_pid).then(|| ())?;
343        self.process(sibling_pid)
344    }
345    fn find_sibling_in_refreshed_processes<F, T>(
346        &mut self,
347        pid: DeltaPid,
348        extract_args: &F,
349    ) -> Option<T>
350    where
351        F: Fn(&[String]) -> ProcessArgs<T>,
352        Self: Sized,
353    {
354        /*
355
356        $ start_blame_of.sh src/main.rs | delta
357
358        \_ /usr/bin/some-terminal-emulator
359        |   \_ common_git_and_delta_ancestor
360        |       \_ /bin/sh /opt/git/start_blame_of.sh src/main.rs
361        |       |   \_ /bin/sh /opt/some/wrapper git blame src/main.rs
362        |       |       \_ /usr/bin/git blame src/main.rs
363        |       \_ /bin/sh /opt/some/wrapper delta
364        |           \_ delta
365
366        Walk up the process tree of delta and of every matching other process, counting the steps
367        along the way.
368        Find the common ancestor processes, calculate the distance, and select the one with the shortest.
369
370        */
371
372        let this_start_time = self.process(pid)?.start_time();
373
374        let mut pid_distances = HashMap::<DeltaPid, usize>::new();
375        let mut collect_parent_pids = |pid, distance| {
376            pid_distances.insert(pid, distance);
377        };
378
379        iter_parents(self, pid, &mut collect_parent_pids);
380
381        let process_start_time_difference_less_than_3s = |a, b| (a as i64 - b as i64).abs() < 3;
382
383        let cmdline_of_closest_matching_process = self
384            .processes()
385            .iter()
386            .filter(|(_, proc)| {
387                process_start_time_difference_less_than_3s(this_start_time, proc.start_time())
388            })
389            .filter_map(|(&pid, proc)| match extract_args(proc.cmd()) {
390                ProcessArgs::Args(args) => {
391                    let mut length_of_process_chain = usize::MAX;
392
393                    let mut sum_distance = |pid, distance| {
394                        if length_of_process_chain == usize::MAX {
395                            if let Some(distance_to_first_common_parent) = pid_distances.get(&pid) {
396                                length_of_process_chain =
397                                    distance_to_first_common_parent + distance;
398                            }
399                        }
400                    };
401                    iter_parents(self, pid.as_u32(), &mut sum_distance);
402
403                    if length_of_process_chain == usize::MAX {
404                        None
405                    } else {
406                        Some((length_of_process_chain, args))
407                    }
408                }
409                _ => None,
410            })
411            .min_by_key(|(distance, _)| *distance)
412            .map(|(_, result)| result);
413
414        cmdline_of_closest_matching_process
415    }
416}
417
418impl ProcessInterface for ProcInfo {
419    type Out = Process;
420
421    fn my_pid(&self) -> DeltaPid {
422        std::process::id()
423    }
424    fn refresh_process(&mut self, pid: DeltaPid) -> bool {
425        self.info
426            .refresh_process_specifics(Pid::from_u32(pid), ProcessRefreshKind::new())
427    }
428    fn process(&self, pid: DeltaPid) -> Option<&Self::Out> {
429        self.info.process(Pid::from_u32(pid))
430    }
431    fn processes(&self) -> &HashMap<Pid, Self::Out> {
432        self.info.processes()
433    }
434    fn refresh_processes(&mut self) {
435        self.info
436            .refresh_processes_specifics(ProcessRefreshKind::new())
437    }
438}
439
440fn calling_process_cmdline<P, F, T>(mut info: P, extract_args: F) -> Option<T>
441where
442    P: ProcessInterface,
443    F: Fn(&[String]) -> ProcessArgs<T>,
444{
445    #[cfg(test)]
446    {
447        if let Some(args) = tests::FakeParentArgs::get() {
448            match extract_args(&args) {
449                ProcessArgs::Args(result) => return Some(result),
450                _ => return None,
451            }
452        }
453    }
454
455    let my_pid = info.my_pid();
456
457    // 1) Try the parent process(es). If delta is set as the pager in git, then git is the parent process.
458    // If delta is started by a script check the parent's parent as well.
459    let mut current_pid = my_pid;
460    'parent_iter: for depth in [1, 2, 3] {
461        let parent = match info.parent_process(current_pid) {
462            None => {
463                break 'parent_iter;
464            }
465            Some(parent) => parent,
466        };
467        let parent_pid = parent.pid();
468
469        match extract_args(parent.cmd()) {
470            ProcessArgs::Args(result) => return Some(result),
471            ProcessArgs::ArgError => return None,
472
473            // 2) The 1st parent process was something else, this can happen if git output is piped into delta, e.g.
474            // `git blame foo.txt | delta`. When the shell sets up the pipe it creates the two processes, the pids
475            // are usually consecutive, so naively check if the process with `my_pid - 1` matches.
476            ProcessArgs::OtherProcess if depth == 1 => {
477                let sibling = info.naive_sibling_process(current_pid);
478                if let Some(proc) = sibling {
479                    if let ProcessArgs::Args(result) = extract_args(proc.cmd()) {
480                        return Some(result);
481                    }
482                }
483            }
484            // This check is not done for the parent's parent etc.
485            ProcessArgs::OtherProcess => {}
486        }
487        current_pid = parent_pid;
488    }
489
490    /*
491    3) Neither parent(s) nor the direct sibling were a match.
492    The most likely case is that the input program of the pipe wrote all its data and exited before delta
493    started, so no command line can be parsed. Same if the data was piped from an input file.
494
495    There might also be intermediary scripts in between or piped input with a gap in pids or (rarely)
496    randomized pids, so check processes for the closest match in the process tree.
497    The size of this process tree can be reduced by only refreshing selected processes.
498
499    100 /usr/bin/some-terminal-emulator
500    124  \_ -shell
501    301  |   \_ /usr/bin/git blame src/main.rs
502    302  |       \_ wraps_delta.sh
503    303  |           \_ delta
504    304  |               \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
505    125  \_ -shell
506    800  |   \_ /usr/bin/git blame src/main.rs
507    200  |   \_ delta
508    400  |       \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
509    126  \_ -shell
510    501  |   \_ /bin/sh /wrapper/for/git blame src/main.rs
511    555  |   |   \_ /usr/bin/git blame src/main.rs
512    502  |   \_ delta
513    567  |       \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
514
515    */
516
517    // Also `add` because `A_has_pid101 | delta_has_pid102`, but if A is a wrapper which then calls
518    // git (no `exec`), then the final pid of the git process might be 103 or greater.
519    let pid_range = my_pid.saturating_sub(10)..my_pid.saturating_add(10);
520    for p in pid_range {
521        // Processes which were not refreshed do not exist for sysinfo, so by selectively
522        // letting it know about processes the `find_sibling..` function will only
523        // consider these.
524        if info.process(p).is_none() {
525            info.refresh_process(p);
526        }
527    }
528
529    match info.find_sibling_in_refreshed_processes(my_pid, &extract_args) {
530        None => {
531            #[cfg(not(target_os = "linux"))]
532            let full_scan = true;
533
534            // The full scan is expensive on Linux and rarely successful, so disable it by default.
535            #[cfg(target_os = "linux")]
536            let full_scan = std::env::var("DELTA_CALLING_PROCESS_QUERY_ALL")
537                .map_or(false, |v| !["0", "false", "no"].iter().any(|&n| n == v));
538
539            if full_scan {
540                info.refresh_processes();
541                info.find_sibling_in_refreshed_processes(my_pid, &extract_args)
542            } else {
543                None
544            }
545        }
546        some => some,
547    }
548}
549
550// Walk up the process tree, calling `f` with the pid and the distance to `starting_pid`.
551// Prerequisite: `info.refresh_processes()` has been called.
552fn iter_parents<P, F>(info: &P, starting_pid: DeltaPid, f: F)
553where
554    P: ProcessInterface,
555    F: FnMut(DeltaPid, usize),
556{
557    fn inner_iter_parents<P, F>(info: &P, pid: DeltaPid, mut f: F, distance: usize)
558    where
559        P: ProcessInterface,
560        F: FnMut(u32, usize),
561    {
562        // Probably bad input, not a tree:
563        if distance > 2000 {
564            return;
565        }
566        if let Some(proc) = info.process(pid) {
567            if let Some(pid) = proc.parent() {
568                f(pid, distance);
569                inner_iter_parents(info, pid, f, distance + 1)
570            }
571        }
572    }
573    inner_iter_parents(info, starting_pid, f, 1)
574}
575
576#[cfg(test)]
577pub mod tests {
578
579    use super::*;
580
581    use itertools::Itertools;
582    use std::cell::RefCell;
583    use std::rc::Rc;
584
585    thread_local! {
586        static FAKE_ARGS: RefCell<TlsState<Vec<String>>> = RefCell::new(TlsState::None);
587    }
588
589    #[derive(Debug, PartialEq)]
590    enum TlsState<T> {
591        Once(T),
592        Scope(T),
593        With(usize, Rc<Vec<T>>),
594        None,
595        Invalid,
596    }
597
598    // When calling `FakeParentArgs::get()`, it can return `Some(values)` which were set earlier
599    // during in the #[test]. Otherwise returns None.
600    // This value can be valid once: `FakeParentArgs::once(val)`, for the entire scope:
601    // `FakeParentArgs::for_scope(val)`, or can be different values every time `get()` is called:
602    // `FakeParentArgs::with([val1, val2, val3])`.
603    // It is an error if `once` or `with` values remain unused, or are overused.
604    // Note: The values are stored per-thread, so the expectation is that no thread boundaries are
605    // crossed.
606    pub struct FakeParentArgs {}
607    impl FakeParentArgs {
608        pub fn once(args: &str) -> Self {
609            Self::new(args, TlsState::Once, "once")
610        }
611        pub fn for_scope(args: &str) -> Self {
612            Self::new(args, TlsState::Scope, "for_scope")
613        }
614        fn new<F>(args: &str, initial: F, from_: &str) -> Self
615        where
616            F: Fn(Vec<String>) -> TlsState<Vec<String>>,
617        {
618            let string_vec = args.split(' ').map(str::to_owned).collect();
619            if FAKE_ARGS.with(|a| a.replace(initial(string_vec))) != TlsState::None {
620                Self::error(from_);
621            }
622            FakeParentArgs {}
623        }
624        pub fn with(args: &[&str]) -> Self {
625            let with = TlsState::With(
626                0,
627                Rc::new(
628                    args.iter()
629                        .map(|a| a.split(' ').map(str::to_owned).collect())
630                        .collect(),
631                ),
632            );
633            if FAKE_ARGS.with(|a| a.replace(with)) != TlsState::None || args.is_empty() {
634                Self::error("with creation");
635            }
636            FakeParentArgs {}
637        }
638        pub fn get() -> Option<Vec<String>> {
639            FAKE_ARGS.with(|a| {
640                let old_value = a.replace_with(|old_value| match old_value {
641                    TlsState::Once(_) => TlsState::Invalid,
642                    TlsState::Scope(args) => TlsState::Scope(args.clone()),
643                    TlsState::With(n, args) => TlsState::With(*n + 1, Rc::clone(args)),
644                    TlsState::None => TlsState::None,
645                    TlsState::Invalid => TlsState::Invalid,
646                });
647
648                match old_value {
649                    TlsState::Once(args) | TlsState::Scope(args) => Some(args),
650                    TlsState::With(n, args) if n < args.len() => Some(args[n].clone()),
651                    TlsState::None => None,
652                    TlsState::Invalid | TlsState::With(_, _) => Self::error("get"),
653                }
654            })
655        }
656        pub fn are_set() -> bool {
657            FAKE_ARGS.with(|a| *a.borrow() != TlsState::None)
658        }
659        fn error(where_: &str) -> ! {
660            panic!(
661                "test logic error (in {}): wrong FakeParentArgs scope?",
662                where_
663            );
664        }
665    }
666    impl Drop for FakeParentArgs {
667        fn drop(&mut self) {
668            // Clears an Invalid state and tests if a Once or With value has been used.
669            FAKE_ARGS.with(|a| {
670                let old_value = a.replace(TlsState::None);
671                match old_value {
672                    TlsState::With(n, args) => {
673                        if n != args.len() {
674                            Self::error("drop with")
675                        }
676                    }
677                    TlsState::Once(_) | TlsState::None => Self::error("drop"),
678                    TlsState::Scope(_) | TlsState::Invalid => {}
679                }
680            });
681        }
682    }
683
684    #[test]
685    fn test_guess_git_blame_filename_extension() {
686        use ProcessArgs::Args;
687
688        fn make_string_vec(args: &[&str]) -> Vec<String> {
689            args.iter().map(|&x| x.to_owned()).collect::<Vec<String>>()
690        }
691        let args = make_string_vec(&["git", "blame", "hello", "world.txt"]);
692        assert_eq!(
693            guess_git_blame_filename_extension(&args),
694            Args("txt".into())
695        );
696
697        let args = make_string_vec(&[
698            "git",
699            "blame",
700            "-s",
701            "-f",
702            "hello.txt",
703            "--date=2015",
704            "--date",
705            "now",
706        ]);
707        assert_eq!(
708            guess_git_blame_filename_extension(&args),
709            Args("txt".into())
710        );
711
712        let args = make_string_vec(&["git", "blame", "-s", "-f", "--", "hello.txt"]);
713        assert_eq!(
714            guess_git_blame_filename_extension(&args),
715            Args("txt".into())
716        );
717
718        let args = make_string_vec(&["git", "blame", "--", "--not.an.argument"]);
719        assert_eq!(
720            guess_git_blame_filename_extension(&args),
721            Args("argument".into())
722        );
723
724        let args = make_string_vec(&["foo", "bar", "-a", "--123", "not.git"]);
725        assert_eq!(
726            guess_git_blame_filename_extension(&args),
727            ProcessArgs::OtherProcess
728        );
729
730        let args = make_string_vec(&["git", "blame", "--help.txt"]);
731        assert_eq!(
732            guess_git_blame_filename_extension(&args),
733            ProcessArgs::ArgError
734        );
735
736        let args = make_string_vec(&["git", "-c", "a=b", "blame", "main.rs"]);
737        assert_eq!(guess_git_blame_filename_extension(&args), Args("rs".into()));
738
739        let args = make_string_vec(&["git", "blame", "README"]);
740        assert_eq!(
741            guess_git_blame_filename_extension(&args),
742            Args("README".into())
743        );
744
745        let args = make_string_vec(&["git", "blame", ""]);
746        assert_eq!(guess_git_blame_filename_extension(&args), Args("".into()));
747    }
748
749    #[derive(Debug)]
750    struct FakeProc {
751        #[allow(dead_code)]
752        pid: DeltaPid,
753        start_time: u64,
754        cmd: Vec<String>,
755        ppid: Option<DeltaPid>,
756    }
757    impl Default for FakeProc {
758        fn default() -> Self {
759            Self {
760                pid: 0,
761                start_time: 0,
762                cmd: Vec::new(),
763                ppid: None,
764            }
765        }
766    }
767    impl FakeProc {
768        fn new(pid: DeltaPid, start_time: u64, cmd: Vec<String>, ppid: Option<DeltaPid>) -> Self {
769            FakeProc {
770                pid,
771                start_time,
772                cmd,
773                ppid,
774            }
775        }
776    }
777
778    impl ProcActions for FakeProc {
779        fn cmd(&self) -> &[String] {
780            &self.cmd
781        }
782        fn parent(&self) -> Option<DeltaPid> {
783            self.ppid
784        }
785        fn pid(&self) -> DeltaPid {
786            self.pid
787        }
788        fn start_time(&self) -> u64 {
789            self.start_time
790        }
791    }
792
793    #[derive(Debug)]
794    struct MockProcInfo {
795        delta_pid: DeltaPid,
796        info: HashMap<Pid, FakeProc>,
797    }
798    impl Default for MockProcInfo {
799        fn default() -> Self {
800            Self {
801                delta_pid: 0,
802                info: HashMap::new(),
803            }
804        }
805    }
806    impl MockProcInfo {
807        fn with(processes: &[(DeltaPid, u64, &str, Option<DeltaPid>)]) -> Self {
808            MockProcInfo {
809                delta_pid: processes.last().map(|p| p.0).unwrap_or(1),
810                info: processes
811                    .iter()
812                    .map(|(pid, start_time, cmd, ppid)| {
813                        let cmd_vec = cmd.split(' ').map(str::to_owned).collect();
814                        (
815                            Pid::from_u32(*pid),
816                            FakeProc::new(*pid, *start_time, cmd_vec, *ppid),
817                        )
818                    })
819                    .collect(),
820            }
821        }
822    }
823
824    impl ProcessInterface for MockProcInfo {
825        type Out = FakeProc;
826
827        fn my_pid(&self) -> DeltaPid {
828            self.delta_pid
829        }
830        fn process(&self, pid: DeltaPid) -> Option<&Self::Out> {
831            self.info.get(&Pid::from_u32(pid))
832        }
833        fn processes(&self) -> &HashMap<Pid, Self::Out> {
834            &self.info
835        }
836        fn refresh_processes(&mut self) {}
837        fn refresh_process(&mut self, _pid: DeltaPid) -> bool {
838            true
839        }
840    }
841
842    fn set(arg1: &[&str]) -> HashSet<String> {
843        arg1.iter().map(|&s| s.to_owned()).collect()
844    }
845
846    #[test]
847    fn test_process_testing() {
848        {
849            let _args = FakeParentArgs::once("git blame hello");
850            assert_eq!(
851                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
852                Some("hello".into())
853            );
854        }
855        {
856            let _args = FakeParentArgs::once("git blame world.txt");
857            assert_eq!(
858                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
859                Some("txt".into())
860            );
861        }
862        {
863            let _args = FakeParentArgs::for_scope("git blame hello world.txt");
864            assert_eq!(
865                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
866                Some("txt".into())
867            );
868
869            assert_eq!(
870                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
871                Some("txt".into())
872            );
873        }
874    }
875
876    #[test]
877    #[should_panic]
878    fn test_process_testing_assert() {
879        let _args = FakeParentArgs::once("git blame do.not.panic");
880        assert_eq!(
881            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
882            Some("panic".into())
883        );
884
885        calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
886    }
887
888    #[test]
889    #[should_panic]
890    fn test_process_testing_assert_never_used() {
891        let _args = FakeParentArgs::once("never used");
892
893        // causes a panic while panicking, so can't test:
894        // let _args = FakeParentArgs::for_scope(&"never used");
895        // let _args = FakeParentArgs::once(&"never used");
896    }
897
898    #[test]
899    fn test_process_testing_scope_can_remain_unused() {
900        let _args = FakeParentArgs::for_scope("never used");
901    }
902
903    #[test]
904    fn test_process_testing_n_times_panic() {
905        let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
906        assert_eq!(
907            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
908            Some("once".into())
909        );
910
911        assert_eq!(
912            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
913            Some("twice".into())
914        );
915    }
916
917    #[test]
918    #[should_panic]
919    fn test_process_testing_n_times_unused() {
920        let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
921    }
922
923    #[test]
924    #[should_panic]
925    fn test_process_testing_n_times_underused() {
926        let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
927        assert_eq!(
928            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
929            Some("once".into())
930        );
931    }
932
933    #[test]
934    #[should_panic]
935    #[ignore]
936    fn test_process_testing_n_times_overused() {
937        let _args = FakeParentArgs::with(&["git blame once"]);
938        assert_eq!(
939            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
940            Some("once".into())
941        );
942        // ignored: dropping causes a panic while panicking, so can't test
943        calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
944    }
945
946    #[test]
947    fn test_process_blame_no_parent_found() {
948        let two_trees = MockProcInfo::with(&[
949            (2, 100, "-shell", None),
950            (3, 100, "git blame src/main.rs", Some(2)),
951            (4, 100, "call_delta.sh", None),
952            (5, 100, "delta", Some(4)),
953        ]);
954        assert_eq!(
955            calling_process_cmdline(two_trees, guess_git_blame_filename_extension),
956            None
957        );
958    }
959
960    #[test]
961    fn test_process_blame_info_with_parent() {
962        let no_processes = MockProcInfo::with(&[]);
963        assert_eq!(
964            calling_process_cmdline(no_processes, guess_git_blame_filename_extension),
965            None
966        );
967
968        let parent = MockProcInfo::with(&[
969            (2, 100, "-shell", None),
970            (3, 100, "git blame hello.txt", Some(2)),
971            (4, 100, "delta", Some(3)),
972        ]);
973        assert_eq!(
974            calling_process_cmdline(parent, guess_git_blame_filename_extension),
975            Some("txt".into())
976        );
977
978        let grandparent = MockProcInfo::with(&[
979            (2, 100, "-shell", None),
980            (3, 100, "git blame src/main.rs", Some(2)),
981            (4, 100, "call_delta.sh", Some(3)),
982            (5, 100, "delta", Some(4)),
983        ]);
984        assert_eq!(
985            calling_process_cmdline(grandparent, guess_git_blame_filename_extension),
986            Some("rs".into())
987        );
988    }
989
990    #[test]
991    fn test_process_blame_info_with_sibling() {
992        let sibling = MockProcInfo::with(&[
993            (2, 100, "-xterm", None),
994            (3, 100, "-shell", Some(2)),
995            (4, 100, "git blame src/main.rs", Some(3)),
996            (5, 100, "delta", Some(3)),
997        ]);
998        assert_eq!(
999            calling_process_cmdline(sibling, guess_git_blame_filename_extension),
1000            Some("rs".into())
1001        );
1002
1003        let indirect_sibling = MockProcInfo::with(&[
1004            (2, 100, "-xterm", None),
1005            (3, 100, "-shell", Some(2)),
1006            (4, 100, "Git.exe blame --correct src/main.abc", Some(3)),
1007            (
1008                10,
1009                100,
1010                "Git.exe blame --ignored-child src/main.def",
1011                Some(4),
1012            ),
1013            (5, 100, "delta.sh", Some(3)),
1014            (20, 100, "delta", Some(5)),
1015        ]);
1016        assert_eq!(
1017            calling_process_cmdline(indirect_sibling, guess_git_blame_filename_extension),
1018            Some("abc".into())
1019        );
1020
1021        let indirect_sibling2 = MockProcInfo::with(&[
1022            (2, 100, "-xterm", None),
1023            (3, 100, "-shell", Some(2)),
1024            (4, 100, "git wrap src/main.abc", Some(3)),
1025            (10, 100, "git blame src/main.def", Some(4)),
1026            (5, 100, "delta.sh", Some(3)),
1027            (20, 100, "delta", Some(5)),
1028        ]);
1029        assert_eq!(
1030            calling_process_cmdline(indirect_sibling2, guess_git_blame_filename_extension),
1031            Some("def".into())
1032        );
1033
1034        // 3 blame processes, 2 with matching start times, pick the one with lower
1035        // distance but larger start time difference.
1036        let indirect_sibling_start_times = MockProcInfo::with(&[
1037            (2, 100, "-xterm", None),
1038            (3, 100, "-shell", Some(2)),
1039            (4, 109, "git wrap src/main.abc", Some(3)),
1040            (10, 109, "git blame src/main.def", Some(4)),
1041            (20, 100, "git wrap1 src/main.abc", Some(3)),
1042            (21, 100, "git wrap2 src/main.def", Some(20)),
1043            (22, 101, "git blame src/main.not", Some(21)),
1044            (23, 102, "git blame src/main.this", Some(20)),
1045            (5, 100, "delta.sh", Some(3)),
1046            (20, 100, "delta", Some(5)),
1047        ]);
1048        assert_eq!(
1049            calling_process_cmdline(
1050                indirect_sibling_start_times,
1051                guess_git_blame_filename_extension
1052            ),
1053            Some("this".into())
1054        );
1055    }
1056
1057    #[test]
1058    fn test_describe_calling_process_grep() {
1059        let no_processes = MockProcInfo::with(&[]);
1060        assert_eq!(
1061            calling_process_cmdline(no_processes, describe_calling_process),
1062            None
1063        );
1064
1065        let empty_command_line = CommandLine {
1066            long_options: [].into(),
1067            short_options: [].into(),
1068            last_arg: Some("hello.txt".to_string()),
1069        };
1070        let parent = MockProcInfo::with(&[
1071            (2, 100, "-shell", None),
1072            (3, 100, "git grep pattern hello.txt", Some(2)),
1073            (4, 100, "delta", Some(3)),
1074        ]);
1075        assert_eq!(
1076            calling_process_cmdline(parent, describe_calling_process),
1077            Some(CallingProcess::GitGrep(empty_command_line.clone()))
1078        );
1079
1080        let parent = MockProcInfo::with(&[
1081            (2, 100, "-shell", None),
1082            (3, 100, "Git.exe grep pattern hello.txt", Some(2)),
1083            (4, 100, "delta", Some(3)),
1084        ]);
1085        assert_eq!(
1086            calling_process_cmdline(parent, describe_calling_process),
1087            Some(CallingProcess::GitGrep(empty_command_line))
1088        );
1089
1090        for grep_command in &[
1091            "/usr/local/bin/rg pattern hello.txt",
1092            "RG.exe pattern hello.txt",
1093            "/usr/local/bin/ack pattern hello.txt",
1094            "ack.exe pattern hello.txt",
1095        ] {
1096            let parent = MockProcInfo::with(&[
1097                (2, 100, "-shell", None),
1098                (3, 100, grep_command, Some(2)),
1099                (4, 100, "delta", Some(3)),
1100            ]);
1101            assert_eq!(
1102                calling_process_cmdline(parent, describe_calling_process),
1103                Some(CallingProcess::OtherGrep)
1104            );
1105        }
1106
1107        let git_grep_command =
1108            "git grep -ab --function-context -n --show-function -W --foo=val pattern hello.txt";
1109
1110        let expected_result = Some(CallingProcess::GitGrep(CommandLine {
1111            long_options: set(&["--function-context", "--show-function", "--foo"]),
1112            short_options: set(&["-a", "-b", "-n", "-W"]),
1113            last_arg: Some("hello.txt".to_string()),
1114        }));
1115
1116        let parent = MockProcInfo::with(&[
1117            (2, 100, "-shell", None),
1118            (3, 100, git_grep_command, Some(2)),
1119            (4, 100, "delta", Some(3)),
1120        ]);
1121        assert_eq!(
1122            calling_process_cmdline(parent, describe_calling_process),
1123            expected_result
1124        );
1125
1126        let grandparent = MockProcInfo::with(&[
1127            (2, 100, "-shell", None),
1128            (3, 100, git_grep_command, Some(2)),
1129            (4, 100, "call_delta.sh", Some(3)),
1130            (5, 100, "delta", Some(4)),
1131        ]);
1132        assert_eq!(
1133            calling_process_cmdline(grandparent, describe_calling_process),
1134            expected_result
1135        );
1136    }
1137
1138    #[test]
1139    fn test_describe_calling_process_git_show() {
1140        for (command, expected_extension) in [
1141            (
1142                "/usr/local/bin/git show --abbrev-commit -w 775c3b84:./src/hello.rs",
1143                "rs",
1144            ),
1145            (
1146                "/usr/local/bin/git show --abbrev-commit -w HEAD~1:Makefile",
1147                "Makefile",
1148            ),
1149            (
1150                "git -c x.y=z show --abbrev-commit -w 775c3b84:./src/hello.bye.R",
1151                "R",
1152            ),
1153        ] {
1154            let parent = MockProcInfo::with(&[
1155                (2, 100, "-shell", None),
1156                (3, 100, command, Some(2)),
1157                (4, 100, "delta", Some(3)),
1158            ]);
1159            if let Some(CallingProcess::GitShow(cmd_line, ext)) =
1160                calling_process_cmdline(parent, describe_calling_process)
1161            {
1162                assert_eq!(cmd_line.long_options, set(&["--abbrev-commit"]));
1163                assert_eq!(cmd_line.short_options, set(&["-w"]));
1164                assert_eq!(ext, Some(expected_extension.to_string()));
1165            } else {
1166                unreachable!();
1167            }
1168        }
1169    }
1170
1171    #[test]
1172    fn test_process_calling_cmdline() {
1173        // Github runs CI tests for arm under qemu where where sysinfo can not find the parent process.
1174        if std::env::vars().any(|(key, _)| key == "CROSS_RUNNER" || key == "QEMU_LD_PREFIX") {
1175            return;
1176        }
1177
1178        let mut info = ProcInfo::new();
1179        info.refresh_processes();
1180        let mut ppid_distance = Vec::new();
1181
1182        iter_parents(&info, std::process::id(), |pid, distance| {
1183            ppid_distance.push(pid as i32);
1184            ppid_distance.push(distance as i32)
1185        });
1186
1187        assert!(ppid_distance[1] == 1);
1188
1189        fn find_calling_process(args: &[String], want: &[&str]) -> ProcessArgs<()> {
1190            if args.iter().any(|have| want.iter().any(|want| want == have)) {
1191                ProcessArgs::Args(())
1192            } else {
1193                ProcessArgs::ArgError
1194            }
1195        }
1196
1197        // Tests that caller is something like "cargo test" or "cargo tarpaulin"
1198        let find_test = |args: &[String]| find_calling_process(args, &["t", "test", "tarpaulin"]);
1199        assert_eq!(calling_process_cmdline(info, find_test), Some(()));
1200
1201        let nonsense = ppid_distance
1202            .iter()
1203            .map(|i| i.to_string())
1204            .join("Y40ii4RihK6lHiK4BDsGSx");
1205
1206        let find_nothing = |args: &[String]| find_calling_process(args, &[&nonsense]);
1207        assert_eq!(calling_process_cmdline(ProcInfo::new(), find_nothing), None);
1208    }
1209}