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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use lazy_static::lazy_static;
use semver::Version;
use std::env;
use std::fmt;
use std::path::Path;
use std::process::Command;

#[derive(Debug)]
pub enum GcovToolError {
    ProcessFailure,
    Failure((String, String, String)),
}

impl fmt::Display for GcovToolError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            GcovToolError::ProcessFailure => write!(f, "Failed to execute gcov process"),
            GcovToolError::Failure((ref path, ref stdout, ref stderr)) => {
                writeln!(f, "gcov execution failed on {}", path)?;
                writeln!(f, "gcov stdout: {}", stdout)?;
                writeln!(f, "gcov stderr: {}", stderr)
            }
        }
    }
}

fn get_gcov() -> String {
    if let Ok(s) = env::var("GCOV") {
        s
    } else {
        "gcov".to_string()
    }
}

pub fn run_gcov(
    gcno_path: &Path,
    branch_enabled: bool,
    working_dir: &Path,
) -> Result<(), GcovToolError> {
    let mut command = Command::new(get_gcov());
    let command = if branch_enabled {
        command.arg("-b").arg("-c")
    } else {
        &mut command
    };
    let status = command
        .arg(gcno_path)
        .arg("-i") // Generate intermediate gcov format, faster to parse.
        .current_dir(working_dir);

    let output = if let Ok(output) = status.output() {
        output
    } else {
        return Err(GcovToolError::ProcessFailure);
    };

    if !output.status.success() {
        return Err(GcovToolError::Failure((
            gcno_path.to_str().unwrap().to_string(),
            String::from_utf8_lossy(&output.stdout).to_string(),
            String::from_utf8_lossy(&output.stderr).to_string(),
        )));
    }

    Ok(())
}

pub fn get_gcov_version() -> &'static Version {
    lazy_static! {
        static ref V: Version = {
            let output = Command::new(get_gcov())
                .arg("--version")
                .output()
                .expect("Failed to execute `gcov`. `gcov` is required (it is part of GCC).");
            assert!(output.status.success(), "`gcov` failed to execute.");
            let output = String::from_utf8(output.stdout).unwrap();
            parse_version(&output)
        };
    }
    &V
}

pub fn get_gcov_output_ext() -> &'static str {
    lazy_static! {
        static ref E: &'static str = {
            let min_ver = Version::new(9, 1, 0);
            if get_gcov_version() >= &min_ver {
                ".gcov.json.gz"
            } else {
                ".gcov"
            }
        };
    }
    &E
}

fn parse_version(gcov_output: &str) -> Version {
    let mut versions: Vec<_> = gcov_output
        .split(|c| c == ' ' || c == '\n')
        .filter_map(|value| Version::parse(value).ok())
        .collect();
    assert!(!versions.is_empty(), "no version found for `gcov`.");

    versions.pop().unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_version() {
        assert_eq!(
            parse_version("gcov (Ubuntu 4.3.0-12ubuntu2) 4.3.0 20170406"),
            Version::new(4, 3, 0)
        );
        assert_eq!(
            parse_version("gcov (Ubuntu 4.9.0-12ubuntu2) 4.9.0 20170406"),
            Version::new(4, 9, 0)
        );
        assert_eq!(
            parse_version("gcov (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"),
            Version::new(6, 3, 0)
        );
        assert_eq!(parse_version("gcov (GCC) 12.2.0"), Version::new(12, 2, 0));
    }
}