Testing With Unused Arguments
I recently wrote a constant folding function for clippy. When I had something that could conceivably work, I asked myself: “How the [CENSORED] do I test this?”
The function signature is:
pub fn constant(cx: &Context, e: &Expr) -> Option<Constant>;
Notice the &Context
being the first parameter? This is a reference to a
rustc::lint::Context
, which is used for constant lookup (how ironic!) and
unless you have a lint context laying around, you’re out of luck. As it’s
optimized for helpfulness to lint plugins, it has ties to just about all of
rustc
. This thing is [CENSORED] huge! Worse, the context is loaned out
to most other methods in that module, too.
Since we already did the constant lookup elsewhere and know how it works, we
can simply test the other things. So I tried to use conditional compilation
and a zero-sized struct
:
#[cfg(not(test))]
use rustc::lint::Context;
#[cfg(test)]
struct Context;
However, our constant-lookup function requires some field in the Context, so I also tried to conditionally compile it:
#[cfg(test)]
fn fetch_path(cx: &Context, e: &Expr) -> Option<Constant> { None }
#[cfg(not(test))]
fn fetch_path(cx: &Context, e: &Expr) -> Option<Constant> {
.. // actual implementation here
}
Alas, I couldn’t get it to compile. Interestingly, a simple reduced example worked correctly. However, sensing that fixing the compilation issue would be taking more time than I was willing to expend, I turned to one ugly trick that I still had up my sleeve: null.
Now I can hear some of you shouting: “But that’s unsafe!”
Remember that my tests never actually use the Context
– let’s call that our
testing invariant: The Context
is never dereferenced. So we can give in a
null
Context
-ptr into the constant
function, safe in the knowledge that
it won’t
eat our laundry
– as long our invariant holds.
So to get a null &Context
, I created the following function:
fn ctx() -> &'static Context<'static, 'static> {
unsafe {
let x : *const Context<'static, 'static> = std::ptr::null();
mem::transmute(x)
}
}
Had I wanted to really get fancy (and avoid unsafe code), I could have
created a NoDeref
struct that panic!
s on dereferencing, which would have
looked a lot like
this.
However, this would have required me to change all my &Context
-taking
functions to be generic over a T: Deref<Context>
. As it stands, I’ll go the
unsafe route.
Bonus: There has been some useful discussion on this – as it turns out, this technique actually relies on undefined behavior and may lead to nasal daemons even if the reference is never dereferenced!
Our friendly neighborhood Rust guru eddyb explains:
You don’t have a pointer, you have a reference which guarantees a few things, including non-null, dereferenceable and valid data behind the pointer. LLVM already knows about the first two, and will optimize accordingly.
No, I don’t believe using
Deref
is a good idea. Creating aContext
shouldn’t be more than 20-30 lines, whereas theNoDeref
thing will be more work.
In my case it isn’t, but there is a third… well, Option
. github user
birkenfeld got around to change the structure to use an Option<&Context>
instead, which is Rust’s way of naming a nullable pointer to a Context
.
This is even more useful than the NoDeref
trick, because it allows us
to expose the option of not using a Context
(and thus save resources)
where we don’t need it.
How do you structure your Rust code to allow for testability? How do you work
around god objects like Context
in your tests? Tell me on
/r/rust or
rust-lang users!