use shimforge::{Session, mock};
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use std::sync::{Mutex, MutexGuard};
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn serial() -> MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(|error| error.into_inner())
}
fn detached_handle() -> File {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
use std::os::fd::FromRawFd;
let mut ends = [0; 2];
let created = unsafe { libc::pipe(ends.as_mut_ptr()) };
assert_eq!(created, 0);
unsafe { libc::close(ends[1]) };
unsafe { File::from_raw_fd(ends[0]) }
}
#[cfg(target_os = "windows")]
{
use std::ffi::c_void;
use std::os::windows::io::FromRawHandle;
#[link(name = "kernel32")]
unsafe extern "system" {
fn CreatePipe(
read: *mut *mut c_void,
write: *mut *mut c_void,
attributes: *const c_void,
size: u32,
) -> i32;
fn CloseHandle(handle: *mut c_void) -> i32;
}
let mut read = std::ptr::null_mut();
let mut write = std::ptr::null_mut();
let created = unsafe { CreatePipe(&mut read, &mut write, std::ptr::null(), 0) };
assert_ne!(created, 0);
unsafe { CloseHandle(write) };
unsafe { File::from_raw_handle(read) }
}
}
fn prepare_cache(root: &Path) -> Result<(), String> {
fs::create_dir_all(root).map_err(|error| format!("cannot prepare cache: {error}"))?;
Ok(())
}
fn describe(path: &Path) -> &'static str {
if path.is_dir() {
"directory"
} else if path.exists() {
"file"
} else {
"missing"
}
}
fn read_header(path: &str) -> io::Result<String> {
let mut reader = BufReader::new(File::open(path)?);
let mut line = String::new();
reader.read_line(&mut line)?;
Ok(line.trim_end().to_owned())
}
fn append_entry(path: &str, entry: &str) -> io::Result<usize> {
let mut file = File::open(path)?;
file.write_all(entry.as_bytes())?;
Ok(entry.len())
}
#[test]
fn directory_creation_succeeds_without_creating_a_directory() {
let _serial = serial();
let root = Path::new("virtual/cache/reports");
assert!(!root.exists());
let mut session = Session::new();
let create = mock!(
session,
fs::create_dir_all::<&Path>,
fn(&Path) -> io::Result<()>
);
create
.expect()
.with(|path| **path == *Path::new("virtual/cache/reports"))
.once()
.returning(|_| Ok(()));
assert!(prepare_cache(root).is_ok());
session.verify();
assert!(!root.exists());
}
#[test]
fn directory_creation_failures_reach_the_caller() {
let _serial = serial();
let mut session = Session::new();
let create = mock!(
session,
fs::create_dir_all::<&Path>,
fn(&Path) -> io::Result<()>
);
create
.expect()
.once()
.returning(|_| Err(io::ErrorKind::PermissionDenied.into()));
let error = prepare_cache(Path::new("virtual/cache/reports")).unwrap_err();
assert!(error.starts_with("cannot prepare cache:"), "{error}");
}
#[test]
fn path_predicates_answer_for_paths_that_do_not_exist() {
let _serial = serial();
let missing = Path::new("virtual/archive");
assert_eq!(describe(missing), "missing");
{
let mut session = Session::new();
let is_dir = mock!(session, Path::is_dir, fn(&Path) -> bool);
is_dir.expect().once().returns(true);
assert_eq!(describe(missing), "directory");
session.verify();
}
{
let mut session = Session::new();
let is_dir = mock!(session, Path::is_dir, fn(&Path) -> bool);
is_dir.expect().once().returns(false);
let exists = mock!(session, Path::exists, fn(&Path) -> bool);
exists
.expect()
.with(|path| **path == *Path::new("virtual/archive"))
.once()
.returns(true);
assert_eq!(describe(missing), "file");
session.verify();
}
assert_eq!(describe(missing), "missing");
}
#[test]
fn a_predicate_mock_can_require_an_exact_number_of_questions() {
let _serial = serial();
let mut session = Session::new();
let is_dir = mock!(session, Path::is_dir, fn(&Path) -> bool);
let expected = is_dir.expect().times(3).returns(true);
for name in ["virtual/a", "virtual/b", "virtual/c"] {
assert!(Path::new(name).is_dir());
}
assert_eq!(expected.calls(), 3);
session.restore();
assert!(!Path::new("virtual/a").is_dir());
}
#[test]
fn a_line_is_read_from_a_handle_that_never_reaches_disk() {
let _serial = serial();
let mut session = Session::new();
let open = mock!(session, File::open::<&str>, fn(&str) -> io::Result<File>);
open.expect()
.with(|path| **path == *"virtual/inventory.csv")
.once()
.returning(|_| Ok(detached_handle()));
let read_line = mock!(
session,
BufReader::<File>::read_line,
fn(&mut BufReader<File>, &mut String) -> io::Result<usize>
);
read_line.expect().once().returning(|_, line| {
line.push_str("sku,count,location\n");
Ok(line.len())
});
assert_eq!(
read_header("virtual/inventory.csv").unwrap(),
"sku,count,location"
);
session.verify();
}
#[test]
fn a_write_is_accepted_without_a_writable_handle() {
let _serial = serial();
let mut session = Session::new();
let open = mock!(session, File::open::<&str>, fn(&str) -> io::Result<File>);
open.expect().once().returning(|_| Ok(detached_handle()));
let write_all = mock!(
session,
File::write_all,
fn(&mut File, &[u8]) -> io::Result<()>
);
write_all
.expect()
.with(|_, bytes| **bytes == *b"sku-77,3,aisle-2\n")
.once()
.returning(|_, _| Ok(()));
assert_eq!(
append_entry("virtual/inventory.csv", "sku-77,3,aisle-2\n").unwrap(),
17
);
session.verify();
}
#[test]
fn a_write_error_is_reported_to_the_caller() {
let _serial = serial();
let mut session = Session::new();
let open = mock!(session, File::open::<&str>, fn(&str) -> io::Result<File>);
open.expect().once().returning(|_| Ok(detached_handle()));
let write_all = mock!(
session,
File::write_all,
fn(&mut File, &[u8]) -> io::Result<()>
);
write_all
.expect()
.once()
.returning(|_, _| Err(io::ErrorKind::StorageFull.into()));
assert_eq!(
append_entry("virtual/inventory.csv", "sku-77,3,aisle-2\n")
.unwrap_err()
.kind(),
io::ErrorKind::StorageFull
);
}