✅ 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:
8
test/001-default.ini
Normal file
8
test/001-default.ini
Normal 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
|
18
test/002-extruders_1_runout.ini
Normal file
18
test/002-extruders_1_runout.ini
Normal 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
|
32
test/003-extruders_3_runout.ini
Normal file
32
test/003-extruders_3_runout.ini
Normal 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
40
test/README.md
Normal 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
52
test/unit_tests.cpp
Normal 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
73
test/unit_tests.h
Normal 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()
|
Reference in New Issue
Block a user