Demilade Sonuga's blog
All postsImplementing Trait Objects II
In this post, we're going to test our function trait objects.
What Exactly Are We Testing?
The requirements for our boxed functions:
- Function behaviour. If we have a boxed function
f
, we must be able to treatf
as if it was a normal function. - Ability to modify the environment, like a regular closure.
- Trait implementations.
- Ability to use them with vectors (because we're going to do that in our event handling scheme).
Dummy Allocators
Again, we need dummy allocators. For now, we're just going to copy and paste:
In boxed_fn.rs
#[cfg(test)]
mod tests {
// Convenience function for getting the always fail allocator
fn failing_allocator() -> *mut FailingAllocator {
&mut FailingAllocator as *mut _
}
// Convenience function for getting the always successful allocator
fn successful_allocator() -> *mut SuccessfulAllocator {
&mut SuccessfulAllocator as *mut _
}
// Dummy allocator that we can depend on to always succeed
struct SuccessfulAllocator;
use std::alloc::Global as PlatformAllocator;
use std::alloc::Layout;
use std::ptr::NonNull;
use std::alloc::Allocator as StdAllocator;
// Use your computer's allocator to allocate and deallocate memory
// Much more reliable than using our own custom allocator,
// so we can depend on it succeeding (under normal circumstances)
unsafe impl Allocator for SuccessfulAllocator {
unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
let mem_layout = Layout::from_size_align(size, alignment).unwrap();
let mem = PlatformAllocator.allocate(mem_layout).unwrap();
let ptr = mem.as_ptr() as *mut u8;
Some(ptr)
}
unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {
let mem_layout = Layout::from_size_align(size_to_dealloc, 1).unwrap();
PlatformAllocator.deallocate(NonNull::new(ptr).unwrap(), mem_layout);
}
}
// Dummy allocator we can depend on to always fail
struct FailingAllocator;
unsafe impl Allocator for FailingAllocator {
unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
None
}
unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {}
}
}
And for the testing utility to see the tests:
In main.rs
mod boxed;
mod vec;
mod boxed_fn; // NEW
mod event_hook;
Testing
Let's start from closure behaviour. The test must demonstrate that a BoxedFn
behaves like a function and
can manipulate its environment.
#[cfg(test)]
mod tests {
use super::*;
use crate::event_hook::EventInfo;
#[test]
fn test_fn_call() {
let mut was_called = false;
let f: _ = BoxedFn::new(|_| {
was_called = true;
}, successful_allocator());
assert!(!was_called);
f(EventInfo::Timer);
assert!(was_called);
}
// ...Others
}
This test creates a variable was_called
and initializes it to false. The closure in BoxedFn
, when called,
sets was_called
to true. So, if invoking the BoxedFn
instance results in a change of the was_called
variable, two things have been demonstrated: the ability of the BoxedFn
to behave like a normal
function and its ability to modify its environment.
Running the tests should result in a success. Remember to mess around with the tests and experiment.
Next, we're going to test the ability to use the BoxedFn
with vectors.
#[cfg(test)]
mod tests {
// ...Others
use crate::vec;
// ...Others
#[test]
fn test_vec_of_boxed_fn() {
let mut no_of_fns_called = 0;
let allocator = successful_allocator();
let mut v: vec::Vec<BoxedFn> = vec::Vec::with_capacity(3, allocator);
v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
v.iter().for_each(|f| f(EventInfo::Timer));
assert_eq!(no_of_fns_called, 3);
}
// ...Others
}
This test creates a vector of 3 BoxedFn
s each of which increases a single variable in the enviornment by 1
anytime the function is called. Calling all three functions will result in that variable ending up with the
value 3. This test simply demonstrates that the BoxedFn
still behaves the way it ought to behave even
when it's in a vector.
To test the Clone
implementation, we demonstrate that a cloned BoxedFn
still does the exact same thing
that the original BoxedFn
would have done.
#[cfg(test)]
mod tests {
// ...Others
#[test]
fn test_clone() {
let allocator = successful_allocator();
let mut x = 0;
let f = BoxedFn::new(|_| x += 1, allocator);
let g = f.clone();
core::mem::drop(f);
assert_eq!(x, 0);
g(EventInfo::Timer);
assert_eq!(x, 1);
}
// ...Others
}
This test first creates a BoxedFn
, f
. Then it creates a clone of f
: g
. It effectively demonstrates
that the Clone
implementation works because calling the clone g
delivers the same effect as calling the
original.
As for the Drop
implementation, I'll leave you to decide how to test that.
Take Away
For the full code, go to the repo
In The Next Post
We get on with the event handling scheme