moirai-async 0.7.0

Async/await support for Moirai concurrency library
Documentation
use super::*;

mod blocking;
mod ordering;
use crate::io::{AsyncLength, AsyncReadAt};
use std::io::SeekFrom;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

fn test_path(name: &str) -> PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock must be after unix epoch")
        .as_nanos();
    std::env::temp_dir().join(format!(
        "moirai_async_fs_{name}_{}_{}",
        std::process::id(),
        nonce
    ))
}

#[test]
fn test_file_options() {
    let options = FileOpenOptions::read_only();
    assert!(options.read);
    assert!(!options.write);

    let options = FileOpenOptions::write_only();
    assert!(!options.read);
    assert!(options.write);

    let options = FileOpenOptions::append_only();
    assert!(!options.read);
    assert!(options.write);
    assert!(options.append);
}

#[test]
fn test_file_stats() {
    let stats = FileStats::default();
    assert_eq!(stats.bytes_read, 0);
    assert_eq!(stats.bytes_written, 0);
    assert_eq!(stats.read_operations, 0);
    assert_eq!(stats.write_operations, 0);
    assert_eq!(stats.seek_operations, 0);
}

#[test]
fn test_file_write_read_append_and_stats_values() {
    let path = test_path("roundtrip.txt");
    futures::executor::block_on(async {
        write_str(&path, "alpha")
            .await
            .expect("write_str must succeed");
        append_str(&path, "-beta")
            .await
            .expect("append_str must succeed");

        let contents = read_to_string(&path)
            .await
            .expect("read_to_string must succeed");
        assert_eq!(contents, "alpha-beta");

        let mut file = File::open(&path).await.expect("open must succeed");
        let mut prefix = [0_u8; 5];
        let bytes_read = file.read(&mut prefix).await.expect("read must succeed");
        assert_eq!(bytes_read, 5);
        assert_eq!(&prefix, b"alpha");
        assert_eq!(file.stats().bytes_read, 5);
        assert_eq!(file.stats().read_operations, 1);

        let position = file
            .stream_position()
            .await
            .expect("stream_position must succeed");
        assert_eq!(position, 5);
        let new_position = file
            .seek(SeekFrom::Start(6))
            .await
            .expect("seek must succeed");
        assert_eq!(new_position, 6);
        assert_eq!(file.stats().seek_operations, 1);
    });
    std::fs::remove_file(&path).expect("test file cleanup must succeed");
}

#[test]
fn test_file_positioned_read_preserves_cursor_length_and_stream_stats() {
    let path = test_path("positioned.bin");
    let expected = b"0123456789";
    std::fs::write(&path, expected).expect("positioned source write must succeed");

    futures::executor::block_on(async {
        let mut file = File::open(&path).await.expect("open must succeed");
        let mut prefix = [0_u8; 2];
        let prefix_read = file
            .read(&mut prefix)
            .await
            .expect("stream read must succeed");
        assert_eq!(prefix_read, prefix.len());
        assert_eq!(&prefix, b"01");

        let mut positioned = [0_u8; 4];
        file.read_at(6, &mut positioned)
            .await
            .expect("positioned read must succeed");
        assert_eq!(&positioned, b"6789");
        assert_eq!(
            file.stream_position().await.expect("position must succeed"),
            2
        );
        assert_eq!(
            file.len().await.expect("length must succeed"),
            u64::try_from(expected.len()).expect("fixture length fits u64")
        );
        assert!(!file.is_empty().await.expect("empty query must succeed"));
        assert_eq!(file.stats().bytes_read, 2);
        assert_eq!(file.stats().read_operations, 1);
    });

    std::fs::remove_file(&path).expect("positioned file cleanup must succeed");
}

#[test]
fn test_file_positioned_read_reports_short_source() {
    let path = test_path("positioned-short.bin");
    std::fs::write(&path, b"0123456789").expect("short positioned source write must succeed");

    futures::executor::block_on(async {
        let file = File::open(&path).await.expect("open must succeed");
        let mut output = [0_u8; 4];
        let error = file
            .read_at(8, &mut output)
            .await
            .expect_err("positioned read must reject a short source");
        assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof);
        assert_eq!(&output[..2], b"89");
    });

    std::fs::remove_file(&path).expect("short positioned file cleanup must succeed");
}

#[test]
fn test_file_positioned_read_validates_empty_and_overflow_ranges() {
    let path = test_path("positioned-range.bin");
    std::fs::write(&path, b"").expect("positioned source write must succeed");

    futures::executor::block_on(async {
        let file = File::open(&path).await.expect("open must succeed");
        assert_eq!(file.len().await.expect("length must succeed"), 0);
        assert!(file.is_empty().await.expect("empty query must succeed"));

        let mut empty = [];
        file.read_at(u64::MAX, &mut empty)
            .await
            .expect("empty positioned read must not inspect its offset");

        let mut output = [0xA5_u8; 1];
        let error = file
            .read_at(u64::MAX, &mut output)
            .await
            .expect_err("overflowing range must fail before I/O");
        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
        assert_eq!(output, [0xA5]);
    });

    std::fs::remove_file(&path).expect("positioned range file cleanup must succeed");
}

#[test]
fn test_file_copy_and_directory_values() {
    let dir = test_path("dir");
    let source = dir.join("source.bin");
    let dest = dir.join("dest.bin");
    let renamed = dir.join("renamed.bin");
    futures::executor::block_on(async {
        create_dir(&dir).await.expect("create_dir must succeed");
        assert!(dir.is_dir());
        write(&source, b"0123456789")
            .await
            .expect("source write must succeed");
        let copied = copy(&source, &dest).await.expect("copy must succeed");
        assert_eq!(copied, 10);
        let dest_bytes = read(&dest).await.expect("read copied file must succeed");
        assert_eq!(dest_bytes, b"0123456789");
        let dest_metadata = metadata(&dest).await.expect("metadata must succeed");
        assert!(dest_metadata.is_file());
        assert_eq!(dest_metadata.len(), 10);
        rename(&dest, &renamed).await.expect("rename must succeed");
        assert!(!dest.exists());
        let renamed_bytes = read(&renamed)
            .await
            .expect("read renamed file must succeed");
        assert_eq!(renamed_bytes, b"0123456789");
        remove_file(&source)
            .await
            .expect("remove source must succeed");
        assert!(!source.exists());
        remove_file(&renamed)
            .await
            .expect("remove renamed dest must succeed");
        assert!(!renamed.exists());
        remove_dir(&dir).await.expect("remove dir must succeed");
        assert!(!dir.exists());
    });
}

#[test]
fn test_recursive_directory_values() {
    let root = test_path("recursive-dir");
    let leaf = root.join("a").join("b").join("c");
    let marker = leaf.join("marker.bin");
    let expected: Vec<u8> = (0_u8..=63).map(|value| value.wrapping_mul(29)).collect();

    futures::executor::block_on(async {
        create_dir_all(&leaf)
            .await
            .expect("create_dir_all must succeed");
        assert!(leaf.is_dir());
        write(&marker, &expected)
            .await
            .expect("nested marker write must succeed");
        let actual = read(&marker)
            .await
            .expect("nested marker read must succeed");
        assert_eq!(actual, expected);
        remove_dir_all(&root)
            .await
            .expect("remove_dir_all must succeed");
        assert!(!root.exists());
    });
}