Skip to content

Commit

Permalink
✅ Add unit testing framework (#26948)
Browse files Browse the repository at this point in the history
- Add a framework to build and execute unit tests for Marlin.
- Enable unit test execution as part of PR checks.

---------

Co-authored-by: Costas Basdekis <[email protected]>
Co-authored-by: Scott Lahteine <[email protected]>
  • Loading branch information
3 people authored Apr 13, 2024
1 parent cf7c86d commit d10861e
Show file tree
Hide file tree
Showing 21 changed files with 710 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#
# test-builds.yml
# ci-build-tests.yml
# Do test builds to catch compile errors
#

name: CI
name: CI - Build Tests

on:
pull_request:
Expand All @@ -27,7 +27,7 @@ on:

jobs:
test_builds:
name: Run All Tests
name: Build Test
if: github.repository == 'MarlinFirmware/Marlin'

runs-on: ubuntu-latest
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/ci-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#
# ci-unit-tests.yml
# Build and execute unit tests to catch functional issues in code
#

name: CI - Unit Tests

on:
pull_request:
branches:
- bugfix-2.1.x
# Cannot be enabled on 2.1.x until it contains the unit test framework
#- 2.1.x
paths-ignore:
- config/**
- data/**
- docs/**
- '**/*.md'
push:
branches:
- bugfix-2.1.x
# Cannot be enabled on 2.1.x until it contains the unit test framework
#- 2.1.x
paths-ignore:
- config/**
- data/**
- docs/**
- '**/*.md'

jobs:
# This runs all unit tests as a single job. While it should be possible to break this up into
# multiple jobs, they currently run quickly and finish long before the compilation tests.
run_unit_tests:
name: Unit Test
# These tests will only be able to run on the bugfix-2.1.x branch, until the next release
# pulls them into additional branches.
if: github.repository == 'MarlinFirmware/Marlin'

runs-on: ubuntu-latest

steps:
- name: Check out the PR
uses: actions/checkout@v4

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}

- name: Select Python 3.9
uses: actions/setup-python@v5
with:
python-version: '3.9'
architecture: 'x64'

- name: Install PlatformIO
run: |
pip install -U platformio
pio upgrade --dev
pio pkg update --global
- name: Run All Unit Tests
run: |
make unit-test-all-local
34 changes: 30 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ help:
@echo "make tests-single-local-docker : Run a single test locally, using docker"
@echo "make tests-all-local : Run all tests locally"
@echo "make tests-all-local-docker : Run all tests locally, using docker"
@echo "make setup-local-docker : Build the local docker image"
# @echo "make unit-test-single-ci : Run a single code test from inside the CI"
# @echo "make unit-test-single-local : Run a single code test locally"
# @echo "make unit-test-single-local-docker : Run a single code test locally, using docker-compose"
@echo "make unit-test-all-local : Run all code tests locally"
@echo "make unit-test-all-local-docker : Run all code tests locally, using docker-compose"
@echo "make setup-local-docker : Setup local docker-compose"
@echo ""
@echo "Options for testing:"
@echo " TEST_TARGET Set when running tests-single-*, to select the"
Expand Down Expand Up @@ -43,7 +48,7 @@ tests-single-local:
tests-single-local-docker:
@if ! test -n "$(TEST_TARGET)" ; then echo "***ERROR*** Set TEST_TARGET=<your-module> or use make tests-all-local-docker" ; return 1; fi
@if ! $(CONTAINER_RT_BIN) images -q $(CONTAINER_IMAGE) > /dev/null ; then $(MAKE) setup-local-docker ; fi
$(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) $(MAKE) tests-single-local TEST_TARGET=$(TEST_TARGET) VERBOSE_PLATFORMIO=$(VERBOSE_PLATFORMIO) GIT_RESET_HARD=$(GIT_RESET_HARD) ONLY_TEST="$(ONLY_TEST)"
$(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) make tests-single-local TEST_TARGET=$(TEST_TARGET) VERBOSE_PLATFORMIO=$(VERBOSE_PLATFORMIO) GIT_RESET_HARD=$(GIT_RESET_HARD) ONLY_TEST="$(ONLY_TEST)"

