1 unstable release
0.1.0 | Jul 15, 2023 |
---|
#2630 in Rust patterns
27 downloads per month
23KB
128 lines
tested-trait
tested-trait
provides two macros -- tested_trait
and test_impl
-- that make it
possible to include associated tests in trait definitions and instantiate associated tests
to test implementations of the trait.
Example
Consider a memory allocator trait like GlobalAlloc
.
The alloc
method takes a Layout
describing size and alignment requirements, and returns a pointer -- the returned pointer
should adhere to layout description, but nothing enforces this contract.
By annotating the trait definition with the tested_trait
macro, a test can be associated
with the trait to verify that allocations result in validly aligned pointers -- at least for a
simple sequence of allocations:
use std::alloc::Layout;
#[tested_trait]
trait Allocator {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8;
#[test]
fn alloc_respects_alignment() where Self: Default {
let mut alloc = Self::default();
let layout = Layout::from_size_align(10, 4).unwrap();
for _ in 0..10 {
let ptr = unsafe { alloc.alloc(layout) };
assert_eq!(ptr.align_offset(layout.align()), 0);
}
}
}
Note the test's where Self: Default
bound, which it uses to construct an allocator.
Unlike freestanding #[test]
s, associated tests may have where
clauses to require additional
functionality for testing purposes.
Implementers can then use test_impl
to verify that their allocators pass this tests and any
others associated with the trait.
For instance, we can test the default system allocator:
use std::alloc;
#[test_impl]
impl Allocator for alloc::System {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
alloc::GlobalAlloc::alloc(self, layout)
}
}
... and a flawed allocator that ignores alignment:
struct BadAllocator<const SIZE: usize> {
buf: Box<[u8; SIZE]>,
next: usize,
}
// Note the `BadAllocator<1024>: Allocator` argument here -- the implementation is generic,
// so we use it to specify which concrete implementation should be tested.
#[test_impl(BadAllocator<1024>: Allocator)]
impl<const SIZE: usize> Allocator for BadAllocator<SIZE> {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
if self.next + layout.size() <= self.buf.len() {
let ptr = &mut self.buf[self.next] as *mut u8;
self.next += layout.size();
ptr
} else {
core::ptr::null_mut()
}
}
}
// Implement Default since the associated tests require it -- if this implementation
// is omitted, the #[test_impl] attribute will emit a compilation error.
impl<const SIZE: usize> Default for BadAllocator<SIZE> {
fn default() -> Self {
Self { buf: Box::new([0; SIZE]), next: 0 }
}
}
Features
- Associating tests with trait definitions
- Running associated tests against non-generic trait implementations and concrete instantiations of generic implementations (see below)
- Most of the standard
#[test]
syntax (see below) - Understandable names for generated tests: currently, annotating
impl<T> Foo<T> for Bar<T>
withtest_impl
generates tests namedtested_trait_test_impl_Foo_{N}
-- ideally they'd be namedtested_trait_test_impl_Foo<{T}>_for_Bar<{T}>
, but converting types into valid identifiers is difficult - Testing trait implementations for unsized types
- Support for property-based tests with
quickcheck
andproptest
-
#![no_std]
support: this crate itself is#![no-std]
, but the tests it defines requirestd::println!
and [std::panic::catch_unwind()
]
Testing generic implementations
Generic implementations of traits generate concrete implementations for each instantiation of
their generic parameters. It's impossible to test all of these implementations, so annotating a
generic implementation with just #[test_impl]
fails to compile:
# use tested_trait::{tested_trait, test_impl};
#[tested_trait]
trait Wrapper<T> {
fn wrap(value: T) -> Self;
fn unwrap(self) -> T;
#[test]
fn wrap_then_unwrap() where T: Default + PartialEq + Clone {
let value = T::default();
assert!(Self::wrap(value.clone()).unwrap() == value);
}
}
#[test_impl]
impl<T> Wrapper<T> for Option<T> {
fn wrap(value: T) -> Self {
Some(value)
}
fn unwrap(self) -> T {
self.unwrap()
}
}
To test such an implementation, pass a non-empty list of Type: Trait
arguments to
test_impl
to specify which concrete implementations to test:
#[test_impl(Option<u32>: Wrapper<u32>, Option<String>: Wrapper<String>)]
impl<T> Wrapper<T> for Option<T> {
fn wrap(value: T) -> Self {
Some(value)
}
fn unwrap(self) -> T {
self.unwrap()
}
}
Supported #[test]
syntax
Most of the standard #[test]
syntax is supported:
#[tested_trait]
trait Foo {
#[test]
fn standard_test() {}
#[test]
fn result_returning_test() -> Result<(), String> {
Ok(())
}
#[test]
#[should_panic]
fn should_panic_test1() {
panic!()
}
#[test]
#[should_panic = "ahhh"]
fn should_panic_test2() {
panic!("ahhhhh")
}
#[test]
#[should_panic(expected = "ahhh")]
fn should_panic_test3() {
panic!("ahhhhh")
}
}
#[test_impl]
impl Foo for () {}
Comparison to trait_tests
This crate provides similar functionality to the trait_tests
crate, with the following
notable differences:
trait_tests
defines tests in separateFooTests
traits, while this crate defines them inline in trait definitionstrait_tests
allows placing bounds onFooTests
traits, while this crate allows placing them on test functions themselvestrait_tests
defines tests as unmarked associated functions, while this crate supports the standard#[test]
syntax and the niceties that come with it- From my testing, this crate's macros are more hygienic and robust to varying inputs than those
of
trait_tests
License: MIT OR Apache-2.0
Dependencies
~0.4–0.9MB
~19K SLoC