async-fd-lock 0.2.0

Advisory cross-platform file locks using file descriptors with async support by spawning blocking tasks.
Documentation
use adaptor::*;
use futures::future::join_all;
use paste::paste;
use std::io::ErrorKind;
use tempfile::tempdir;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

mod adaptor;

macro_rules! generate_tests {
    ($($blocking_first:ident)?, $prefix_first:ident; $($blocking_second:ident)?, $prefix_second:ident) => {
        paste! {
            #[tokio::test]
            async fn [<$($blocking_first _)? $prefix_first _read_ $($blocking_second _)? $prefix_second _read_lock>]() {
                let dir = tempdir().unwrap();
                let path = dir.path().join("lockfile");

                let l0 = $($blocking_first::)? file_create(&path).await.unwrap();
                let l1 = $($blocking_second::)? file_open(path).await.unwrap();

                let _g0 = l0.[<$prefix_first _lock_read_async>]().await.unwrap();
                let _g1 = l1.[<$prefix_second _lock_read_async>]().await.unwrap();
            }

            #[tokio::test]
            async fn [<$($blocking_first _)? $prefix_first _write_ $($blocking_second _)? $prefix_second _write_lock>]() {
                let dir = tempdir().unwrap();
                let path = dir.path().join("lockfile");

                #[allow(unused_mut)]
                let mut l0 = $($blocking_first::)? file_create(&path).await.unwrap();
                #[allow(unused_mut)]
                let mut l1 = $($blocking_second::)? file_open(path).await.unwrap();

                let g0 = l0.[<$prefix_first _lock_write_async>]().await.unwrap();
                let (l1, err) = l1.[<$prefix_second _lock_write_async>]().await.unwrap_err().into();

                assert!(matches!(err.kind(), ErrorKind::WouldBlock));
                drop(g0);

                let _g1 = l1.[<$prefix_second _lock_write_async>]().await.unwrap();
            }

            #[tokio::test]
            async fn [<$($blocking_first _)? $prefix_first _read_ $($blocking_second _)? $prefix_second _write_lock>]() {
                let dir = tempdir().unwrap();
                let path = dir.path().join("lockfile");

                let l0 = $($blocking_first::)? file_create(&path).await.unwrap();
                #[allow(unused_mut)]
                let mut l1 = $($blocking_second::)? file_open(path).await.unwrap();

                let g0 = l0.[<$prefix_first _lock_read_async>]().await.unwrap();
                let (l1, err) = l1.[<$prefix_second _lock_write_async>]().await.unwrap_err().into();

                assert!(matches!(err.kind(), ErrorKind::WouldBlock));
                drop(g0);

                let _g1 = l1.[<$prefix_second _lock_write_async>]().await.unwrap();
            }

            #[tokio::test]
            async fn [<$($blocking_first _)? $prefix_first _write_ $($blocking_second _)? $prefix_second _read_lock>]() {
                let dir = tempdir().unwrap();
                let path = dir.path().join("lockfile");

                #[allow(unused_mut)]
                let mut l0 = $($blocking_first::)? file_create(&path).await.unwrap();
                let l1 = $($blocking_second::)? file_open(path).await.unwrap();

                let g0 = l0.[<$prefix_first _lock_write_async>]().await.unwrap();
                let (l1, err) = l1.[<$prefix_second _lock_read_async>]().await.unwrap_err().into();

                assert!(matches!(err.kind(), ErrorKind::WouldBlock));
                drop(g0);

                let _g1 = l1.[<$prefix_second _lock_read_async>]().await.unwrap();
            }
        }
    };
}

generate_tests!(blocking, try;     blocking, try);
generate_tests!(blocking, try;             , try);
generate_tests!(        , try;     blocking, try);
generate_tests!(        , try;             , try);

generate_tests!(blocking, try;             , timeout);
generate_tests!(        , try;             , timeout);

generate_tests!(        , timeout; blocking, try);
generate_tests!(        , timeout;         , try);

generate_tests!(        , timeout;         , timeout);

#[tokio::test]
async fn io_read() {
    const BYTES: &[u8] = b"Hello, world!";

    let dir = tempdir().unwrap();
    let path = dir.path().join("lockfile");

    {
        let file = tokio::fs::File::create(&path).await.unwrap(); // Create the file.
        let mut guard = file.try_lock_write_async().await.unwrap();

        {
            let file = tokio::fs::File::open(&path).await.unwrap(); // Create the file.
            let _ = file.try_lock_write_async().await.unwrap_err();
        }

        guard.write_all(BYTES).await.unwrap();
    }

    {
        let guards = join_all((0..5).map(|_| async {
            let file = File::open(&path).await.unwrap(); // Open it in read-only mode.
            file.try_lock_read_async().await.unwrap()
        }))
        .await;
        join_all(guards.into_iter().map(|mut guard| async move {
            let mut buffer = Vec::new();
            guard.read_to_end(&mut buffer).await.unwrap();
            assert_eq!(&buffer, BYTES);
        }))
        .await;
    }
}

#[cfg(windows)]
mod windows {
    use super::*;
    use async_fd_lock::blocking::{LockRead, LockWrite};
    use std::os::windows::fs::OpenOptionsExt;

    #[test]
    fn try_lock_error() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("lockfile");

        // On Windows, opening with an access_mode as 0 will prevent all locking operations from succeeding, simulating an I/O error.
        let l0 = std::fs::File::options()
            .create(true)
            .read(true)
            .write(true)
            .truncate(true)
            .access_mode(0)
            .open(path)
            .unwrap();

        let (l0, err1) = l0.try_lock_read().unwrap_err().into();
        assert!(matches!(err1.kind(), ErrorKind::PermissionDenied));

        let (_l0, err2) = l0.try_lock_write().unwrap_err().into();
        assert!(matches!(err2.kind(), ErrorKind::PermissionDenied));
    }
}