use std::collections::BTreeSet;
use smtlite::{Model, Solution};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BranchRec {
pub site: u64,
pub taken: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum FlipOutcome {
Verified,
ProvenUnflippable,
SolverUnknown,
#[default]
NotPosed,
RunFailed,
Unconfirmed,
AlreadyExplored,
}
#[derive(Clone, Debug, Default)]
pub struct FlipRec {
pub at: u64,
pub input: Vec<(String, u64)>,
pub verified: bool,
pub outcome: FlipOutcome,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum PropertyOutcome {
Confirmed,
Proven,
SolverUnknown,
#[default]
NotPosed,
RunFailed,
Unreproduced,
}
#[derive(Clone, Debug, Default)]
pub struct PropertyRec {
pub label: String,
pub outcome: PropertyOutcome,
pub violation: Option<(u64, u64)>,
pub input: Vec<(String, u64)>,
}
#[derive(Clone, Debug, Default)]
pub struct SinkRec {
pub site: u64,
pub kind: String,
pub args: Vec<(String, u64)>,
}
#[derive(Clone, Debug, Default)]
pub struct RunResult {
pub result: Option<i64>,
pub trapped: Option<String>,
pub log: Vec<BranchRec>,
pub sinks: Vec<SinkRec>,
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub result: Option<i64>,
pub runs: usize,
pub flips_verified: usize,
pub sinks: Vec<SinkRec>,
pub properties: Vec<PropertyRec>,
pub flips: Vec<FlipRec>,
pub gaps: Vec<String>,
pub exhausted: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Opts {
pub max_runs: usize,
pub max_flips: usize,
}
impl Default for Opts {
fn default() -> Self {
Self {
max_runs: 64,
max_flips: 32,
}
}
}
pub trait Frontend {
type Seed: Clone;
type Walk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String>;
fn walk(&self, seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk>;
fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution;
fn site(&self, walk: &Self::Walk, i: usize) -> u64;
fn model_inputs(&self, model: &Model, walk: &Self::Walk) -> Vec<(String, u64)>;
fn apply_inputs(&self, seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed;
fn walk_gaps(&self, _walk: &Self::Walk) -> Vec<String> {
Vec::new()
}
fn walk_sinks(&self, _walk: &Self::Walk) -> Vec<SinkRec> {
Vec::new()
}
fn properties(&self, _walk: &Self::Walk) -> Vec<String> {
Vec::new()
}
fn check_property(&self, _walk: &mut Self::Walk, _label: &str) -> Solution {
Solution::Unknown
}
fn property_violated(&self, _run: &RunResult, _label: &str) -> Option<(u64, u64)> {
None
}
}
type PathKey = Vec<(u64, bool)>;
fn path_key(log: &[BranchRec]) -> PathKey {
log.iter().map(|b| (b.site, b.taken)).collect()
}
struct Pending<S> {
seed: S,
run: RunResult,
}
struct Attempt<S> {
rec: FlipRec,
verified: bool,
ran: bool,
gap: Option<String>,
child: Option<Pending<S>>,
}
#[must_use]
pub fn explore<F: Frontend>(frontend: &F, base: &F::Seed, opts: &Opts) -> Report {
let mut report = Report::default();
let mut seen: BTreeSet<PathKey> = BTreeSet::new();
report.runs = 1;
let first = match frontend.run(base) {
Ok(r) => r,
Err(e) => {
report.gaps.push(format!("run failed: {e}"));
return report;
}
};
if first.trapped.is_none() {
report.result = first.result;
}
seen.insert(path_key(&first.log));
let mut frontier = vec![Pending { seed: base.clone(), run: first }];
let mut incomplete = false;
while let Some(item) = frontier.pop() {
if report.runs >= opts.max_runs || report.flips_verified >= opts.max_flips {
return report;
}
if let Some(t) = &item.run.trapped {
report.gaps.push(format!("run trapped: {t}"));
}
let Some(mut walk) = frontend.walk(&item.seed, &item.run.log) else {
report
.gaps
.push("walk could not replay the concrete path".to_string());
incomplete = true;
continue;
};
report.sinks.extend(frontend.walk_sinks(&walk));
report.gaps.extend(frontend.walk_gaps(&walk));
let (mut spent, mut flipped) = (0usize, 0usize);
for label in frontend.properties(&walk) {
if item.run.trapped.is_some() {
report.properties.push(PropertyRec {
label,
..PropertyRec::default()
});
continue;
}
let (rec, ran, skipped) = pose_property(
frontend,
&item.seed,
&mut walk,
&label,
report.runs,
opts,
);
report.runs += usize::from(ran);
incomplete |= skipped;
report.properties.push(rec);
}
let mut attempts = Vec::new();
for i in (0..item.run.log.len()).rev() {
if report.runs + spent >= opts.max_runs
|| report.flips_verified + flipped >= opts.max_flips
{
incomplete = true;
break;
}
let a = attempt(frontend, &item.seed, &item.run.log, &mut walk, i, &seen);
spent += usize::from(a.ran);
flipped += usize::from(a.verified);
if a.verified {
if let Some(child) = &a.child {
seen.insert(path_key(&child.run.log));
}
}
attempts.push(a);
}
for a in attempts {
report.runs += usize::from(a.ran);
if let Some(g) = a.gap {
report.gaps.push(g);
}
report.flips.push(a.rec);
if a.verified {
report.flips_verified += 1;
if let Some(child) = a.child {
frontier.push(child);
}
}
}
}
report.exhausted = !incomplete;
report
}
fn pose_property<F: Frontend>(
frontend: &F,
seed: &F::Seed,
walk: &mut F::Walk,
label: &str,
runs_spent: usize,
opts: &Opts,
) -> (PropertyRec, bool, bool) {
let mut rec = PropertyRec {
label: label.to_string(),
outcome: PropertyOutcome::NotPosed,
violation: None,
input: Vec::new(),
};
match frontend.check_property(walk, label) {
Solution::Unsat => rec.outcome = PropertyOutcome::Proven,
Solution::Unknown => rec.outcome = PropertyOutcome::SolverUnknown,
Solution::Sat(model) => {
if runs_spent >= opts.max_runs {
return (rec, false, true);
}
let inputs = frontend.model_inputs(&model, walk);
let child_seed = frontend.apply_inputs(seed, &inputs);
rec.input = inputs;
match frontend.run(&child_seed) {
Ok(run) if run.trapped.is_none() => {
match frontend.property_violated(&run, label) {
Some(violation) => {
rec.outcome = PropertyOutcome::Confirmed;
rec.violation = Some(violation);
}
None => rec.outcome = PropertyOutcome::Unreproduced,
}
}
_ => rec.outcome = PropertyOutcome::RunFailed,
}
return (rec, true, false);
}
}
(rec, false, false)
}
fn attempt<F: Frontend>(
frontend: &F,
seed: &F::Seed,
log: &[BranchRec],
walk: &mut F::Walk,
i: usize,
seen: &BTreeSet<PathKey>,
) -> Attempt<F::Seed> {
let at = frontend.site(walk, i);
let solution = frontend.flip(walk, i);
let Solution::Sat(m) = &solution else {
let outcome = match solution {
Solution::Unsat => FlipOutcome::ProvenUnflippable,
Solution::Unknown => FlipOutcome::SolverUnknown,
_ => FlipOutcome::NotPosed,
};
let gap = (outcome == FlipOutcome::SolverUnknown)
.then(|| format!("solver Unknown at {at:#x}"));
return Attempt {
rec: FlipRec { at, input: Vec::new(), verified: false, outcome },
verified: false,
ran: false,
gap,
child: None,
};
};
let inputs = frontend.model_inputs(m, walk);
let child_seed = frontend.apply_inputs(seed, &inputs);
let child_run = match frontend.run(&child_seed) {
Ok(r) => r,
Err(_) => {
return Attempt {
rec: FlipRec {
at,
input: inputs,
verified: false,
outcome: FlipOutcome::RunFailed,
},
verified: false,
ran: true,
gap: None,
child: None,
};
}
};
let prefix_ok = child_run.log.len() > i
&& child_run.log[..i]
.iter()
.zip(&log[..i])
.all(|(c, p)| c.site == p.site && c.taken == p.taken);
let flipped =
prefix_ok && child_run.log[i].site == log[i].site && child_run.log[i].taken != log[i].taken;
let child_path = path_key(&child_run.log);
let verified = flipped && !seen.contains(&child_path);
let outcome = if verified {
FlipOutcome::Verified
} else if flipped {
FlipOutcome::AlreadyExplored
} else {
FlipOutcome::Unconfirmed
};
Attempt {
rec: FlipRec { at, input: inputs, verified, outcome },
verified,
ran: true,
gap: None,
child: verified.then_some(Pending { seed: child_seed, run: child_run }),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Toy {
claim: Claim,
}
#[derive(Clone, Copy)]
enum Claim {
None,
Witnessed,
Denied,
Pair,
}
#[derive(Clone)]
struct Seed {
x: u8,
}
struct Walk {
solver: smtlite::Solver,
x: smtlite::Bv,
conds: Vec<smtlite::Bv>,
taken: Vec<bool>,
sites: Vec<u64>,
path: Vec<smtlite::Bv>,
}
impl Frontend for Toy {
type Seed = Seed;
type Walk = Walk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String> {
let mut log = Vec::new();
let x = seed.x;
log.push(BranchRec { site: 0x100, taken: x < 0x10 });
if x < 0x10 {
log.push(BranchRec { site: 0x200, taken: x == 0x42 });
}
Ok(RunResult { result: Some(i64::from(x)), trapped: None, log, sinks: Vec::new() })
}
fn walk(&self, _seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk> {
let mut solver = smtlite::Solver::new();
let x = solver.var("x", 8);
let mut conds = Vec::new();
let mut taken = Vec::new();
let mut sites = Vec::new();
let mut path = Vec::new();
for b in log {
let cond = match b.site {
0x100 => x.ult(&smtlite::Bv::val(0x10, 8)),
0x200 => x.eq(&smtlite::Bv::val(0x42, 8)),
_ => return None,
};
path.push(if b.taken { cond.clone() } else { cond.not() });
conds.push(cond);
taken.push(b.taken);
sites.push(b.site);
}
Some(Walk { solver, x, conds, taken, sites, path })
}
fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution {
let g_i = if walk.taken[i] {
walk.conds[i].not()
} else {
walk.conds[i].clone()
};
let mut assumptions = walk.path[..i].to_vec();
assumptions.push(g_i);
walk.solver.check_all(&assumptions)
}
fn site(&self, walk: &Self::Walk, i: usize) -> u64 {
walk.sites[i]
}
fn model_inputs(&self, model: &Model, _walk: &Self::Walk) -> Vec<(String, u64)> {
model.get("x").map(|v| vec![("x".to_string(), v)]).unwrap_or_default()
}
fn apply_inputs(&self, _seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed {
Seed { x: inputs.first().map_or(0, |(_, v)| *v as u8) }
}
fn properties(&self, _walk: &Self::Walk) -> Vec<String> {
match self.claim {
Claim::None => Vec::new(),
Claim::Witnessed | Claim::Denied => vec!["x != 3".to_string()],
Claim::Pair => {
vec!["x != 3".to_string(), "x != 3 (second)".to_string()]
}
}
}
fn check_property(&self, walk: &mut Self::Walk, _label: &str) -> Solution {
let mut assumptions = walk.path.clone();
assumptions.push(walk.x.eq(&smtlite::Bv::val(3, 8)));
walk.solver.check_all(&assumptions)
}
fn property_violated(&self, run: &RunResult, _label: &str) -> Option<(u64, u64)> {
match self.claim {
Claim::None | Claim::Denied => None,
Claim::Witnessed | Claim::Pair => (run.result == Some(3)).then_some((3, 3)),
}
}
}
#[test]
fn the_loop_verifies_a_real_flip_and_proves_the_unreachable_one() {
let r = explore(
&Toy { claim: Claim::None },
&Seed { x: 0 },
&Opts { max_runs: 64, max_flips: 32 },
);
assert_eq!(r.runs, 3, "flips={:?}", r.flips);
assert_eq!(r.flips_verified, 1, "flips={:?}", r.flips);
assert_eq!(
r.flips.iter().map(|f| (f.at, f.outcome)).collect::<Vec<_>>(),
vec![
(0x200, FlipOutcome::ProvenUnflippable),
(0x100, FlipOutcome::Verified),
(0x100, FlipOutcome::AlreadyExplored),
]
);
assert!(r.exhausted);
}
#[derive(Clone, Copy, Default)]
struct Siblings {
stuck: bool,
fail_high_x: bool,
fail_high_z: bool,
walk_fails: bool,
}
#[derive(Clone)]
struct SSeed {
x: u8,
y: u8,
z: u8,
}
struct SWalk {
solver: smtlite::Solver,
conds: Vec<smtlite::Bv>,
taken: Vec<bool>,
sites: Vec<u64>,
path: Vec<smtlite::Bv>,
last_flip: usize,
}
const SITE_VAR: [(u64, usize); 3] = [(0x100, 0), (0x200, 1), (0x300, 2)];
impl Frontend for Siblings {
type Seed = SSeed;
type Walk = SWalk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String> {
if (self.fail_high_x && seed.x >= 0x10)
|| (self.fail_high_z && seed.z >= 0x10)
{
return Err("the frontend refused this seed".to_string());
}
let log = vec![
BranchRec { site: 0x100, taken: seed.x < 0x10 },
BranchRec { site: 0x200, taken: seed.y < 0x10 },
BranchRec { site: 0x300, taken: seed.z < 0x10 },
];
Ok(RunResult { result: Some(0), trapped: None, log, sinks: Vec::new() })
}
fn walk(&self, _seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk> {
if self.walk_fails {
return None;
}
let mut solver = smtlite::Solver::new();
let vars = [
solver.var("x", 8),
solver.var("y", 8),
solver.var("z", 8),
];
let mut conds = Vec::new();
let mut taken = Vec::new();
let mut sites = Vec::new();
let mut path = Vec::new();
for b in log {
let k = SITE_VAR.iter().find(|(site, _)| *site == b.site)?.1;
let cond = vars[k].ult(&smtlite::Bv::val(0x10, 8));
path.push(if b.taken { cond.clone() } else { cond.not() });
conds.push(cond);
taken.push(b.taken);
sites.push(b.site);
}
Some(SWalk { solver, conds, taken, sites, path, last_flip: 0 })
}
fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution {
walk.last_flip = i;
if self.stuck {
return Solution::Unknown;
}
let g_i = if walk.taken[i] {
walk.conds[i].not()
} else {
walk.conds[i].clone()
};
let mut assumptions = walk.path[..i].to_vec();
assumptions.push(g_i);
walk.solver.check_all(&assumptions)
}
fn site(&self, walk: &Self::Walk, i: usize) -> u64 {
walk.sites[i]
}
fn model_inputs(&self, model: &Model, walk: &Self::Walk) -> Vec<(String, u64)> {
["x", "y", "z"]
.iter()
.enumerate()
.map(|(k, name)| {
let v = if k > walk.last_flip {
0x10
} else {
model.get(name).unwrap_or(0x10)
};
((*name).to_string(), v)
})
.collect()
}
fn apply_inputs(&self, _seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed {
let v = |name: &str| {
inputs
.iter()
.find(|(n, _)| n == name)
.map_or(0, |(_, v)| *v as u8)
};
SSeed { x: v("x"), y: v("y"), z: v("z") }
}
}
#[test]
fn a_flip_onto_a_path_still_on_the_frontier_is_not_a_new_flip() {
let r = explore(&Siblings::default(), &SSeed { x: 0, y: 0, z: 0 }, &Opts::default());
assert!(r.exhausted);
assert_eq!(7, r.flips_verified, "flips={:?}", r.flips);
assert_eq!(
7,
r.flips.iter().filter(|f| f.verified).count(),
"every counted verification must also be a verified record"
);
}
#[test]
fn a_budget_that_cuts_the_decision_loop_is_not_exhaustion() {
let r = explore(
&Siblings::default(),
&SSeed { x: 0, y: 0, z: 0 },
&Opts { max_runs: 2, max_flips: 32 },
);
assert_eq!(r.runs, 2);
assert_eq!(1, r.flips.len(), "flips={:?}", r.flips);
assert!(!r.exhausted);
}
#[test]
fn a_solver_bound_is_a_gap_and_an_unresolved_outcome() {
let r = explore(
&Siblings { stuck: true, ..Siblings::default() },
&SSeed { x: 0, y: 0, z: 0 },
&Opts::default(),
);
assert_eq!(3, r.flips.len());
assert!(r.flips.iter().all(|f| f.outcome == FlipOutcome::SolverUnknown));
assert_eq!(3, r.gaps.iter().filter(|g| g.starts_with("solver Unknown at")).count());
assert!(r.exhausted);
}
#[test]
fn a_failed_verification_run_is_recorded_and_counted() {
let r = explore(
&Siblings { fail_high_x: true, ..Siblings::default() },
&SSeed { x: 0, y: 0, z: 0 },
&Opts::default(),
);
assert!(
r.flips
.iter()
.any(|f| f.at == 0x100 && f.outcome == FlipOutcome::RunFailed && !f.verified),
"flips={:?}",
r.flips
);
assert_eq!(
0,
r.flips.iter().filter(|f| f.verified && f.at == 0x100).count(),
"a run that failed at the engine level is not a verified flip"
);
assert!(r.exhausted);
}
#[test]
fn a_budget_break_that_leaves_the_frontier_empty_is_not_exhaustion() {
let r = explore(
&Siblings { fail_high_z: true, ..Siblings::default() },
&SSeed { x: 0, y: 0, z: 0 },
&Opts { max_runs: 2, max_flips: 32 },
);
assert_eq!(r.runs, 2);
assert_eq!(1, r.flips.len(), "flips={:?}", r.flips);
assert_eq!(FlipOutcome::RunFailed, r.flips[0].outcome);
assert!(!r.exhausted);
}
struct NoDecisions;
struct NWalk {
solver: smtlite::Solver,
x: smtlite::Bv,
}
impl Frontend for NoDecisions {
type Seed = u8;
type Walk = NWalk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String> {
Ok(RunResult {
result: Some(i64::from(*seed)),
trapped: None,
log: Vec::new(),
sinks: Vec::new(),
})
}
fn walk(&self, _seed: &Self::Seed, _log: &[BranchRec]) -> Option<Self::Walk> {
let mut solver = smtlite::Solver::new();
let x = solver.var("value", 8);
Some(NWalk { solver, x })
}
fn flip(&self, _walk: &mut Self::Walk, _i: usize) -> Solution {
Solution::Unknown
}
fn site(&self, _walk: &Self::Walk, _i: usize) -> u64 {
0
}
fn model_inputs(&self, model: &Model, _walk: &Self::Walk) -> Vec<(String, u64)> {
model
.get("value")
.map(|v| vec![("value".to_string(), v)])
.unwrap_or_default()
}
fn apply_inputs(&self, _seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed {
inputs.first().map_or(0, |(_, v)| *v as u8)
}
fn properties(&self, _walk: &Self::Walk) -> Vec<String> {
vec!["p1".to_string(), "p2".to_string()]
}
fn check_property(&self, walk: &mut Self::Walk, _label: &str) -> Solution {
let q = walk.x.eq(&smtlite::Bv::val(0, 8));
walk.solver.check_all(&[q])
}
fn property_violated(&self, _run: &RunResult, _label: &str) -> Option<(u64, u64)> {
None
}
}
#[test]
fn a_property_refused_its_verification_run_is_not_exhaustion() {
let r = explore(&NoDecisions, &0, &Opts { max_runs: 2, max_flips: 32 });
assert_eq!(r.runs, 2);
assert_eq!(2, r.properties.len());
assert_eq!(PropertyOutcome::Unreproduced, r.properties[0].outcome);
assert_eq!(PropertyOutcome::NotPosed, r.properties[1].outcome);
assert!(!r.exhausted);
}
#[test]
fn a_path_whose_walk_fails_is_not_exhaustion() {
let r = explore(
&Siblings { walk_fails: true, ..Siblings::default() },
&SSeed { x: 0, y: 0, z: 0 },
&Opts::default(),
);
assert_eq!(r.runs, 1);
assert!(r.flips.is_empty());
assert_eq!(
vec!["walk could not replay the concrete path".to_string()],
r.gaps
);
assert!(!r.exhausted);
}
struct Trapped;
impl Frontend for Trapped {
type Seed = u8;
type Walk = smtlite::Solver;
fn run(&self, _seed: &Self::Seed) -> Result<RunResult, String> {
Ok(RunResult {
result: None,
trapped: Some("out of bounds memory access".to_string()),
log: Vec::new(),
sinks: Vec::new(),
})
}
fn walk(&self, _seed: &Self::Seed, _log: &[BranchRec]) -> Option<Self::Walk> {
Some(smtlite::Solver::new())
}
fn flip(&self, _walk: &mut Self::Walk, _i: usize) -> Solution {
Solution::Unknown
}
fn site(&self, _walk: &Self::Walk, _i: usize) -> u64 {
0
}
fn model_inputs(&self, _model: &Model, _walk: &Self::Walk) -> Vec<(String, u64)> {
Vec::new()
}
fn apply_inputs(&self, _seed: &Self::Seed, _inputs: &[(String, u64)]) -> Self::Seed {
0
}
fn properties(&self, _walk: &Self::Walk) -> Vec<String> {
vec!["out <= 0x10".to_string()]
}
fn check_property(&self, _walk: &mut Self::Walk, _label: &str) -> Solution {
Solution::Unknown
}
}
#[test]
fn a_trapped_path_declares_its_properties_as_not_posed() {
let r = explore(&Trapped, &0, &Opts::default());
assert_eq!(1, r.properties.len());
assert_eq!("out <= 0x10", r.properties[0].label);
assert_eq!(PropertyOutcome::NotPosed, r.properties[0].outcome);
assert_eq!(vec!["run trapped: out of bounds memory access".to_string()], r.gaps);
assert!(r.exhausted);
}
#[test]
fn path_identity_counts_sites_not_only_directions() {
let direct = [
BranchRec { site: 0x100, taken: true },
BranchRec { site: 0x200, taken: false },
];
let indirect = [
BranchRec { site: 0x100, taken: true },
BranchRec { site: 0x300, taken: false },
];
assert_eq!(
direct.iter().map(|b| b.taken).collect::<Vec<_>>(),
indirect.iter().map(|b| b.taken).collect::<Vec<_>>(),
"the two runs take the same directions"
);
assert_ne!(
path_key(&direct),
path_key(&indirect),
"but they are not the same path"
);
}
#[test]
fn a_property_is_confirmed_only_by_the_re_run() {
let r = explore(
&Toy { claim: Claim::Witnessed },
&Seed { x: 0 },
&Opts { max_runs: 64, max_flips: 32 },
);
let confirmed = &r.properties[0];
assert_eq!(confirmed.label, "x != 3");
assert_eq!(confirmed.outcome, PropertyOutcome::Confirmed);
assert_eq!(confirmed.violation, Some((3, 3)));
assert_eq!(confirmed.input, vec![("x".to_string(), 3)]);
assert_eq!(r.properties.len(), 2);
assert_eq!(r.properties[1].outcome, PropertyOutcome::Proven);
assert_eq!(r.runs, 4, "flips={:?}", r.flips);
assert_eq!(r.flips_verified, 1, "flips={:?}", r.flips);
assert!(r.exhausted);
}
#[test]
fn a_sat_model_without_a_violating_readback_is_unreproduced() {
let r = explore(
&Toy { claim: Claim::Denied },
&Seed { x: 0 },
&Opts { max_runs: 64, max_flips: 32 },
);
assert_eq!(r.properties[0].outcome, PropertyOutcome::Unreproduced);
assert_eq!(r.properties[0].violation, None);
assert_eq!(r.properties[0].input, vec![("x".to_string(), 3)]);
}
#[test]
fn a_sat_property_with_no_run_budget_is_not_posed() {
let r = explore(
&Toy { claim: Claim::Pair },
&Seed { x: 0 },
&Opts { max_runs: 2, max_flips: 32 },
);
assert_eq!(r.properties.len(), 2);
assert_eq!(r.properties[0].outcome, PropertyOutcome::Confirmed);
assert_eq!(r.properties[1].outcome, PropertyOutcome::NotPosed);
assert_eq!(r.runs, 2, "the budget is a hard cap, not a target");
assert!(!r.exhausted);
}
}