geoffwilliams@home:~$

PlatformIO SimAVR Unit Testing for Arduino

Automated testing on embedded isn’t as widespread as you would think. In a way it makes sense, you plugin your LED and the light blinks - pretty hard to unit test this kind of thing vs just upload and hope for the best, but being able to test the result of regular computation such as use of encryption libraries is a non-brainer for this kind of testing.

In my case, I wanted to test my use of Authenticated encryption with associated data (AEAD) with ChaChaPoly and the Arduino Crypto Library.

Coding for testability

  • Testable code needs to be split from your main.cpp and placed under /lib - see the notes in the README.md PlatformIO generates for you in this directory.
  • Break apart your codes into discrete units of testable logic. int add(int a, int b) is the classic demo of this
  • For hardware interaction (LoRA, PWM, etc), mock or skip entirely

Native

When most people talk about unit testing Arduino code, this is what they are talking about. Take the add(int a, int b) function described above. In theory, since it has no dependency on any Arduino hardware, you can compile it with the GNU libraries from your system, surround it in unity calls and check it returns the correct value.

Unfortunately, this all falls apart if you accidentally leave in a stray Serial.print() or #include <Arduino.h> in your library code. You can get around this to some degree with shimming and mocking and I was almost successful with this approach but when I went to hook-up the encryption library I wanted to use, I got compile errors from deep within the library and at this point I called time on this approach.

Physical

PlatformIO can upload and run your tests, then report back the results. This is just about ok if you are plugged in with a normal cable but I’m using a fragile serial programmer and there is a 100% chance of me breaking something if I leave this plugged in for a few days on the coffee table.

serial programmer

Cloud

PlatformIO calls this remote development. I’m sure there are other companies that will rent remote access to physical hardware too. If this is your day job, its worth looking into before building out your own testing. I didn’t look at this in depth.

Emulator

So this pretty much leaves us with emulators. Emulators are great for testing out what an almost real board would do with our codes. We don’t need to rip out all of those Serial.print() calls or have to keep adding things to mock to our test fixtures. In our encryption testing scenario which just involves maths, everything should work the same as on our board - nice.

There are of course a few gotchas with this approach:

  • ATmega328 is almost 20 years old at this point, important libraries such as RadioLib already announced they are dropping support due to limited flash size
  • Despite this, the Arduino framework itself is not obsolete. ESP32 is largely a drop-in replacement and gives modern features like WIFI and more flash. While ESP32 does have its own ESP-IDF framework, this is much lower level then Arduino and harder for casual coders to deal with.
  • Most of the emulator examples are for bigger RISC systems like HiFive1
  • The Arduino based examples expect setup() and loop() as usual, but this leads to the emulator hanging forever unless killed

Which emulator?

The three ones from the PlatformIO manual are:

Qemu

I almost got Qemu fully working with this platformio.ini snippet. Saving here for posterity:

[env:qemu]
platform = atmelavr
board = pro8MHzatmega328
framework = arduino
test_framework = unity
lib_deps =
    ${common.lib_deps}
test_testing_command =
    qemu-system-avr
    -no-reboot
    -nographic
    -machine
    uno
    -bios
    ${platformio.build_dir}/${this.__env__}/firmware.elf

I was able to install qemu-system-avr with sudo apt install qemu-system-misc and then use this as my test command. The tests ran but got stuck in loop() unless manually killed.

SimAVR

I did manage to get this one working, like this:

[env:simavr]
platform = atmelavr
framework = arduino
board = uno
test_framework = unity
lib_deps =
    ${common.lib_deps}
platform_packages =
    platformio/tool-simavr
test_speed = 9600
test_testing_command =
    ./always_timeout.sh
    60s
    ${platformio.packages_dir}/tool-simavr/bin/simavr
    -m
    atmega328p
    -f
    16000000L
    ${platformio.build_dir}/${this.__env__}/firmware.elf

I was not able to break out of loop() to end emulation with any trick I could think of but I was able to kill the emulator externally with the helper script always_timeout.sh and have it always return status 0:

#!/bin/sh
timeout "$@"
exit 0

This worked better then I had hoped, PlatformIO correctly reports passing status:

Testing...
Loaded 3072 bytes at 0
Loaded 194 bytes at 800100
whatever you like..
test/test_suite.cpp:24:test_function_should_doBlahAndBlah:PASS.
test/test_suite.cpp:25:test_function_should_doAlsoDoBlah:PASS.
.
-----------------------.
2 Tests 0 Failures 0 Ignored .
OK.
signal caught, simavr terminating

----------------------------- simavr:* [PASSED] Took 60.46 seconds -----------------------------

=========================================== SUMMARY ===========================================
Environment    Test    Status    Duration
-------------  ------  --------  ------------
physical       *       SKIPPED
simavr         *       PASSED    00:01:00.456
========================== 2 test cases: 2 succeeded in 00:01:00.456 ==========================

And failing status:

Testing...
Loaded 3228 bytes at 0
Loaded 226 bytes at 800100
whatever you like..
test/test_suite.cpp:24:test_function_should_doBlahAndBlah:PASS.
test/test_suite.cpp:17:test_function_should_doAlsoDoBlah:FAIL: kaboom!.
.
-----------------------.
2 Tests 1 Failures 0 Ignored .
FAIL.
signal caught, simavr terminating

----------------------------- simavr:* [FAILED] Took 60.60 seconds -----------------------------

=========================================== SUMMARY ===========================================
Environment    Test    Status    Duration
-------------  ------  --------  ------------
physical       *       SKIPPED
simavr         *       FAILED    00:01:00.598

___________________________________________ simavr:* ___________________________________________
test/test_suite.cpp:17:test_function_should_doAlsoDoBlah:FAIL: kaboom!.

===================== 2 test cases: 1 failed, 1 succeeded in 00:01:00.598 =====================

I don’t see why the same trick wouldn’t work with qemu as well. Tests are run in the pio terminal (gave up on the toolbar):

pio test --environment simavr --without-uploading  -vvv

The only downside to this approach is you have to wait for the emulator to be killed by timeout.

Summary

This side quest took longer then I had hoped but in the end the solution I came up with seems so simple its almost like cheating(!)

Now I can get back to what I was doing. Here it is, doing its thing:

Testing...
Loaded 7674 bytes at 0
Loaded 456 bytes at 800100
XXXXXXXXXXXXXX..
00: 6E 2E 35 9A 25 68 F9 80 41 BA 7 28 DD D 69 81..
16: E9 7E 7A EC 1D 43 60 C2 A 27 AF CC FD 9F AE B..
32: F9 1B 65 C5 52 47 33 AB 8F 59 3D AB CD 62 B3 57..
48: 16 39 D6 24 E6 51 52 AB 8F 53 C 35 9F 8 61 D8..
64: 7 CA D BF 50 D 6A 61 56 A3 8E 8 8A 22 B6 5E..
80: 52 BC 51 4D 16 CC F8 6 81 8C E9 1A B7 79 37 36..
96: 5A F9 B BF 74 A3 5B E6 B4 B 8E ED F2 78 5E 42..
112: 87 4D ..

Once tests are passing, I can just click the upload toolbar button.

Happy coding.

Example PlatformIO project git repository

https://github.com/GeoffWilliams/pio-simavr-unit-test-example

Post comment

Markdown is allowed, HTML is not. All comments are moderated.