• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dandavison / delta / 6914707237

18 Nov 2023 03:11PM UTC coverage: 59.338% (+1.1%) from 58.209%
6914707237

push

github

web-flow
fixed typos (#1553)

2386 of 4021 relevant lines covered (59.34%)

1.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.64
/src/utils/process.rs
1
use std::collections::{HashMap, HashSet};
2
use std::path::Path;
3
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
4

5
use lazy_static::lazy_static;
6
use sysinfo::{Pid, PidExt, Process, ProcessExt, ProcessRefreshKind, SystemExt};
7

8
pub type DeltaPid = u32;
9

10
#[derive(Clone, Debug, PartialEq, Eq)]
11
pub 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

23
impl CallingProcess {
24
    pub fn paths_in_input_are_relative_to_cwd(&self) -> bool {
25
        match self {
4✔
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)]
36
pub struct CommandLine {
37
    pub long_options: HashSet<String>,
38
    pub short_options: HashSet<String>,
39
    last_arg: Option<String>,
40
}
41

42
lazy_static! {
43
    static ref CALLER: Arc<(Mutex<CallingProcess>, Condvar)> =
44
        Arc::new((Mutex::new(CallingProcess::Pending), Condvar::new()));
45
}
46

47
pub 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))]
65
pub 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)]
77
pub fn calling_process() -> Box<CallingProcess> {
3✔
78
    type _UnusedImport = MutexGuard<'static, i8>;
79

80
    if crate::utils::process::tests::FakeParentArgs::are_set() {
3✔
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();
4✔
88
        if *caller == CallingProcess::Pending {
1✔
89
            *caller = determine_calling_process();
1✔
90
        }
91

92
        Box::new(caller.clone())
1✔
93
    }
94
}
95

96
fn determine_calling_process() -> CallingProcess {
97
    calling_process_cmdline(ProcInfo::new(), describe_calling_process)
5✔
98
        .unwrap_or(CallingProcess::None)
3✔
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)]
104
pub 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

113
pub fn git_blame_filename_extension() -> Option<String> {
×
114
    calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension)
1✔
115
}
116

117
pub fn guess_git_blame_filename_extension(args: &[String]) -> ProcessArgs<String> {
1✔
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 =
1✔
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() {
5✔
130
            Some(arg) => ProcessArgs::Args(arg.to_string()),
131
            None => ProcessArgs::ArgError,
132
        },
133
        [git, "blame"] if is_git_binary(git) => ProcessArgs::ArgError,
4✔
134
        _ => ProcessArgs::OtherProcess,
135
    }
136
}
137

