Add unit testing framework (#26948)

- 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 <costas.basdekis@gmail.com>
Co-authored-by: Scott Lahteine <thinkyhead@users.noreply.github.com>
This commit is contained in:
Jason Smith
2024-04-13 12:06:08 -07:00
committed by GitHub
parent cf7c86d581
commit d10861e478
21 changed files with 711 additions and 112 deletions

8
test/001-default.ini Normal file
View File

@@ -0,0 +1,8 @@
# This file should remain empty except for the motherboard.
# If changes are needed by tests, it should be performed in another configuration.
[config:base]
ini_use_config = base
# Unit tests must use BOARD_SIMULATED to run natively in Linux
motherboard = BOARD_SIMULATED

View File

@@ -0,0 +1,18 @@
#
# Test configuration with a single extruder and a filament runout sensor
#
[config:base]
ini_use_config = base
# Unit tests must use BOARD_SIMULATED to run natively in Linux
motherboard = BOARD_SIMULATED
# Options to support runout sensors test
filament_runout_sensor = on
fil_runout_pin = 4 # dummy
advanced_pause_feature = on
emergency_parser = on
nozzle_park_feature = on
# Option to support testing parsing with parentheses comments enabled
paren_comments = on

View File

@@ -0,0 +1,32 @@
#
# Test configuration with three extruders and filament runout sensors
#
[config:base]
ini_use_config = base
# Unit tests must use BOARD_SIMULATED to run natively in Linux
motherboard = BOARD_SIMULATED
# Options to support runout sensor tests on three extruders.
# Options marked "dummy" are simply required to pass sanity checks.
extruders = 3
temp_sensor_1 = 1
temp_sensor_2 = 1
temp_2_pin = 4 # dummy
temp_3_pin = 4 # dummy
heater_2_pin = 4 # dummy
e2_step_pin = 4 # dummy
e2_dir_pin = 4 # dummy
e2_enable_pin = 4 # dummy
e3_step_pin = 4 # dummy
e3_dir_pin = 4 # dummy
e3_enable_pin = 4 # dummy
num_runout_sensors = 3
filament_runout_sensor = on
fil_runout_pin = 4 # dummy
fil_runout2_pin = 4 # dummy
fil_runout3_pin = 4 # dummy
filament_runout_script = "M600 %%c"
advanced_pause_feature = on
emergency_parser = on
nozzle_park_feature = on

40
test/README.md Normal file
View File

@@ -0,0 +1,40 @@
# Testing Marlin
Marlin included two types of automated tests:
- [Build Tests](../buildroot/tests) to catch syntax and code build errors.
- Unit Tests (this folder) to catch implementation errors.
This document focuses on Unit tests.
## Unit tests
Unit testing allows for functional testing of Marlin logic on a local machine. This strategy is available to all developers, and will be able to be used on generic GitHub workers to automate testing. While PlatformIO does support the execution of unit tests on target controllers, that is not yet implemented and not really practical. This would require dedicated testing labs, and would be less broadly usable than testing directly on the development or build machine.
Unit tests verify the behavior of small discrete sections of Marlin code. By thoroughly unit testing important parts of Marlin code, we effectively provide "guard rails" which will prevent major regressions in behavior. As long as all submissions go through the Pull Request process and execute automated checks, it is possible to catch most major issues prior to completion of a PR.
## What unit tests can and can't do
Unit tests can be used to validate the logic of single functions or whole features, as long as that function or feature doesn't depend on real hardware. So, for example, we can test whether a G-code command is parsed correctly and produces all the expected state changes, but we can't test whether a G-code triggered an endstop or the filament runout sensor without adding a new layer to simulate pins.
Generally speaking, the types of errors caught by unit tests are most often caught in the initial process of writing the tests, and thereafter they shore up the codebase against regressions, especially in core classes and types, which can be very useful for refactoring.
### Unit test FAQ
#### Q: Isn't writing unit tests a lot of work?
A: Yes, and it can be especially difficult with existing code that wasn't designed for unit testing. Some common sense should be used to decide where to employ unit testing, and at what level to perform it. While unit testing takes effort, it pays dividends in preventing regressions, and helping to pinpoint the source of failures when they do occur.
#### Q: Will this make refactoring harder?
A: Yes and No. Of course if you refactor code that unit tests use directly, it will have to be reworked as well. It actually can make refactoring more efficient, by providing assurance that the mechanism still works as intended.
#### Q: How can I debug one of these failing unit tests?
A: That's a great question, without a known immediate answer. It is likely possible to debug them interactively through PlatformIO, but that can at times take some creativity to configure. Unit tests are generally extremely small, so even without interactive debugging it can get you fairly close to the cause of the problem.
### Unit test architecture
We are currently using [PlatformIO unit tests](https://docs.platformio.org/en/latest/plus/unit-testing.html).
Since Marlin only compiles code required by the configuration, a separate test binary must be generated for any configuration change. The following process is used to unit test a variety of configurations:
1. This folder contains a set of INI configuration files (See `config.ini`), each containing a distinct set of configuration options for unit testing. All applicable unit tests will be run for each of these configurations.
2. The `Marlin/tests` folder contains the CPP code for all Unit Tests. Marlin macros (`ENABLED(feature)`, `TERN(FEATURE, A, B)`, etc.) are used to determine which tests should be registered and to alter test behavior.
3. The `linux_native_test` PlatformIO environment specifies a script to collect all the tests from this folder and add them to PlatformIO's list of test targets.
4. Tests are built and executed by the `Makefile` commands `unit-test-all-local` or `unit-test-all-local-docker`.

52
test/unit_tests.cpp Normal file
View File

@@ -0,0 +1,52 @@
/**
* 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/>.
*
*/
/**
* Provide the main() function used for all compiled unit test binaries.
* It collects all the tests defined in the code and runs them through Unity.
*/
#include "unit_tests.h"
static std::list<MarlinTest*> all_marlin_tests;
MarlinTest::MarlinTest(const std::string _name, const void(*_test)(), const int _line)
: name(_name), test(_test), line(_line) {
all_marlin_tests.push_back(this);
}
void MarlinTest::run() {
UnityDefaultTestRun((UnityTestFunction)test, name.c_str(), line);
}
void run_all_marlin_tests() {
for (const auto registration : all_marlin_tests) {
registration->run();
}
}
int main(int argc, char **argv) {
UNITY_BEGIN();
run_all_marlin_tests();
UNITY_END();
return 0;
}

73
test/unit_tests.h Normal file
View File

@@ -0,0 +1,73 @@
/**
* 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/>.
*
*/
#pragma once
#include <list>
#include <string>
#include <unity.h>
// Include MarlinConfig so configurations are available to all tests
#include "src/inc/MarlinConfig.h"
/**
* Class that allows us to dynamically collect tests
*/
class MarlinTest {
public:
MarlinTest(const std::string name, const void(*test)(), const int line);
/**
* Run the test via Unity
*/
void run();
/**
* The name, a pointer to the function, and the line number. These are
* passed to the Unity test framework.
*/
const std::string name;
const void(*test)();
const int line;
};
/**
* Internal macros used by MARLIN_TEST
*/
#define _MARLIN_TEST_CLASS_NAME(SUITE, NAME) MarlinTestClass_##SUITE##_##NAME
#define _MARLIN_TEST_INSTANCE_NAME(SUITE, NAME) MarlinTestClass_##SUITE##_##NAME##_instance
/**
* Macro to define a test. This will create a class with the test body and
* register it with the global list of tests.
*
* Usage:
* MARLIN_TEST(test_suite_name, test_name) {
* // Test body
* }
*/
#define MARLIN_TEST(SUITE, NAME) \
class _MARLIN_TEST_CLASS_NAME(SUITE, NAME) : public MarlinTest { \
public: \
_MARLIN_TEST_CLASS_NAME(SUITE, NAME)() : MarlinTest(#NAME, (const void(*)())&TestBody, __LINE__) {} \
static void TestBody(); \
}; \
const _MARLIN_TEST_CLASS_NAME(SUITE, NAME) _MARLIN_TEST_INSTANCE_NAME(SUITE, NAME); \
void _MARLIN_TEST_CLASS_NAME(SUITE, NAME)::TestBody()