use crate::sonar;
use eyre::{Context, Result};
use serde::Deserialize;
use std::{collections::HashSet, fs::File, path::PathBuf};
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct LogicState {
pub been_true: bool,
pub been_false: bool,
}
#[derive(Debug, Deserialize)]
pub enum CoverageStat {
Line(usize),
Branch(LogicState),
Condition(Vec<LogicState>),
}
#[derive(Debug, Deserialize)]
pub struct Trace {
pub line: usize,
pub address: HashSet<u64>,
pub length: usize,
pub stats: CoverageStat,
pub fn_name: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SourceFile {
path: Vec<String>,
content: String,
traces: Vec<Trace>,
covered: usize,
coverable: usize,
}
#[derive(Debug, Deserialize)]
pub struct CoverageReport {
files: Vec<SourceFile>,
}
pub struct Tarpaulin {
json: PathBuf,
}
impl Tarpaulin {
pub fn new<P>(json: P) -> Self
where
P: Into<PathBuf>,
{
Self { json: json.into() }
}
fn to_line_to_cover(trace: Trace) -> sonar::LineToCover {
let line_number = trace.line;
let (covered, branches_to_cover, covered_branches) = match trace.stats {
CoverageStat::Line(covered_lines) => {
let covered = covered_lines != 0;
(covered, None, None)
}
CoverageStat::Branch(logic_state) => {
let mut covered_branches = 0_usize;
if logic_state.been_true {
covered_branches = covered_branches.saturating_add(1);
}
if logic_state.been_false {
covered_branches = covered_branches.saturating_add(1);
}
let covered = covered_branches != 0;
(covered, Some(2), Some(covered_branches))
}
CoverageStat::Condition(logic_states) => {
let branches_to_cover = logic_states.len().saturating_mul(2);
let covered_branches =
logic_states.iter().fold(0_usize, |mut total, logic_state| {
if logic_state.been_true {
total = total.saturating_add(1);
}
if logic_state.been_false {
total = total.saturating_add(1);
}
total
});
let covered = covered_branches != 0;
(covered, Some(branches_to_cover), Some(covered_branches))
}
};
sonar::LineToCover {
line_number,
covered,
branches_to_cover,
covered_branches,
}
}
fn to_file(source_file: SourceFile) -> Result<sonar::File> {
let path = source_file
.path
.iter()
.fold(PathBuf::new(), |path, block| path.join(block))
.strip_prefix(std::env::current_dir()?)?
.to_path_buf();
let line_to_cover = source_file
.traces
.into_iter()
.map(Self::to_line_to_cover)
.collect();
let file = sonar::File {
path,
line_to_cover,
};
Ok(file)
}
pub fn to_coverage(report: CoverageReport) -> Result<sonar::Coverage> {
report.files.into_iter().map(Self::to_file).collect()
}
}
impl std::convert::TryInto<sonar::Coverage> for Tarpaulin {
type Error = eyre::Error;
fn try_into(self) -> Result<sonar::Coverage> {
let file = File::open(&self.json).with_context(|| {
format!(
"failed to open 'cargo-tarpaulin' report from '{:?}' file",
self.json
)
})?;
let report = serde_json::from_reader::<_, CoverageReport>(&file)
.context("failed to be parsed as a 'CoverageReport'")?;
let coverage = Self::to_coverage(report)?;
info!("{} files reported for coverage", coverage.file.len());
Ok(coverage)
}
}