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>), GitLog(CommandLine),
15 GitReflog(CommandLine),
16 GitGrep(CommandLine),
17 OtherGrep, None, Pending, }
21impl 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 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#[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 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#[derive(Debug, PartialEq, Eq)]
104pub enum ProcessArgs<T> {
105 Args(T),
107 ArgError,
109 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 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 ProcessArgs::ArgError
186 }
187 }
188 }
189 Some(s) if is_any_of(s.to_str(), ["rg", "ack", "sift"]) => {
192 ProcessArgs::Args(CallingProcess::OtherGrep)
193 }
194 Some(_) => {
195 ProcessArgs::OtherProcess
198 }
199 _ => {
200 ProcessArgs::OtherProcess
203 }
204 },
205 _ => {
206 ProcessArgs::OtherProcess
208 }
209 }
210}
211
212fn is_git_binary(git: &str) -> bool {
213 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
221fn 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(_) => { }
250 }
251 }
252}
253
254fn 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 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 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 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 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 ProcessArgs::OtherProcess => {}
486 }
487 current_pid = parent_pid;
488 }
489
490 let pid_range = my_pid.saturating_sub(10)..my_pid.saturating_add(10);
520 for p in pid_range {
521 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 #[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
550fn 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 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 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 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 }
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 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 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 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 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}