Mocktopus icon indicating copy to clipboard operation
Mocktopus copied to clipboard

Mocks closures respect no lifetime constraints

Open CodeSandwich opened this issue 6 years ago • 3 comments

This code compiles causing conversion of &str to &'static str:

#[mockable]
fn terrible(_: &str) -> &'static str {
    "a"
}

#[test]
fn terrible_test() {
    // This closure has type `for <'a> fn(&'a str) -> MockResult<(&'a str,), &'a str>`
    terrible.mock_safe(|a| MockResult::Return(a));
    let x = "abc".to_string();
    let y = x.as_str();
    let z: &'static str = terrible(y);
    assert_eq!("abc", z);
}

CodeSandwich avatar Feb 10 '19 02:02 CodeSandwich

Probably should enforce on closure passed to mock_safe to have output 'static

CodeSandwich avatar Feb 23 '19 15:02 CodeSandwich

I can't find solution for this problem. I've wasted a lot of time and failed miserably.

It boils down to casting functions, which both base (mocked function) and mock ultimately are.

assume lifetimes 'a < 'b < 'static
mock &'a      -> &'static // genuine mock, if used as substitute for base, will accept any valid base's argument and produce result which can be used as base's
base &'b      -> &'b      // base function, we need substitute for it
evil &'static -> &'a      // evil mock, does not accept every possible valid base's argument and can produce result which can't be used as valid base's result
cast &'static -> &'a      // function to which every function above can be casted

The order on this chart is not random: function can always be casted to function below. This is because functions are contravariant regarding their inputs and covariant regarding outputs. So mock can be casted to base, base to evil and evil to cast.

Lifetime casting is the biggest enemy, every function implicitly casts to any fn type lower on chart making distinguishing mock, base and evil impossible.

Base mustn't be casted at all in order to check if proposed mock can be casted to it or not. If base is not casted, mock can be casted to base and evil can't. But if base gets casted to cast, both mock and evil can be casted to cast as well and evil can't be rejected.

Whatever invariant wrappers base gets covered in, the casting happens BEFORE wrapping making everything work. It does not matter if base comes in form of fn item, fn pointer or Fn reference, it always gets casted, it in fact IS itself and all functions below.

It's not possible to retrieve original base argument types either, Rust does not provide type system tools for it. Implementing some trait for function (generic fn pointer or generic Fn reference, it's not possible for specific fn item, which is a pity) does not help either, because once again function gets casted before trait is used on it.

CodeSandwich avatar Mar 31 '19 17:03 CodeSandwich

So the problem can't be solved in elastic, per-function way, but can it be solved with some general, stiff rules? Yes and no.

What we need is to force mock to have inputs living shorter than any possible input required by base and output living longer than any possible base output. The output is trivial: just force it to be 'static. The inputs on the other hand are problematic.

We need to enforce arguments to have lifetime of only the mock call duration, that's the only lifetime that is guaranteed to not outlive any possible argument of base. Unfortunately Rust does not have necessary tooling. GAT may or may not be able to provide them, but it still wouldn't be enough.

In perfectly working system every &str argument would have lifetime of call. The String would still have static lifetime. But what about &'static str? It could be a casterd &str (arguments are contravariant), so it probably should have lifetime somehow lowered to call length. But then the String will have to get a lifetime too. How to write mock function returning anything when even static arguments can't exit it?

I don't think that this problem can be solved in any reasonable type system. The only option there is left is to allow mocks to be safe only when they don't have or ignore arguments. Otherwise there is no way to prevent implicit lifetime escalation.

CodeSandwich avatar Apr 03 '19 21:04 CodeSandwich