Initial version

This commit is contained in:
Mark Hildreth 2021-01-17 15:57:52 -05:00
parent c51fa67447
commit daf14d6449
7 changed files with 396 additions and 0 deletions

55
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Rust
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --all
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets -- -D warnings

24
Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "test-context"
version = "0.1.0"
edition = "2018"
description = "A library for providing custom setup/teardown for Rust tests without needing a test harness"
homepage = "https://github.com/markhildreth/test-context"
repository = "https://github.com/markhildreth/test-context"
readme = "README.md"
authors = ["Mark Hildreth <mark.k.hildreth@gmail.com>"]
license = "MIT"
categories = ["development-tools::testing"]
[dependencies]
test-context-macros = { version = "0.1.0", path = "macros" }
async-trait = "0.1.42"
[dev-dependencies]
tokio = { version = "1.0", features = ["macros", "rt"]}
futures = "0.3.12"
[workspace]
members = [
"macros",
]

56
README.md Normal file
View file

@ -0,0 +1,56 @@
# test-context
A library for providing custom setup/teardown for Rust tests without needing a test harness.
```rust
use test_context::{test_context, TestContext};
struct MyContext {
value: String
}
impl TestContext for MyContext {
fn setup() -> MyContext {
MyContext { value: "Hello, world!".to_string() }
}
fn teardown(self) {
// Perform any teardown you wish.
}
}
#[test_context(MyContext)]
#[test]
fn test_works(ctx: &mut MyContext) {
assert_eq!(ctx.value, "Hello, world!");
}
```
Works with other test wrappers like [`actix_rt::test`](https://docs.rs/actix-rt/1.1.1/actix_rt/attr.test.html) or
[`tokio::test`](https://docs.rs/tokio/1.0.2/tokio/attr.test.html) that turn your test function into an async
function.
```rust
use test_context::{test_context, AsyncTestContext};
struct MyAsyncContext {
value: String
}
#[async_trait::async_trait]
impl AsyncTestContext for MyAsyncContext {
async fn setup() -> MyAsyncContext {
MyAsyncContext { value: "Hello, world!".to_string() }
}
async fn teardown(self) {
// Perform any teradown you wish.
}
}
#[test_context(MyAsyncContext)]
#[tokio::test]
async fn test_works(ctx: &mut MyAsyncContext) {
assert_eq!(ctx.value, "Hello, World!");
}
```

17
macros/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "test-context-macros"
version = "0.1.0"
edition = "2018"
description = "Macro crate for test-context"
homepage = "https://github.com/markhildreth/test-context"
repository = "https://github.com/markhildreth/test-context"
authors = ["Mark Hildreth <mark.k.hildreth@gmail.com>"]
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
quote = "1.0.3"
syn = { version = "^1", features = ["full"] }

85
macros/src/lib.rs Normal file
View file

@ -0,0 +1,85 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
/// Macro to use on tests to add the setup/teardown functionality of your context.
///
/// Ordering of this attribute is important, and typically `test_context` should come
/// before other test attributes. For example, the following is valid:
///
/// ```ignore
/// #[test_context(MyContext)]
/// #[test]
/// #[ignore]
/// fn my_test() {
/// }
/// ```
///
/// The following is NOT valid...
///
/// ```ignore
/// #[test]
/// #[ignore]
/// #[test_context(MyContext)]
/// fn my_test() {
/// }
/// ```
#[proc_macro_attribute]
pub fn test_context(attr: TokenStream, item: TokenStream) -> TokenStream {
let context_type = syn::parse_macro_input!(attr as syn::Ident);
let input = syn::parse_macro_input!(item as syn::ItemFn);
let ret = &input.sig.output;
let name = &input.sig.ident;
let inner_body = &input.block;
let attrs = &input.attrs;
let is_async = input.sig.asyncness.is_some();
let wrapped_name = format_ident!("__test_context_wrapped_{}", name);
let outer_body = if is_async {
quote! {
{
use futures::FutureExt;
let mut ctx = <#context_type as test_context::AsyncTestContext>::setup().await;
let wrapped_ctx = &mut ctx;
let result = async move {
std::panic::AssertUnwindSafe(
#wrapped_name(wrapped_ctx)
).catch_unwind().await
}.await;
<#context_type as test_context::AsyncTestContext>::teardown(ctx).await;
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
}
} else {
quote! {
{
let mut ctx = <#context_type as test_context::TestContext>::setup();
let mut wrapper = std::panic::AssertUnwindSafe(&mut ctx);
let result = std::panic::catch_unwind(move || {
#wrapped_name(*wrapper);
});
<#context_type as test_context::TestContext>::teardown(ctx);
if let Err(err) = result {
std::panic::resume_unwind(err);
}
}
}
};
let async_tag = if is_async {
quote! { async }
} else {
quote! {}
};
let result = quote! {
#(#attrs)*
#async_tag fn #name() #ret #outer_body
#async_tag fn #wrapped_name(ctx: &mut #context_type) #ret #inner_body
};
result.into()
}

83
src/lib.rs Normal file
View file

@ -0,0 +1,83 @@
//! A library for providing custom setup/teardown for Rust tests without needing a test harness.
//!
//! ```
//! use test_context::{test_context, TestContext};
//!
//! struct MyContext {
//! value: String
//! }
//!
//! impl TestContext for MyContext {
//! fn setup() -> MyContext {
//! MyContext { value: "Hello, world!".to_string() }
//! }
//!
//! fn teardown(self) {
//! // Perform any teardown you wish.
//! }
//! }
//!
//! #[test_context(MyContext)]
//! #[test]
//! fn test_works(ctx: &mut MyContext) {
//! assert_eq!(ctx.value, "Hello, world!");
//! }
//! ```
//!
//! Works with other test wrappers like [`actix_rt::test`](https://docs.rs/actix-rt/1.1.1/actix_rt/attr.test.html) or
//! [`tokio::test`](https://docs.rs/tokio/1.0.2/tokio/attr.test.html) that turn your test function into an async
//! function.
//!
//! ```
//! use test_context::{test_context, AsyncTestContext};
//!
//! struct MyAsyncContext {
//! value: String
//! }
//!
//! #[async_trait::async_trait]
//! impl AsyncTestContext for MyAsyncContext {
//! async fn setup() -> MyAsyncContext {
//! MyAsyncContext { value: "Hello, world!".to_string() }
//! }
//!
//! async fn teardown(self) {
//! // Perform any teradown you wish.
//! }
//! }
//!
//! #[test_context(MyAsyncContext)]
//! #[tokio::test]
//! async fn test_works(ctx: &mut MyAsyncContext) {
//! assert_eq!(ctx.value, "Hello, World!");
//! }
//! ```
pub use test_context_macros::test_context;
/// The trait to implement to get setup/teardown functionality for tests.
pub trait TestContext
where
Self: Sized,
{
/// Create the context. This is run once before each test that uses the context.
fn setup() -> Self;
/// Perform any additional cleanup of the context besides that already provided by
/// normal "drop" semantics.
fn teardown(self) {}
}
/// The trait to implement to get setup/teardown functionality for async tests.
#[async_trait::async_trait]
pub trait AsyncTestContext
where
Self: Sized,
{
/// Create the context. This is run once before each test that uses the context.
async fn setup() -> Self;
/// Perform any additional cleanup of the context besides that already provided by
/// normal "drop" semantics.
async fn teardown(self) {}
}

76
tests/test.rs Normal file
View file

@ -0,0 +1,76 @@
use test_context::{test_context, AsyncTestContext, TestContext};
struct Context {
n: u32,
}
impl TestContext for Context {
fn setup() -> Self {
Self { n: 1 }
}
fn teardown(self) {
if self.n != 1 {
panic!("Number changed");
}
}
}
#[test_context(Context)]
#[test]
fn test_sync_setup(ctx: &mut Context) {
assert_eq!(ctx.n, 1);
}
#[test_context(Context)]
#[test]
#[should_panic(expected = "Number changed")]
fn test_sync_teardown(ctx: &mut Wrapper) {
ctx.n = 2;
}
#[test_context(Context)]
#[test]
#[should_panic(expected = "Number changed")]
fn test_panicking_teardown(ctx: &mut Context) {
ctx.n = 2;
panic!("First panic");
}
struct AsyncContext {
n: u32,
}
#[async_trait::async_trait]
impl AsyncTestContext for AsyncContext {
async fn setup() -> Self {
Self { n: 1 }
}
async fn teardown(self) {
if self.n != 1 {
panic!("Number changed");
}
}
}
#[test_context(AsyncContext)]
#[tokio::test]
async fn test_async_setup(ctx: &mut AsyncContext) {
assert_eq!(ctx.n, 1);
}
#[test_context(AsyncContext)]
#[tokio::test]
#[should_panic(expected = "Number changed")]
async fn test_async_teardown(ctx: &mut AsyncContext) {
ctx.n = 2;
}
#[test_context(AsyncContext)]
#[tokio::test]
#[should_panic(expected = "Number changed")]
async fn test_async_panicking_teardown(ctx: &mut AsyncContext) {
ctx.n = 2;
panic!("First panic");
}