cargo_tarpaulin/report/
coveralls.rs

1use crate::config::Config;
2use crate::errors::RunError;
3use crate::traces::{CoverageStat, TraceMap};
4use coveralls_api::*;
5use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8use tracing::{info, warn};
9
10fn get_git_info(manifest_path: &Path) -> Result<GitInfo, String> {
11    let dir_path = manifest_path
12        .parent()
13        .ok_or_else(|| format!("failed to get parent for path: {}", manifest_path.display()))?;
14    let repo = git2::Repository::discover(dir_path).map_err(|err| {
15        format!(
16            "failed to open git repository: {}: {}",
17            dir_path.display(),
18            err
19        )
20    })?;
21
22    let head = repo
23        .head()
24        .map_err(|err| format!("failed to get repository head: {err}"))?;
25    let branch = git2::Branch::wrap(head);
26    let branch_name = branch
27        .name()
28        .map_err(|err| format!("failed to get branch name: {err}"))?;
29    let get_string = |data: Option<&str>| match data {
30        Some(str) => Ok(str.to_string()),
31        None => Err("string is not valid utf-8".to_string()),
32    };
33    let branch_name = get_string(branch_name)?;
34    let commit = repo
35        .head()
36        .unwrap()
37        .peel_to_commit()
38        .map_err(|err| format!("failed to get commit: {err}"))?;
39
40    let author = commit.author();
41    let committer = commit.committer();
42
43    let mut remotes = vec![];
44    if let Ok(remote_names) = repo.remotes() {
45        for name in remote_names.into_iter().filter_map(|x| x) {
46            if let Ok(url) = repo.find_remote(name) {
47                if let Some(url) = url.url() {
48                    remotes.push(Remote {
49                        name: name.to_string(),
50                        url: url.to_string(),
51                    });
52                }
53            }
54        }
55    }
56    Ok(GitInfo {
57        head: Head {
58            id: commit.id().to_string(),
59            author_name: get_string(author.name())?,
60            author_email: get_string(author.email())?,
61            committer_name: get_string(committer.name())?,
62            committer_email: get_string(committer.email())?,
63            message: get_string(commit.message())?,
64        },
65        branch: branch_name,
66        remotes,
67    })
68}
69
70fn get_identity(ci_tool: &Option<CiService>, key: &str) -> Identity {
71    match ci_tool {
72        Some(ref service) => {
73            let service_object = match Service::from_ci(service.clone()) {
74                Some(s) => s,
75                None => Service {
76                    name: service.clone(),
77                    job_id: Some(key.to_string()),
78                    number: None,
79                    build_url: None,
80                    branch: None,
81                    pull_request: None,
82                },
83            };
84            let key = if service == &CiService::Travis {
85                String::new()
86            } else {
87                key.to_string()
88            };
89            Identity::ServiceToken(key, service_object)
90        }
91        _ => Identity::best_match_with_token(key.to_string()),
92    }
93}
94
95pub fn export(coverage_data: &TraceMap, config: &Config) -> Result<(), RunError> {
96    if let Some(ref key) = config.coveralls {
97        let id = get_identity(&config.ci_tool, key);
98
99        let mut report = CoverallsReport::new(id);
100        for file in &coverage_data.files() {
101            let rel_path = get_rel_path(config, file);
102            let mut lines: HashMap<usize, usize> = HashMap::new();
103            let fcov = coverage_data.get_child_traces(file);
104
105            for c in fcov {
106                match c.stats {
107                    CoverageStat::Line(hits) => {
108                        lines.insert(c.line as usize, hits as usize);
109                    }
110                    _ => {
111                        info!("Support for coverage statistic not implemented or supported for coveralls.io");
112                    }
113                }
114            }
115            if !lines.is_empty() {
116                if let Ok(source) = Source::new(&rel_path, file, &lines, &None, false) {
117                    report.add_source(source);
118                }
119            }
120        }
121
122        match get_git_info(&config.manifest()) {
123            Ok(git_info) => {
124                report.set_detailed_git_info(git_info);
125                info!("Git info collected");
126            }
127            Err(err) => warn!("Failed to collect git info: {}", err),
128        }
129
130        let res = if let Some(uri) = &config.report_uri {
131            info!("Sending report to endpoint: {}", uri);
132            report.send_to_endpoint(uri)
133        } else {
134            info!("Sending coverage data to coveralls.io");
135            report.send_to_coveralls()
136        };
137        if config.debug {
138            if let Ok(text) = serde_json::to_string(&report) {
139                info!("Attempting to write coveralls report to coveralls.json");
140                let file_path = config.output_dir().join("coveralls.json");
141                let _ = fs::write(file_path, text);
142            } else {
143                warn!("Failed to serialise coverage report");
144            }
145        }
146        match res {
147            Ok(s) => {
148                info!("Report successfully uploaded, you can find it at {}", s.url);
149                Ok(())
150            }
151            Err(e) => Err(RunError::CovReport(format!("Coveralls send failed. {e}"))),
152        }
153    } else {
154        Err(RunError::CovReport(
155            "No coveralls key specified.".to_string(),
156        ))
157    }
158}
159
160fn get_rel_path(config: &Config, file: &&PathBuf) -> PathBuf {
161    if cfg!(windows) {
162        let rel_path_with_windows_path_separator = config.strip_base_dir(file);
163        let rel_path_with_windows_path_separator_as_str =
164            String::from(rel_path_with_windows_path_separator.to_str().unwrap());
165        let rel_path_with_linux_path_separator =
166            rel_path_with_windows_path_separator_as_str.replace('\\', "/");
167
168        PathBuf::from(rel_path_with_linux_path_separator)
169    } else {
170        config.strip_base_dir(file)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use std::{path::PathBuf, process::Command};
178
179    #[test]
180    fn git_info_correct() {
181        let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
182        let res = match get_git_info(&manifest) {
183            Ok(r) => r,
184            Err(e) => {
185                if e.starts_with("failed to get branch name:") {
186                    // Pull requests don't get access to working git env
187                    return;
188                } else {
189                    panic!("Unexpected failure to get git info:\n{}", e);
190                }
191            }
192        };
193
194        let git_output = Command::new("git")
195            .args(["log", "-1", "--pretty=format:%H %an %ae"])
196            .output()
197            .unwrap();
198
199        let output = String::from_utf8(git_output.stdout).unwrap();
200
201        let expected = format!(
202            "{} {} {}",
203            res.head.id, res.head.author_name, res.head.author_email
204        );
205
206        assert_eq!(output, expected);
207    }
208
209    #[test]
210    fn error_if_no_git() {
211        let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../Cargo.toml");
212        println!("{:?}", manifest);
213        assert!(get_git_info(&manifest).is_err());
214    }
215
216    #[test]
217    #[cfg_attr(target_family = "windows", ignore)]
218    fn get_rel_path_coveralls_friendly_on_linux() {
219        let config = Config::default();
220        let file = PathBuf::from("src/report/coveralls.rs");
221        let rel_path = get_rel_path(&config, &&file);
222
223        assert_eq!(rel_path, PathBuf::from("src/report/coveralls.rs"));
224    }
225
226    #[test]
227    #[cfg_attr(not(target_family = "windows"), ignore)]
228    fn get_rel_path_coveralls_friendly_on_windows() {
229        let config = Config::default();
230        let file = PathBuf::from("src\\report\\coveralls.rs");
231        let rel_path = get_rel_path(&config, &&file);
232
233        assert_eq!(rel_path, PathBuf::from("src/report/coveralls.rs"));
234    }
235}