cargo_tarpaulin/report/
coveralls.rs1use 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 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}