Skip to content

Commit

Permalink
Merge pull request #43 from palfrey/file-locking
Browse files Browse the repository at this point in the history
File locking
  • Loading branch information
palfrey committed Nov 21, 2021
2 parents d4bf34a + 93c9706 commit c16b9ed
Show file tree
Hide file tree
Showing 10 changed files with 664 additions and 213 deletions.
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

0 comments on commit c16b9ed

Please sign in to comment.