use {
crate::{
Pbt,
hash::random_state,
reflection::{Parts, erased_vec_ops_of},
},
core::{any::TypeId, fmt::Write as _},
std::{
env::{self, current_dir},
fs::{File, OpenOptions, create_dir_all},
io::{BufRead as _, BufReader, Write as _},
path::PathBuf,
},
};
#[inline]
fn env_flag(name: &str) -> bool {
matches!(env::var(name), Ok(value) if !value.is_empty() && value != "0")
}
#[inline]
#[expect(
clippy::expect_used,
reason = "Internal invariants: violations should fail loudly."
)]
fn workspace_root() -> PathBuf {
let cwd = current_dir().expect("INTERNAL ERROR (`pbt`): couldn't read the current directory");
cwd.ancestors()
.find(|dir| dir.join("Cargo.lock").exists())
.expect("INTERNAL ERROR (`pbt`): couldn't find the Cargo workspace root")
.to_owned()
}
#[inline]
fn dir() -> PathBuf {
if let Ok(cache_dir) = env::var("PBT_CACHE_DIR")
&& !cache_dir.is_empty()
{
PathBuf::from(cache_dir)
} else {
workspace_root().join(".pbt")
}
}
#[inline]
#[expect(
clippy::expect_used,
reason = "Internal invariants: violations should fail loudly."
)]
pub(crate) fn jsonl_path<T>() -> PathBuf
where
T: 'static,
{
let ty = TypeId::of::<T>();
let erased_vec_ops = erased_vec_ops_of(ty);
let type_name = (erased_vec_ops.name)();
let mut jsonl_filename = String::new();
let () = write!(
jsonl_filename,
"{:016X}",
random_state().hash_one(type_name),
)
.expect("INTERNAL ERROR (`pbt`): couldn't write to a `String`");
let () = jsonl_filename.push('-');
let () = jsonl_filename.extend(
type_name
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }),
);
let () = jsonl_filename.push_str(".jsonl");
dir().join(jsonl_filename)
}
#[inline]
#[must_use]
#[expect(
clippy::expect_used,
reason = "Internal invariants: violations should fail loudly."
)]
#[expect(
clippy::absolute_paths,
reason = "`core::io::ErrorKind` is unstable; filesystem errors use stable `std::io`."
)]
pub fn replay<T>() -> Vec<T>
where
T: Pbt,
{
if env_flag("PBT_NO_REPLAY") {
Vec::new()
} else {
match File::open(jsonl_path::<T>()) {
Ok(file) => {
let () = file
.lock_shared()
.expect("INTERNAL ERROR (`pbt`): couldn't lock persisted witnesses");
BufReader::new(file)
.lines()
.map(|line_result| {
let line = line_result
.expect("INTERNAL ERROR (`pbt`): couldn't read persisted witnesses");
let json = serde_json::from_str(&line)
.expect("INTERNAL ERROR (`pbt`): couldn't parse persisted JSONL");
Parts::deserialize(&json).expect(
"INTERNAL ERROR (`pbt`): couldn't deserialize a persisted witness",
)
})
.collect()
}
Err(error) => {
assert_eq!(
error.kind(),
std::io::ErrorKind::NotFound,
"INTERNAL ERROR (`pbt`): couldn't read persisted witnesses",
);
Vec::new()
}
}
}
}
#[inline]
#[expect(
clippy::expect_used,
reason = "Internal invariants: violations should fail loudly."
)]
pub(crate) fn witness<T>(t: &T)
where
T: Pbt,
{
if env_flag("PBT_NO_PERSIST") {
return;
}
let json = t.clone().deconstruct().serialize();
let mut record =
serde_json::to_vec(&json).expect("INTERNAL ERROR (`pbt`): couldn't serialize a witness");
let () = record.push(b'\n');
let path = jsonl_path::<T>();
let () = create_dir_all(dir())
.expect("INTERNAL ERROR (`pbt`): couldn't create the persistence directory");
let mut file = OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(path)
.expect("INTERNAL ERROR (`pbt`): couldn't persist a witness to the filesystem");
let () = file
.lock()
.expect("INTERNAL ERROR (`pbt`): couldn't lock persisted witnesses");
for line_result in BufReader::new(&file).lines() {
let line = line_result.expect("INTERNAL ERROR (`pbt`): couldn't read persisted witnesses");
let persisted: serde_json::Value = serde_json::from_str(&line)
.expect("INTERNAL ERROR (`pbt`): couldn't parse persisted JSONL");
if persisted == json {
return;
}
}
let () = file
.write_all(&record)
.expect("INTERNAL ERROR (`pbt`): couldn't persist a witness");
}
#[cfg(test)]
mod tests {
#![expect(
clippy::expect_used,
reason = "Subprocess and filesystem failures should fail tests loudly."
)]
use {
super::*,
crate::reflection::register_globally,
core::time::Duration,
std::{
fs::{self, File},
io::ErrorKind,
process::{self, Command, Stdio},
thread,
},
};
const CHILD_WITNESS: &str = "PBT_TEST_CHILD_WITNESS";
const START_FILE: &str = "PBT_TEST_START_FILE";
const WITNESS_ELEMENTS: usize = 0x0400;
#[test]
fn concurrent_witness_child() {
let Ok(element_text) = env::var(CHILD_WITNESS) else {
return;
};
let start_file =
PathBuf::from(env::var(START_FILE).expect("parent should provide the start file"));
while !start_file.exists() {
thread::sleep(Duration::from_millis(1));
}
let element = element_text
.parse::<usize>()
.expect("parent should provide a `usize` witness");
let () = register_globally::<[usize; WITNESS_ELEMENTS]>();
witness(&[element; WITNESS_ELEMENTS]);
}
#[test]
#[cfg_attr(miri, ignore = "Miri does not support spawning subprocesses.")]
fn concurrent_witnesses_remain_valid_jsonl() {
const DISTINCT_WITNESSES: usize = 4;
const PROCESSES: usize = 16;
let root = env::temp_dir().join(format!("pbt-concurrent-witnesses-{}", process::id()));
match fs::remove_dir_all(&root) {
Ok(()) => {}
Err(error) => assert_eq!(
error.kind(),
ErrorKind::NotFound,
"couldn't remove a stale persistence test directory",
),
}
let () = fs::create_dir_all(&root).expect("couldn't create the persistence test directory");
let cache_dir = root.join("cache");
let start_file = root.join("start");
let test_binary = env::current_exe().expect("couldn't locate the test binary");
let mut children = (0..DISTINCT_WITNESSES)
.cycle()
.take(PROCESSES)
.map(|element| {
Command::new(&test_binary)
.arg("--exact")
.arg("persist::tests::concurrent_witness_child")
.arg("--quiet")
.stdout(Stdio::null())
.env("PBT_CACHE_DIR", &cache_dir)
.env(CHILD_WITNESS, element.to_string())
.env(START_FILE, &start_file)
.spawn()
.expect("couldn't spawn a persistence test subprocess")
})
.collect::<Vec<_>>();
let _start = File::create(&start_file).expect("couldn't release test subprocesses");
for child in &mut children {
assert!(
child
.wait()
.expect("couldn't wait for a test subprocess")
.success(),
"persistence test subprocess failed",
);
}
let mut corpus_entries =
fs::read_dir(&cache_dir).expect("couldn't read the persistence test directory");
let corpus_path = corpus_entries
.next()
.expect("persistence test did not create a corpus")
.expect("couldn't read the persisted corpus entry")
.path();
assert!(
corpus_entries.next().is_none(),
"persistence test unexpectedly created multiple corpora",
);
let mut actual = BufReader::new(
File::open(corpus_path).expect("couldn't open the persisted test corpus"),
)
.lines()
.map(|line| line.expect("couldn't read the persisted test corpus"))
.collect::<Vec<_>>();
for line in &actual {
let _: serde_json::Value =
serde_json::from_str(line).expect("concurrent persistence corrupted JSONL");
}
actual.sort_unstable();
let () = register_globally::<[usize; WITNESS_ELEMENTS]>();
let mut expected = (0..DISTINCT_WITNESSES)
.map(|element| {
serde_json::to_string(&[element; WITNESS_ELEMENTS].deconstruct().serialize())
.expect("couldn't serialize an expected test witness")
})
.collect::<Vec<_>>();
expected.sort_unstable();
assert_eq!(actual, expected);
let () = fs::remove_dir_all(root).expect("couldn't remove the persistence test directory");
}
}