Skip to content

Interactive serial mocks #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/issue_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## System

- OS: _(Travis/OSX/Linux/Windows)_
- `ruby -v`:
- `bundle -v`:
- `bundle info arduino_ci`:
- `gcc -v`:
- Arduino IDE version:
- URL of failing Travis CI job:
- URL of your Arduino project:


## Issue / Feature Request Summary


## Arduino or Unit Test Code, Illustrating the Problem


## Arduino Architecture(s) Affected
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Highlights from `CHANGELOG.md`

* See CHANGELOG.md for more


## Issues Fixed

* Fixes #3000
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Yaml files support select/reject critera for paths of unit tests for targeted testing
- Pins now track history and can report it in Ascii (big- or little-endian) for digital sequences
- Pins now accept an array (or string) of input bits for providing pin values across multiple reads
- FlashStringHelper (and related macros) compilation mocks
- SoftwareSerial. That took a while.
- Queue template implementation
- Table template implementation
- ObservableDataStream and DataStreamObserver pattern implementation
- DeviceUsingBytes and implementation of mocked serial device

### Changed
- Unit test executables print to STDERR just in case there are segfaults. Uh, just in case I ever write any.
Expand All @@ -24,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `arduino_ci_remote.rb` no longer makes unnecessary changes to the board being tested
- Scripts no longer crash if there is no `test/` directory
- Scripts no longer crash if there is no `examples/` directory
- `assureTrue` and `assureFalse` now `assure` instead of just `assert`ing.

### Security

Expand Down
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ unittest(example_godmode_stuff)
}
```

#### Pin Histories

Of course, it's possible that your code might flip the bit more than once in a function. For that scenario, you may want to examine the history of a pin's commanded outputs:

```C++
Expand Down Expand Up @@ -102,6 +104,9 @@ unittest(pin_history)
}
```


#### Pin Futures

Reading the pin more than once per function is also a possibility. In that case, we want to queue up a few values for the `digitalRead` or `analogRead` to find.

```C++
Expand Down Expand Up @@ -134,6 +139,9 @@ unittest(pin_read_history)
}
```

#### Serial Data


A more complicated example: working with serial port IO. Let's say I have the following function:

```C++
Expand Down Expand Up @@ -191,10 +199,10 @@ unittest(two_flips)
}
```

#### Pin History as ASCII



Finally, there are some cases where you want to use a pin as a serial port. There are history functions for that too.
For additional complexity, there are some cases where you want to use a pin as a serial port. There are history functions for that too.

```C++
int myPin = 3;
Expand All @@ -217,6 +225,45 @@ Finally, there are some cases where you want to use a pin as a serial port. The
assertEqual("Yes", state->digitalPin[myPin].toAscii(offset, bigEndian));
```

Instead of queueing bits as ASCII for future use with `toAscii`, you can send those bits directly (and immediately) to the output using `outgoingFromAscii`. Likewise, you can reinterpret/examine (as ASCII) the bits you have previously queued up by calling `incomingToAscii` on the PinHistory object.


#### Interactivity of "Devices" with Observers

Even pin history and input/output buffers aren't capable of testing interactive code. For example, queueing the canned responses from a serial device before the requests are even sent to it is not a sane test environment; the library under test will see the entire future waiting for it on the input pin instead of a buffer that fills and empties over time. This calls for something more complicated.

In this example, we create a simple class to emulate a Hayes modem. (For more information, dig into the `DataStreamObserver` code on which `DeviceUsingBytes` is based.

```c++
class FakeHayesModem : public DeviceUsingBytes {
public:
String mLast;

FakeHayesModem() : DeviceUsingBytes() {
mLast = "";
addResponseLine("AT", "OK");
addResponseLine("ATV1", "NO CARRIER");
}
virtual ~FakeHayesModem() {}
virtual void onMatchInput(String output) { mLast = output; }
};

