max-heller / tested-trait

Associate tests with Rust traits
Apache License 2.0
7 stars 0 forks source link

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

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:

License: MIT OR Apache-2.0