This commit is contained in:
John 2024-02-14 18:17:24 +00:00
parent f030c43724
commit 8a8e114cbc
3 changed files with 99 additions and 0 deletions

View File

@ -0,0 +1,32 @@
## CRF Constraints
### 1
Test setup in coroutine => Test coroutine must run as soon as its constructed, so as to allow it to
create mock objects, watches etc before the mock source runs. Also in phase 2, coroutine can create mock
sources.
### 2
Mock args passed by ref => MUT must not do anything between making a mock call and the coroutine body
receiving the args. Note that the coroutine CAN run during this period, since the args are owned
by the mock source.
### 3
Mock return passed by ref => Test coroutine must not do anything between Return() and the mock source receiving the
return value. Mock source could in principle run during this period, since coro owns the return object, but
in practice it won't because it's waiting for the return value.
### 4
Updated cardinality state => coroutine must run so that it can do SATISFY(), RETIRE() or run to exit BEFORE
allowing GMock/CotestWatcher code to query eg IsSatisfied() etc. This is extra_iteration_requested reason
inside TestCoroutine::DestructionIterations().
### 5
Global mock handler finding => After getting an event that is a mock call, test coroutines must call DROP()
ACCEPT() or RETURN() on the event before doing anything elase with the cotest UI. This is because the
process of deciding which expectation or coroutine will handle the call is global and must complete before
anything happens that could generate more mock calls.
### 6
True event sequence => Most of the time, a LAUNCH() or RETURN() should be followed by NEXT_EVENT() because
they will usually lead to an event for the test coro to pick up and handle. There is an exception in the
case of a second coroutine whose response is to exit. See test case ObserverQueue.

View File

@ -0,0 +1,13 @@
## Design Notes
- A "corouine" is officially a class or factory object bound to a coro body.
It creates an RAII object, so an instance must be created and should be in
a local scope (i.e. of the test case). Coroutines are implemented as lambdas
because a lambda can be a C++20 coroutine, and we can capture the mock objects.
This breeches S/S guidelines, but the UI is designed to make unsafe usafe difficult.
- All cardinality functionality belongs interior to the coroutine. Therefore WillOnce(), Times()
etc are not supported when routing calls to a coroutine. That's why the interior UI
should not say "EXPECT". Filtering (ultimately) can be done in interior or exterior,
so we don't want to be too imperitive in the terminology here because the coro can just
drop the call. Call it WATCH_CALL().

View File

@ -0,0 +1,54 @@
## Notes on hyper-flexible ordering
This analysis is of a version of cotest that allows test corotuines more
flexibility, so that they can queue up incoming events and then
pull them from the queue out of order.
### NEXT_EVENT_ONLY_FROM(DC) or BLOCK_EVENT()
- Consider launches A() and B() invoking mockA() and mockB() respectively. Suppose the
test case requires:
- A() must be called before B() eg because at least one has side effects and
they affect each other's behaviour.
- But mockA()'s handling in the test depends on an arg to mockB()
- Assume it isn't enough for mockB() to affect mockA()'s return value
(because we could leave mockA.RETURN() until after mockB.ACCEPT())
- Assume it isn't enough to do checking using EXPECT_EQ() etc either
- This leaves the case where the decision of whether to accept or drop
mockA() depends on mockB()'s argument.
- Now, based on GMock design:
- In order to have mockB() arguments we need to have entered ShouldHandleCall()
on B(). If we look at B's PreMock, we might see arguments for a mock
call that is accepted by a higher prio expectation or watch. So
higher prio expectations/watches on mockB() can affect the arg values we see.
- We have to decide whether to accept or drop mockA() before we exit
ShouldHandleCall() on A(). Our decision here will obviously affect
the lower prio expectations/watches on mockA().
- So we're stuck with searching mockB() higher priorities before mockA()
lower priorities, which is out of sequence. Consequences:
- Thread safety: we'd re-enter GMock's call hander search. At present this
causes a deadlock, but that could be changed.
- Expectations would be OK, if we checked eg mockA() highers,
mockB() highers, mockA() lowers, mockB() lowers. This generalises
to checking in sequence within each prio level. An expectation only
exists at one prio level, so it would see everything in the right order.
Call this a shear ordering.
- If another coro is watching mockA() at a lower prio and mockB() at
a higher prio, and it expects to see mockA() first, then we have
conflicting requirements. If the original coro gets its way, then
this coro will see mockB() and may drop it, fail a check, etc.
So shear ordering is not good enough for coroutines.
- Note that this is not an isue of side-effects inside ShouldHandleCall()
and purely relates to the order in which mock calls are passed to it.
### Architectural interpretation
- Coroutines should be able to infer the order in which mock calls
are happening from the order in which they are passed to
ShouldHandleCall(). Shear ordering prevents this.
- Without threads, this can be well-defined because coroutines have
well-defined ordering.
- With threads, we'd probably want an overall serialisation/rendez-vous point.
- To provide for the above scenario without using shear ordering,
we need to intervene and change the global ordering as seen
by GMock search. This should be explicit, since it will have side
effects on other coroutines.
- Leads to the PreMockSynchroniser and CRF constraint #5