Skip to content

Allow methods on mocks to be called with a range of times in a Sequence#660

Merged
asomers merged 1 commit intoasomers:masterfrom
8uurg:master
Nov 22, 2025
Merged

Allow methods on mocks to be called with a range of times in a Sequence#660
asomers merged 1 commit intoasomers:masterfrom
8uurg:master

Conversation

@8uurg
Copy link
Copy Markdown
Contributor

@8uurg 8uurg commented Jul 19, 2025

The requirement that the count on the times an expectation is an exact value is restricting, as I sometimes do not wish to define the amount of times a mocked method is called. For example, in one of my own codebases I have a method that is called at least once, but may be called multiple times without issue. As a simple minimum example of what works now:

let mut seq = Sequence::new();
let mut mock = MockFoo::new();
mock.expect_baz()
    .times(1..)
    .returning(|| ())
    .in_sequence(&mut seq);

mock.expect_bar()
    .times(1)
    .returning(|_| ())
    .in_sequence(&mut seq);

mock.baz();
mock.baz(); // repeated some random amount of times between 0..10
mock.bar(0);

The goal of this PR is to relax the exact value requirement, i.e., to allow a method to be called a variable number of times, within a range, including zero. It does so while maintaining the original behavior for exact cases, while enabling sequences to mark an earlier expectation as done to allow indefinite (with unset times()) to enforce ordering between calls.

For example, the following would work

let mut seq = Sequence::new();
let mut mock = MockFoo::new();

mock.expect_bar()
    .with(predicate::eq(0))
    .returning(|_| ())
    .in_sequence(&mut seq);
mock.expect_bar()
    .with(predicate::eq(1))
    .returning(|_| ())
    .in_sequence(&mut seq);
mock.expect_bar()
    .with(predicate::eq(2))
    .returning(|_| ())
    .in_sequence(&mut seq);

mock.bar(0);
mock.bar(0);
mock.bar(1);
mock.bar(1);
mock.bar(1);
mock.bar(2);

While altering the ordering would result in a failure:

let mut seq = Sequence::new();
let mut mock = MockFoo::new();

mock.expect_bar()
    .with(predicate::eq(0))
    .returning(|_| ())
    .in_sequence(&mut seq);
mock.expect_bar()
    .with(predicate::eq(1))
    .returning(|_| ())
    .in_sequence(&mut seq);
mock.expect_bar()
    .with(predicate::eq(2))
    .returning(|_| ())
    .in_sequence(&mut seq);

mock.bar(0);
mock.bar(1);
mock.bar(1);
mock.bar(1);
mock.bar(2);
mock.bar(0);

@8uurg 8uurg requested a review from asomers as a code owner July 19, 2025 21:49
Copy link
Copy Markdown
Owner

@asomers asomers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a very good feature. I definitely want to get it merged. In order to help me review it, could you please adjust the doc comments on SeqHandle to reflect the give states mentioned in the commit message, and how they relate to seq and min_seq? If it would make sense, you could even replace those two variables with an Enum.

And you'll have to rebase it, because I just committed some related comments to the master branch. Also, could you please wrap all lines to 80 columns and add a changelog entry?

Comment thread mockall/tests/mock_struct.rs Outdated
}

#[test]
#[should_panic]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please put an "expected" message here?

Comment thread mockall/src/lib.rs Outdated
/// mock1.bar();
/// ```
///
/// But, the previous count needs to suffice before a sequence may make progress
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"needs to suffice" doesn't make sense. Do you mean "must be satisfied"? Also, there should be a period at the end of the sentence.

Comment thread mockall/src/lib.rs
/// Furthermore, sequences are greedy, and will only perform the earliest element in sequence
/// that is allowed. This results in the following example failing the second expectation, as
/// the first expectation is used for both calls to `foo`.
/// ```should_panic
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an "expected" message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the normal tests, doctests don't seem to have this feature. I've extended the documentation to clarify this, instead.

@8uurg
Copy link
Copy Markdown
Contributor Author

8uurg commented Sep 5, 2025

I've added and expanded the documentation, fixed line-wrapping (in so far I am able to without breaking things) added a changelog entry, and squashed all of these changes into the original commit.

The requirement that the count on the times an expectation is an exact value is restricting,
as I sometimes do not wish to define the amount of times a mocked method is called.
For example, in one of my own codebases I have a method that is called at least once,
but may be called multiple times without issue.

The goal of this PR is to relax this exact requirement, i.e., to allow a method to be called a variable
number of times, within a range. This range may include zero, and may go off to infinity*. It does
so while maintaining the original behavior for exact cases, while allowing these new cases to be
handled.

A SeqHandle after the change can be interpreted to be in one of five states:
1. Uncallable - Other elements earlier in the sequence need to be satisfied first.
2. Callable   - Previous element(s) are satisified, and we can progress to the current element,
                but preceding elements may still be called further.
3. Called     - The method has been called, and preceding elements may no longer be called
                otherwise, this would constitute an ordering violation.
                However, the current count has not been satisfied yet, and the next element
                in sequence is not allowed to be called.
4. Satisfied  - This element in the sequence has had its lower bound satisfied, and the next
                element in sequence is allowed to be called.
5. History    - This element can no longer be called, as succeeding elements have been performed
                in the sequence.

Not all of these states are always possible, or in use, and these states may overlap.

For example, if the range includes calling zero times, a method cannot be in state 3.
Furthermore, once a method is (2) callable it may also be deemed to be in state (4) - satisified,
as it does not need to be called in order to be satisified. As such, these steps which may be
called zero times in the sequence may be *skipped*. This is covered by test `ok_variable_count_skip`.

For any element which has to be called at least once, skipping is not a possibility. However, state
3 still does not exist, as a single call suffices to go immediately to state 4, satisfied.

To encode these particularities this each handle now has two properties:
1. seq     - the position ( / state) within the sequence that we are processing, calling will move
             the counter to this position.
2. min_seq - the lowest permissible position in this sequence before this element is deemed callable.
             Note: is not incremented when the new handle being requested is allowed to be zero count
             to allow for skipping.
             If the minimum is greater than zero times, min_seq is updated to seq if minimum count
             is 1, or seq + 1 if minimum count is at least two, to ensure the previous element has
             has gone through the called state.

When a method which has a corresponding sequence is being called, it is checked whether:
1. The current sequence state is equal to seq, or, if minimum count is at least two, seq + 1
   In this case, the method is currently in called or satisified state, check whether this state
   needs to be updated and update accordingly.
2. The current sequence state is at least min_seq, if (1) was not the case, we are in callable state.
   Sequence is updated to seq, potentially skipping elements.

If the state precedes min_seq, we are in state (1) Uncallable. If the sequence state is greater than
seq, or seq + 1 if the minimum call count is at least 2, we are in state (5) History.

Finally, in the original implementation, a sequence was always in line with the state provided by Times:
Being in a certain spot in the sequence implies certain expectations have ran their course - with exact
counts any preceding elements would have been fully satisfied, unable to handle any further calls
and hence marked as done.

With the potential indefinite count this change provides, an earlier expectation may match arguments
and proceed to fail as their spot in the sequence has passed, without this task being done.
This change ensures that an Expectation is marked as done if their states in the Sequence
have passed, as to allow earlier elements in a sequence to be succeeded once further methods are called.
This is covered in the test `single_method_variable_count_mixed`
@asomers
Copy link
Copy Markdown
Owner

asomers commented Nov 22, 2025

I hijacked the PR and rebased it in order to resolve a merge conflict.

@asomers asomers merged commit b2133c9 into asomers:master Nov 22, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants