Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocks closures respect no lifetime constraints #38

Open
CodeSandwich opened this issue Feb 10, 2019 · 3 comments
Open

Mocks closures respect no lifetime constraints #38

CodeSandwich opened this issue Feb 10, 2019 · 3 comments

Comments

@CodeSandwich
Copy link
Owner

CodeSandwich commented Feb 10, 2019

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
Copy link
Owner Author

CodeSandwich commented Feb 23, 2019

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

@CodeSandwich
Copy link
Owner Author

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
Copy link
Owner Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant