asomers / mockall

A powerful mock object library for Rust
Apache License 2.0
1.5k stars 62 forks source link

Mock trait with methods that return iterators #547

Open vitornesello opened 8 months ago

vitornesello commented 8 months ago

Hello,

I am trying to mock a trait with two iterator methods. iter and iter_mut, each of which returns an iterator to references and mutable references of a parametric type.

The code below does not compile, but the problem comes from the iter_mut method. The other one works just fine.

I could not find an example in the tests that resemble what I am trying to do.

#[automock(type T=String;)]
trait Foo {
    type T;
    fn iter(&self) -> impl Iterator<Item = usize>;
    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Self::T>;
}

the compiler raises the following errors:

playground-rs on  master +/- [?] is 📦 v0.1.0 via 🦀 v1.75.0
❯ cargo build
   Compiling playground-rs v0.1.0 (/Users/vitornesello/code/playground-rs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:19
  |
3 | #[automock(type T=String;)]
  |                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
3 | #[automock(type T=S'static tring;)]
  |                    +++++++

error[E0637]: `&` without an explicit lifetime name cannot be used here
 --> src/main.rs:3:19
  |
3 | #[automock(type T=String;)]
  |                   ^ explicit lifetime name needed here
  |
help: consider introducing a higher-ranked lifetime here
  |
3 ~ #[automock(type T=S'a tring;)]
4 | trait Foo {
5 |     type T;
6 |     fn iter(&self) -> impl Iterator<Item = usize>;
7 ~     fn iter_mut(&mut self) -> impl for<'a> Iterator<Item = &mut Self::T>;
  |

Some errors have detailed explanations: E0106, E0637.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `playground-rs` (bin "playground-rs") due to 2 previous errors
asomers commented 8 months ago

This is a complicated trait. Does it work if you define that method like this? fn iter_mut(&mut self) -> impl Iterator<Item = &'static mut Self::T>;

vitornesello commented 8 months ago

with this modification, I have the following error:

   Compiling playground-rs v0.1.0 (/Users/vitornesello/code/playground-rs)
error[E0310]: the associated type `<Self as Foo>::T` may not live long enough
 --> src/main.rs:8:45
  |
8 |     fn iter_mut(&mut self) -> impl Iterator<Item = &'static mut Self::T>;
  |                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |                                             |
  |                                             the associated type `<Self as Foo>::T` must be valid for the static lifetime...
  |                                             ...so that the reference type `&'static mut <Self as Foo>::T` does not outlive the data it points at
  |
  = help: consider adding an explicit lifetime bound `<Self as Foo>::T: 'static`...
asomers commented 8 months ago

Did you try following that suggestion and adding the bound?

vitornesello commented 8 months ago

I could make it work like this, but I not sure if it is exactly what the suggestion was saying:

#[automock(type T=String;)]
trait Foo {
    type T: 'static;
    fn iter(&self) -> impl Iterator<Item = usize>;
    fn iter_mut(&mut self) -> impl Iterator<Item = &'static mut <Self as Foo>::T>;
}

However, it seems odd to me to require a static lifetime in this trait. Am I missing something?

asomers commented 8 months ago

That's actually not as odd as you might think. In order to store the expectation, Mockall requires that the lifetime of return values be either the same as the object itself, or else 'static. And "the same as the object itself" only works for a few common cases like &T. https://docs.rs/mockall/latest/mockall/#reference-return-values

vitornesello commented 8 months ago

Thanks a lot! I realize that I need a deeper understanding of lifetimes. I am quite new to the concept.

mrchilliballs commented 8 months ago

That's actually not as odd as you might think. In order to store the expectation, Mockall requires that the lifetime of return values be either the same as the object itself, or else 'static. And "the same as the object itself" only works for a few common cases like &T. https://docs.rs/mockall/latest/mockall/#reference-return-values

Could you consider clarifying what "the same as the object itself" means in the documentation? I stumbled upon the issue a while ago, and clearer documentation would have been helpful. Thanks!

asomers commented 8 months ago

When mocking a function that returns a reference, Mockall stores that referent within the mock object. For example:

#[automock]
pub trait Foo {
    fn foo(&self) -> &i32;
}

#[test]
fn t() {
    let mut mock = MockFoo::new();
    mock.expect_foo()
        .return_ref(42i32);
}

As you can see, we store a real i32 within the Mock object. So that i32 must have that same lifetime as the mock object itself. Mockall also has a few hard-coded special cases for methods that return references. For example, a method that returns a &str reference will take a String argument in its return_ref function. And if the function returns a 'static reference, then you can pass any anything you want to return_const or returning.