#![doc = include_str!("../README.md")]
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::fs::{File, OpenOptions, create_dir, hard_link, metadata, read_dir, remove_file, rename};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use biometrics::{Collector, Counter};
use tatl::{HeyListen, Stationary};
use utilz::lockfile::Lockfile;
use handled::{SError, SExpr};
#[allow(non_snake_case)]
pub fn LOCKFILE<P: AsRef<Path>>(root: P) -> PathBuf {
root.as_ref().to_path_buf().join("LOCKFILE")
}
#[allow(non_snake_case)]
pub fn MANIFEST<P: AsRef<Path>>(root: P) -> PathBuf {
root.as_ref().to_path_buf().join("MANIFEST")
}
#[allow(non_snake_case)]
pub fn TEMPORARY<P: AsRef<Path>>(root: P) -> PathBuf {
root.as_ref().to_path_buf().join("MANIFEST.tmp")
}
#[allow(non_snake_case)]
pub fn BACKUP<P: AsRef<Path>>(root: P, idx: u64) -> PathBuf {
root.as_ref().to_path_buf().join(format!("MANIFEST.{idx}"))
}
const TX_SEPARATOR: &str = "--------";
pub fn extract_backup<P: AsRef<Path>>(path: P) -> Option<u64> {
let path = match path.as_ref().file_name() {
Some(path) => path.to_str(),
None => {
return None;
}
};
let path = match path {
Some(path) => path,
None => {
return None;
}
};
if let Some(path) = path.strip_prefix("MANIFEST.") {
path.parse::<u64>().ok()
} else {
None
}
}
static LOCK_OBTAINED: Counter = Counter::new("mani.lock_obtained");
static LOCK_NOT_OBTAINED: Counter = Counter::new("mani.lock_not_obtained");
static LOCK_NOT_OBTAINED_MONITOR: Stationary =
Stationary::new("mani.lock_not_obtained", &LOCK_NOT_OBTAINED);
pub fn register_biometrics(collector: Collector) {
collector.register_counter(&LOCK_OBTAINED);
collector.register_counter(&LOCK_NOT_OBTAINED);
}
pub fn register_monitors(hey_listen: &mut HeyListen) {
hey_listen.register_stationary(&LOCK_NOT_OBTAINED_MONITOR);
}
const PHASE: &str = "mani";
pub const CODE_SYSTEM_ERROR: &str = "system-error";
pub const CODE_CORRUPTION: &str = "corruption";
pub const CODE_NEWLINE_DISALLOWED: &str = "newline-disallowed";
pub const CODE_MANIFEST_EXISTS: &str = "manifest-exists";
pub const CODE_MANIFEST_NOT_EXIST: &str = "manifest-not-exist";
pub const CODE_LOCK_NOT_OBTAINED: &str = "lock-not-obtained";
fn error(code: &str) -> SError {
SError::new(PHASE).with_code(code)
}
fn system_error(what: impl AsRef<str>) -> SError {
error(CODE_SYSTEM_ERROR)
.with_message("manifest system error")
.with_string_field("what", what.as_ref())
}
fn corruption(what: impl AsRef<str>) -> SError {
error(CODE_CORRUPTION)
.with_message("manifest corruption")
.with_string_field("what", what.as_ref())
}
fn newline_disallowed(what: impl AsRef<str>) -> SError {
error(CODE_NEWLINE_DISALLOWED)
.with_message("manifest string contains newline")
.with_string_field("what", what.as_ref())
}
fn manifest_exists(path: impl AsRef<Path>) -> SError {
error(CODE_MANIFEST_EXISTS)
.with_message("manifest exists")
.with_string_field("path", path.as_ref().to_string_lossy().as_ref())
}
fn manifest_not_exist(path: impl AsRef<Path>) -> SError {
error(CODE_MANIFEST_NOT_EXIST)
.with_message("manifest does not exist")
.with_string_field("path", path.as_ref().to_string_lossy().as_ref())
}
fn lock_not_obtained(path: impl AsRef<Path>) -> SError {
error(CODE_LOCK_NOT_OBTAINED)
.with_message("manifest lock could not be obtained")
.with_string_field("path", path.as_ref().to_string_lossy().as_ref())
}
fn error_field<'a>(err: &'a SError, name: &str) -> Option<&'a SExpr> {
match err.detail() {
SExpr::List(fields) => fields.iter().find_map(|field| match field {
SExpr::List(pair) if pair.len() == 2 => match &pair[0] {
SExpr::Atom(field_name) if field_name == name => Some(&pair[1]),
_ => None,
},
_ => None,
}),
_ => None,
}
}
pub fn error_code(err: &SError) -> Option<&str> {
match error_field(err, "code") {
Some(SExpr::Atom(code)) => Some(code.as_str()),
_ => None,
}
}
pub fn is_manifest_exists(err: &SError) -> bool {
error_code(err) == Some(CODE_MANIFEST_EXISTS)
}
pub fn is_manifest_not_exist(err: &SError) -> bool {
error_code(err) == Some(CODE_MANIFEST_NOT_EXIST)
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "binaries", derive(arrrg_derive::CommandLine))]
pub struct ManifestOptions {
#[cfg_attr(
feature = "binaries",
arrrg(flag, "Fail if the manifest directory exists.")
)]
fail_if_exists: bool,
#[cfg_attr(
feature = "binaries",
arrrg(flag, "Fail if the manifest directory does not exist.")
)]
fail_if_not_exist: bool,
#[cfg_attr(feature = "binaries", arrrg(flag, "Fail if the manifest is locked."))]
fail_if_locked: bool,
#[cfg_attr(
feature = "binaries",
arrrg(
optional,
"Ratio of (bytes in the log):(bytes in memory) at which log will rollover."
)
)]
log_rollover_ratio: u64,
}
impl Default for ManifestOptions {
fn default() -> Self {
Self {
fail_if_exists: false,
fail_if_not_exist: false,
fail_if_locked: false,
log_rollover_ratio: 2,
}
}
}
pub struct Manifest {
options: ManifestOptions,
_lockfile: Lockfile,
root: PathBuf,
strs: BTreeSet<String>,
info: BTreeMap<char, String>,
last_rollover: u64,
poison: Option<SError>,
}
impl Manifest {
pub fn open<P: AsRef<Path>>(options: ManifestOptions, root: P) -> Result<Self, SError> {
let root = root.as_ref().to_path_buf();
if root.is_dir() && options.fail_if_exists {
return Err(manifest_exists(root));
}
if !root.is_dir() && options.fail_if_not_exist {
return Err(manifest_not_exist(root));
} else if !root.is_dir() {
create_dir(&root).map_err(|err| {
system_error(err.to_string())
.with_string_field("root", root.to_string_lossy().as_ref())
})?;
}
let lockfile = if options.fail_if_locked {
Lockfile::lock(LOCKFILE(&root)).map_err(|err| {
system_error(err.to_string())
.with_string_field("root", root.to_string_lossy().as_ref())
})?
} else {
Lockfile::wait(LOCKFILE(&root)).map_err(|err| {
system_error(err.to_string())
.with_string_field("root", root.to_string_lossy().as_ref())
})?
};
match lockfile {
Some(_lockfile) => {
LOCK_OBTAINED.click();
let manifest = MANIFEST(&root);
let (strs, info) = Self::read_mani(&manifest)?;
let last_rollover = Self::next_manifest_identifier(&root)?;
let mut this = Self {
options,
_lockfile,
root,
strs,
info,
last_rollover,
poison: None,
};
if manifest.is_file() {
this.rollover()?;
}
Ok(this)
}
None => {
LOCK_NOT_OBTAINED.click();
Err(lock_not_obtained(LOCKFILE(root)))
}
}
}
pub fn strs(&self) -> impl Iterator<Item = &str> {
self.strs.iter().map(|s| s.as_str())
}
pub fn info(&self, c: char) -> Option<&str> {
self.info.get(&c).map(|s| s.as_str())
}
pub fn size(&self) -> u64 {
let strs: u64 = self.strs.iter().map(|s| s.len() as u64).sum();
let info: u64 = self.info.values().map(|s| s.len() as u64).sum();
strs + info
}
pub fn apply(&mut self, edit: Edit) -> Result<(), SError> {
self._apply(&MANIFEST(&self.root), edit, true)
}
pub fn rollover(&mut self) -> Result<(), SError> {
let edit = Self::to_edit(&self.strs, &self.info);
let next_id = self.last_rollover;
self.last_rollover += 1;
let back = BACKUP(&self.root, next_id);
self.poison(hard_link(MANIFEST(&self.root), back))?;
let tmp = TEMPORARY(&self.root);
if tmp.exists() {
self.poison(remove_file(&tmp))?
}
self._apply(&tmp, edit, false)?;
self.poison(rename(&tmp, MANIFEST(&self.root)))?;
Ok(())
}
pub fn verify<P: AsRef<Path>>(
_options: ManifestOptions,
root: P,
) -> impl Iterator<Item = SError> {
let mut errs = Vec::new();
let mut ids = Vec::new();
let rd = match read_dir(&root) {
Ok(rd) => rd,
Err(err) => {
errs.push(err.into());
return errs.into_iter();
}
};
for dir in rd {
let dir = match dir {
Ok(dir) => dir,
Err(err) => {
errs.push(err.into());
continue;
}
};
let path = dir.path();
if let Some(id) = extract_backup(path) {
ids.push(id);
}
}
ids.sort();
let mut paths = Vec::new();
for id in ids.into_iter() {
paths.push((id, BACKUP(&root, id)));
}
if paths.is_empty() {
paths.push((0, MANIFEST(&root)));
} else {
paths.push((paths[paths.len() - 1].0 + 1, MANIFEST(&root)));
}
let mut prev = None;
for (id, path) in paths.into_iter() {
let (strs, info) = match Self::read_mani(&path) {
Ok(mani) => mani,
Err(err) => {
errs.push(err);
continue;
}
};
let edit = Self::to_edit(&strs, &info);
if let Some((prev_id, prev_edit)) = prev {
if prev_id + 1 != id {
errs.push(corruption(format!("MANIFEST has gaps at {id}")));
} else {
let first = match Self::read_first_edit(&path) {
Ok(first) => first,
Err(err) => {
errs.push(err);
prev = None;
continue;
}
};
if Some(prev_edit) != first {
errs.push(corruption(format!(
"MANIFEST rollover to {id} does not match rollup of {prev_id}"
)));
} else {
println!("{} rolled over properly", path.to_string_lossy());
}
}
}
prev = Some((id, edit));
}
errs.into_iter()
}
fn apply_edit(edit: &Edit, strs: &mut BTreeSet<String>, info: &mut BTreeMap<char, String>) {
for path in edit.rm_strs.iter() {
strs.remove(path);
}
for path in edit.add_strs.iter() {
strs.insert(String::from(path));
}
for (key, value) in edit.info.iter() {
info.insert(*key, value.clone());
}
}
fn to_edit(strs: &BTreeSet<String>, info: &BTreeMap<char, String>) -> Edit {
let mut edit = Edit::default();
for s in strs.iter() {
edit.add(s)
.expect("previously added string should always add");
}
for (c, s) in info.iter() {
edit.info(*c, s)
.expect("previously added info should always add");
}
edit
}
fn _apply(&mut self, output: &PathBuf, edit: Edit, allow_rollover: bool) -> Result<(), SError> {
let was_empty = self.strs.is_empty();
let mut edit_str = String::new();
Self::apply_edit(&edit, &mut self.strs, &mut self.info);
fn to_crc_line(line: String) -> String {
let cksum = crc32c::crc32c(line.as_bytes());
format!("{cksum:08x}{line}\n")
}
for path in edit.rm_strs.iter() {
edit_str += &to_crc_line("-".to_owned() + path);
}
for path in edit.add_strs.iter() {
edit_str += &to_crc_line("+".to_owned() + path);
}
for (key, value) in edit.info.iter() {
edit_str += &to_crc_line(format!("{key}{value}"));
}
edit_str += TX_SEPARATOR;
edit_str += "\n";
let mut fout = OpenOptions::new().create(true).append(true).open(output)?;
self.poison(fout.write_all(edit_str.as_bytes()))?;
self.poison(fout.flush())?;
self.poison(fout.sync_data())?;
if allow_rollover {
let on_disk_bytes = self.poison(metadata(output))?.len();
let in_memory_bytes = self.size();
if on_disk_bytes > self.options.log_rollover_ratio * in_memory_bytes && !was_empty {
self.rollover()?;
}
}
Ok(())
}
fn poison<T, E>(&mut self, res: Result<T, E>) -> Result<T, SError>
where
SError: From<E>,
{
match res {
Ok(t) => Ok(t),
Err(e) => {
if self.poison.is_none() {
self.poison = Some(e.into());
}
Err(self.poison.as_ref().unwrap().clone())
}
}
}
fn read_mani<P: AsRef<Path>>(
path: P,
) -> Result<(BTreeSet<String>, BTreeMap<char, String>), SError> {
let mut strs = BTreeSet::new();
let mut info = BTreeMap::new();
let iter = ManifestIterator::open(path)?;
for edit in iter {
let edit = edit?;
Self::apply_edit(&edit, &mut strs, &mut info);
}
Ok((strs, info))
}
fn read_first_edit<P: AsRef<Path>>(path: P) -> Result<Option<Edit>, SError> {
let mut iter = ManifestIterator::open(path)?;
match iter.next() {
Some(Ok(edit)) => Ok(Some(edit)),
Some(Err(err)) => Err(err),
None => Ok(None),
}
}
fn next_manifest_identifier<P: AsRef<Path>>(root: P) -> Result<u64, SError> {
let mut max_next_id = 0;
for dir in read_dir(root.as_ref())? {
let dir = dir?;
let path = dir.path();
if let Some(next_id) = extract_backup(path) {
max_next_id = std::cmp::max(max_next_id, next_id);
}
}
Ok(max_next_id + 1)
}
}
#[derive(Debug, Default, Eq, PartialEq)]
pub struct Edit {
add_strs: BTreeSet<String>,
rm_strs: BTreeSet<String>,
info: BTreeMap<char, String>,
}
impl Edit {
pub fn add(&mut self, s: &str) -> Result<(), SError> {
let s = Self::check_str(s)?;
self.add_strs.insert(s);
Ok(())
}
pub fn added(&self) -> impl Iterator<Item = &String> {
self.add_strs.iter()
}
pub fn rm(&mut self, s: &str) -> Result<(), SError> {
let s = Self::check_str(s)?;
self.rm_strs.insert(s);
Ok(())
}
pub fn rmed(&self) -> impl Iterator<Item = &String> {
self.rm_strs.iter()
}
pub fn info(&mut self, c: char, s: &str) -> Result<(), SError> {
Self::check_str(&c.to_string())?;
let s = Self::check_str(s)?;
self.info.insert(c, s);
Ok(())
}
pub fn get_info(&self, c: char) -> Option<&String> {
self.info.get(&c)
}
fn check_str(s: &str) -> Result<String, SError> {
if s.chars().any(|c| c == '\n') {
Err(newline_disallowed(
"added strings must not contain newlines",
))
} else {
Ok(s.to_owned())
}
}
}
pub struct ManifestIterator {
file: Option<BufReader<File>>,
poison: Option<SError>,
}
impl ManifestIterator {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, SError> {
if path.as_ref().is_dir() {
return Err(corruption("MANIFEST file is a directory"));
}
if !path.as_ref().is_file() {
return Ok(Self {
file: None,
poison: None,
});
}
let file = Some(BufReader::new(File::open(path)?));
Ok(Self { file, poison: None })
}
fn poison<E: Into<SError>>(&mut self, err: E) -> Option<Result<Edit, SError>> {
let err = err.into();
self.poison = Some(err.clone());
self.file = None;
Some(Err(err))
}
}
impl Iterator for ManifestIterator {
type Item = Result<Edit, SError>;
fn next(&mut self) -> Option<Self::Item> {
let file = match &mut self.file {
Some(file) => file,
None => {
return None;
}
};
let mut edit = Edit::default();
for (idx, line) in file.lines().enumerate() {
let line = match line {
Ok(line) => line,
Err(err) => {
return self.poison(err);
}
};
if !line.is_ascii() {
return Some(Err(corruption(format!("line {idx} is not ascii"))));
}
if line == TX_SEPARATOR {
return Some(Ok(edit));
} else if line.len() > 9 {
let crc32c_expected = match u32::from_str_radix(&line[..8], 16) {
Ok(crc32c_expected) => crc32c_expected,
Err(err) => {
return self.poison(corruption(format!(
"crc32c is not hex on line {idx}: {err}"
)));
}
};
if crc32c::crc32c(&line.as_bytes()[8..]) != crc32c_expected {
return self.poison(corruption(format!("crc32c failure on line {idx}")));
}
let action = line.as_bytes()[8] as char;
if action == '+' {
if let Err(err) = edit.add(&line[9..]) {
return self.poison(err);
}
} else if action == '-' {
if let Err(err) = edit.rm(&line[9..]) {
return self.poison(err);
}
} else if action == '\n' {
return self.poison(corruption("operation \\n is not supported"));
} else if let Err(err) = edit.info(action, &line[9..]) {
return self.poison(err);
}
} else {
return self.poison(corruption(format!("unhandled case on line {idx}")));
}
}
self.file = None;
None
}
}
#[cfg(test)]
mod tests {
use std::fs::{read_to_string, remove_dir_all};
use guacamole::{FromGuacamole, Guacamole};
use super::*;
fn test_root(root: &str, line: u32) -> PathBuf {
let root: String = root
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
let path = PathBuf::from(format!("{root}_{line}"));
if path.exists() {
remove_dir_all(&path).expect("could not prepare for test");
}
path
}
#[test]
fn lockfile_const() {
assert_eq!("/path/to/LOCKFILE", LOCKFILE("/path/to").to_string_lossy());
assert_eq!("/path/to/LOCKFILE", LOCKFILE("/path/to/").to_string_lossy());
}
#[test]
fn test_test_root() {
let line = line!();
let root = test_root(module_path!(), line);
assert_eq!(PathBuf::from(format!("mani__tests_{line}")), root);
}
#[test]
fn not_exist_defaults() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let _mani = Manifest::open(opts, &root).unwrap();
assert!(!root.join("MANIFEST").exists());
}
#[test]
fn not_exist_fail_if_not_exist() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions {
fail_if_not_exist: true,
..Default::default()
};
let err = match Manifest::open(opts, root) {
Ok(_) => panic!("expected manifest-not-exist error"),
Err(err) => err,
};
assert!(is_manifest_not_exist(&err));
}
#[test]
fn not_exist_fail_if_exists() {
let root = test_root(module_path!(), line!());
let mut opts = ManifestOptions::default();
let mut _mani = Manifest::open(opts.clone(), &root);
opts.fail_if_exists = true;
let err = match Manifest::open(opts, &root) {
Ok(_) => panic!("expected manifest-exists error"),
Err(err) => err,
};
assert!(is_manifest_exists(&err));
}
#[test]
fn simple_addition() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let mut mani = Manifest::open(opts.clone(), &root).unwrap();
let mut edit = Edit::default();
edit.add("thing one").unwrap();
edit.add("thing two").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
}
#[test]
fn removal() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let mut mani = Manifest::open(opts.clone(), &root).unwrap();
let mut edit = Edit::default();
edit.add("thing one").unwrap();
edit.add("thing two").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
let mut edit = Edit::default();
edit.rm("thing one").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"a4e79c62+thing two
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
assert!(root.join("MANIFEST").exists());
assert!(root.join("MANIFEST.1").exists());
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
--------
6c866914-thing one
--------
",
read_to_string(root.join("MANIFEST.1")).unwrap()
);
}
#[test]
fn open_after_rollover() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let mut mani = Manifest::open(opts.clone(), &root).unwrap();
let mut edit = Edit::default();
edit.add("thing one").unwrap();
edit.add("thing two").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
mani.rollover().unwrap();
drop(mani);
let _mani = Manifest::open(opts.clone(), &root).unwrap();
}
#[test]
fn info() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let mut mani = Manifest::open(opts.clone(), &root).unwrap();
let mut edit = Edit::default();
edit.add("thing one").unwrap();
edit.add("thing two").unwrap();
edit.info('C', "some-client-identifier").unwrap();
edit.info('A', "71332261daaa6dc30ad627b09349c6af").unwrap();
edit.info('R', "00000000000000000000000000000000").unwrap();
edit.info('T', "e0785f2a185aaf6fe0a099bc98ce1e70").unwrap();
edit.info('S', "9ba1e4d7aa7a39a91b00d90c36436414").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
4d82ac08A71332261daaa6dc30ad627b09349c6af
a2281ab8Csome-client-identifier
442a0186R00000000000000000000000000000000
1736a268S9ba1e4d7aa7a39a91b00d90c36436414
79810703Te0785f2a185aaf6fe0a099bc98ce1e70
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
}
#[test]
fn iterator() {
let root = test_root(module_path!(), line!());
let opts = ManifestOptions::default();
let mut mani = Manifest::open(opts.clone(), &root).unwrap();
let mut edit = Edit::default();
edit.add("thing one").unwrap();
edit.info('1', "thing one metadata").unwrap();
mani.apply(edit).unwrap();
let mut edit = Edit::default();
edit.add("thing two").unwrap();
edit.info('2', "thing two metadata").unwrap();
mani.apply(edit).unwrap();
assert_eq!(
"dcab9d28+thing one
a4e79c62+thing two
05a03b0d1thing one metadata
bc9dae362thing two metadata
--------
",
read_to_string(root.join("MANIFEST")).unwrap()
);
assert_eq!(
"dcab9d28+thing one
05a03b0d1thing one metadata
--------
a4e79c62+thing two
bc9dae362thing two metadata
--------
",
read_to_string(root.join("MANIFEST.1")).unwrap()
);
let mut iter = ManifestIterator::open(root.join("MANIFEST.1")).unwrap();
let edit = iter.next().unwrap().unwrap();
assert_eq!(1, edit.add_strs.len());
assert!(edit.add_strs.contains("thing one"));
assert_eq!(0, edit.rm_strs.len());
assert_eq!(1, edit.info.len());
assert_eq!(
Some("thing one metadata"),
edit.info.get(&'1').map(|s| s.as_str())
);
let edit = iter.next().unwrap().unwrap();
assert_eq!(1, edit.add_strs.len());
assert!(edit.add_strs.contains("thing two"));
assert_eq!(0, edit.rm_strs.len());
assert_eq!(1, edit.info.len());
assert_eq!(
Some("thing two metadata"),
edit.info.get(&'2').map(|s| s.as_str())
);
assert!(iter.next().is_none());
}
struct GuacamoleParameters {
iterations: usize,
num_strs: usize,
info_set: &'static [char],
num_to_add: u64,
num_to_rm: u64,
num_info: u64,
retries: u64,
seed: u64,
options: ManifestOptions,
}
fn build_string(params: &GuacamoleParameters, seed: usize) -> String {
let mut guac2 = Guacamole::new((seed % params.num_strs) as u64);
let val1 = u64::from_guacamole(&mut (), &mut guac2);
let val2 = u64::from_guacamole(&mut (), &mut guac2);
let val3 = u64::from_guacamole(&mut (), &mut guac2);
format!("string:{val1}_{val2}_{val3}")
}
fn build_info(params: &GuacamoleParameters, guac: &mut Guacamole) -> (char, String) {
let info_set_idx = usize::from_guacamole(&mut (), guac) % params.info_set.len();
assert!(info_set_idx < params.info_set.len());
(params.info_set[info_set_idx], {
let val1 = u64::from_guacamole(&mut (), guac);
let val2 = u64::from_guacamole(&mut (), guac);
let val3 = u64::from_guacamole(&mut (), guac);
format!("info:{val1}_{val2}_{val3}")
})
}
fn build_edit_randomly(
params: &GuacamoleParameters,
mani: &Manifest,
guac: &mut Guacamole,
) -> Edit {
let mut edit = Edit::default();
let num_to_add = u64::from_guacamole(&mut (), guac) % params.num_to_add;
let num_to_rm = u64::from_guacamole(&mut (), guac) % params.num_to_rm;
let num_info = u64::from_guacamole(&mut (), guac) % params.num_info;
'to_add: for _ in 0..num_to_add {
let mut retries = 0;
let s = loop {
let s = build_string(params, usize::from_guacamole(&mut (), guac));
if !mani.strs.contains(&s) {
break s;
}
if retries >= params.retries {
break 'to_add;
}
retries += 1;
};
edit.add(&s).unwrap();
}
'to_rm: for _ in 0..num_to_rm {
let mut retries = 0;
let s = loop {
let s = build_string(params, usize::from_guacamole(&mut (), guac));
if mani.strs.contains(&s) {
break s;
}
if retries >= params.retries {
break 'to_rm;
}
retries += 1;
};
edit.rm(&s).unwrap();
}
for _ in 0..num_info {
let (c, s) = build_info(params, guac);
edit.info(c, &s).unwrap();
}
edit
}
fn guacamole(root: PathBuf, params: GuacamoleParameters) {
let mut mani = Manifest::open(params.options.clone(), &root).unwrap();
let mut guac = Guacamole::new(params.seed);
for _ in 0..params.iterations {
let edit = build_edit_randomly(¶ms, &mani, &mut guac);
mani.apply(edit)
.expect("that the manifest will apply cleanly");
}
let mut found = false;
for err in Manifest::verify(params.options.clone(), &root) {
eprintln!("error: {err}");
found = true;
}
if found {
panic!("the test encountered one or more errors listed above");
}
}
fn seed_guacamole(root: PathBuf, seed: u64) {
let params = GuacamoleParameters {
iterations: 1_000,
num_strs: 1_000,
info_set: &['a', 'b', 'c', 'x', 'y', 'z'],
num_to_add: 16,
num_to_rm: 4,
num_info: 2,
retries: 5,
seed,
options: ManifestOptions::default(),
};
guacamole(root, params);
}
#[test]
fn guacamole5138398090444284702() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 5138398090444284702);
}
#[test]
fn guacamole8642048216479126580() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 8642048216479126580);
}
#[test]
fn guacamole12537599354512252906() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 12537599354512252906);
}
#[test]
fn guacamole6529534099269622277() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 6529534099269622277);
}
#[test]
fn guacamole10358298905320955451() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 10358298905320955451);
}
#[test]
fn guacamole16454412700383951410() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 16454412700383951410);
}
#[test]
fn guacamole9511704728995062883() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 9511704728995062883);
}
#[test]
fn guacamole8803705925841794254() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 8803705925841794254);
}
#[test]
fn guacamole5789036567548627700() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 5789036567548627700);
}
#[test]
fn guacamole5103614593344147373() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 5103614593344147373);
}
#[test]
fn guacamole11818449540631631494() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 11818449540631631494);
}
#[test]
fn guacamole8550330511375764379() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 8550330511375764379);
}
#[test]
fn guacamole6201227612047565798() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 6201227612047565798);
}
#[test]
fn guacamole1708810195070469314() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 1708810195070469314);
}
#[test]
fn guacamole16522073696062617966() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 16522073696062617966);
}
#[test]
fn guacamole5455191989502336247() {
let root = test_root(module_path!(), line!());
seed_guacamole(root, 5455191989502336247);
}
}