use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const CACHE_DIR: &str = ".koala/cache/invariants";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropertyResult {
Pass {
cases_run: u32,
},
Fail {
seed: String,
counter_example: String,
},
}
#[derive(Debug)]
pub enum PersistError {
Io(io::Error),
}
impl std::fmt::Display for PersistError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "io: {e}"),
}
}
}
impl std::error::Error for PersistError {}
pub fn persist_counter_example(
repo_root: &Path,
invariant_id: &str,
seed: &str,
counter_example: &str,
) -> Result<PathBuf, PersistError> {
let dir = repo_root.join(CACHE_DIR);
fs::create_dir_all(&dir).map_err(PersistError::Io)?;
let path = dir.join(format!("{invariant_id}.json"));
let body = format!(
"{{\n \"invariant\": \"{invariant_id}\",\n \"seed\": {seed:?},\n \"counter_example\": {counter_example:?}\n}}\n"
);
fs::write(&path, body).map_err(PersistError::Io)?;
Ok(path)
}
pub fn persisted_path(repo_root: &Path, invariant_id: &str) -> Option<PathBuf> {
let p = repo_root
.join(CACHE_DIR)
.join(format!("{invariant_id}.json"));
p.is_file().then_some(p)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use tempfile::TempDir;
fn balance_non_negative(ops: &[(bool, u32)]) -> bool {
let mut bal: i64 = 0;
for &(is_deposit, amount) in ops {
if is_deposit {
bal += amount as i64;
} else {
let amt = (amount as i64).min(bal); bal -= amt;
}
}
bal >= 0
}
proptest! {
#[test]
fn balance_holds(ops in prop::collection::vec((any::<bool>(), 0u32..1_000_000), 0..50)) {
prop_assert!(balance_non_negative(&ops));
}
}
fn balance_non_negative_buggy(ops: &[(bool, u32)]) -> bool {
let mut bal: i64 = 0;
for &(is_deposit, amount) in ops {
if is_deposit {
bal += amount as i64;
} else {
bal -= amount as i64; }
}
bal >= 0
}
#[test]
fn counter_example_persisted() {
let mut runner = proptest::test_runner::TestRunner::deterministic();
let strategy = prop::collection::vec((any::<bool>(), 0u32..1_000_000), 0..10);
let result = runner.run(&strategy, |ops| {
prop_assert!(balance_non_negative_buggy(&ops));
Ok(())
});
let err = result.expect_err("buggy invariant must fail under proptest");
let seed = format!("{:?}", runner.rng());
let counter_example = format!("{err:?}");
let tmp = TempDir::new().unwrap();
let path = persist_counter_example(
tmp.path(),
"business.balance-non-negative",
&seed,
&counter_example,
)
.unwrap();
assert!(path.is_file());
assert!(
path.ends_with(".koala/cache/invariants/business.balance-non-negative.json"),
"path was {}",
path.display()
);
let body = fs::read_to_string(&path).unwrap();
assert!(body.contains("\"invariant\": \"business.balance-non-negative\""));
assert!(body.contains("\"seed\""));
assert!(body.contains("\"counter_example\""));
assert_eq!(
persisted_path(tmp.path(), "business.balance-non-negative").as_deref(),
Some(path.as_path())
);
}
}