tests-all-local:
export PATH="./buildroot/bin/:./buildroot/tests/:${PATH}" \
Expand All @@ -52,10 +57,31 @@ tests-all-local:

tests-all-local-docker:
@if ! $(CONTAINER_RT_BIN) images -q $(CONTAINER_IMAGE) > /dev/null ; then $(MAKE) setup-local-docker ; fi
$(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) $(MAKE) tests-all-local VERBOSE_PLATFORMIO=$(VERBOSE_PLATFORMIO) GIT_RESET_HARD=$(GIT_RESET_HARD)
$(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) make tests-all-local VERBOSE_PLATFORMIO=$(VERBOSE_PLATFORMIO) GIT_RESET_HARD=$(GIT_RESET_HARD)

#unit-test-single-ci:
# export GIT_RESET_HARD=true
# $(MAKE) unit-test-single-local TEST_TARGET=$(TEST_TARGET)

# TODO: How can we limit tests with ONLY_TEST with platformio?
#unit-test-single-local:
# @if ! test -n "$(TEST_TARGET)" ; then echo "***ERROR*** Set TEST_TARGET=<your-module> or use make unit-test-all-local" ; return 1; fi
# platformio run -t marlin_$(TEST_TARGET)

#unit-test-single-local-docker:
# @if ! test -n "$(TEST_TARGET)" ; then echo "***ERROR*** Set TEST_TARGET=<your-module> or use make unit-test-all-local-docker" ; return 1; fi
# @if ! $(CONTAINER_RT_BIN) images -q $(CONTAINER_IMAGE) > /dev/null ; then $(MAKE) setup-local-docker ; fi
# $(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) make unit-test-single-local TEST_TARGET=$(TEST_TARGET) ONLY_TEST="$(ONLY_TEST)"

unit-test-all-local:
platformio run -t test-marlin -e linux_native_test

unit-test-all-local-docker:
@if ! $(CONTAINER_RT_BIN) images -q $(CONTAINER_IMAGE) > /dev/null ; then $(MAKE) setup-local-docker ; fi
$(CONTAINER_RT_BIN) run $(CONTAINER_RT_OPTS) $(CONTAINER_IMAGE) make unit-test-all-local

setup-local-docker:
$(CONTAINER_RT_BIN) build -t $(CONTAINER_IMAGE) -f docker/Dockerfile .
$(CONTAINER_RT_BIN) buildx build -t $(CONTAINER_IMAGE) -f docker/Dockerfile .

PINS := $(shell find Marlin/src/pins -mindepth 2 -name '*.h')

Expand Down
5 changes: 4 additions & 1 deletion Marlin/src/HAL/LINUX/hardware/Timer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ Timer::Timer() {
}

Timer::~Timer() {
timer_delete(timerid);
if (timerid != 0) {
timer_delete(timerid);
timerid = 0;
}
}