unittest(modem_hardware)
{
GodmodeState* state = GODMODE();
state->reset();
FakeHayesModem m;
m.attach(&Serial);

Serial.write("AT\n");
assertEqual("AT\n", state->serialPort[0].dataOut);
assertEqual("OK\n", m.mLast);
}
```

Note that instead of setting `mLast = output` in the `onMatchInput()` function for test purposes, we could just as easily queue some bytes to state->serialPort[0].dataIn for the library under test to find on its next `peek()` or `read()`. Or we could execute some action on a digital or analog input pin; the possibilities are fairly endless in this regard, although you will have to define them yourself -- from scratch -- extending the `DataStreamObserver` class to emulate your physical device.


## Overriding default build behavior

You can add `.arduino-ci.yml` files to the project directory (which will then apply to both `test/` and `examples/`), as well as to the `test/` directory and each example directory in `examples/`. All defined fields can be overridden.
Expand Down
93 changes: 93 additions & 0 deletions SampleProjects/TestSomething/test/deviceusingbytes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#include <Arduino.h>
#include <ArduinoUnitTests.h>
#include <SoftwareSerial.h>
#include <ci/DeviceUsingBytes.h>

// DeviceUsingBytes extends DataStreamObserver,
// so we will be able to attach this class to an
// ObservableDataStream object, of which the pin
// history (soft-serial) and HardwareSerial
// objects are.
class FakeHayesModem : public DeviceUsingBytes {
public:
String mLast;
bool mMatchedInput;

FakeHayesModem() : DeviceUsingBytes() {
mLast = "";
mMatchedInput = false;
addResponseLine("AT", "OK");
addResponseLine("ATV1", "NO CARRIER");
}

virtual ~FakeHayesModem() {}

virtual void onMatchInput(String output) {
mLast = output;
mMatchedInput = true;
}
};

unittest(modem_hardware)
{
GodmodeState* state = GODMODE();
state->reset();

String cmd = "AT\n";

FakeHayesModem m;
m.attach(&Serial);
assertEqual(0, Serial.available());
assertFalse(m.mMatchedInput);
assertEqual("", m.mMessage);

for (int i = 0; i < cmd.length(); ++i) {
assertEqual(i, m.mMessage.length()); // before we write, length should equal i
Serial.write(cmd[i]);
}
assertEqual(0, m.mMessage.length()); // should have matched and reset

assertEqual("", state->serialPort[0].dataIn);
assertEqual("AT\n", state->serialPort[0].dataOut);

assureTrue(m.mMatchedInput);
//assertEqual(3, Serial.available());
assertEqual("OK\n", m.mLast);
}

unittest(modem_software)
{
GodmodeState* state = GODMODE();
state->reset();

bool bigEndian = false;
bool flipLogic = false;
SoftwareSerial ss(1, 2, flipLogic);
ss.listen();

String cmd = "AT\n";

FakeHayesModem m;
m.attach(&state->digitalPin[2]);
assertEqual(0, ss.available());
assertFalse(m.mMatchedInput);
assertEqual("", m.mMessage);

for (int i = 0; i < cmd.length(); ++i) {
assertEqual(i, m.mMessage.length()); // before we write, length should equal i
assertEqual(cmd.substr(0, i), state->digitalPin[2].toAscii(1, bigEndian));
assertEqual(cmd.substr(0, i), m.mMessage);
ss.write(cmd[i]);
}
assertEqual(0, m.mMessage.length()); // should have matched and reset

assertEqual("", state->digitalPin[1].incomingToAscii(1, bigEndian));
assertEqual("AT\n", state->digitalPin[2].toAscii(1, bigEndian));


assureTrue(m.mMatchedInput);
//assertEqual(3, Serial.available());
assertEqual("OK\n", m.mLast);
}

unittest_main()
107 changes: 107 additions & 0 deletions SampleProjects/TestSomething/test/observabledatastream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include <Arduino.h>
#include <ArduinoUnitTests.h>
#include <ci/ObservableDataStream.h>

class Source : public ObservableDataStream {
public:
Source() : ObservableDataStream() {}

// expose protected functions
void doBit(bool val) { advertiseBit(val); }
void doByte(unsigned char val) { advertiseByte(val); }
};

class Sink : public DataStreamObserver {
public:
bool lastBit;
unsigned char lastByte;

Sink() : DataStreamObserver(false, false) {}

virtual String observerName() const { return "Sink"; }
virtual void onBit(bool val) { lastBit = val; }
virtual void onByte(unsigned char val) { lastByte = val; }
};

class BitpackSink : public DataStreamObserver {
public:
bool lastBit;
unsigned char lastByte;

BitpackSink() : DataStreamObserver(true, true) {}

virtual String observerName() const { return "BitpackSink"; }
virtual void onBit(bool val) { lastBit = val; }
virtual void onByte(unsigned char val) { lastByte = val; }
};

unittest(attach_sink_to_src)
{
Source src = Source();
Sink dst = Sink();

dst.lastByte = 'z';
src.addObserver("foo", &dst);
src.doByte('a');
assertEqual('a', dst.lastByte);
src.removeObserver("foo");
src.doByte('b');
assertEqual('a', dst.lastByte);
}

unittest(attach_src_to_sink)
{
Source src = Source();
Sink dst = Sink();

dst.attach(&src);
src.doByte('f');
assertEqual('f', dst.lastByte);
}

// 01010100 T if bigendian
unittest(bitpack)
{
Source src = Source();
Sink dst = Sink();
BitpackSink bst = BitpackSink();

bool message[8] = {0, 1, 0, 1, 0, 1, 0, 0};

bst.lastByte = 'f';
dst.lastByte = 'f';
bst.attach(&src);
dst.attach(&src);

for (int i = 0; i < 8; ++i) {
src.doBit(message[i]);
assertEqual(message[i], bst.lastBit);
assertEqual(message[i], dst.lastBit);
}

assertEqual('f', dst.lastByte); // not doing bitpacking
assertEqual('T', bst.lastByte); // should have formed a binary T char by now
assertNotEqual('*', bst.lastByte); // backwards endianness
}

// 01010100 T if bigendian
unittest(from_pinhistory)
{
GodmodeState* state = GODMODE();
state->reset();

BitpackSink bst = BitpackSink();
bst.attach(&state->digitalPin[2]);
bst.lastByte = 'f';

bool message[8] = {0, 1, 0, 1, 0, 1, 0, 0};
for (int i = 0; i < 8; ++i) {
digitalWrite(2, message[i]);
assertEqual(message[i], bst.lastBit);
}

assertEqual('T', bst.lastByte); // should have formed a binary T char by now
assertNotEqual('*', bst.lastByte); // backwards endianness
}

unittest_main()
2 changes: 1 addition & 1 deletion SampleProjects/TestSomething/test/queue.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include <ArduinoUnitTests.h>
#include <Queue.h>
#include <ci/Queue.h>

unittest(basic_queue_dequeue_and_size)
{
Expand Down
15 changes: 9 additions & 6 deletions SampleProjects/TestSomething/test/softwareserial.cpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
#include <ArduinoUnitTests.h>
#include <SoftwareSerial.h>

bool bigEndian = false;
bool flipLogic = false;

unittest(software_input_output)
{
GodmodeState* state = GODMODE();
state->reset();

SoftwareSerial ss(1, 2, false);
SoftwareSerial ss(1, 2, flipLogic);

assertEqual(-1, ss.peek());

state->digitalPin[1].fromAscii("Holy crap ", true);
state->digitalPin[1].fromAscii("this took a lot of prep work", true);
state->digitalPin[1].fromAscii("Holy crap ", bigEndian);
state->digitalPin[1].fromAscii("this took a lot of prep work", bigEndian);

assertFalse(ss.isListening());
assertEqual(-1, ss.peek());
Expand All @@ -28,17 +31,17 @@ unittest(software_input_output)
ss.write('b');
ss.write('A');
ss.write('r');
assertEqual("bAr", state->digitalPin[2].toAscii(1, true));
assertEqual("bAr", state->digitalPin[2].toAscii(1, bigEndian));
}

unittest(print) {
GodmodeState* state = GODMODE();
state->reset();

SoftwareSerial ss(1, 2, false);
SoftwareSerial ss(1, 2, flipLogic);
ss.listen();
ss.print(1.3, 2);
assertEqual("1.30", state->digitalPin[2].toAscii(1, true));
assertEqual("1.30", state->digitalPin[2].toAscii(1, bigEndian));
}

unittest_main()
Loading