mimic++ v1
Loading...
Searching...
No Matches
sequence

Sequences enable deterministic ordering between multiple expectations. More...

Collaboration diagram for sequence:

Classes

class  mimicpp::Sequence
 The user level sequence object. More...
 

Enumerations

enum class  mimicpp::SequenceId : std::size_t
 Strong type for internally used sequence ids. More...
 

Functions

auto mimicpp::expect::in_sequence (Sequence &sequence, const std::size_t times=1u)
 Attaches the expectation onto a sequence.
 
template<std::size_t size>
auto mimicpp::expect::in_sequences (const std::reference_wrapper< Sequence >(&sequences)[size], const std::size_t times=1u)
 Attaches the expectation onto the listed sequences.
 

Detailed Description

Sequences enable deterministic ordering between multiple expectations.

Their aim is to provide a convenient way for users, to circumvent the rather loosely ordering of expectations, which is by design. By default, if two or more expectations would match a call, the last created one is used. If multiple expectations are built one after another, that may be rather unintuitive design, but when it comes to scoping, it's usually preferable to match the expectations from within the current scope, even if there exist another similar expectation from the outside scope.

As I have no idea how to use one strategy for one case and the other for the other case, I decided to take the same route as trompeloeil.

See also
https://github.com/rollbear/trompeloeil/blob/main/docs/CookBook.md#sequences

Either way, this is sometimes not enough, when we want to enforce deterministic ordering between two or more expectations. That's where sequences come into play. The expectation, which gets attached first must match before all subsequent expectations. If that one is fulfilled, the next one in the row must be matched; and so on.

namespace matches = mimicpp::matches;
namespace expect = mimicpp::expect;
mimicpp::Mock<void(int)> mock{};
mimicpp::Sequence sequence{};
SCOPED_EXP mock.expect_call(matches::ne(0))
| expect::in_sequence(sequence);
SCOPED_EXP mock.expect_call(matches::le(42))
| expect::in_sequence(sequence);
// a call with arg != 0 is expected before a call with arg <= 42
mock(42); // matches the first expectation
mock(0); // matches the second expectation
// note: when we change the call order, the sequence would be violated

Sequences can also enforce orders on expectations, which refer to different mocks.

namespace expect = mimicpp::expect;
mimicpp::Mock<void(int)> mock1{};
mimicpp::Mock<void(int)> mock2{};
mimicpp::Sequence sequence{};
SCOPED_EXP mock2.expect_call(_) // mock2 must go first
| expect::in_sequence(sequence);
SCOPED_EXP mock1.expect_call(_) // mock1 must go second
| expect::in_sequence(sequence);
mock2(42);
mock1(1337);

Sequenced and non-sequenced expectations may be arbitrarily mixed; even if this can be very difficult to trace, by simply reviewing the code.

namespace expect = mimicpp::expect;
mimicpp::Mock<void(int)> mock{};
SCOPED_EXP mock.expect_call(42); // (1)
SCOPED_EXP mock.expect_call(1337); // (2)
mimicpp::Sequence sequence{};
SCOPED_EXP mock.expect_call(1337) // (3)
| expect::in_sequence(sequence);
SCOPED_EXP mock.expect_call(1337); // (4)
SCOPED_EXP mock.expect_call(42) // (5)
| expect::in_sequence(sequence);
mock(42); // matches (1), because (5) is the second element of the sequence
mock(1337); // matches (4), because it's the "youngest" available alternative
mock(1337); // matches (3), because it's "younger" than (2). So, the first element of the sequence got matched.
mock(42); // matches (5). The sequence is now fulfilled.
mock(1337); // finally matches (2)

It's totally fine to attach expectations to sequences, which are already queried for matches. Sequences do not have to be setup in one go.

Thread-Safety

Sequences are not thread-safe and are never intended to be. If one attempts to enforce a strong ordering between multiple threads without any explicit synchronisation, that attempt is doomed to fail.

Afterthoughts

Sequence policies are treated as times_policy and therefore can not be mixed with e.g. Times. At a first glance this seems rather unintuitive, as a sequence does not actually mind how often an expectation is matched, but rather if one expectation has been matched before another. This being true, there is still a huge intersection between those cases, because a sequence still must know how often an expectation is expected to match. This would lead to higher coupling between two rather unrelated domains, but isn't the actual deal-breaker. The actual reason is, that Times is rather permissive designed and thus allow for a wide range of valid configurations. Especially the cases, where a range of possible calls is configured (like at_least), makes it very hard for a sequence to reliably determine, whether an expectation shall match or not. When users define expectations not precise enough, this quickly leads to ambiguities between multiple expectations which may and will result in surprising outcomes.

Enumeration Type Documentation

◆ SequenceId

enum class mimicpp::SequenceId : std::size_t
strong

Strong type for internally used sequence ids.

Function Documentation

◆ in_sequence()

auto mimicpp::expect::in_sequence ( Sequence & sequence,
const std::size_t times = 1u )
inlinenodiscard

Attaches the expectation onto a sequence.

Parameters
sequenceThe sequence to be attached to.
timesThe expected times.
namespace matches = mimicpp::matches;
namespace expect = mimicpp::expect;
mimicpp::Mock<void(int)> mock{};
mimicpp::Sequence sequence{};
SCOPED_EXP mock.expect_call(matches::ne(0))
| expect::in_sequence(sequence);
SCOPED_EXP mock.expect_call(matches::le(42))
| expect::in_sequence(sequence);
// a call with arg != 0 is expected before a call with arg <= 42
mock(42); // matches the first expectation
mock(0); // matches the second expectation
// note: when we change the call order, the sequence would be violated
namespace expect = mimicpp::expect;
mimicpp::Mock<void(int)> mock{};
SCOPED_EXP mock.expect_call(42); // (1)
SCOPED_EXP mock.expect_call(1337); // (2)
mimicpp::Sequence sequence{};
SCOPED_EXP mock.expect_call(1337) // (3)
| expect::in_sequence(sequence);
SCOPED_EXP mock.expect_call(1337); // (4)
SCOPED_EXP mock.expect_call(42) // (5)
| expect::in_sequence(sequence);
mock(42); // matches (1), because (5) is the second element of the sequence
mock(1337); // matches (4), because it's the "youngest" available alternative
mock(1337); // matches (3), because it's "younger" than (2). So, the first element of the sequence got matched.
mock(42); // matches (5). The sequence is now fulfilled.
mock(1337); // finally matches (2)
Here is the call graph for this function:

◆ in_sequences()

template<std::size_t size>
auto mimicpp::expect::in_sequences ( const std::reference_wrapper< Sequence >(&) sequences[size],
const std::size_t times = 1u )
nodiscard

Attaches the expectation onto the listed sequences.

Parameters
sequencesThe sequences to be attached to.
timesThe expected times.
namespace expect = mimicpp::expect;
mimicpp::Mock<void()> mock{};
mimicpp::Sequence sequence1{};
mimicpp::Sequence sequence2{};
SCOPED_EXP mock.expect_call() // (1)
| expect::in_sequence(sequence1);
SCOPED_EXP mock.expect_call() // (2)
| expect::in_sequences({sequence1, sequence2});
SCOPED_EXP mock.expect_call() // (3)
| expect::in_sequence(sequence2);
mock(); // (1) is used here, because (3) is second in sequence2 and (2) is second in sequence1
mock(); // (2) is used here, because it's the first in both sequences now
mock(); // now (3) is used
Here is the call graph for this function: