use std::collections::BTreeMap;
use crate::lcov_cov::FileReport;
use crate::workspace::Member;
#[derive(Debug, Default)]
pub(crate) struct AttributionOutcome<'f> {
pub(crate) by_member: BTreeMap<String, Vec<&'f FileReport>>,
pub(crate) unattributed: Vec<&'f FileReport>,
}
pub(crate) fn attribute<'f>(files: &'f [FileReport], members: &[Member]) -> AttributionOutcome<'f> {
let mut order: Vec<usize> = (0..members.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(members[i].manifest_dir.components().count()));
let mut out = AttributionOutcome::default();
'files: for f in files {
for &i in &order {
if f.filename.starts_with(&members[i].manifest_dir) {
out.by_member.entry(members[i].name.clone()).or_default().push(f);
continue 'files;
}
}
out.unattributed.push(f);
}
out
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::path::PathBuf;
use super::*;
fn entry(path: &str) -> FileReport {
FileReport {
filename: PathBuf::from(path),
lines_total: 10,
lines_covered: 5,
coverable_lines: (1..=10).collect(),
uncovered_lines: (6..=10).collect(),
}
}
fn member(name: &str, manifest_dir: &str) -> Member {
Member {
name: name.to_owned(),
manifest_dir: PathBuf::from(manifest_dir),
min_lines_percent: None,
expect_no_coverable_lines: false,
}
}
#[test]
fn assigns_files_to_matching_crates() {
let files = vec![
entry("/repo/crates/alpha/src/lib.rs"),
entry("/repo/crates/alpha/src/util.rs"),
entry("/repo/crates/beta/src/lib.rs"),
];
let members = [member("alpha", "/repo/crates/alpha"), member("beta", "/repo/crates/beta")];
let out = attribute(&files, &members);
assert_eq!(out.by_member["alpha"].len(), 2);
assert_eq!(out.by_member["beta"].len(), 1);
assert!(out.unattributed.is_empty());
}
#[test]
fn unmatched_files_are_reported_separately() {
let files = vec![entry("/repo/crates/alpha/src/lib.rs"), entry("/elsewhere/build-script.rs")];
let members = [member("alpha", "/repo/crates/alpha")];
let out = attribute(&files, &members);
assert_eq!(out.by_member["alpha"].len(), 1);
assert_eq!(out.unattributed.len(), 1);
assert_eq!(out.unattributed[0].filename, PathBuf::from("/elsewhere/build-script.rs"));
}
#[test]
fn longest_prefix_wins_for_nested_members() {
let files = vec![entry("/repo/crates/alpha/sub/src/lib.rs")];
let members = [
member("alpha-outer", "/repo/crates/alpha"),
member("alpha-sub", "/repo/crates/alpha/sub"),
];
let out = attribute(&files, &members);
assert!(!out.by_member.contains_key("alpha-outer"));
assert_eq!(out.by_member["alpha-sub"].len(), 1);
}
#[test]
fn similar_named_member_does_not_steal_files() {
let files = vec![entry("/repo/crates/alpha-extras/src/lib.rs")];
let members = [
member("alpha", "/repo/crates/alpha"),
member("alpha-extras", "/repo/crates/alpha-extras"),
];
let out = attribute(&files, &members);
assert!(!out.by_member.contains_key("alpha"));
assert_eq!(out.by_member["alpha-extras"].len(), 1);
}
#[test]
fn empty_inputs() {
let files: Vec<FileReport> = Vec::new();
let members: Vec<Member> = Vec::new();
let out = attribute(&files, &members);
assert!(out.by_member.is_empty());
assert!(out.unattributed.is_empty());
}
}