138
pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess> {
1✔
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() {
1✔
151
            Some(s) if s.to_str().map(is_git_binary).unwrap_or(false) => {
5✔
152
                let mut args = args.skip_while(|s| {
153
                    *s != "diff" && *s != "show" && *s != "log" && *s != "reflog" && *s != "grep"
6✔
154
                });
155
                match args.next() {
2✔
156
                    Some("diff") => {
4✔
157
                        ProcessArgs::Args(CallingProcess::GitDiff(parse_command_line(args)))
2✔
158
                    }
159
                    Some("show") => {
2✔
160
                        let command_line = parse_command_line(args);
1✔
161
                        let extension = if let Some(last_arg) = &command_line.last_arg {
1✔
162
                            match last_arg.split_once(':') {
1✔
163
                                Some((_, suffix)) => {
1✔
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))
1✔
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") => {
2✔
180
                        ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
2✔
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"]) => {
1✔
192
                ProcessArgs::Args(CallingProcess::OtherGrep)
1✔
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

212
fn is_git_binary(git: &str) -> bool {
×
213
    // Ignore case, for e.g. NTFS or APFS file systems
214
    Path::new(git)
3✔
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')
225
fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
1✔
226
    mut args_it: ArgsI,
227
    skip_this_plus_parameter: SkipI,
228
) -> Vec<&'a str>
229
where
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("--") => {
1✔
240
                result.extend(args_it);
×
241
                break result;
×
242
            }
243
            Some(arg) if arg_follows_space.contains(arg) => {
1✔
244
                let _skip_parameter = args_it.next();
×
245
            }
246
            Some(arg) if !arg.starts_with('-') => {
1✔
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"})
256
fn parse_command_line<'a>(args: impl Iterator<Item = &'a str>) -> CommandLine {
2✔
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 {
6✔
262
        if s == "--" {
×
263
            break;
×
264
        } else if s.starts_with("--") {
2✔
265
            long_options.insert(s.split('=').next().unwrap().to_owned());
6✔
266
        } else if let Some(suffix) = s.strip_prefix('-') {
×
267
            short_options.extend(suffix.chars().map(|c| format!("-{c}")));
1✔
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

280
struct ProcInfo {
281
    info: sysinfo::System,
282
}
283
impl 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);
17✔
291

292
        ProcInfo {
293
            info: sysinfo::System::new(),
294
        }
295
    }
296
}
297

298
trait 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

305
impl<T> ProcActions for T
306
where
307
    T: ProcessExt,
308
{
309
    fn cmd(&self) -> &[String] {
×
310
        ProcessExt::cmd(self)
8✔
311
    }
312
    fn parent(&self) -> Option<DeltaPid> {
×
313
        ProcessExt::parent(self).map(|p| p.as_u32())
8✔
314
    }
315
    fn pid(&self) -> DeltaPid {
×
316
        ProcessExt::pid(self).as_u32()
6✔
317
    }
318
    fn start_time(&self) -> u64 {
×
319
        ProcessExt::start_time(self)
4✔
320
    }
321
}
322

323
trait 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> {
3✔
335
        self.refresh_process(pid).then_some(())?;
1✔
336
        let parent_pid = self.process(pid)?.parent()?;
2✔
337
        self.refresh_process(parent_pid).then_some(())?;
1✔
338
        self.process(parent_pid)
×
339
    }
340
    fn naive_sibling_process(&mut self, pid: DeltaPid) -> Option<&Self::Out> {
1✔
341
        let sibling_pid = pid - 1;
3✔
342
        self.refresh_process(sibling_pid).then_some(())?;
2✔
343
        self.process(sibling_pid)
×
344
    }
345
    fn find_sibling_in_refreshed_processes<F, T>(
4✔
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();
5✔
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;
14✔
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())
5✔
388
            })
389
            .filter_map(|(&pid, proc)| match extract_args(proc.cmd()) {
12✔
390
                ProcessArgs::Args(args) => {
1✔
391
                    let mut length_of_process_chain = usize::MAX;
2✔
392

393
                    let mut sum_distance = |pid, distance| {
2✔
394
                        if length_of_process_chain == usize::MAX {
2✔
395
                            if let Some(distance_to_first_common_parent) = pid_distances.get(&pid) {
2✔
396
                                length_of_process_chain =
1✔
397
                                    distance_to_first_common_parent + distance;
×
398
                            }
399
                        }
400
                    };
401
                    iter_parents(self, pid.as_u32(), &mut sum_distance);
2✔
402

403
                    if length_of_process_chain == usize::MAX {
1✔
404
                        None
1✔
405
                    } else {
406
                        Some((length_of_process_chain, args))
1✔
407
                    }
408
                }
409
                _ => None,
3✔
410
            })
411
            .min_by_key(|(distance, _)| *distance)
×
412
            .map(|(_, result)| result);
×
413

414
        cmdline_of_closest_matching_process
3✔
415
    }
416
}
417

418
impl ProcessInterface for ProcInfo {
419
    type Out = Process;
420

421
    fn my_pid(&self) -> DeltaPid {
422
        std::process::id()
4✔
423
    }
424
    fn refresh_process(&mut self, pid: DeltaPid) -> bool {
425
        self.info
6✔
426
            .refresh_process_specifics(Pid::from_u32(pid), ProcessRefreshKind::new())
6✔
427
    }
428
    fn process(&self, pid: DeltaPid) -> Option<&Self::Out> {
429
        self.info.process(Pid::from_u32(pid))
9✔
430
    }
431
    fn processes(&self) -> &HashMap<Pid, Self::Out> {
432
        self.info.processes()
2✔
433
    }
434
    fn refresh_processes(&mut self) {
×
435
        self.info
1✔
436
            .refresh_processes_specifics(ProcessRefreshKind::new())
1✔
437
    }
438
}
439

440
fn calling_process_cmdline<P, F, T>(mut info: P, extract_args: F) -> Option<T>
9✔
441
where
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] {
9✔
461
        let parent = match info.parent_process(current_pid) {
15✔
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()) {
11✔
470
            ProcessArgs::Args(result) => return Some(result),
1✔
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 => {
9✔
477
                let sibling = info.naive_sibling_process(current_pid);
3✔
478
                if let Some(proc) = sibling {
3✔
479
                    if let ProcessArgs::Args(result) = extract_args(proc.cmd()) {
5✔
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() {
4✔
525
            info.refresh_process(p);
×
526
        }
527
    }
528

529
    match info.find_sibling_in_refreshed_processes(my_pid, &extract_args) {
4✔
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,
1✔
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.
552
fn iter_parents<P, F>(info: &P, starting_pid: DeltaPid, f: F)
553
where
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)
2✔
558
    where
559
        P: ProcessInterface,
560
        F: FnMut(u32, usize),
561
    {
562
        // Probably bad input, not a tree:
563
        if distance > 2000 {
7✔
564
            return;
×
565
        }
566
        if let Some(proc) = info.process(pid) {
6✔
567
            if let Some(pid) = proc.parent() {
6✔
568
                f(pid, distance);
×
569
                inner_iter_parents(info, pid, f, distance + 1)
6✔
570
            }
571
        }
572
    }
573
    inner_iter_parents(info, starting_pid, f, 1)
3✔
574
}
575

576
#[cfg(test)]
577
pub 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 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc