Testing With Unused Arguments

17 August 2015

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 a Context shouldn’t be more than 20-30 lines, whereas the NoDeref 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.