use std::collections::HashMap;
use super::db::{Db, DbItem, DbType, HEADER, ItemKind};
use super::decl::{BinDecl, CovGroupDecl, CoverpointDecl, CrossDecl, RangeItem};
use super::instance::ItemRef;
use super::registry::Registry;
use super::value::{CovError, Eval, Expr, KernelId, TypeInfo, Val};
struct Env(HashMap<KernelId, Val>);
impl Eval for Env {
fn eval(&mut self, id: KernelId, _args: &[Val]) -> Result<Val, CovError> {
self.0.get(&id).cloned().ok_or(CovError::NoKernel(id))
}
}
fn env(a: i128) -> Env {
Env(HashMap::from([(0, Val::Int(a))]))
}
fn single(v: i128) -> Vec<RangeItem> {
vec![RangeItem::Single(Expr::int(v))]
}
fn group_decl(name: &str) -> CovGroupDecl {
let mut a = CoverpointDecl::new("a", TypeInfo::unsigned(8), Expr::Kernel(0));
a.bins.push(BinDecl::ranges("lo", single(0)));
a.bins.push(BinDecl::ranges("hi", single(1)));
CovGroupDecl::new(name).coverpoint(a)
}
fn registry(decl: CovGroupDecl) -> (Registry, usize) {
let mut reg = Registry::new();
let ty = reg.define(decl, &mut env(0)).unwrap();
(reg, ty)
}
fn item(kind: ItemKind, name: &str, at_least: u64, weight: i64, bins: &[(&str, u64)]) -> DbItem {
DbItem {
kind,
name: name.to_string(),
at_least,
weight,
bins: bins.iter().map(|(n, h)| (n.to_string(), *h)).collect(),
}
}
fn near(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "{a} != {b}");
}
const SAMPLE_DB: &str = "vj-coverage-db 1\n\
covergroup g\n\
coverpoint a at_least=1 weight=1\n\
bin lo 4\n\
bin hi 0\n";
#[test]
fn the_text_format_is_exactly_as_documented() {
let db = Db {
types: vec![DbType {
name: "g".into(),
items: vec![
item(ItemKind::Coverpoint, "a", 1, 1, &[("lo", 4), ("hi", 0)]),
item(ItemKind::Cross, "x", 2, 3, &[("<lo,p>", 7)]),
],
}],
};
assert_eq!(
db.render(),
"vj-coverage-db 1\ncovergroup g\ncoverpoint a at_least=1 weight=1\nbin lo 4\nbin hi 0\n\
cross x at_least=2 weight=3\nbin <lo,p> 7\n"
);
assert_eq!(HEADER, "vj-coverage-db 1");
assert_eq!(Db::parse(SAMPLE_DB).unwrap().render(), SAMPLE_DB);
}
#[test]
fn render_and_parse_round_trip_awkward_names() {
let names = [
"auto[0:3]",
"<a[1],b>",
"with space",
"100%",
"tab\there",
"naïve",
"%",
"",
];
let bins: Vec<(&str, u64)> = names.iter().map(|n| (*n, 2)).collect();
let db = Db {
types: vec![DbType {
name: "cg with space".into(),
items: vec![item(ItemKind::Coverpoint, "odd name", 5, -2, &bins)],
}],
};
let text = db.render();
assert_eq!(text.lines().count(), 1 + 1 + 1 + names.len());
assert_eq!(Db::parse(&text), Ok(db));
}
#[test]
fn escapes_keep_every_line_to_whitespace_separated_tokens() {
let db = Db {
types: vec![DbType {
name: "g".into(),
items: vec![item(
ItemKind::Coverpoint,
"a",
1,
1,
&[("x y", 1), ("p%q", 2)],
)],
}],
};
let text = db.render();
assert!(text.contains("bin x%20y 1\n"), "{text}");
assert!(text.contains("bin p%25q 2\n"), "{text}");
}
#[test]
fn blank_lines_and_comments_are_ignored_wherever_they_are() {
let text = "# written by hand\n\nvj-coverage-db 1\n# why\ncovergroup g\n\n coverpoint a at_least=1 weight=1 \nbin lo 1\n";
let db = Db::parse(text).unwrap();
assert_eq!(db.types[0].items[0].bins, [("lo".to_string(), 1)]);
}
fn parse_err(text: &str) -> (usize, String) {
match Db::parse(text) {
Err(CovError::BadDb { line, detail }) => (line, detail),
other => panic!("expected a BadDb error, got {other:?}"),
}
}
#[test]
fn malformed_databases_are_errors_with_a_line_number() {
assert_eq!(parse_err("").0, 1);
assert_eq!(parse_err("\n\n").0, 1);
let (line, detail) = parse_err("covergroup g\n");
assert_eq!(line, 1);
assert!(detail.contains("header"), "{detail}");
assert_eq!(parse_err("vj-coverage-db 2\n").0, 1);
let h = "vj-coverage-db 1\n";
let (line, d) = parse_err(&format!("{h}coverpoint a at_least=1 weight=1\n"));
assert_eq!(line, 2);
assert!(d.contains("before any covergroup"), "{d}");
let (line, d) = parse_err(&format!("{h}covergroup g\nbin lo 1\n"));
assert_eq!(line, 3);
assert!(d.contains("before any coverpoint or cross"), "{d}");
assert_eq!(
parse_err(&format!(
"{h}covergroup g\ncoverpoint a at_least=x weight=1\n"
))
.0,
3
);
assert_eq!(
parse_err(&format!(
"{h}covergroup g\ncoverpoint a weight=1 at_least=1\n"
))
.0,
3
);
assert_eq!(
parse_err(&format!(
"{h}covergroup g\ncoverpoint a at_least=1 weight=1\nbin lo -1\n"
))
.0,
4
);
assert_eq!(parse_err(&format!("{h}covergroup g\ncoverbin a\n")).0, 3);
assert_eq!(parse_err(&format!("{h}covergroup\n")).0, 2);
assert_eq!(parse_err(&format!("{h}covergroup a%zz\n")).0, 2);
assert_eq!(parse_err(&format!("{h}covergroup a%4\n")).0, 2);
assert_eq!(parse_err(&format!("{h}covergroup a%FF\n")).0, 2);
}
#[test]
fn duplicate_names_in_a_database_are_errors() {
let h = "vj-coverage-db 1\ncovergroup g\ncoverpoint a at_least=1 weight=1\nbin lo 1\n";
assert!(parse_err(&format!("{h}bin lo 2\n")).1.contains("twice"));
assert!(
parse_err(&format!("{h}coverpoint a at_least=1 weight=1\n"))
.1
.contains("twice")
);
assert!(parse_err(&format!("{h}covergroup g\n")).1.contains("twice"));
Db::parse(&format!("{h}cross a at_least=1 weight=1\n")).unwrap();
}
#[test]
fn merge_adds_hits_of_equal_names_and_keeps_the_larger_at_least_and_weight() {
let mut a = Db {
types: vec![DbType {
name: "g".into(),
items: vec![item(
ItemKind::Coverpoint,
"a",
1,
5,
&[("lo", 1), ("hi", 0)],
)],
}],
};
let b = Db {
types: vec![
DbType {
name: "g".into(),
items: vec![
item(ItemKind::Coverpoint, "a", 3, 2, &[("hi", 4), ("mid", 9)]),
item(ItemKind::Cross, "a", 1, 1, &[("lo", 8)]),
],
},
DbType {
name: "h".into(),
items: vec![],
},
],
};
a.merge(&b);
let g = a.ty("g").unwrap();
let cp = g.item(ItemKind::Coverpoint, "a").unwrap();
assert_eq!((cp.at_least, cp.weight), (3, 5));
assert_eq!(
cp.bins,
[
("lo".to_string(), 1),
("hi".to_string(), 4),
("mid".to_string(), 9)
]
);
assert_eq!(g.item(ItemKind::Cross, "a").unwrap().bins[0].1, 8);
assert!(a.ty("h").is_some());
}
#[test]
fn merge_saturates_instead_of_wrapping() {
let big = |h| Db {
types: vec![DbType {
name: "g".into(),
items: vec![item(ItemKind::Coverpoint, "a", 1, 1, &[("lo", h)])],
}],
};
let mut a = big(u64::MAX);
a.merge(&big(5));
assert_eq!(a.types[0].items[0].bins[0].1, u64::MAX);
}
fn filled(decl: CovGroupDecl, samples: &[i128]) -> Registry {
let (mut reg, ty) = registry(decl);
let h = reg.instantiate(ty, &[], &mut env(0)).unwrap().0;
for &s in samples {
reg.sample(h, &mut env(s), &[]).unwrap();
}
reg
}
#[test]
fn snapshot_holds_the_cumulative_hits_of_every_instance_of_a_type() {
let (mut reg, ty) = registry(group_decl("g"));
let h1 = reg.instantiate(ty, &[], &mut env(0)).unwrap().0;
let h2 = reg.instantiate(ty, &[], &mut env(0)).unwrap().0;
for (h, v) in [(h1, 0), (h1, 0), (h2, 0), (h2, 1)] {
reg.sample(h, &mut env(v), &[]).unwrap();
}
let db = reg.snapshot_db().unwrap();
let a = db.ty("g").unwrap().item(ItemKind::Coverpoint, "a").unwrap();
assert_eq!(a.bins, [("lo".to_string(), 3), ("hi".to_string(), 1)]);
assert_eq!((a.at_least, a.weight), (1, 1));
}
#[test]
fn snapshot_leaves_out_types_without_instances() {
let (mut reg, _) = registry(group_decl("g"));
reg.define(group_decl("never_built"), &mut env(0)).unwrap();
assert_eq!(reg.snapshot_db().unwrap(), Db::default());
}
#[test]
fn snapshot_records_crosses_beside_coverpoints() {
let mut b = CoverpointDecl::new("b", TypeInfo::unsigned(8), Expr::Kernel(0));
b.bins.push(BinDecl::ranges("p", single(0)));
let decl = group_decl("g")
.coverpoint(b)
.cross(CrossDecl::new("x", &["a", "b"]));
let reg = filled(decl, &[0]);
let db = reg.snapshot_db().unwrap();
let t = db.ty("g").unwrap();
assert_eq!(t.items.len(), 3);
let x = t.item(ItemKind::Cross, "x").unwrap();
assert!(x.bins.contains(&("<lo,p>".to_string(), 1)), "{x:?}");
}
#[test]
fn a_saved_database_loads_into_a_fresh_run_and_raises_merged_coverage() {
let saved = filled(group_decl("g"), &[0, 0, 0, 0])
.snapshot_db()
.unwrap()
.render();
let (mut reg, ty) = registry(group_decl("g").type_option("merge_instances", Expr::int(1)));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 0.0);
let s = reg.load_db(&saved).unwrap();
assert_eq!(s.merged, ["g::a"]);
assert!(s.ignored_types.is_empty() && s.ignored_items.is_empty());
let sc = reg.type_score(ty, ItemRef::Group).unwrap();
near(sc.percent, 50.0); assert_eq!((sc.covered, sc.total), (1, 2));
}
#[test]
fn loaded_hits_add_to_the_hits_of_this_run() {
let decl = || {
group_decl("g")
.option("at_least", Expr::int(3))
.type_option("merge_instances", Expr::int(1))
};
let db =
"vj-coverage-db 1\ncovergroup g\ncoverpoint a at_least=3 weight=1\nbin lo 2\nbin hi 0\n";
let (mut reg, ty) = registry(decl());
let h = reg.instantiate(ty, &[], &mut env(0)).unwrap().0;
reg.load_db(db).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 0.0);
reg.sample(h, &mut env(0), &[]).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 50.0);
let (mut reg, ty) = registry(decl());
reg.instantiate(ty, &[], &mut env(0)).unwrap();
reg.load_db(db).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 0.0);
reg.load_db(db).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 50.0);
}
#[test]
fn without_merging_a_loaded_database_counts_as_one_more_instance() {
let (mut reg, ty) = registry(group_decl("g"));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
reg.load_db(SAMPLE_DB).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 25.0);
near(
reg.type_score(ty, ItemRef::Coverpoint(0)).unwrap().percent,
25.0,
);
let h = reg.instances_of(ty).unwrap()[0];
near(reg.inst_score(h, ItemRef::Group).unwrap().percent, 0.0);
}
#[test]
fn names_the_design_does_not_have_are_ignored() {
let db = "vj-coverage-db 1\n\
covergroup g\n\
coverpoint a at_least=1 weight=1\n\
bin lo 1\n\
bin no_such_bin 50\n\
coverpoint ghost at_least=1 weight=1\n\
bin lo 9\n\
cross a at_least=1 weight=1\n\
bin lo 9\n\
covergroup not_in_this_design\n\
coverpoint a at_least=1 weight=1\n\
bin lo 9\n";
let (mut reg, ty) = registry(group_decl("g").type_option("merge_instances", Expr::int(1)));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
let s = reg.load_db(db).unwrap();
assert_eq!(s.merged, ["g::a"]);
assert_eq!(s.ignored_items, ["g::ghost", "g::a"]);
assert_eq!(s.ignored_types, ["not_in_this_design"]);
let sc = reg.type_score(ty, ItemRef::Group).unwrap();
assert_eq!((sc.covered, sc.total), (1, 2));
near(sc.percent, 50.0);
}
#[test]
fn a_malformed_database_changes_nothing() {
let (mut reg, ty) = registry(group_decl("g").type_option("merge_instances", Expr::int(1)));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
let bad =
"vj-coverage-db 1\ncovergroup g\ncoverpoint a at_least=1 weight=1\nbin lo 5\nbin hi oops\n";
assert!(matches!(
reg.load_db(bad),
Err(CovError::BadDb { line: 5, .. })
));
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 0.0);
assert_eq!(
reg.snapshot_db().unwrap().ty("g").unwrap().items[0].bins[0].1,
0
);
}
#[test]
fn saving_after_loading_accumulates_across_runs() {
let run1 = filled(group_decl("g"), &[0, 0])
.snapshot_db()
.unwrap()
.render();
let (mut reg, ty) = registry(group_decl("g"));
let h = reg.instantiate(ty, &[], &mut env(0)).unwrap().0;
reg.load_db(&run1).unwrap();
reg.sample(h, &mut env(1), &[]).unwrap();
let run2 = reg.snapshot_db().unwrap();
let a = run2
.ty("g")
.unwrap()
.item(ItemKind::Coverpoint, "a")
.unwrap();
assert_eq!(a.bins, [("lo".to_string(), 2), ("hi".to_string(), 1)]);
let (mut reg, ty) = registry(group_decl("g").type_option("merge_instances", Expr::int(1)));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
reg.load_db(&run2.render()).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 100.0);
}
#[test]
fn loaded_bins_without_a_matching_live_bin_survive_into_the_next_save() {
let db = "vj-coverage-db 1\ncovergroup g\ncoverpoint a at_least=1 weight=1\nbin mid 6\n";
let (mut reg, ty) = registry(group_decl("g"));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
reg.load_db(db).unwrap();
let saved = reg.snapshot_db().unwrap();
let a = saved
.ty("g")
.unwrap()
.item(ItemKind::Coverpoint, "a")
.unwrap();
assert!(a.bins.contains(&("mid".to_string(), 6)), "{a:?}");
}
#[test]
fn set_db_name_is_remembered_and_reset_clears_it_and_the_loaded_data() {
let (mut reg, ty) = registry(group_decl("g").type_option("merge_instances", Expr::int(1)));
assert_eq!(reg.db_name(), None);
reg.set_db_name("run1.cov");
assert_eq!(reg.db_name(), Some("run1.cov"));
reg.set_db_name("run2.cov");
assert_eq!(reg.db_name(), Some("run2.cov"));
reg.instantiate(ty, &[], &mut env(0)).unwrap();
reg.load_db(SAMPLE_DB).unwrap();
reg.reset();
assert_eq!(reg.db_name(), None);
let ty = reg
.define(
group_decl("g").type_option("merge_instances", Expr::int(1)),
&mut env(0),
)
.unwrap();
reg.instantiate(ty, &[], &mut env(0)).unwrap();
near(reg.type_score(ty, ItemRef::Group).unwrap().percent, 0.0);
}