use std::collections::VecDeque;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use crate::atom::{Atom, AtomTable};
use crate::io::{CompletionRing, IoCompletion, IoOp, IoResult, StatxData};
use crate::native::{
BifRegistryImpl, FileIoCompletion, FileIoContinuation, FileIoFacility, ProcessContext,
};
use crate::process::Process;
use crate::term::Term;
use crate::term::binary::Binary;
use crate::term::boxed::{Cons, Tuple};
use super::{del_dir, del_file, file_info, list_dir, make_dir, register_file_meta_bifs, rename};
const PID: u64 = 105;
#[derive(Default)]
struct MockRing {
next_op_id: Mutex<u64>,
submitted: Mutex<Vec<IoOp>>,
}
impl MockRing {
fn submitted(&self) -> Vec<IoOp> {
self.submitted.lock().expect("submitted lock").clone()
}
}
impl CompletionRing for MockRing {
fn submit(&self, op: IoOp) -> u64 {
self.submitted.lock().expect("submitted lock").push(op);
let mut next = self.next_op_id.lock().expect("next op id lock");
let op_id = *next;
*next += 1;
op_id
}
fn poll_completions(&self, _timeout: std::time::Duration) -> Vec<IoCompletion> {
Vec::new()
}
fn pending_count(&self) -> usize {
self.submitted.lock().map(|ops| ops.len()).unwrap_or(0)
}
fn shutdown(&self) {}
}
#[derive(Default)]
struct MockFileIoFacility {
ring: MockRing,
completions: Mutex<VecDeque<FileIoCompletion>>,
}
impl MockFileIoFacility {
fn push_completion(&self, continuation: FileIoContinuation, result: io::Result<IoResult>) {
self.completions
.lock()
.expect("completions lock")
.push_back(FileIoCompletion {
op_id: 1,
continuation,
completion: IoCompletion { op_id: 1, result },
});
}
fn submitted(&self) -> Vec<IoOp> {
self.ring.submitted()
}
}
impl FileIoFacility for MockFileIoFacility {
fn submit_file_io(&self, _pid: u64, op: IoOp, _continuation: FileIoContinuation) -> u64 {
self.ring.submit(op)
}
fn track_submitted_file_io(&self, _pid: u64, op_id: u64, continuation: FileIoContinuation) {
self.completions
.lock()
.expect("completions lock")
.push_back(FileIoCompletion {
op_id,
continuation,
completion: IoCompletion {
op_id,
result: Ok(IoResult::Completed),
},
});
}
fn take_file_io_completion(&self, _pid: u64) -> Option<FileIoCompletion> {
self.completions
.lock()
.expect("completions lock")
.pop_front()
}
fn cancel_pending_file_io_for_pid(&self, _pid: u64) {}
fn ring(&self) -> &dyn CompletionRing {
&self.ring
}
}
fn heap_context<'a>(
process: &'a mut Process,
atom_table: Arc<AtomTable>,
facility: Arc<MockFileIoFacility>,
) -> ProcessContext<'a> {
let mut context = ProcessContext::new();
context.set_atom_table(Some(atom_table));
context.set_file_io_facility(Some(facility));
context.attach_process(process, 0);
context
}
fn binary(context: &mut ProcessContext<'_>, bytes: &[u8]) -> Term {
context.alloc_binary(bytes).expect("binary allocation")
}
fn tuple(term: Term) -> Tuple {
Tuple::new(term).expect("tuple result")
}
fn temp_path(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("beamr-{name}-{}", std::process::id()));
path
}
fn remove_path(path: &PathBuf) {
match fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(_error) => {}
}
match fs::remove_dir(path) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(_error) => {}
}
}
#[test]
fn registers_file_metadata_bifs() {
let atom_table = AtomTable::with_common_atoms();
let registry = BifRegistryImpl::new();
register_file_meta_bifs(®istry, &atom_table).expect("registration");
let erlang = atom_table.intern("erlang");
for (name, arity) in [
("file_info", 1),
("list_dir", 1),
("make_dir", 1),
("del_file", 1),
("del_dir", 1),
("rename", 2),
] {
assert!(
registry
.lookup(erlang, atom_table.intern(name), arity)
.is_some(),
"missing erlang:{name}/{arity}"
);
}
}
#[test]
fn file_info_submits_statx_and_finishes_record_tuple() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockFileIoFacility::default());
let mut process = Process::new(PID, 256);
let mut context = heap_context(&mut process, Arc::clone(&atom_table), Arc::clone(&facility));
let filename = binary(&mut context, b"/tmp/beamr-stat-test");
let result = file_info(&[filename], &mut context).expect("submit placeholder");
assert_eq!(result, Term::atom(Atom::OK));
assert!(context.take_suspend().is_some());
assert!(matches!(
facility.submitted().as_slice(),
[IoOp::Statx {
dir_fd,
path,
flags,
mask,
}] if *dir_fd == libc::AT_FDCWD
&& path == &PathBuf::from("/tmp/beamr-stat-test")
&& *flags == libc::AT_SYMLINK_NOFOLLOW
&& *mask == super::statx_basic_stats_mask()
));
facility.push_completion(
FileIoContinuation::FileInfo,
Ok(IoResult::StatResult(StatxData {
mode: libc::S_IFREG as u32 | 0o644,
size: 12,
dev_major: 1,
dev_minor: 2,
inode: 99,
nlink: 1,
uid: 501,
gid: 20,
atime_sec: 10,
mtime_sec: 11,
ctime_sec: 12,
..StatxData::default()
})),
);
let result = file_info(&[filename], &mut context).expect("file_info tuple");
let tuple = tuple(result);
assert_eq!(tuple.arity(), 14);
assert_eq!(
tuple.get(0),
Some(Term::atom(atom_table.intern("file_info")))
);
assert_eq!(tuple.get(1), Some(Term::small_int(12)));
assert_eq!(tuple.get(2), Some(Term::atom(atom_table.intern("regular"))));
assert_eq!(
tuple.get(3),
Some(Term::atom(atom_table.intern("read_write")))
);
assert_eq!(tuple.get(4), Some(Term::small_int(10)));
assert_eq!(tuple.get(5), Some(Term::small_int(11)));
assert_eq!(tuple.get(6), Some(Term::small_int(12)));
assert_eq!(tuple.get(8), Some(Term::small_int(1)));
assert_eq!(tuple.get(11), Some(Term::small_int(99)));
}
#[test]
fn list_dir_returns_ok_list_of_filename_binaries() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockFileIoFacility::default());
let mut process = Process::new(PID, 256);
let mut context = heap_context(&mut process, atom_table, Arc::clone(&facility));
let dirname = binary(&mut context, b"/tmp/beamr-list-test");
let result = list_dir(&[dirname], &mut context).expect("submit placeholder");
assert_eq!(result, Term::atom(Atom::OK));
assert!(matches!(
facility.submitted().as_slice(),
[IoOp::ListDir { path }] if path == &PathBuf::from("/tmp/beamr-list-test")
));
facility.push_completion(
FileIoContinuation::ListDir,
Ok(IoResult::DirList(vec![
b"a.txt".to_vec(),
b"b.bin".to_vec(),
])),
);
let result = list_dir(&[dirname], &mut context).expect("list_dir result");
let tuple = tuple(result);
assert_eq!(tuple.get(0), Some(Term::atom(Atom::OK)));
let mut tail = tuple.get(1).expect("list");
let mut names = Vec::new();
while tail != Term::NIL {
let cons = Cons::new(tail).expect("cons");
names.push(
Binary::new(cons.head())
.expect("filename binary")
.as_bytes()
.to_vec(),
);
tail = cons.tail();
}
assert_eq!(names, vec![b"a.txt".to_vec(), b"b.bin".to_vec()]);
}
#[test]
fn completion_handlers_reject_mismatched_file_io_continuations() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockFileIoFacility::default());
let mut process = Process::new(PID, 256);
let mut context = heap_context(&mut process, Arc::clone(&atom_table), Arc::clone(&facility));
let filename = binary(&mut context, b"/tmp/beamr-mismatched-completion");
facility.push_completion(
FileIoContinuation::ListDir,
Ok(IoResult::StatResult(StatxData {
mode: libc::S_IFREG as u32,
..StatxData::default()
})),
);
let result = file_info(&[filename], &mut context).expect("file_info mismatch result");
assert_error_reason(result, Atom::UNKNOWN_ERROR);
facility.push_completion(
FileIoContinuation::FileInfo,
Ok(IoResult::DirList(vec![b"entry".to_vec()])),
);
let result = list_dir(&[filename], &mut context).expect("list_dir mismatch result");
assert_error_reason(result, Atom::UNKNOWN_ERROR);
facility.push_completion(FileIoContinuation::FileInfo, Ok(IoResult::Completed));
let result = make_dir(&[filename], &mut context).expect("make_dir mismatch result");
assert_error_reason(result, Atom::UNKNOWN_ERROR);
}
#[test]
fn metadata_error_completion_returns_errno_reason_tuple() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockFileIoFacility::default());
let mut process = Process::new(PID, 256);
let mut context = heap_context(&mut process, atom_table, Arc::clone(&facility));
let filename = binary(&mut context, b"/tmp/beamr-missing-file");
facility.push_completion(
FileIoContinuation::DelFile,
Err(io::Error::from_raw_os_error(libc::ENOENT)),
);
let result = del_file(&[filename], &mut context).expect("del_file error result");
assert_error_reason(result, Atom::ENOENT);
}
#[test]
fn metadata_operations_submit_and_finish_ok() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockFileIoFacility::default());
let mut process = Process::new(PID, 256);
let mut context = heap_context(&mut process, atom_table, Arc::clone(&facility));
let dir = binary(&mut context, b"/tmp/beamr-meta-dir");
let file = binary(&mut context, b"/tmp/beamr-meta-dir/file");
let renamed = binary(&mut context, b"/tmp/beamr-meta-dir/renamed");
assert_eq!(
make_dir(&[dir], &mut context).expect("make_dir submit"),
Term::atom(Atom::OK)
);
facility.push_completion(FileIoContinuation::MakeDir, Ok(IoResult::Completed));
assert_eq!(
make_dir(&[dir], &mut context).expect("make_dir completion"),
Term::atom(Atom::OK)
);
assert_eq!(
rename(&[file, renamed], &mut context).expect("rename submit"),
Term::atom(Atom::OK)
);
facility.push_completion(FileIoContinuation::Rename, Ok(IoResult::Completed));
assert_eq!(
rename(&[file, renamed], &mut context).expect("rename completion"),
Term::atom(Atom::OK)
);
assert_eq!(
del_file(&[renamed], &mut context).expect("del_file submit"),
Term::atom(Atom::OK)
);
facility.push_completion(FileIoContinuation::DelFile, Ok(IoResult::Completed));
assert_eq!(
del_file(&[renamed], &mut context).expect("del_file completion"),
Term::atom(Atom::OK)
);
assert_eq!(
del_dir(&[dir], &mut context).expect("del_dir submit"),
Term::atom(Atom::OK)
);
facility.push_completion(FileIoContinuation::DelDir, Ok(IoResult::Completed));
assert_eq!(
del_dir(&[dir], &mut context).expect("del_dir completion"),
Term::atom(Atom::OK)
);
assert!(matches!(
facility.submitted().as_slice(),
[
IoOp::MakeDir { .. },
IoOp::Rename { .. },
IoOp::DelFile { .. },
IoOp::DelDir { .. },
]
));
}
#[test]
fn blocking_helpers_cover_real_directory_cycle() {
let dir = temp_path("meta-cycle-dir");
let file = dir.join("file.txt");
let renamed = dir.join("renamed.txt");
remove_path(&file);
remove_path(&renamed);
remove_path(&dir);
fs::create_dir(&dir).expect("create temp dir");
fs::write(&file, b"beamr").expect("create file");
let entries = super::read_dir_entries(&dir).expect("list temp dir");
assert!(entries.contains(&b"file.txt".to_vec()));
fs::rename(&file, &renamed).expect("rename file");
fs::remove_file(&renamed).expect("remove file");
fs::remove_dir(&dir).expect("remove dir");
}
fn assert_error_reason(result: Term, reason: Atom) {
let tuple = tuple(result);
assert_eq!(tuple.arity(), 2);
assert_eq!(tuple.get(0), Some(Term::atom(Atom::ERROR)));
assert_eq!(tuple.get(1), Some(Term::atom(reason)));
}