mod bfs;
use crate::{Expectation, Model};
mod dfs;
mod explorer;
mod path;
mod visitor;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::num::NonZeroUsize;
use std::time::Instant;
pub use path::*;
pub use visitor::*;
#[must_use = "This code constructs a builder, not a checker. \
Consider calling spawn_bfs() or spawn_dfs()."]
pub struct CheckerBuilder<M: Model> {
model: M,
target_generated_count: Option<NonZeroUsize>,
thread_count: usize,
visitor: Option<Box<dyn CheckerVisitor<M> + Send + Sync>>,
}
impl<M: Model> CheckerBuilder<M> {
pub(crate) fn new(model: M) -> Self {
Self {
model,
target_generated_count: None,
thread_count: 1,
visitor: None,
}
}
pub fn serve(self, addresses: impl std::net::ToSocketAddrs) -> std::sync::Arc<impl Checker<M>>
where M: 'static + Model + Send + Sync,
M::Action: Debug + Send + Sync,
M::State: Debug + Hash + Send + Sync,
{
explorer::serve(self, addresses)
}
#[must_use = "Checkers run on background threads. \
Consider calling join() or report(...), for example."]
pub fn spawn_bfs(self) -> impl Checker<M>
where M: Model + Send + Sync + 'static,
M::State: Hash + Send + Sync + 'static,
{
bfs::BfsChecker::spawn(self)
}
#[must_use = "Checkers run on background threads. \
Consider calling join() or report(...), for example."]
pub fn spawn_dfs(self) -> impl Checker<M>
where M: Model + Send + Sync + 'static,
M::State: Hash + Send + Sync + 'static,
{
dfs::DfsChecker::spawn(self)
}
pub fn target_generated_count(self, target_generated_count: usize) -> Self {
Self { target_generated_count: NonZeroUsize::new(target_generated_count), .. self }
}
pub fn threads(self, thread_count: usize) -> Self {
Self { thread_count, .. self }
}
pub fn visitor(self, visitor: impl CheckerVisitor<M> + Send + Sync + 'static) -> Self {
Self { visitor: Some(Box::new(visitor)), .. self }
}
}
pub trait Checker<M: Model> {
fn model(&self) -> &M;
fn generated_count(&self) -> usize;
fn discoveries(&self) -> HashMap<&'static str, Path<M::State, M::Action>>;
fn join(self) -> Self;
fn is_done(&self) -> bool;
fn discovery(&self, name: &'static str) -> Option<Path<M::State, M::Action>> {
self.discoveries().remove(name)
}
fn report(self, w: &mut impl std::io::Write) -> Self
where M::Action: Debug,
M::State: Debug,
Self: Sized,
{
let method_start = Instant::now();
while !self.is_done() {
let _ = writeln!(w, "Checking. generated={}", self.generated_count());
std::thread::sleep(std::time::Duration::from_millis(1_000));
}
let _ = writeln!(w, "Done. generated={}, sec={}",
self.generated_count(),
method_start.elapsed().as_secs());
for (name, path) in self.discoveries() {
let _ = write!(w, "Discovered \"{}\" {} {}",
name, self.discovery_classification(name), path);
}
self
}
fn discovery_classification(&self, name: &str) -> &'static str {
let properties = self.model().properties();
let property = properties.iter().find(|p| p.name == name).unwrap();
match property.expectation {
Expectation::Always | Expectation::Eventually => "counterexample",
Expectation::Sometimes => "example",
}
}
fn assert_properties(&self)
where M::Action: Debug,
M::State: Debug,
{
for p in self.model().properties() {
match p.expectation {
Expectation::Always => self.assert_no_discovery(p.name),
Expectation::Eventually => self.assert_no_discovery(p.name),
Expectation::Sometimes => { self.assert_any_discovery(p.name); },
}
}
}
fn assert_any_discovery(&self, name: &'static str) -> Path<M::State, M::Action> {
if let Some(found) = self.discovery(name) { return found }
assert!(self.is_done(),
"Discovery for \"{}\" not found, but model checking is incomplete.", name);
panic!("Discovery for \"{}\" not found.", name);
}
fn assert_no_discovery(&self, name: &'static str)
where M::Action: Debug,
M::State: Debug,
{
if let Some(found) = self.discovery(name) {
panic!("Unexpected \"{}\" {} {}Last state: {:?}\n",
name, self.discovery_classification(name), found, found.last_state());
}
assert!(self.is_done(),
"Discovery for \"{}\" not found, but model checking is incomplete.",
name);
}
fn assert_discovery(&self, name: &'static str, actions: Vec<M::Action>)
where M::State: Debug + PartialEq,
M::Action: Debug + PartialEq,
{
let mut additional_info: Vec<&'static str> = Vec::new();
let found = self.assert_any_discovery(name);
for init_state in self.model().init_states() {
if let Some(path) = Path::from_actions(self.model(), init_state, &actions) {
let property = self.model().property(name);
match property.expectation {
Expectation::Always => {
if !(property.condition)(self.model(), path.last_state()) { return }
}
Expectation::Eventually => {
let states = path.into_states();
let is_liveness_satisfied = states.iter().any(|s| {
(property.condition)(self.model(), s)
});
let is_path_terminal = {
let mut actions = Vec::new();
self.model().actions(&states.last().unwrap(), &mut actions);
actions.is_empty()
};
if !is_liveness_satisfied && is_path_terminal { return }
if is_liveness_satisfied {
additional_info.push("incorrect counterexample satisfies eventually property");
}
if !is_path_terminal {
additional_info.push("incorrect counterexample is nonterminal");
}
}
Expectation::Sometimes => {
if (property.condition)(self.model(), path.last_state()) { return }
}
}
}
}
let additional_info = if additional_info.is_empty() {
"".to_string()
} else {
format!(" ({})", additional_info.join("; "))
};
panic!("Invalid discovery for \"{}\"{}, but a valid one was found. found={:?}",
name, additional_info, found.into_actions());
}
}
type EventuallyBits = id_set::IdSet;
#[cfg(test)]
mod test_eventually_property_checker {
use crate::{Checker, Property};
use crate::test_util::dgraph::DGraph;
fn eventually_odd() -> Property<DGraph> {
Property::eventually("odd", |_, s| s % 2 == 1)
}
#[test]
fn can_validate() {
DGraph::with_property(eventually_odd())
.with_path(vec![1])
.with_path(vec![2, 3])
.with_path(vec![2, 6, 7])
.with_path(vec![4, 9, 10])
.check().assert_properties();
DGraph::with_property(eventually_odd())
.with_path(vec![1]).check().assert_properties();
DGraph::with_property(eventually_odd())
.with_path(vec![2, 3]).check().assert_properties();
DGraph::with_property(eventually_odd())
.with_path(vec![2, 6, 7]).check().assert_properties();
DGraph::with_property(eventually_odd())
.with_path(vec![4, 9, 10]).check().assert_properties();
}
#[test]
fn can_discover_counterexample() {
assert_eq!(
DGraph::with_property(eventually_odd())
.with_path(vec![0, 1])
.with_path(vec![0, 2])
.check().discovery("odd").unwrap().into_states(),
vec![0, 2]);
assert_eq!(
DGraph::with_property(eventually_odd())
.with_path(vec![0, 1])
.with_path(vec![2, 4])
.check().discovery("odd").unwrap().into_states(),
vec![2, 4]);
assert_eq!(
DGraph::with_property(eventually_odd())
.with_path(vec![0, 1, 4, 6])
.with_path(vec![2, 4, 8])
.check().discovery("odd").unwrap().into_states(),
vec![2, 4, 6]);
}
#[test]
fn fixme_can_miss_counterexample_when_revisiting_a_state() {
assert_eq!(
DGraph::with_property(eventually_odd())
.with_path(vec![0, 2, 4, 2])
.check().discovery("odd"),
None);
assert_eq!(
DGraph::with_property(eventually_odd())
.with_path(vec![0, 2, 4])
.with_path(vec![1, 4, 6])
.check().discovery("odd"),
None);
}
}
#[cfg(test)]
mod test_path {
use super::*;
use crate::fingerprint;
use crate::test_util::linear_equation_solver::LinearEquation;
use std::collections::VecDeque;
#[test]
fn can_build_path_from_fingerprints() {
let fp = |a: u8, b: u8| fingerprint(&(a, b));
let model = LinearEquation { a: 2, b: 10, c: 14 };
let fingerprints = VecDeque::from(vec![
fp(0, 0),
fp(0, 1),
fp(1, 1),
fp(2, 1),
]);
let path = Path::from_fingerprints(&model, fingerprints.clone());
assert_eq!(
path.last_state(),
&(2,1));
assert_eq!(
path.last_state(),
&Path::final_state(&model, fingerprints).unwrap());
}
}
#[cfg(test)]
mod test_report {
use super::*;
use crate::test_util::linear_equation_solver::LinearEquation;
#[test]
fn report_includes_property_names_and_paths() {
let mut written: Vec<u8> = Vec::new();
LinearEquation { a: 2, b: 10, c: 14 }.checker()
.spawn_bfs().report(&mut written);
let output = String::from_utf8(written).unwrap();
assert!(
output.starts_with("\
Checking. generated=1\n\
Done. generated=12, sec="),
"Output did not start as expected (see test). output={:?}`", output);
assert!(
output.ends_with("\
Discovered \"solvable\" example Path[3]:\n\
- IncreaseX\n\
- IncreaseX\n\
- IncreaseY\n"),
"Output did not end as expected (see test). output={:?}`", output);
let mut written: Vec<u8> = Vec::new();
LinearEquation { a: 2, b: 10, c: 14 }.checker()
.spawn_dfs().report(&mut written);
let output = String::from_utf8(written).unwrap();
assert!(
output.starts_with("\
Checking. generated=1\n\
Done. generated=55, sec="),
"Output did not start as expected (see test). output={:?}`", output);
assert!(
output.ends_with("\
Discovered \"solvable\" example Path[27]:\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n\
- IncreaseY\n"),
"Output did not end as expected (see test). output={:?}`", output);
}
}