cargo-sonar 0.14.1

Helper to transform reports from Rust tooling for code quality, into valid Sonar report
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: Unused fields are part of the deserialized schema from 'cargo-tarpaulin'
#[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)
    }
}