Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File locking #43

Merged
merged 21 commits into from
Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- beta
- nightly
- 1.39.0
features:
- default
- file_locks
steps:
- uses: actions/checkout@v2.3.4
- uses: actions-rs/toolchain@v1.0.7
Expand All @@ -33,17 +36,42 @@ jobs:
with:
command: fmt
args: -- --check
if: ${{ matrix.features == 'default' }}
- name: Clippy
uses: actions-rs/cargo@v1.0.3
env:
RUSTFLAGS: -Dwarnings
with:
command: clippy
if: ${{ matrix.features == 'default' }}
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' }}
- name: Build and test
uses: actions-rs/cargo@v1.0.3
with:
command: test
args: --features ${{ matrix.features }}

multi-os-testing:
name: Test suite
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- windows-latest
- macos-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: actions-rs/toolchain@v1.0.7
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- name: Build and test
uses: actions-rs/cargo@v1.0.3
with:
command: test
args: --all-features

minimal-versions:
name: minimal versions check
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions serial_test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ keywords = ["sequential"]
lazy_static = "1.2"
parking_lot = ">= 0.10, < 0.12"
serial_test_derive = { version = "~0.5.1", path = "../serial_test_derive" }
fslock = {version = "0.2", optional = true}

[features]
default = []
file_locks = ["fslock"]

[package.metadata.docs.rs]
all-features = true
72 changes: 72 additions & 0 deletions serial_test/src/code_lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use lazy_static::lazy_static;
use parking_lot::ReentrantMutex;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, RwLock};

lazy_static! {
static ref LOCKS: Arc<RwLock<HashMap<String, ReentrantMutex<()>>>> =
Arc::new(RwLock::new(HashMap::new()));
}

fn check_new_key(name: &str) {
// Check if a new key is needed. Just need a read lock, which can be done in sync with everyone else
let new_key = {
let unlock = LOCKS.read().unwrap();
!unlock.deref().contains_key(name)
};
if new_key {
// This is the rare path, which avoids the multi-writer situation mostly
LOCKS
.write()
.unwrap()
.deref_mut()
.insert(name.to_string(), ReentrantMutex::new(()));
}
}

#[doc(hidden)]
pub fn local_serial_core_with_return<E>(
name: &str,
function: fn() -> Result<(), E>,
) -> Result<(), E> {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
function()
}

#[doc(hidden)]
pub fn local_serial_core(name: &str, function: fn()) {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
function();
}

#[doc(hidden)]
pub async fn local_async_serial_core_with_return<E>(
name: &str,
fut: impl std::future::Future<Output = Result<(), E>>,
) -> Result<(), E> {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
fut.await
}

#[doc(hidden)]
pub async fn local_async_serial_core(name: &str, fut: impl std::future::Future<Output = ()>) {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
fut.await;
}
86 changes: 86 additions & 0 deletions serial_test/src/file_lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use fslock::LockFile;
use std::{env, fs, path::Path};

struct Lock {
lockfile: LockFile,
}

impl Lock {
fn unlock(self: &mut Lock) {
self.lockfile.unlock().unwrap();
println!("Unlock");
}
}

fn do_lock(path: &str) -> Lock {
if !Path::new(path).exists() {
fs::write(path, "").unwrap_or_else(|_| panic!("Lock file path was {:?}", path))
}
let mut lockfile = LockFile::open(path).unwrap();
println!("Waiting on {:?}", path);
lockfile.lock().unwrap();
println!("Locked for {:?}", path);
Lock { lockfile }
}

fn path_for_name(name: &str) -> String {
let mut pathbuf = env::temp_dir();
pathbuf.push(format!("serial-test-{}", name));
pathbuf.into_os_string().into_string().unwrap()
}

fn make_lock_for_name_and_path(name: &str, path: Option<&str>) -> Lock {
if let Some(opt_path) = path {
do_lock(opt_path)
} else {
let default_path = path_for_name(name);
do_lock(&default_path)
}
}

#[doc(hidden)]
pub fn fs_serial_core(name: &str, path: Option<&str>, function: fn()) {
let mut lock = make_lock_for_name_and_path(name, path);
function();
lock.unlock();
}

#[doc(hidden)]
pub fn fs_serial_core_with_return<E>(
name: &str,
path: Option<&str>,
function: fn() -> Result<(), E>,
) -> Result<(), E> {
let mut lock = make_lock_for_name_and_path(name, path);
let ret = function();
lock.unlock();
ret
}

