1use deflate::deflate_bytes_gzip;
2use reqwest::{
3 blocking::{
4 multipart::{Form, Part},
5 Client,
6 },
7 StatusCode,
8};
9use serde::{
10 ser::{SerializeStruct, Serializer},
11 Deserialize, Serialize,
12};
13use std::collections::HashMap;
14use std::env::var;
15use std::fmt;
16use std::fs::File;
17use std::io;
18use std::io::prelude::*;
19use std::path::Path;
20use std::str::FromStr;
21
22#[derive(
24 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize, Serialize,
25)]
26pub struct BranchData {
27 pub line_number: usize,
28 pub block_name: usize,
29 pub branch_number: usize,
30 pub hits: usize,
31}
32
33fn expand_lines(lines: &HashMap<usize, usize>, line_count: usize) -> Vec<Option<usize>> {
35 (0..line_count)
36 .map(|x| match lines.get(&(x + 1)) {
37 Some(x) => Some(*x),
38 None => None,
39 })
40 .collect::<Vec<Option<usize>>>()
41}
42
43fn expand_branches(branches: &Vec<BranchData>) -> Vec<usize> {
46 branches
47 .iter()
48 .flat_map(|x| vec![x.line_number, x.block_name, x.branch_number, x.hits])
49 .collect::<Vec<usize>>()
50}
51
52#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize, Serialize)]
54pub struct Source {
55 name: String,
57 source_digest: String,
59 coverage: Vec<Option<usize>>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 branches: Option<Vec<usize>>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 source: Option<String>,
70}
71
72impl Source {
73 pub fn new(
80 repo_path: &Path,
81 path: &Path,
82 lines: &HashMap<usize, usize>,
83 branches: &Option<Vec<BranchData>>,
84 include_source: bool,
85 ) -> Result<Source, io::Error> {
86 let mut code = File::open(path)?;
87 let mut content = String::new();
88 code.read_to_string(&mut content)?;
89 let src = if include_source {
90 Some(content.clone())
91 } else {
92 None
93 };
94
95 let brch = match branches {
96 &Some(ref b) => Some(expand_branches(&b)),
97 &None => None,
98 };
99 let line_count = content.lines().count();
100 Ok(Source {
101 name: repo_path.to_str().unwrap_or("").to_string(),
102 source_digest: format!("{:x}", md5::compute(content)),
103 coverage: expand_lines(lines, line_count),
104 branches: brch,
105 source: src,
106 })
107 }
108}
109
110#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize, Serialize)]
111pub struct Head {
112 pub id: String,
113 pub author_name: String,
114 pub author_email: String,
115 pub committer_name: String,
116 pub committer_email: String,
117 pub message: String,
118}
119
120#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize, Serialize)]
121pub struct Remote {
122 pub name: String,
123 pub url: String,
124}
125
126#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize, Serialize)]
127pub struct GitInfo {
128 pub head: Head,
129 pub branch: String,
130 pub remotes: Vec<Remote>,
131}
132
133#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
136pub enum CiService {
137 Travis,
138 TravisPro,
139 Circle,
140 Semaphore,
141 Jenkins,
142 Codeship,
143 Other(String),
146}
147
148impl FromStr for CiService {
149 type Err = ();
150
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 let res = match s {
153 "travis-ci" => CiService::Travis,
154 "travis-pro" => CiService::TravisPro,
155 "circle-ci" => CiService::Circle,
156 "semaphore" => CiService::Semaphore,
157 "jenkins" => CiService::Jenkins,
158 "codeship" => CiService::Codeship,
159 e => CiService::Other(e.to_string()),
160 };
161 Ok(res)
162 }
163}
164
165impl CiService {
166 fn value<'a>(&'a self) -> &'a str {
167 use CiService::*;
168 match *self {
171 Travis => "travis-ci",
172 TravisPro => "travis-pro",
173 Other(ref x) => x.as_str(),
174 Circle => "circle-ci",
175 Semaphore => "semaphore",
176 Jenkins => "jenkins",
177 Codeship => "codeship",
178 }
179 }
180}
181
182#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
190pub struct Service {
191 pub name: CiService,
193 pub job_id: Option<String>,
195 pub number: Option<String>,
197 pub build_url: Option<String>,
199 pub branch: Option<String>,
201 pub pull_request: Option<String>,
203}
204
205impl Service {
206 pub fn from_env() -> Option<Self> {
207 if var("TRAVIS").is_ok() {
208 Some(Self::get_travis_env())
209 } else if var("CIRCLECI").is_ok() {
210 Some(Self::get_circle_env())
211 } else if var("JENKINS_URL").is_ok() {
212 Some(Self::get_jenkins_env())
213 } else if var("SEMAPHORE").is_ok() {
214 Some(Self::get_semaphore_env())
215 } else {
216 Self::get_generic_env()
217 }
218 }
219
220 pub fn from_ci(ci: CiService) -> Option<Self> {
221 use CiService::*;
222 match ci {
223 Travis | TravisPro => {
224 let mut temp = Self::get_travis_env();
225 temp.name = ci;
226 Some(temp)
227 }
228 Circle => Some(Self::get_circle_env()),
229 Semaphore => Some(Self::get_semaphore_env()),
230 Jenkins => Some(Self::get_jenkins_env()),
231 _ => Self::get_generic_env(),
232 }
233 }
234
235 pub fn get_travis_env() -> Self {
238 let id = var("TRAVIS_JOB_ID").ok();
239 let pr = match var("TRAVIS_PULL_REQUEST") {
240 Ok(ref s) if s != "false" => Some(s.to_string()),
241 _ => None,
242 };
243 let branch = var("TRAVIS_BRANCH").ok();
244 Service {
245 name: CiService::Travis,
246 job_id: id,
247 number: None,
248 build_url: None,
249 pull_request: pr,
250 branch: branch,
251 }
252 }
253
254 pub fn get_circle_env() -> Self {
255 let num = var("CIRCLE_BUILD_NUM").ok();
256 let branch = var("CIRCLE_BRANCH").ok();
257 Service {
258 name: CiService::Circle,
259 job_id: None, number: num,
261 build_url: None,
262 pull_request: None,
263 branch: branch,
264 }
265 }
266
267 pub fn get_jenkins_env() -> Self {
268 let num = var("BUILD_NUM").ok();
269 let url = var("BUILD_URL").ok();
270 let branch = var("GIT_BRANCH").ok();
271 Service {
272 name: CiService::Jenkins,
273 job_id: None, number: num,
275 build_url: url,
276 pull_request: None,
277 branch: branch,
278 }
279 }
280
281 pub fn get_semaphore_env() -> Self {
282 let num = var("SEMAPHORE_BUILD_NUMBER").ok();
283 let pr = var("PULL_REQUEST_NUMBER").ok();
284 Service {
285 name: CiService::Semaphore,
286 job_id: None,
287 number: num,
288 pull_request: pr,
289 branch: None,
290 build_url: None,
291 }
292 }
293
294 pub fn get_generic_env() -> Option<Self> {
295 let name = var("CI_NAME").ok();
296 let num = var("CI_BUILD_NUMBER").ok();
297 let id = var("CI_JOB_ID").ok();
298 let url = var("CI_BUILD_URL").ok();
299 let branch = var("CI_BRANCH").ok();
300 let pr = var("CI_PULL_REQUEST").ok();
301 if name.is_some()
302 || num.is_some()
303 || id.is_some()
304 || url.is_some()
305 || branch.is_some()
306 || pr.is_some()
307 {
308 let name = name.unwrap_or_else(|| "unknown".to_string());
309
310 Some(Service {
311 name: CiService::from_str(&name).unwrap(),
312 job_id: id,
313 number: num,
314 pull_request: pr,
315 branch: branch,
316 build_url: url,
317 })
318 } else {
319 None
320 }
321 }
322}
323
324#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
326pub enum Identity {
327 RepoToken(String),
328 ServiceToken(String, Service),
329}
330
331impl Identity {
332 pub fn from_token() -> Option<Self> {
336 match var("COVERALLS_REPO_TOKEN") {
337 Ok(token) => Some(Identity::RepoToken(token)),
338 _ => None,
339 }
340 }
341
342 pub fn from_env() -> Option<Self> {
344 let token = match var("COVERALLS_REPO_TOKEN") {
345 Ok(token) => token,
346 _ => String::new(),
347 };
348 match Service::from_env() {
349 Some(s) => Some(Identity::ServiceToken(token, s)),
350 _ => None,
351 }
352 }
353
354 pub fn best_match() -> Option<Self> {
357 if let Some(s) = Self::from_env() {
358 Some(s)
359 } else if let Some(s) = Self::from_token() {
360 Some(s)
361 } else {
362 None
363 }
364 }
365
366 pub fn best_match_with_token(token: String) -> Self {
367 if let Some(Identity::ServiceToken(_, s)) = Self::from_env() {
368 Identity::ServiceToken(token, s)
369 } else {
370 Identity::RepoToken(token)
371 }
372 }
373}
374
375pub struct CoverallsReport {
378 id: Identity,
379 source_files: Vec<Source>,
381 commit: Option<String>,
383 git: Option<GitInfo>,
385 client: Client,
387}
388
389#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize)]
390pub struct Response {
391 pub message: String,
392 pub url: String,
393}
394
395#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize)]
396pub struct ErrorResponse {
397 pub error: bool,
398 pub message: String,
399}
400
401impl fmt::Display for ErrorResponse {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 write!(f, "{}", self.message)
404 }
405}
406
407#[derive(Debug, thiserror::Error)]
408pub enum Error {
409 #[error("http error: {0}")]
410 Http(reqwest::Error),
411 #[error("{0}")]
412 Api(ErrorResponse),
413 #[error("unrecognized API error: {0}")]
414 UnrecognizedMessage(String),
415}
416
417impl From<reqwest::Error> for Error {
418 fn from(other: reqwest::Error) -> Self {
419 Self::Http(other)
420 }
421}
422
423impl CoverallsReport {
424 pub fn new(id: Identity) -> CoverallsReport {
427 CoverallsReport {
428 id: id,
429 source_files: Vec::new(),
430 commit: None,
431 git: None,
432 client: Client::new(),
433 }
434 }
435
436 pub fn add_source(&mut self, source: Source) {
438 self.source_files.push(source);
439 }
440
441 pub fn set_commit(&mut self, commit: &str) {
443 self.commit = Some(commit.to_string());
444 self.git = None;
445 }
446
447 pub fn set_detailed_git_info(&mut self, git: GitInfo) {
449 self.git = Some(git);
450 self.commit = None;
451 }
452
453 pub fn send_to_coveralls(&self) -> Result<Response, Error> {
456 self.send_to_endpoint("https://coveralls.io/api/v1/jobs")
457 }
458
459 pub fn send_to_endpoint(&self, url: &str) -> Result<Response, Error> {
461 let body = match serde_json::to_vec(&self) {
462 Ok(body) => body,
463 Err(e) => panic!("Error {}", e),
464 };
465
466 let body = deflate_bytes_gzip(&body);
467
468 let form = Form::new().part(
469 "json_file",
470 Part::bytes(body).mime_str("gzip/json")?.file_name("report"),
471 );
472
473 let response = self.client.post(url).multipart(form).send()?;
474
475 let code = response.status();
476 let text = response.text()?;
477 match code {
478 StatusCode::OK => match serde_json::from_str(&text) {
479 Ok(resp) => Ok(resp),
480 Err(_e) => Err(Error::UnrecognizedMessage(text)),
481 },
482 _ => match serde_json::from_str(&text) {
483 Ok(resp) => Err(Error::Api(resp)),
484 Err(_e) => Err(Error::UnrecognizedMessage(text)),
485 },
486 }
487 }
488}
489
490impl Serialize for CoverallsReport {
491 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
492 where
493 S: Serializer,
494 {
495 let size = 1 + match self.id {
496 Identity::RepoToken(_) => 1 + self.commit.is_some() as usize,
497 Identity::ServiceToken(_, _) => 2 + self.commit.is_some() as usize,
498 };
499 let mut s = serializer.serialize_struct("CoverallsReport", size)?;
500 match self.id {
501 Identity::RepoToken(ref r) => {
502 s.serialize_field("repo_token", &r)?;
503 }
504 Identity::ServiceToken(ref r, ref serv) => {
505 if !r.is_empty() {
506 s.serialize_field("repo_token", &r)?;
507 }
508 s.serialize_field("service_name", serv.name.value())?;
509 if let Some(ref id) = serv.job_id {
510 s.serialize_field("service_job_id", id)?;
511 }
512 if let Some(ref num) = serv.number {
513 s.serialize_field("service_number", &num)?;
514 }
515 if let Some(ref url) = serv.build_url {
516 s.serialize_field("service_build_url", &url)?;
517 }
518 if let Some(ref branch) = serv.branch {
519 s.serialize_field("service_branch", &branch)?;
520 }
521 if let Some(ref pr) = serv.pull_request {
522 s.serialize_field("service_pull_request", &pr)?;
523 }
524 }
525 }
526 if let Some(ref sha) = self.commit {
527 s.serialize_field("commit_sha", &sha)?;
528 }
529 if let Some(ref git) = self.git {
530 s.serialize_field("git", &git)?;
531 }
532 s.serialize_field("source_files", &self.source_files)?;
533 s.end()
534 }
535}
536
537#[cfg(test)]
538mod tests {
539
540 use crate::*;
541 use std::collections::HashMap;
542
543 #[test]
544 fn test_expand_lines() {
545 let line_count = 10;
546 let mut example: HashMap<usize, usize> = HashMap::new();
547 example.insert(5, 1);
548 example.insert(6, 1);
549 example.insert(8, 2);
550
551 let expected = vec![
552 None,
553 None,
554 None,
555 None,
556 Some(1),
557 Some(1),
558 None,
559 Some(2),
560 None,
561 None,
562 ];
563
564 assert_eq!(expand_lines(&example, line_count), expected);
565 }
566
567 #[test]
568 fn test_branch_expand() {
569 let b1 = BranchData {
570 line_number: 3,
571 block_name: 1,
572 branch_number: 1,
573 hits: 1,
574 };
575 let b2 = BranchData {
576 line_number: 4,
577 block_name: 1,
578 branch_number: 2,
579 hits: 0,
580 };
581
582 let v = vec![b1, b2];
583 let actual = expand_branches(&v);
584 let expected = vec![3, 1, 1, 1, 4, 1, 2, 0];
585 assert_eq!(actual, expected);
586 }
587}