use std::collections::{BTreeMap, VecDeque};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::time::SystemTime;
use crate::{Clock, Env, FileSystem, Invocation, Output, ProcessRunner};
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug, Clone, Copy)]
pub struct FixedClock(SystemTime);
impl FixedClock {
pub const fn new(at: SystemTime) -> Self {
Self(at)
}
}
impl Clock for FixedClock {
fn now(&self) -> SystemTime {
self.0
}
}
#[derive(Debug, Clone, Default)]
pub struct MapEnv(BTreeMap<String, String>);
impl MapEnv {
#[must_use]
pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.0.insert(key.into(), value.into());
self
}
}
impl Env for MapEnv {
fn var(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}
#[derive(Debug, Default)]
pub struct MemoryFs {
files: Mutex<BTreeMap<PathBuf, Vec<u8>>>,
failures: Mutex<VecDeque<io::ErrorKind>>,
}
impl MemoryFs {
pub fn fail_next(&self, kind: io::ErrorKind) {
lock(&self.failures).push_back(kind);
}
fn injected_failure(&self) -> io::Result<()> {
lock(&self.failures)
.pop_front()
.map_or(Ok(()), |kind| Err(io::Error::from(kind)))
}
}
impl FileSystem for MemoryFs {
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
self.injected_failure()?;
lock(&self.files)
.get(path)
.cloned()
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))
}
fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
self.injected_failure()?;
lock(&self.files).insert(path.to_path_buf(), contents.to_vec());
Ok(())
}
fn exists(&self, path: &Path) -> bool {
lock(&self.files).contains_key(path)
}
}
#[derive(Debug, Default)]
pub struct ScriptedRunner {
results: Mutex<VecDeque<io::Result<Output>>>,
seen: Mutex<Vec<Invocation>>,
}
impl ScriptedRunner {
pub fn push(&self, result: io::Result<Output>) {
lock(&self.results).push_back(result);
}
pub fn invocations(&self) -> Vec<Invocation> {
lock(&self.seen).clone()
}
}
impl ProcessRunner for ScriptedRunner {
fn run(&self, invocation: &Invocation) -> io::Result<Output> {
lock(&self.seen).push(invocation.clone());
lock(&self.results)
.pop_front()
.unwrap_or_else(|| Err(io::Error::from(io::ErrorKind::NotFound)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn memory_fs_reports_missing_files_and_existence() {
let fs = MemoryFs::default();
let path = Path::new("pyproject.toml");
assert!(!fs.exists(path));
assert_eq!(fs.read(path).unwrap_err().kind(), io::ErrorKind::NotFound);
fs.write(path, b"[tool.odx]\n").unwrap();
assert!(fs.exists(path));
}
#[test]
fn memory_fs_write_failure_is_consumed_once() {
let fs = MemoryFs::default();
fs.fail_next(io::ErrorKind::StorageFull);
let path = Path::new("odx.lock");
assert_eq!(
fs.write(path, b"x").unwrap_err().kind(),
io::ErrorKind::StorageFull
);
fs.write(path, b"x").unwrap();
assert!(fs.exists(path));
}
#[test]
fn scripted_runner_without_results_reports_missing_program() {
let runner = ScriptedRunner::default();
let err = runner.run(&Invocation::new("mergiraf")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert_eq!(runner.invocations().len(), 1);
}
#[test]
fn lock_recovers_from_poisoned_mutex() {
let shared = Arc::new(Mutex::new(1));
let poisoner = Arc::clone(&shared);
let joined = thread::spawn(move || {
let _guard = poisoner.lock().unwrap();
panic!("poison the mutex");
})
.join();
assert!(joined.is_err());
assert_eq!(*lock(&shared), 1);
}
}