use std::collections::{HashMap, HashSet};
use rustc_hash::{FxHashMap, FxHashSet};
use std::rc::Rc;
use crate::sema::{EventId, HandlerAction, MachineId, Program, StateId, Temperature};
use crate::checker::engine::culture_order;
use crate::checker::runtime::{Machine, Monitor, Runtime, GOTO_EVENT};
use crate::checker::value::event_cs_name;
#[derive(Debug, Clone)]
pub struct CoverageTables {
pub nodes: Vec<String>,
plain: Vec<u32>,
tempered: Vec<u32>,
receive_target: Vec<u32>,
}
impl CoverageTables {
pub fn new(prog: &Program) -> CoverageTables {
let mut nodes = Vec::new();
let mut index: HashMap<String, u32> = HashMap::new();
let mut intern = |s: String| -> u32 {
*index.entry(s.clone()).or_insert_with(|| {
nodes.push(s);
(nodes.len() - 1) as u32
})
};
let mut plain = vec![0; prog.states.len()];
let mut tempered = vec![0; prog.states.len()];
let mut receive_target = Vec::with_capacity(prog.machines.len());
for m in &prog.machines {
for s in &m.states {
let st = prog.state(*s);
let base = format!("PImplementation.{}.{}", m.cs_name, st.name);
plain[s.idx()] = intern(base.clone());
let suffix = match st.temperature {
Temperature::Hot => "[hot]",
Temperature::Cold => "[cold]",
_ => "",
};
tempered[s.idx()] = intern(format!("{}{}", base, suffix));
}
receive_target.push(intern(format!("PImplementation.{}.StateMachine", m.cs_name)));
}
CoverageTables { nodes, plain, tempered, receive_target }
}
pub(crate) fn plain(&self, s: StateId) -> u32 {
self.plain[s.idx()]
}
fn tempered(&self, s: StateId) -> u32 {
self.tempered[s.idx()]
}
}
pub type PairSet = FxHashSet<(u32, u32)>;
#[derive(Debug, Clone, Default)]
pub struct CoverageInfo {
pub declared: Vec<(MachineId, bool)>,
pub received: PairSet,
pub sent: PairSet,
pub links: PairSet,
pub monitor_states: FxHashSet<StateId>,
pub monitor_transitions: FxHashSet<(StateId, StateId)>,
}
impl CoverageInfo {
pub fn merge(&mut self, other: CoverageInfo) {
for d in other.declared {
if !self.declared.contains(&d) {
self.declared.push(d);
}
}
self.received.extend(other.received);
self.sent.extend(other.sent);
self.links.extend(other.links);
self.monitor_states.extend(other.monitor_states);
self.monitor_transitions.extend(other.monitor_transitions);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sender {
Machine(usize),
Monitor(usize),
}
#[derive(Debug, Clone, Copy)]
struct InboxEntry {
sender: Sender,
state: u32,
event: u32,
}
#[derive(Debug, Clone, Default)]
struct CoverageRun {
monitor_previous: FxHashMap<usize, StateId>,
dequeued: Option<u32>,
inbox: FxHashMap<MachineId, Vec<InboxEntry>>,
graph_dequeued: FxHashMap<usize, InboxEntry>,
}
#[derive(Debug, Clone)]
pub struct Coverage {
tables: Rc<CoverageTables>,
info: CoverageInfo,
run: CoverageRun,
}
impl Coverage {
pub fn new(tables: Rc<CoverageTables>, info: CoverageInfo) -> Coverage {
Coverage { tables, info, run: CoverageRun::default() }
}
pub fn into_info(self) -> CoverageInfo {
self.info
}
fn add_event(&mut self, target: MachineId, entry: InboxEntry) {
self.run.inbox.entry(target).or_default().push(entry);
}
fn pop_event(&mut self, target: MachineId, event: u32) -> Option<InboxEntry> {
let inbox = self.run.inbox.get_mut(&target)?;
let mut result = None;
let mut i = inbox.len();
while i > 0 {
i -= 1;
if inbox[i].event == event {
result = Some(inbox.remove(i));
}
}
result
}
fn link_popped(&mut self, decl: MachineId, target: u32) {
if let Some(e) = self.run.inbox.get_mut(&decl).and_then(|i| i.pop()) {
self.link(e.state, target);
}
}
fn link(&mut self, a: u32, b: u32) {
self.info.links.insert((a, b));
}
fn event_handled(&mut self, state: u32) {
if let Some(ev) = self.run.dequeued.take() {
self.info.received.insert((state, ev));
}
}
fn link_transition(&mut self, m: usize, current: u32, new_state: Option<u32>) {
if let Some(i) = self.run.graph_dequeued.remove(&m) {
if i.sender != Sender::Machine(m) || i.state != current {
self.link(i.state, current);
}
}
if let Some(n) = new_state {
self.link(current, n);
}
}
}
impl Runtime<'_> {
fn cov_machine(&mut self, m: usize) -> Option<(&mut Coverage, u32)> {
let cov = self.coverage.as_mut()?;
let state = cov.tables.plain(self.machines[m].state?);
Some((cov, state))
}
fn cov_monitor(&mut self, mon: usize) -> Option<(&mut Coverage, StateId, MachineId)> {
let cov = self.coverage.as_mut()?;
let Monitor { state: Some(s), decl, .. } = self.monitors[mon] else { return None };
Some((cov, s, decl))
}
pub(crate) fn cov_declare(&mut self, decl: MachineId, monitor: bool) {
if let Some(c) = self.coverage.as_mut() {
if !c.info.declared.iter().any(|(d, _)| *d == decl) {
c.info.declared.push((decl, monitor));
}
}
}
pub(crate) fn cov_send(&mut self, sender: usize, target_decl: MachineId, e: EventId) {
let Some((cov, state)) = self.cov_machine(sender) else { return };
cov.info.sent.insert((state, e.0));
cov.add_event(target_decl, InboxEntry { sender: Sender::Machine(sender), state, event: e.0 });
}
pub(crate) fn cov_raise(&mut self, m: usize, e: EventId) {
let decl = self.machines[m].decl;
let Some((cov, state)) = self.cov_machine(m) else { return };
cov.info.sent.insert((state, e.0));
cov.add_event(decl, InboxEntry { sender: Sender::Machine(m), state, event: e.0 });
}
pub(crate) fn cov_dequeue(&mut self, m: usize, e: EventId) {
let decl = self.machines[m].decl;
let Some(cov) = self.coverage.as_mut() else { return };
cov.run.dequeued = Some(e.0);
if let Some(info) = cov.pop_event(decl, e.0) {
cov.run.graph_dequeued.insert(m, info);
}
}
pub(crate) fn cov_default(&mut self, m: usize) {
let ev = self.prog.null_event.0;
let Machine { decl, state, .. } = self.machines[m];
let Some(cov) = self.coverage.as_mut() else { return };
cov.run.dequeued = Some(ev);
let Some(state) = state.map(|s| cov.tables.plain(s)) else { return };
cov.add_event(decl, InboxEntry { sender: Sender::Machine(m), state, event: ev });
match cov.pop_event(decl, ev) {
Some(i) => {
cov.run.graph_dequeued.insert(m, i);
}
None => {
cov.run.graph_dequeued.remove(&m);
}
}
}
pub(crate) fn cov_handle_raised(&mut self, m: usize, e: EventId) {
let decl = self.machines[m].decl;
let Some(cov) = self.coverage.as_mut() else { return };
let run = &mut cov.run;
run.dequeued = Some(e.0);
let found = run.inbox.get(&decl).and_then(|inbox| inbox.iter().rev().find(|i| i.event == e.0).copied());
if let Some(info) = found {
run.graph_dequeued.insert(m, info);
}
}
pub(crate) fn cov_execute_action(&mut self, m: usize) {
let Some((cov, state)) = self.cov_machine(m) else { return };
cov.event_handled(state);
cov.link_transition(m, state, None);
}
pub(crate) fn cov_goto(&mut self, m: usize, target: StateId) {
let Some((cov, state)) = self.cov_machine(m) else { return };
cov.event_handled(state);
let target = cov.tables.plain(target);
cov.link_transition(m, state, Some(target));
}
pub(crate) fn cov_receive(&mut self, m: usize, e: EventId) {
let decl = self.machines[m].decl;
let Some((cov, state)) = self.cov_machine(m) else { return };
cov.info.received.insert((state, e.0));
let found = cov.run.inbox.get_mut(&decl).and_then(|inbox| {
let i = inbox.iter().rposition(|i| i.event == e.0)?;
Some(inbox.remove(i))
});
if let Some(info) = found {
let target = cov.tables.receive_target[decl.idx()];
cov.link(info.state, target);
}
}
pub(crate) fn cov_monitor_process(&mut self, mon: usize, e: EventId, sender: Option<usize>) {
let sender_state = sender.and_then(|m| self.machines[m].state);
let Some((cov, s, decl)) = self.cov_monitor(mon) else { return };
let state = cov.tables.plain(s);
cov.info.received.insert((state, e.0));
cov.add_event(decl, InboxEntry { sender: Sender::Monitor(mon), state, event: e.0 });
if let Some(src) = sender_state {
let src = cov.tables.plain(src);
cov.link(src, state);
}
}
pub(crate) fn cov_monitor_do(&mut self, mon: usize) {
let Some((cov, s, decl)) = self.cov_monitor(mon) else { return };
let target = cov.tables.tempered(s);
cov.link_popped(decl, target);
}
pub(crate) fn cov_monitor_raise(&mut self, mon: usize, e: EventId) {
let Some((cov, s, decl)) = self.cov_monitor(mon) else { return };
let state = cov.tables.plain(s);
cov.info.sent.insert((state, e.0));
cov.add_event(decl, InboxEntry { sender: Sender::Monitor(mon), state, event: e.0 });
}
pub(crate) fn cov_monitor_enter(&mut self, mon: usize) {
let Some((cov, s, decl)) = self.cov_monitor(mon) else { return };
let target = cov.tables.tempered(s);
cov.info.monitor_states.insert(s);
if let Some(previous) = cov.run.monitor_previous.insert(mon, s) {
cov.info.monitor_transitions.insert((previous, s));
}
cov.link_popped(decl, target);
}
}
fn event_full_name(prog: &Program, ev: u32) -> String {
if ev == GOTO_EVENT.0 {
return "PChecker.Runtime.StateMachines.Events.GotoStateEvent".into();
}
let e = EventId(ev);
if e == prog.null_event {
return "PChecker.Runtime.Events.DefaultEvent".into();
}
format!("PImplementation.{}", event_cs_name(prog, e))
}
fn is_builtin(prog: &Program, ev: u32) -> bool {
ev == GOTO_EVENT.0 || EventId(ev) == prog.null_event
}
fn after_dot(s: &str) -> &str {
match s.rfind('.') {
Some(i) if i > 0 => &s[i + 1..],
_ => s,
}
}
fn before_dot(s: &str) -> &str {
match s.rfind('.') {
Some(i) if i > 0 => &s[..i],
_ => s,
}
}
fn sorted_names(names: impl Iterator<Item = String>) -> Vec<String> {
let mut v: Vec<String> = names.map(|n| after_dot(&n).to_string()).collect();
v.sort_by(|a, b| culture_order(a, b));
v
}
fn percent(covered: usize, total: usize) -> String {
if total == 0 {
"100.0".into()
} else {
format!("{:.1}", covered as f64 * 100.0 / total as f64)
}
}
pub fn coverage_report(prog: &Program, tables: &CoverageTables, info: &CoverageInfo) -> String {
let node = |n: u32| tables.nodes[n as usize].as_str();
let mut registered: Vec<(MachineId, u32, Vec<u32>)> = Vec::new();
for (decl, _) in &info.declared {
for s in &prog.machine(*decl).states {
let evs: Vec<u32> = prog
.state(*s)
.handlers
.iter()
.filter(|h| matches!(h.action, HandlerAction::Do(_) | HandlerAction::Goto { .. }))
.map(|h| h.event.0)
.collect();
if !evs.is_empty() {
registered.push((*decl, tables.plain(*s), evs));
}
}
}
let uncovered = |state: u32, evs: &[u32]| -> Vec<u32> {
evs.iter().copied().filter(|e| !info.received.contains(&(state, *e))).collect::<Vec<u32>>()
};
let total: usize = registered.iter().map(|(_, _, e)| e.len()).sum();
let total_uncovered: usize = registered.iter().map(|(_, s, e)| uncovered(*s, e).len()).sum();
let mut out = String::new();
let header = |out: &mut String, h: String| {
out.push_str(&h);
out.push('\n');
out.push_str(&"=".repeat(h.chars().count()));
out.push('\n');
};
header(&mut out, format!("Total event coverage: {}%", percent(total - total_uncovered, total)));
let mut machines: Vec<(String, MachineId, bool)> = info
.declared
.iter()
.map(|(d, mon)| (format!("PImplementation.{}", prog.machine(*d).cs_name), *d, *mon))
.collect();
machines.sort_by(|a, b| culture_order(&a.0, &b.0));
for (full, decl, monitor) in machines {
header(&mut out, format!("{}: {}", if monitor { "Monitor" } else { "StateMachine" }, after_dot(&full)));
let mine: Vec<&(MachineId, u32, Vec<u32>)> = registered.iter().filter(|(d, _, _)| *d == decl).collect();
let mt: usize = mine.iter().map(|(_, _, e)| e.len()).sum();
let mu: usize = mine.iter().map(|(_, s, e)| uncovered(*s, e).len()).sum();
out.push_str(&format!("Event coverage: {}%\n", percent(mt - mu, mt)));
for s in &prog.machine(decl).states {
let key = tables.plain(*s);
let state_name = &prog.state(*s).name;
let evs: Vec<u32> = mine.iter().filter(|(_, k, _)| *k == key).flat_map(|(_, _, e)| e.clone()).collect();
let st = evs.len();
let su = uncovered(key, &evs);
out.push('\n');
out.push_str(&format!(
"\tState: {}{}\n",
state_name,
if st > 0 && st == su.len() { " is uncovered" } else { "" }
));
if st == 0 {
out.push_str("\t\tState has no expected events, so coverage is 100%\n");
} else if st != su.len() {
out.push_str(&format!("\t\tState event coverage: {}%\n", percent(st - su.len(), st)));
}
let key_name = node(key);
let mut incoming: HashSet<String> = HashSet::new();
let mut outgoing: HashSet<String> = HashSet::new();
for (a, b) in &info.links {
let (sa, sb) = (node(*a), node(*b));
if before_dot(sa) == full && before_dot(sb) == full {
if sb == key_name {
incoming.insert(after_dot(sa).to_string());
}
if sa == key_name {
outgoing.insert(after_dot(sb).to_string());
}
}
}
let received: HashSet<String> = info
.received
.iter()
.filter(|(n, e)| *n == key && !is_builtin(prog, *e))
.map(|(_, e)| event_full_name(prog, *e))
.collect();
if !received.is_empty() {
out.push_str(&format!("\t\tEvents received: {}\n", sorted_names(received.into_iter()).join(", ")));
}
let sent: HashSet<String> = info
.sent
.iter()
.filter(|(n, e)| *n == key && !is_builtin(prog, *e))
.map(|(_, e)| event_full_name(prog, *e))
.collect();
if !sent.is_empty() {
out.push_str(&format!("\t\tEvents sent: {}\n", sorted_names(sent.into_iter()).join(", ")));
}
if !su.is_empty() {
let names = su.iter().map(|e| event_full_name(prog, *e)).collect::<HashSet<_>>();
out.push_str(&format!("\t\tEvents not covered: {}\n", sorted_names(names.into_iter()).join(", ")));
}
if !incoming.is_empty() {
out.push_str(&format!("\t\tPrevious states: {}\n", sorted_names(incoming.into_iter()).join(", ")));
}
if !outgoing.is_empty() {
out.push_str(&format!("\t\tNext states: {}\n", sorted_names(outgoing.into_iter()).join(", ")));
}
}
out.push('\n');
}
out
}