Description
Looking at the Arduino mocking code, I'm running into some things that are not entirely easy to implement right now. I spent some thought on this and am writing down some of these thoughts there. I don't have a very specific proposal yet when I start writing, so this might turn out to be a bit of a braindump style post, hopefully that will be helpful.
In particular, for my usecase I need:
- Wall time
millis()
/micros()
progression (e.g. one ms in the actual world should just advancemillis()
by one) - Redirecting serial streams to a file (descriptor) or socket or so.
Currently, the way to model behaviour of things like streams, time and pins is by setting variables that will then later be read by the mocking code. This means that you either need to pregenerate all data, or insert the data in a different thread (i.e. every ms update the counters, or whenever data comes in through a file, add it to GODMODE()->serialPort[0].dataIn
, etc.). This is not quite ideal, and when you want to respond to certain events (e.g. whenever a byte is written, immediately reply or toggle a pin), there's no good way to do so (unless you continuously poll in a different thread, I think). I did see that PinHistory has some event/observer mechanism implemented, though I haven't looked at it in detail yet.
A part of this issue has already been discussed in #136 (millis does not advance when delay is not called) and #135 (stream should not be mocked by default).
In general, it seems that it would be helpful if pretty much all of the mocked Arduino API would be able to:
- Generate events to be caught by the unittest code
- Have its return value and further behavior be decided by some policy or custom code
These seem like they could be the same thing, but maybe they are separate (e.g. multiple handlers could respond to the same event, but only one of them actually decides the behavior, maybe?). Also, for unittests, it would be helpful if the behaviour would be easily controllable or switchable. E.g. that one unittest says "only advance time when delay is called", another says "just run against wall time" and a third says "ask me for the millis value every time".
I've been thinking a bit about how this could be modeled, and came up with the concept of a "Behavior" object. A mocked Arduino object, function or set of functions would just be a thin wrapper, that delegates most or all of its methods/function calls to an underlying behaviour object. Such a behaviour object is stored as a reference, so it can be easily replaced at runtime.
For example, StreamBehaviour
could be an interface/abstract class, offering write
and read
functions (basically the same as the virtual methods in the original Stream
class). A simple implementation could be StringStreamBehavior
, that just writes to / reads from two separate strings. A more elaborate implementation could be a FileStreamBehaviour
that reads / writes from a set of files or filedescriptors. A unit test could instantiate these behaviours and assign them to (e.g.) the Serial
objects where they need them (e.g. auto b = new StringStreamBehaviour(); Serial.setBehaviour(b); b.dataIn.append("foo")
).
Note that for Serial
, this probably means that the Stream
class should be made identical to the original Arduino Stream
class (no mocking happening there), and then a MockedStream
or so subclass should be made, which accepts a StreamBehavior
. Then HardwareSerial
could again be a subclass of MockedStream
, which can additionally accept a HardwareSerialBehaviour
object for the extra things (e.g. Serial.begin()
/ Serial.end()
). Maybe the HardwareSerialBehaviour
could include the StreamBehaviour
, or they could be set separately, dunno exactly.
For things like Serial
, the behaviour can be configured directly on the object itself (Serial.setBehaviour()
), but for things like pins that do not have a normal representation, or more abstract things like "timing" (that could control millis()
/ micros()
/ delay()
and delayMicroseconds()
), these behaviors should probably be set on the godmode object or so.
A completely different, more powerful, but also less convenient way of overriding object behaviour would be to really completely replace an object (e.g. replace Serial1
by a custom subclass of HardwareSerial
that defines some custom behaviour). This has the extra advantage that it can also actually add methods, rather then just replace behavior of predefined behavior, but it is also a lot more work and harder to dynamically change behavior. But maybe this is an option to consider in addition to swappable behaviors (something like -DOMIT_SERIAL1
and then allow the unit test to define its own Serial1
object could already be sufficient).
Out of time now, will followup later. In the meanwhile, thoughts as welcome!