1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
// SPDX-License-Identifier: GPL-3.0-or-later

//! Helper for matching [command output](std::process::Output).
use std::borrow::Cow;

/// Match substrings in [command output](std::process::Output).
///
/// Due to the problems described in [`crate::env::ExecutionError::NonZero`], the output of
/// [`crate::env::Environment::output_of()`] doesn't reliably catch the contents of *stderr*
/// in the actual *stderr* variable. For [`Provider`s][crate::provider::Provider], this means
/// that when searching for error strings in command output, **both stdout and stderr** must be
/// checked for occurences of the pattern. This struct removes a lof of the resulting boilerplate
/// code for first converting a `Vec<u8>` into something string-like, and then matching *stdout*
/// and *stderr* against a pattern.
pub struct OutputMatcher<'a> {
    stdout: Cow<'a, str>,
    stderr: Cow<'a, str>,
    status: std::process::ExitStatus,
}

impl<'a> OutputMatcher<'a> {
    /// Create a new instance of [`OutputMatcher`] for the given command output.
    pub fn new(output: &'a std::process::Output) -> Self {
        Self {
            stdout: String::from_utf8_lossy(&output.stdout),
            stderr: String::from_utf8_lossy(&output.stderr),
            status: output.status,
        }
    }

    /// Access stdout with all leading/trailing whitespaces trimmed.
    fn stdout(&self) -> &str {
        self.stdout.trim()
    }

    /// Access stderr with all leading/trailing whitespaces trimmed.
    fn stderr(&self) -> &str {
        self.stderr.trim()
    }

    /// Returns true if either stdout or stderr starts with `pat`.
    pub fn starts_with(&self, pat: &str) -> bool {
        self.stdout().starts_with(pat) || self.stderr().starts_with(pat)
    }

    /// Returns true if either stdout or stderr contains `pat`.
    pub fn contains(&self, pat: &str) -> bool {
        self.stdout().contains(pat) || self.stderr().contains(pat)
    }

    /// Returns true if either stdout or stderr ends with `pat`.
    pub fn ends_with(&self, pat: &str) -> bool {
        self.stdout().ends_with(pat) || self.stderr().ends_with(pat)
    }

    /// Returns the raw error code of the command output.
    pub fn exit_code(&self) -> Option<i32> {
        self.status.code()
    }

    /// Returns true if **both** stdout **and** stderr are empty (except for whitespace)
    pub fn is_empty(&self) -> bool {
        self.stdout().is_empty() && self.stderr().is_empty()
    }
}

impl<'x> From<&'x std::process::Output> for OutputMatcher<'x> {
    fn from(value: &'x std::process::Output) -> Self {
        Self::new(value)
    }
}