Using GitHub Actions and arduino-cli to bring static testing to Arduino libraries
As the size of a library grows, it is common for existing functions to fail due to additions or changes to the functionality. In many cases, the error is not noticed until the function is actually used, and when it is noticed, it is often difficult to deal with.
Furthermore, if the library is multi-platform, it is impossible to manually test all the target environments.
To break through this egregious situation, we will show you how to use GitHub Actions and arduino-cli to perform testing automatically.
Assumption
The library must be maintained on GitHub (private repositories are also acceptable)
What is GitHub Actions?
GitHub Actions 1 is a very useful service that allows you to run arbitrary scripts on the GitHub virtual machine when you push or issue a pull request. It is free of charge, although there is a usage limit.
Create a test
Create a test that lets arduino-cli compile a test sketch on a virtual machine and fails if it encounters a compilation error.
This is only a test to check if it can be compiled, not to find bugs that can be discovered at runtime. You can check for algorithms that can be handled at compile time.
*In many cases, we can find areas where runtime errors are likely to occur at compile time.
Configure directories for testing
Create a sketch to be compiled. Since we are assuming a medium or large library, we put the header files in a subdirectory of the library. This article explains how to split files into subdirectories, if you are interested.
How to include header files in a subdirectory of the library
When you split source files for testing, you can add them under src
. Under src
, source files in subdirectories will also be compiled recursively.
MyLibrary
│
├─ .github
│ └─ workflows
│ └─ ArduinoLint.yml
│
├─ src
│ ├─ MyLibrary
│ │ └─ Algorithm
│ │ └─ Math.hpp
│ └─ MyLibrary.hpp
│
├─ test
│ └─ ArduinoLint
│ ├─ src
│ │ ├─ Algorithm
│ │ │ └─ Math.cpp <-- Test Source Files
│ │ └─ Other
│ │ └─ LinkErrorCheck.cpp <-- Test Source Files
│ └─ ArduinoLint.ino <-- Test Sketches
│
└─ library.properties
src/MyLibrary/Algorithm/Math.hpp
#pragma once
namespace Math
{
inline int Factorial(int n)
{
if (n <= 0)
return 1;
else
return n * Factorial(n - 1);
}
}
src/MyLibrary.hpp
#pragma once
#include <MyLibrary/Algorithm/Math.hpp>
library.properties
name=MyLibrary
version=1.0.0
author=HogeHoge
maintainer=HogeHoge
sentence=HogeHoge
paragraph=HogeHoge
category=Other
url=http://example.com/
architectures=*
Create test sketches
Call the functions defined in the library. All functions must be called, as they will not compile strictly if not called.
test/ArduinoLint/src/Algorithm/Math.cpp
#include <MyLibrary/Algorithm/Math.hpp>
// I won't call you from anywhere.
__attribute__((unused)) static void Test()
{
(void)Math::Factorial(123);
}
__attribute__((unused))
is a function attribute that explicitly states that the function will not be called. If it is not present, a warning will be issued.
Since a Test
function in another test source file will cause a link error, it is made a static function and has an internal linkage.
Functions with return values are cast to void (optional) to make it clear that the caller does not use the return value.
test/ArduinoLint/src/Other/LinkErrorCheck.cpp
#include <MyLibrary.hpp>
Source file to check for link errors due to multiple definitions. ArduinoLint.ino
+ Include headers from this file to trigger link errors.
For example, if a global function is defined in the header file, it will be a multiple definition. The way to deal with this is to inline the functions, etc.
test/ArduinoLint/ArduinoLint.ino
#include <MyLibrary.hpp>
void setup() {}
void loop() {}
To include a file in a subdirectory, it is necessary to include the file that is not in the subdirectory beforehand, so include it here. (Arduino Specifications)
Check the FQBN of the board
When compiling, the string FQBN is used to specify the board. It looks like the following
rp2040:rp2040:rpipico
To examine FQBN, go to File > Preferences and check the box to show compiler output.
Compiling on the target board yields FQBN.
Write test scripts
Edit .github/workflows/ArduinoLint.yml
. You can test multiple board environments simultaneously with the ability to run jobs in parallel (matrix
).
To add a board environment, add the FQBN you just checked to the board:
with the FQBN nori of the other board. If you don’t need the environment, delete it.
name: Arduino Lint
on: # Executed on push and pull requests
push:
paths-ignore:
- "**.md" # Ignore markdown file changes
pull_request:
paths-ignore:
- "**.md"
concurrency: # Stop previous execution in case of continuous push
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ArduinoLint:
name: ${{ matrix.board.fqbn }}
runs-on: ubuntu-latest
strategy:
fail-fast: false # The default is to abort if one of the jobs fails, so do not abort
matrix:
board: # board group
- fqbn: arduino:avr:nano
platform: arduino:avr
- fqbn: arduino:avr:uno
platform: arduino:avr
- fqbn: teensy:avr:teensy35
platform: teensy:avr
url: https://www.pjrc.com/teensy/td_156/package_teensy_index.json # Can also be installed from URL
- fqbn: teensy:avr:teensy40
platform: teensy:avr
url: https://www.pjrc.com/teensy/td_156/package_teensy_index.json
- fqbn: rp2040:rp2040:rpipico
platform: rp2040:rp2040
url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
- fqbn: rp2040:rp2040:rpipicow
platform: rp2040:rp2040
url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Arduino CLI
run: |
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
echo "./bin" >> $GITHUB_PATH
- name: Install board
run: |
arduino-cli config init
arduino-cli config set library.enable_unsafe_install true
if [ -n "${{ matrix.board.url }}" ]; then
arduino-cli config add board_manager.additional_urls ${{ matrix.board.url }}
fi
arduino-cli core update-index
arduino-cli core install ${{ matrix.board.platform }}
# Dependent libraries, if any (public repositories only)
# - name: Install libraries
# run: |
# arduino-cli lib install --git-url https://github.com/adafruit/Adafruit_BusIO.git
# arduino-cli lib install --git-url https://github.com/adafruit/Adafruit_Sensor.git
# arduino-cli lib install --git-url https://github.com/adafruit/Adafruit_BNO055.git
- name: Run test
run: arduino-cli compile --library . -b ${{ matrix.board.fqbn }} ./test/ArduinoLint/ArduinoLint.ino
There is a dubious script
arduino-cli config set library.enable_unsafe_install true
that is required to use the library based on the remote repository URL.arduino-cli compile --library .
to make the repository itself a library. So you can test private repositories as well.
arduino/compile-sketches There is also a way to use the action (Execution time will be a little longer)
name: Arduino Lint
on:
push:
paths-ignore:
- "**.md"
pull_request:
paths-ignore:
- "**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ArduinoLint:
name: ${{ matrix.board.fqbn }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
board:
- fqbn: arduino:avr:nano
- fqbn: teensy:avr:teensy40
platforms: |
- name: teensy:avr
source-url: https://www.pjrc.com/teensy/td_156/package_teensy_index.json
- fqbn: rp2040:rp2040:rpipico
platforms: |
- name: rp2040:rp2040
source-url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Arduino-cli test
uses: arduino/compile-sketches@v1
with:
fqbn: ${{ matrix.board.fqbn }}
platforms: ${{ matrix.board.platforms }}
sketch-paths: |
- ./test/ArduinoLint
libraries: |
- source-path: ./
# - source-url: https://github.com/adafruit/Adafruit_BusIO.git
# - source-url: https://github.com/adafruit/Adafruit_Sensor.git
# - source-url: https://github.com/adafruit/Adafruit_BNO055.git
Push to GitHub
After pushing the changes, you should see the test running on the Actions tab.
Click on a running workflow to see details. If the test passes, the test item will be marked with ✅.
You can also view the log output at runtime.
Status badge in README.md (optional)
The image URL of the status batch can be obtained from the top right corner of the screen in markdown format.
If you stick it in a markdown file such as README.md, you can see the status like this. If the test is failing, it will show Falling
.
Completion “(-”"-)"
The same content as the article is available on GitHub.
We have also introduced this test in our club libraries.
Run in local environment
You can test it by opening ArduinoLint.ino
in the Arduino IDE and compiling it. To run in multiple environments, you have to switch boards manually. if you create a script that compiles with arduino-cli, you can test locally in parallel.
Note for those developing on Windows
Since it runs on an Linux (Ubuntu) machine, unlike Windows, it is case-sensitive for files. Therefore, case must be handled strictly. For example, Arduino.h
as arduino.h
is accidental.
#include <Arduino.h>
// #include <arduino.h> // NO!!
Also, Linux uses /
as a path separator (Arduino for Windows uses \
or /
). So it is easy to adapt the include statement to the Ubuntu version.
#include <MyLibrary/Algorithm/Math.hpp>
// #include <MyLibrary\Algorithm\Math.hpp> // NO!
The End
Introducing tests makes it mentally considerably easier to ensure that other functions are alive (approximately) when changing functions.
It only takes a little effort to implement, so why not give it a try?
If you have any comments or suggestions, please feel free to leave a comment. Thank you for reading this article.
About GitHub Actions
https://docs.github.com/actions/about-github-actions/understanding-github-actions ↩︎