use serde::{Deserialize, Serialize};
use std::cmp::{Ord, Ordering};
use std::collections::btree_map::Iter;
use std::collections::{BTreeMap, HashMap};
use std::fmt::{Display, Formatter, Result};
use std::ops::Add;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
pub struct LogicState {
pub been_true: bool,
pub been_false: bool,
}
impl<'a> Add for &'a LogicState {
type Output = LogicState;
fn add(self, other: &'a LogicState) -> LogicState {
LogicState {
been_true: self.been_true || other.been_true,
been_false: self.been_false || other.been_false,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
pub enum CoverageStat {
Line(u64),
Branch(LogicState),
Condition(Vec<LogicState>),
}
impl Add for CoverageStat {
type Output = CoverageStat;
fn add(self, other: CoverageStat) -> CoverageStat {
match (self, other) {
(CoverageStat::Line(ref l), CoverageStat::Line(ref r)) => CoverageStat::Line(l + r),
(CoverageStat::Branch(ref l), CoverageStat::Branch(ref r)) => {
CoverageStat::Branch(l + r)
}
t => t.0,
}
}
}
impl Display for CoverageStat {
fn fmt(&self, f: &mut Formatter) -> Result {
match *self {
CoverageStat::Line(x) => write!(f, "hits: {}", x),
_ => write!(f, ""),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
pub struct Trace {
pub line: u64,
pub address: Option<u64>,
pub length: usize,
pub stats: CoverageStat,
pub fn_name: Option<String>,
}
impl Ord for Trace {
fn cmp(&self, other: &Trace) -> Ordering {
self.line.cmp(&other.line)
}
fn max(self, other: Trace) -> Trace {
if self.line > other.line {
self
} else {
other
}
}
fn min(self, other: Trace) -> Trace {
if self.line < other.line {
self
} else {
other
}
}
}
pub fn amount_coverable(traces: &[&Trace]) -> usize {
let mut result = 0usize;
for t in traces {
result += match t.stats {
CoverageStat::Branch(_) => 2usize,
CoverageStat::Condition(ref x) => x.len() * 2usize,
_ => 1usize,
};
}
result
}
pub fn amount_covered(traces: &[&Trace]) -> usize {
let mut result = 0usize;
for t in traces {
result += match t.stats {
CoverageStat::Branch(ref x) => (x.been_true as usize) + (x.been_false as usize),
CoverageStat::Condition(ref x) => x.iter().fold(0, |acc, ref x| {
acc + (x.been_true as usize) + (x.been_false as usize)
}),
CoverageStat::Line(ref x) => (*x > 0) as usize,
};
}
result
}
pub fn coverage_percentage(traces: &[&Trace]) -> f64 {
(amount_covered(traces) as f64) / (amount_coverable(traces) as f64)
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct TraceMap {
traces: BTreeMap<PathBuf, Vec<Trace>>,
}
impl TraceMap {
pub fn new() -> TraceMap {
TraceMap {
traces: BTreeMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.traces.is_empty()
}
pub fn iter(&self) -> Iter<PathBuf, Vec<Trace>> {
self.traces.iter()
}
pub fn merge(&mut self, other: &TraceMap) {
for (k, values) in other.iter() {
if !self.traces.contains_key(k) {
self.traces.insert(k.to_path_buf(), values.to_vec());
} else {
let existing = self.traces.get_mut(k).unwrap();
for ref v in values.iter() {
let mut added = false;
if let Some(ref mut t) = existing
.iter_mut()
.find(|ref x| x.line == v.line && x.address == v.address)
{
t.stats = t.stats.clone() + v.stats.clone();
added = true;
}
if !added {
existing.push((*v).clone());
existing.sort_unstable();
}
}
}
}
}
pub fn dedup(&mut self) {
for values in self.traces.values_mut() {
let mut lines: HashMap<u64, CoverageStat> = HashMap::new();
let mut dirty: Vec<u64> = Vec::new();
for v in values.iter() {
lines
.entry(v.line)
.and_modify(|e| {
dirty.push(v.line);
*e = e.clone() + v.stats.clone();
})
.or_insert_with(|| v.stats.clone());
}
for d in &dirty {
let mut first = true;
values.retain(|x| {
let res = x.line != *d;
if !res {
if first {
first = false;
true
} else {
false
}
} else {
res
}
});
if let Some(new_stat) = lines.remove(&d) {
if let Some(ref mut t) = values.iter_mut().find(|x| x.line == *d) {
t.stats = new_stat;
}
}
}
}
}
pub fn add_trace(&mut self, file: &Path, trace: Trace) {
if self.traces.contains_key(file) {
if let Some(trace_vec) = self.traces.get_mut(file) {
trace_vec.push(trace);
trace_vec.sort_unstable();
}
} else {
self.traces.insert(file.to_path_buf(), vec![trace]);
}
}
pub fn get_trace(&self, address: u64) -> Option<&Trace> {
self.all_traces()
.iter()
.find(|x| x.address == Some(address))
.map(|x| *x)
}
pub fn get_trace_mut(&mut self, address: u64) -> Option<&mut Trace> {
for val in self.all_traces_mut() {
if val.address == Some(address) {
return Some(val);
}
}
None
}
pub fn contains_location(&self, file: &Path, line: u64) -> bool {
match self.traces.get(file) {
Some(traces) => traces.iter().any(|x| x.line == line),
None => false,
}
}
pub fn contains_file(&self, file: &Path) -> bool {
self.traces.contains_key(file)
}
pub fn get_child_traces(&self, root: &Path) -> Vec<&Trace> {
self.traces
.iter()
.filter(|&(ref k, _)| k.starts_with(root))
.flat_map(|(_, ref v)| v.iter())
.collect()
}
pub fn get_traces(&self, root: &Path) -> Vec<&Trace> {
if root.is_file() {
self.get_child_traces(root)
} else {
self.traces
.iter()
.filter(|&(ref k, _)| k.parent() == Some(root))
.flat_map(|(_, ref v)| v.iter())
.collect()
}
}
pub fn all_traces(&self) -> Vec<&Trace> {
self.traces.values().flat_map(|ref x| x.iter()).collect()
}
fn all_traces_mut(&mut self) -> Vec<&mut Trace> {
self.traces
.values_mut()
.flat_map(|x| x.iter_mut())
.collect()
}
pub fn files(&self) -> Vec<&PathBuf> {
self.traces.keys().collect()
}
pub fn coverable_in_path(&self, path: &Path) -> usize {
amount_coverable(self.get_child_traces(path).as_slice())
}
pub fn covered_in_path(&self, path: &Path) -> usize {
amount_covered(self.get_child_traces(path).as_slice())
}
pub fn total_coverable(&self) -> usize {
amount_coverable(self.all_traces().as_slice())
}
pub fn total_covered(&self) -> usize {
amount_covered(self.all_traces().as_slice())
}
pub fn coverage_percentage(&self) -> f64 {
coverage_percentage(self.all_traces().as_slice())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn stat_addition() {
let x = CoverageStat::Line(0);
let y = CoverageStat::Line(5);
let z = CoverageStat::Line(7);
let xy = x.clone() + y.clone();
let yx = y.clone() + x.clone();
let yy = y.clone() + y.clone();
let zy = z.clone() + y.clone();
assert_eq!(&xy, &CoverageStat::Line(5));
assert_eq!(&yx, &xy);
assert_eq!(&yy, &CoverageStat::Line(10));
assert_eq!(&zy, &CoverageStat::Line(12));
let tf = LogicState {
been_true: true,
been_false: true,
};
let t = LogicState {
been_true: true,
been_false: false,
};
let f = LogicState {
been_true: false,
been_false: true,
};
let n = LogicState {
been_true: false,
been_false: false,
};
assert_eq!(&t + &f, tf);
assert_eq!(&t + &t, t);
assert_eq!(&tf + &f, tf);
assert_eq!(&tf + &t, tf);
assert_eq!(&t + &n, t);
assert_eq!(&n + &f, f);
assert_eq!(&n + &n, n);
}
#[test]
fn merge_address_mismatch_and_dedup() {
let mut t1 = TraceMap::new();
let mut t2 = TraceMap::new();
let a_trace = Trace {
line: 1,
address: Some(5),
length: 0,
stats: CoverageStat::Line(1),
fn_name: Some(String::from("f")),
};
t1.add_trace(Path::new("file.rs"), a_trace.clone());
t2.add_trace(
Path::new("file.rs"),
Trace {
line: 1,
address: None,
length: 0,
stats: CoverageStat::Line(2),
fn_name: Some(String::from("f")),
},
);
t1.merge(&t2);
assert_eq!(t1.all_traces().len(), 2);
assert_eq!(t1.get_trace(5), Some(&a_trace));
t1.dedup();
let all = t1.all_traces();
assert_eq!(all.len(), 1);
assert_eq!(all[0].stats, CoverageStat::Line(3));
}
#[test]
fn no_merge_dedup_needed() {
let mut t1 = TraceMap::new();
let mut t2 = TraceMap::new();
let a_trace = Trace {
line: 1,
address: Some(5),
length: 0,
stats: CoverageStat::Line(1),
fn_name: Some(String::from("f1")),
};
t1.add_trace(Path::new("file.rs"), a_trace.clone());
t2.add_trace(
Path::new("file.rs"),
Trace {
line: 2,
address: None,
length: 0,
stats: CoverageStat::Line(2),
fn_name: Some(String::from("f2")),
},
);
t1.merge(&t2);
assert_eq!(t1.all_traces().len(), 2);
assert_eq!(t1.get_trace(5), Some(&a_trace));
t1.dedup();
let all = t1.all_traces();
assert_eq!(all.len(), 2);
}
#[test]
fn merge_needed() {
let mut t1 = TraceMap::new();
let mut t2 = TraceMap::new();
t1.add_trace(
Path::new("file.rs"),
Trace {
line: 2,
address: Some(1),
length: 0,
stats: CoverageStat::Line(5),
fn_name: Some(String::from("f")),
},
);
t2.add_trace(
Path::new("file.rs"),
Trace {
line: 2,
address: Some(1),
length: 0,
stats: CoverageStat::Line(2),
fn_name: Some(String::from("f")),
},
);
t1.merge(&t2);
assert_eq!(t1.all_traces().len(), 1);
assert_eq!(
t1.get_trace(1),
Some(&Trace {
line: 2,
address: Some(1),
length: 0,
stats: CoverageStat::Line(7),
fn_name: Some(String::from("f")),
})
);
t1.dedup();
assert_eq!(t1.all_traces().len(), 1);
assert_eq!(
t1.get_trace(1),
Some(&Trace {
line: 2,
address: Some(1),
length: 0,
stats: CoverageStat::Line(7),
fn_name: Some(String::from("f")),
})
);
}
}