#[doc(hidden)]
pub async fn fs_async_serial_core_with_return<E>(
name: &str,
path: Option<&str>,
fut: impl std::future::Future<Output = Result<(), E>>,
) -> Result<(), E> {
let mut lock = make_lock_for_name_and_path(name, path);
let ret = fut.await;
lock.unlock();
ret
}

#[doc(hidden)]
pub async fn fs_async_serial_core(
name: &str,
path: Option<&str>,
fut: impl std::future::Future<Output = ()>,
) {
let mut lock = make_lock_for_name_and_path(name, path);
fut.await;
lock.unlock();
}

#[test]
fn test_serial() {
fs_serial_core("test", None, || {});
}
102 changes: 32 additions & 70 deletions serial_test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]

//! # serial_test
//! `serial_test` allows for the creation of serialised Rust tests using the [serial](attr.serial.html) attribute
//! `serial_test` allows for the creation of serialised Rust tests using the [serial](macro@serial) attribute
//! e.g.
//! ````
//! #[test]
Expand All @@ -14,79 +16,39 @@
//! // Do things
//! }
//! ````
//! Multiple tests with the [serial](attr.serial.html) attribute are guaranteed to be executed in serial. Ordering
//! Multiple tests with the [serial](macro@serial) attribute are guaranteed to be executed in serial. Ordering
//! of the tests is not guaranteed however.
//!
//! For cases like doctests and integration tests where the tests are run as separate processes, we also support
//! [file_serial](macro@file_serial), with similar properties but based off file locking. Note that there are no
//! guarantees about one test with [serial](macro@serial) and another with [file_serial](macro@file_serial)
//! as they lock using different methods.
//! ````
//! #[test]
//! #[file_serial]
//! fn test_serial_three() {
//! // Do things
//! }
//! ````

use lazy_static::lazy_static;
use parking_lot::ReentrantMutex;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, RwLock};

lazy_static! {
static ref LOCKS: Arc<RwLock<HashMap<String, ReentrantMutex<()>>>> =
Arc::new(RwLock::new(HashMap::new()));
}

fn check_new_key(name: &str) {
// Check if a new key is needed. Just need a read lock, which can be done in sync with everyone else
let new_key = {
let unlock = LOCKS.read().unwrap();
!unlock.deref().contains_key(name)
};
if new_key {
// This is the rare path, which avoids the multi-writer situation mostly
LOCKS
.write()
.unwrap()
.deref_mut()
.insert(name.to_string(), ReentrantMutex::new(()));
}
}

#[doc(hidden)]
pub fn serial_core_with_return<E>(name: &str, function: fn() -> Result<(), E>) -> Result<(), E> {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
function()
}

#[doc(hidden)]
pub fn serial_core(name: &str, function: fn()) {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
function();
}

#[doc(hidden)]
pub async fn async_serial_core_with_return<E>(
name: &str,
fut: impl std::future::Future<Output = Result<(), E>>,
) -> Result<(), E> {
check_new_key(name);

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
fut.await
}
mod code_lock;
#[cfg(feature = "file_locks")]
mod file_lock;

#[doc(hidden)]
pub async fn async_serial_core(name: &str, fut: impl std::future::Future<Output = ()>) {
check_new_key(name);
pub use code_lock::{
local_async_serial_core, local_async_serial_core_with_return, local_serial_core,
local_serial_core_with_return,
};

let unlock = LOCKS.read().unwrap();
// _guard needs to be named to avoid being instant dropped
let _guard = unlock.deref()[name].lock();
fut.await
}
#[cfg(feature = "file_locks")]
pub use file_lock::{
fs_async_serial_core, fs_async_serial_core_with_return, fs_serial_core,
fs_serial_core_with_return,
};

// Re-export #[serial].
// Re-export #[serial/file_serial].
#[allow(unused_imports)]
pub use serial_test_derive::serial;

#[cfg(feature = "file_locks")]
pub use serial_test_derive::file_serial;
4 changes: 2 additions & 2 deletions serial_test/tests/tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use serial_test::serial_core;
use serial_test::local_serial_core;

#[test]
fn test_empty_serial_call() {
serial_core("beta", || {
local_serial_core("beta", || {
println!("Bar");
});
}
Loading