void Timer::init(uint32_t sig_id, uint32_t sim_freq, callback_fn* fn) {
Expand Down
2 changes: 2 additions & 0 deletions Marlin/src/HAL/LINUX/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

#ifdef __PLAT_LINUX__
#ifndef UNIT_TEST

//#define GPIO_LOGGING // Full GPIO and Positional Logging

Expand Down Expand Up @@ -135,4 +136,5 @@ int main() {
read_serial.join();
}

#endif // UNIT_TEST
#endif // __PLAT_LINUX__
35 changes: 0 additions & 35 deletions Marlin/src/tests/marlin_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,6 @@
// Startup tests are run at the end of setup()
void runStartupTests() {
// Call post-setup tests here to validate behaviors.

// String with cutoff at 20 chars:
// "F-string, 1234.50, 2"
SString<20> str20;
str20 = F("F-string, ");
str20.append(1234.5f).append(',').append(' ')
.append(2345.67).append(',').append(' ')
.echoln();

// Truncate to "F-string"
str20.trunc(8).echoln();

// 100 dashes, but chopped down to DEFAULT_MSTRING_SIZE (20)
TSS(repchr_t('-', 100)).echoln();

// Hello World!-123456------ <spaces!33
// ^ eol! ... 1234.50*2345.602 = 2895645.67
SString<100> str(F("Hello"));
str.append(F(" World!"));
str += '-';
str += uint8_t(123);
str += F("456");
str += repchr_t('-', 6);
str += Spaces(3);
str += "< spaces!";
str += int8_t(33);
str.eol();
str += "^ eol!";

str.append("...", 1234.5f, '*', p_float_t(2345.602, 3), F(" = "), 1234.5 * 2345.602).echoln();

// Print it again with SERIAL_ECHOLN
auto print_char_ptr = [](char * const str) { SERIAL_ECHOLN(str); };
print_char_ptr(str);

}

// Periodic tests are run from within loop()
Expand Down
5 changes: 5 additions & 0 deletions Marlin/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
These test files are executed by the unit-tests built from the `<root>/test` folder.

These are placed outside of the main PlatformIO test folder so we can collect all test files and compile them into multiple PlatformIO test binaries. This enables tests to be executed against a variety of Marlin configurations.

To execute these tests, refer to the top-level Makefile.
58 changes: 58 additions & 0 deletions Marlin/tests/gcode/test_gcode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Marlin 3D Printer Firmware
* Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
*
* Based on Sprinter and grbl.
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

#include "../test/unit_tests.h"
#include <src/gcode/gcode.h>
#include <src/gcode/parser.h>

MARLIN_TEST(gcode, process_parsed_command) {
GcodeSuite suite;
parser.command_letter = 'G';
parser.codenum = 0;
suite.process_parsed_command(false);
}

MARLIN_TEST(gcode, parse_g1_xz) {
char current_command[] = "G0 X10 Z30";
parser.command_letter = -128;
parser.codenum = -1;
parser.parse(current_command);
TEST_ASSERT_EQUAL('G', parser.command_letter);
TEST_ASSERT_EQUAL(0, parser.codenum);
TEST_ASSERT_TRUE(parser.seen('X'));
TEST_ASSERT_FALSE(parser.seen('Y'));
TEST_ASSERT_TRUE(parser.seen('Z'));
TEST_ASSERT_FALSE(parser.seen('E'));
}

MARLIN_TEST(gcode, parse_g1_nxz) {
char current_command[] = "N123 G0 X10 Z30";
parser.command_letter = -128;
parser.codenum = -1;
parser.parse(current_command);
TEST_ASSERT_EQUAL('G', parser.command_letter);
TEST_ASSERT_EQUAL(0, parser.codenum);
TEST_ASSERT_TRUE(parser.seen('X'));
TEST_ASSERT_FALSE(parser.seen('Y'));
TEST_ASSERT_TRUE(parser.seen('Z'));
TEST_ASSERT_FALSE(parser.seen('E'));
}
36 changes: 36 additions & 0 deletions Marlin/tests/runout/test_runout_sensor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Marlin 3D Printer Firmware
* Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
*
* Based on Sprinter and grbl.
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

#include "../test/unit_tests.h"

#if ENABLED(FILAMENT_RUNOUT_SENSOR)

#include <src/feature/runout.h>

MARLIN_TEST(runout, poll_runout_states) {
FilamentSensorBase sensor;
// Expected default value is one bit set for each extruder
uint8_t expected = static_cast<uint8_t>(~(~0u << NUM_RUNOUT_SENSORS));
TEST_ASSERT_EQUAL(expected, sensor.poll_runout_states());
}

#endif
Loading

0 comments on commit d10861e

Please sign in to comment.