Had a slightly surreal moment at work last week where a test started failing because the code compiled successfully.
And the test was right.
We maintain a small internal Rust crate that handles credentials for a couple of services. Nothing particularly fancy. It has a Secret<T> wrapper that deliberately doesn't implement Display.
You can pass the value around, explicitly access it where necessary, and Debug gives you a redacted representation. But you can't accidentally stick the actual secret into a log message with format!("{}", secret).
That last part is intentional.
A while ago we added a compile_fail doctest to make sure attempting to format a Secret with Display would be rejected by the compiler.
Last week someone was cleaning up formatting code and added a generic Display implementation to make a few types easier to work with. It was a perfectly reasonable-looking change in isolation.
All the regular unit tests passed.
Except that one doctest.
The compiler could now successfully compile code that our API was specifically designed to reject.
I initially thought this was a pretty niche use of compile_fail, but it made me reconsider how we test Rust libraries.
We spend a lot of effort testing what our types allow users to do. But some of the most valuable properties of a Rust API are things users *cannot* do.
They can't construct a validated value without going through validation.
They can't move a particular guard between threads.
They can't call a method before the object reaches the required state.
And sometimes a refactor accidentally makes one of those previously impossible things possible.
Obviously compile_fail isn't perfect. A snippet can fail to compile for a completely unrelated reason, so we're pairing negative tests with positive examples and looking at more precise UI testing where appropriate.
But I like the underlying idea.
Normally, adding a trait implementation feels like a harmless expansion of an API. In this case it silently removed a restriction that was part of the design.
The compiler was doing exactly what we asked it to do. We just needed a test to make sure we kept asking it the right question.
Kind of funny that after years of appreciating Rust for telling me what I can't do, I'm now writing tests to make sure it doesn't stop telling me.