diff --git a/.github/workflows/test-builds.yml b/.github/workflows/ci-build-tests.yml similarity index 98% rename from .github/workflows/test-builds.yml rename to .github/workflows/ci-build-tests.yml index a3cf32739c92..ad37100d6031 100644 --- a/.github/workflows/test-builds.yml +++ b/.github/workflows/ci-build-tests.yml @@ -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: @@ -27,7 +27,7 @@ on: jobs: test_builds: - name: Run All Tests + name: Build Test if: github.repository == 'MarlinFirmware/Marlin' runs-on: ubuntu-latest diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml new file mode 100644 index 000000000000..caed5b1fbc95 --- /dev/null +++ b/.github/workflows/ci-unit-tests.yml @@ -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 diff --git a/Makefile b/Makefile index bc26173aaf36..029ab3ada13f 100644 --- a/Makefile +++ b/Makefile @@ -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" @@ -43,7 +48,7 @@ tests-single-local: tests-single-local-docker: @if ! test -n "$(TEST_TARGET)" ; then echo "***ERROR*** Set TEST_TARGET= 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}" \ @@ -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= 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= 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') diff --git a/Marlin/src/HAL/LINUX/hardware/Timer.cpp b/Marlin/src/HAL/LINUX/hardware/Timer.cpp index 9f0d6a8f3ae2..013690a404bf 100644 --- a/Marlin/src/HAL/LINUX/hardware/Timer.cpp +++ b/Marlin/src/HAL/LINUX/hardware/Timer.cpp @@ -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) { diff --git a/Marlin/src/HAL/LINUX/main.cpp b/Marlin/src/HAL/LINUX/main.cpp index f2af2ff33f52..27a066d619ce 100644 --- a/Marlin/src/HAL/LINUX/main.cpp +++ b/Marlin/src/HAL/LINUX/main.cpp @@ -21,6 +21,7 @@ */ #ifdef __PLAT_LINUX__ +#ifndef UNIT_TEST //#define GPIO_LOGGING // Full GPIO and Positional Logging @@ -135,4 +136,5 @@ int main() { read_serial.join(); } +#endif // UNIT_TEST #endif // __PLAT_LINUX__ diff --git a/Marlin/src/tests/marlin_tests.cpp b/Marlin/src/tests/marlin_tests.cpp index f61f840176f2..89e5664345d8 100644 --- a/Marlin/src/tests/marlin_tests.cpp +++ b/Marlin/src/tests/marlin_tests.cpp @@ -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------ 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() diff --git a/Marlin/tests/README.md b/Marlin/tests/README.md new file mode 100644 index 000000000000..883069f044bd --- /dev/null +++ b/Marlin/tests/README.md @@ -0,0 +1,5 @@ +These test files are executed by the unit-tests built from the `/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. diff --git a/Marlin/tests/gcode/test_gcode.cpp b/Marlin/tests/gcode/test_gcode.cpp new file mode 100644 index 000000000000..be364cb90550 --- /dev/null +++ b/Marlin/tests/gcode/test_gcode.cpp @@ -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 . + * + */ + +#include "../test/unit_tests.h" +#include +#include + +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')); +} diff --git a/Marlin/tests/runout/test_runout_sensor.cpp b/Marlin/tests/runout/test_runout_sensor.cpp new file mode 100644 index 000000000000..2719446437fa --- /dev/null +++ b/Marlin/tests/runout/test_runout_sensor.cpp @@ -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 . + * + */ + +#include "../test/unit_tests.h" + +#if ENABLED(FILAMENT_RUNOUT_SENSOR) + +#include + +MARLIN_TEST(runout, poll_runout_states) { + FilamentSensorBase sensor; + // Expected default value is one bit set for each extruder + uint8_t expected = static_cast(~(~0u << NUM_RUNOUT_SENSORS)); + TEST_ASSERT_EQUAL(expected, sensor.poll_runout_states()); +} + +#endif diff --git a/Marlin/tests/types/test_types.cpp b/Marlin/tests/types/test_types.cpp new file mode 100644 index 000000000000..11ed19f4c3b1 --- /dev/null +++ b/Marlin/tests/types/test_types.cpp @@ -0,0 +1,160 @@ +/** + * 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 . + * + */ + +#include "../test/unit_tests.h" +#include "src/core/types.h" + +MARLIN_TEST(types, XYval_const_as_bools) { + const XYval xy_const_true = {1, 2}; + TEST_ASSERT_TRUE(xy_const_true); + + const XYval xy_const_false = {0, 0}; + TEST_ASSERT_FALSE(xy_const_false); +} + +MARLIN_TEST(types, XYval_non_const_as_bools) { + XYval xy_true = {1, 2}; + TEST_ASSERT_TRUE(xy_true); + + XYval xy_false = {0, 0}; + TEST_ASSERT_FALSE(xy_false); +} + +MARLIN_TEST(types, XYZval_const_as_bools) { + const XYZval xyz_const_true = {1, 2, 3}; + TEST_ASSERT_TRUE(xyz_const_true); + + const XYZval xyz_const_false = {0, 0, 0}; + TEST_ASSERT_FALSE(xyz_const_false); +} + +MARLIN_TEST(types, XYZval_non_const_as_bools) { + XYZval xyz_true = {1, 2, 3}; + TEST_ASSERT_TRUE(xyz_true); + + XYZval xyz_false = {0, 0, 0}; + TEST_ASSERT_FALSE(xyz_false); +} + +MARLIN_TEST(types, XYZEval_const_as_bools) { + const XYZEval xyze_const_true = {1, 2, 3, 4}; + TEST_ASSERT_TRUE(xyze_const_true); + + const XYZEval xyze_const_false = {0, 0, 0, 0}; + TEST_ASSERT_FALSE(xyze_const_false); +} + +MARLIN_TEST(types, XYZEval_non_const_as_bools) { + XYZEval xyze_true = {1, 2, 3, 4}; + TEST_ASSERT_TRUE(xyze_true); + + XYZEval xyze_false = {0, 0, 0, 0}; + TEST_ASSERT_FALSE(xyze_false); +} + +MARLIN_TEST(types, Flags_const_as_bools) { + const Flags<32> flags_const_false = {0}; + TEST_ASSERT_FALSE(flags_const_false); + + const Flags<32> flags_const_true = {1}; + TEST_ASSERT_TRUE(flags_const_true); +} + +MARLIN_TEST(types, Flags_non_const_as_bools) { + Flags<32> flags_false = {0}; + TEST_ASSERT_FALSE(flags_false); + + Flags<32> flags_true = {1}; + TEST_ASSERT_TRUE(flags_true); +} + +MARLIN_TEST(types, AxisFlags_const_as_bools) { + const AxisFlags axis_flags_const_false = {0}; + TEST_ASSERT_FALSE(axis_flags_const_false); + + const AxisFlags axis_flags_const_true = {1}; + TEST_ASSERT_TRUE(axis_flags_const_true); +} + +MARLIN_TEST(types, AxisFlags_non_const_as_bools) { + AxisFlags axis_flags_false = {0}; + TEST_ASSERT_FALSE(axis_flags_false); + + AxisFlags axis_flags_true = {1}; + TEST_ASSERT_TRUE(axis_flags_true); +} + +MARLIN_TEST(types, AxisBits_const_as_bools) { + const AxisBits axis_bits_const_false = {0}; + TEST_ASSERT_FALSE(axis_bits_const_false); + + const AxisBits axis_bits_const_true = {1}; + TEST_ASSERT_TRUE(axis_bits_const_true); +} + +MARLIN_TEST(types, AxisBits_non_const_as_bools) { + AxisBits axis_bits_false = {0}; + TEST_ASSERT_FALSE(axis_bits_false); + + AxisBits axis_bits_true = {1}; + TEST_ASSERT_TRUE(axis_bits_true); +} + +MARLIN_TEST(types, MString1) { + // String with cutoff at 20 chars: + // "F-string, 1234.50, 2" + MString<20> str20; + str20 = F("F-string, "); + str20.append(1234.5f).append(',').append(' ') + .append(2345.67).append(',').append(' '); + + TEST_ASSERT_TRUE(strcmp_P(str20, PSTR("F-string, 1234.50, 2")) == 0); + + // Truncate to "F-string" + str20.trunc(8); + + TEST_ASSERT_FALSE(strcmp_P(&str20, PSTR("F-string")) != 0); +} + +MARLIN_TEST(types, MString2) { + // 100 dashes, but chopped down to DEFAULT_MSTRING_SIZE (20) + TEST_ASSERT_TRUE(TSS(repchr_t('-', 100)).length() == 20); +} + +MARLIN_TEST(types, SString) { + // 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); + + TEST_ASSERT_TRUE(strcmp_P(str, PSTR("Hello World!-123456------ < spaces!33\n^ eol! ... 1234.50*2345.602 = 2895645.67")) == 0); +} diff --git a/README.md b/README.md index 83614ad9ccef..efbba1de27a6 100644 --- a/README.md +++ b/README.md @@ -39,16 +39,16 @@ To build and upload Marlin you will use one of these tools: Marlin is optimized to build with the **PlatformIO IDE** extension for **Visual Studio Code**. You can still build Marlin with **Arduino IDE**, and we hope to improve the Arduino build experience, but at this time PlatformIO is the better choice. +## 8-Bit AVR Boards + +We intend to continue supporting 8-bit AVR boards in perpetuity, maintaining a single codebase that can apply to all machines. We want casual hobbyists and tinkerers and owners of older machines to benefit from the community's innovations just as much as those with fancier machines. Plus, those old AVR-based machines are often the best for your testing and feedback! + ## Hardware Abstraction Layer (HAL) Marlin includes an abstraction layer to provide a common API for all the platforms it targets. This allows Marlin code to address the details of motion and user interface tasks at the lowest and highest levels with no system overhead, tying all events directly to the hardware clock. Every new HAL opens up a world of hardware. At this time we need HALs for RP2040 and the Duet3D family of boards. A HAL that wraps an RTOS is an interesting concept that could be explored. Did you know that Marlin includes a Simulator that can run on Windows, macOS, and Linux? Join the Discord to help move these sub-projects forward! -## 8-Bit AVR Boards - -A core tenet of this project is to keep supporting 8-bit AVR boards while also maintaining a single codebase that applies equally to all machines. We want casual hobbyists to benefit from the community's innovations as much as possible just as much as those with fancier machines. Plus, those old AVR-based machines are often the best for your testing and feedback! - ### Supported Platforms Platform|MCU|Example Boards @@ -71,22 +71,9 @@ A core tenet of this project is to keep supporting 8-bit AVR boards while also m [Teensy 4.1](https://www.pjrc.com/store/teensy41.html)|ARMยฎ Cortex-M7| Linux Native|x86/ARM/etc.|Raspberry Pi -## Submitting Patches - -Proposed patches should be submitted as a Pull Request against the ([bugfix-2.1.x](https://github.com/MarlinFirmware/Marlin/tree/bugfix-2.1.x)) branch. - -- This branch is for fixing bugs and integrating any new features for the duration of the Marlin 2.1.x life-cycle. -- Follow the [Coding Standards](https://marlinfw.org/docs/development/coding_standards.html) to gain points with the maintainers. -- Please submit Feature Requests and Bug Reports to the [Issue Queue](https://github.com/MarlinFirmware/Marlin/issues/new/choose). Support resources are also listed there. -- Whenever you add new features, be sure to add tests to `buildroot/tests` and then run your tests locally, if possible. - - It's optional: Running all the tests on Windows might take a long time, and they will run anyway on GitHub. - - If you're running the tests on Linux (or on WSL with the code on a Linux volume) the speed is much faster. - - You can use `make tests-all-local` or `make tests-single-local TEST_TARGET=...`. - - If you prefer Docker you can use `make tests-all-local-docker` or `make tests-all-local-docker TEST_TARGET=...`. - ## Marlin Support -The Issue Queue is reserved for Bug Reports and Feature Requests. To get help with configuration and troubleshooting, please use the following resources: +The Issue Queue is reserved for Bug Reports and Feature Requests. Please use the following resources for help with configuration and troubleshooting: - [Marlin Documentation](https://marlinfw.org) - Official Marlin documentation - [Marlin Discord](https://discord.gg/n5NJ59y) - Discuss issues with Marlin users and developers @@ -95,59 +82,48 @@ The Issue Queue is reserved for Bug Reports and Feature Requests. To get help wi - Facebook Group ["Marlin Firmware for 3D Printers"](https://www.facebook.com/groups/3Dtechtalk/) - [Marlin Configuration](https://www.youtube.com/results?search_query=marlin+configuration) on YouTube -## Contributors - -Marlin is constantly improving thanks to a huge number of contributors from all over the world bringing their specialties and talents. Huge thanks are due to [all the contributors](https://github.com/MarlinFirmware/Marlin/graphs/contributors) who regularly patch up bugs, help direct traffic, and basically keep Marlin from falling apart. Marlin's continued existence would not be possible without them. - -## Administration +## Contributing Patches -Regular users can open and close their own issues, but only the administrators can do project-related things like add labels, merge changes, set milestones, and kick trolls. The current Marlin admin team consists of: +You can contribute patches by submitting a Pull Request to the ([bugfix-2.1.x](https://github.com/MarlinFirmware/Marlin/tree/bugfix-2.1.x)) branch. - - - -
Project Maintainer
- - ๐Ÿ‡บ๐Ÿ‡ธโ€…โ€…**Scott Lahteine** - โ€…โ€…โ€…โ€…โ€…โ€…[@thinkyhead](https://github.com/thinkyhead) - โ€…โ€…โ€…โ€…โ€…โ€…[โ€…โ€…Donate ๐Ÿ’ธโ€…โ€…](https://www.thinkyhead.com/donate-to-marlin) - - - - ๐Ÿ‡บ๐Ÿ‡ธโ€…โ€…**Roxanne Neufeld** - โ€…โ€…โ€…โ€…โ€…โ€…[@Roxy-3D](https://github.com/Roxy-3D) - - ๐Ÿ‡บ๐Ÿ‡ธโ€…โ€…**Keith Bennett** - โ€…โ€…โ€…โ€…โ€…โ€…[@thisiskeithb](https://github.com/thisiskeithb) - โ€…โ€…โ€…โ€…โ€…โ€…[โ€…โ€…Donate ๐Ÿ’ธโ€…โ€…](https://github.com/sponsors/thisiskeithb) - - ๐Ÿ‡บ๐Ÿ‡ธโ€…โ€…**Jason Smith** - โ€…โ€…โ€…โ€…โ€…โ€…[@sjasonsmith](https://github.com/sjasonsmith) - - - - ๐Ÿ‡ง๐Ÿ‡ทโ€…โ€…**Victor Oliveira** - โ€…โ€…โ€…โ€…โ€…โ€…[@rhapsodyv](https://github.com/rhapsodyv) - - ๐Ÿ‡ฌ๐Ÿ‡งโ€…โ€…**Chris Pepper** - โ€…โ€…โ€…โ€…โ€…โ€…[@p3p](https://github.com/p3p) - -๐Ÿ‡ณ๐Ÿ‡ฟโ€…โ€…**Peter Ellens** - โ€…โ€…โ€…โ€…โ€…โ€…[@ellensp](https://github.com/ellensp) - โ€…โ€…โ€…โ€…โ€…โ€…[โ€…โ€…Donate ๐Ÿ’ธโ€…โ€…](https://ko-fi.com/ellensp) +- We use branches named with a "bugfix" or "dev" prefix to fix bugs and integrate new features. +- Follow the [Coding Standards](https://marlinfw.org/docs/development/coding_standards.html) to gain points with the maintainers. +- Please submit Feature Requests and Bug Reports to the [Issue Queue](https://github.com/MarlinFirmware/Marlin/issues/new/choose). See above for user support. +- Whenever you add new features, be sure to add one or more build tests to `buildroot/tests`. Any tests added to a PR will be run within that PR on GitHub servers as soon as they are pushed. To minimize iteration be sure to run your new tests locally, if possible. + - Local build tests: + - All: `make tests-config-all-local` + - Single: `make tests-config-single-local TEST_TARGET=...` + - Local build tests in Docker: + - All: `make tests-config-all-local-docker` + - Single: `make tests-config-all-local-docker TEST_TARGET=...` + - To run all unit test suites: + - Using PIO: `platformio run -t test-marlin` + - Using Make: `make unit-test-all-local` + - Using Docker + make: `maker unit-test-all-local-docker` + - To run a single unit test suite: + - Using PIO: `platformio run -t marlin_` + - Using make: `make unit-test-single-local TEST_TARGET=` + - Using Docker + make: `maker unit-test-single-local-docker TEST_TARGET=` +- If your feature can be unit tested, add one or more unit tests. For more information see our documentation on [Unit Tests](test). - +## Contributors - ๐Ÿ‡บ๐Ÿ‡ธโ€…โ€…**Bob Kuhn** - โ€…โ€…โ€…โ€…โ€…โ€…[@Bob-the-Kuhn](https://github.com/Bob-the-Kuhn) +Marlin is constantly improving thanks to a huge number of contributors from all over the world bringing their specialties and talents. Huge thanks are due to [all the contributors](https://github.com/MarlinFirmware/Marlin/graphs/contributors) who regularly patch up bugs, help direct traffic, and basically keep Marlin from falling apart. Marlin's continued existence would not be possible without them. - ๐Ÿ‡ณ๐Ÿ‡ฑโ€…โ€…**Erik van der Zalm** - โ€…โ€…โ€…โ€…โ€…โ€…[@ErikZalm](https://github.com/ErikZalm) +## Project Leadership -
+Name|Role|Link|Donate +----|----|----|---- +๐Ÿ‡บ๐Ÿ‡ธ Scott Lahteine|Project Lead|[[@thinkyhead](https://github.com/thinkyhead)]|[๐Ÿ’ธ Donate](https://www.thinkyhead.com/donate-to-marlin) +๐Ÿ‡บ๐Ÿ‡ธ Roxanne Neufeld|Admin|[[@Roxy-3D](https://github.com/Roxy-3D)]| +๐Ÿ‡บ๐Ÿ‡ธ Keith Bennett|Admin|[[@thisiskeithb](https://github.com/thisiskeithb)]|[๐Ÿ’ธ Donate](https://github.com/sponsors/thisiskeithb) +๐Ÿ‡บ๐Ÿ‡ธ Jason Smith|Admin|[[@sjasonsmith](https://github.com/sjasonsmith)]| +๐Ÿ‡ง๐Ÿ‡ท Victor Oliveira|Admin|[[@rhapsodyv](https://github.com/rhapsodyv)]| +๐Ÿ‡ฌ๐Ÿ‡ง Chris Pepper|Admin|[[@p3p](https://github.com/p3p)]| +๐Ÿ‡ณ๐Ÿ‡ฟ Peter Ellens|Admin|[[@ellensp](https://github.com/ellensp)]|[๐Ÿ’ธ Donate](https://ko-fi.com/ellensp) +๐Ÿ‡บ๐Ÿ‡ธ Bob Kuhn|Admin|[[@Bob-the-Kuhn](https://github.com/Bob-the-Kuhn)]| +๐Ÿ‡ณ๐Ÿ‡ฑ Erik van der Zalm|Founder|[[@ErikZalm](https://github.com/ErikZalm)]|[๐Ÿ’ธ Donate](https://flattr.com/submit/auto?user_id=ErikZalm&url=https://github.com/MarlinFirmware/Marlin&title=Marlin&language=&tags=github&category=software) ## License Marlin is published under the [GPL license](/LICENSE) because we believe in open development. The GPL comes with both rights and obligations. Whether you use Marlin firmware as the driver for your open or closed-source product, you must keep Marlin open, and you must provide your compatible Marlin source code to end users upon request. The most straightforward way to comply with the Marlin license is to make a fork of Marlin on Github, perform your modifications, and direct users to your modified fork. - -While we can't prevent the use of this code in products (3D printers, CNC, etc.) that are closed source or crippled by a patent, we would prefer that you choose another firmware or, better yet, make your own. diff --git a/buildroot/bin/restore_configs b/buildroot/bin/restore_configs index ea998484c243..51f72c579258 100755 --- a/buildroot/bin/restore_configs +++ b/buildroot/bin/restore_configs @@ -7,5 +7,5 @@ if [[ $1 == '-d' || $1 == '--default' ]]; then else git checkout Marlin/Configuration.h 2>/dev/null git checkout Marlin/Configuration_adv.h 2>/dev/null - git checkout Marlin/src/pins/ramps/pins_RAMPS.h 2>/dev/null + git checkout Marlin/src/pins/*/pins_*.h 2>/dev/null fi diff --git a/buildroot/share/PlatformIO/scripts/collect-code-tests.py b/buildroot/share/PlatformIO/scripts/collect-code-tests.py new file mode 100644 index 000000000000..a0e0e86b1176 --- /dev/null +++ b/buildroot/share/PlatformIO/scripts/collect-code-tests.py @@ -0,0 +1,59 @@ +# +# collect-code-tests.py +# Convenience script to collect all code tests. Used by env:linux_native_test in native.ini. +# + +import pioutil +if pioutil.is_pio_build(): + + import os, re + Import("env") + Import("projenv") + + os.environ['PATH'] = f"./buildroot/bin/:./buildroot/tests/:{os.environ['PATH']}" + + def collect_test_suites(): + """Get all the test suites""" + from pathlib import Path + return sorted(list(Path("./test").glob("*.ini"))) + + def register_test_suites(): + """Register all the test suites""" + targets = [] + test_suites = collect_test_suites() + for path in test_suites: + name = re.sub(r'^\d+-|\.ini$', '', path.name) + targets += [name]; + + env.AddCustomTarget( + name = f"marlin_{name}", + dependencies = None, + actions = [ + f"echo ====== Configuring for marlin_{name} ======", + "restore_configs", + f"cp -f {path} ./Marlin/config.ini", + "python ./buildroot/share/PlatformIO/scripts/configuration.py", + f"platformio test -e linux_native_test -f {name}", + "restore_configs", + ], + title = "Marlin: {}".format(name.lower().title().replace("_", " ")), + description = ( + f"Run a Marlin test suite, with the appropriate configuration, " + f"that sits in {path}" + ) + ) + + env.AddCustomTarget( + name = "test-marlin", + dependencies = None, + actions = [ + f"platformio run -t marlin_{name} -e linux_native_test" + for name in targets + ], + title = "Marlin: Test all code test suites", + description = ( + f"Run all Marlin code test suites ({len(targets)} found)." + ), + ) + + register_test_suites() diff --git a/buildroot/share/PlatformIO/scripts/preflight-checks.py b/buildroot/share/PlatformIO/scripts/preflight-checks.py index 2e4ab5c92d54..2a5f98dbbfc5 100644 --- a/buildroot/share/PlatformIO/scripts/preflight-checks.py +++ b/buildroot/share/PlatformIO/scripts/preflight-checks.py @@ -71,7 +71,9 @@ def sanity_check_target(): config = env.GetProjectConfig() result = check_envs("env:"+build_env, board_envs, config) - if not result: + # Make sure board is compatible with the build environment. Skip for _test, + # since the board is manipulated as each unit test is executed. + if not result and build_env != "linux_native_test": err = "Error: Build environment '%s' is incompatible with %s. Use one of these environments: %s" % \ ( build_env, motherboard, ", ".join([ e[4:] for e in board_envs if e.startswith("env:") ]) ) raise SystemExit(err) diff --git a/ini/native.ini b/ini/native.ini index d07eaa22051e..332555ed0555 100644 --- a/ini/native.ini +++ b/ini/native.ini @@ -15,13 +15,24 @@ [env:linux_native] platform = native framework = -build_flags = -D__PLAT_LINUX__ -std=gnu++17 -ggdb -g -lrt -lpthread -D__MARLIN_FIRMWARE__ -Wno-expansion-to-defined +build_flags = ${common.build_flags} -D__PLAT_LINUX__ -std=gnu++17 -ggdb -g -lrt -lpthread -D__MARLIN_FIRMWARE__ -Wno-expansion-to-defined build_src_flags = -Wall -IMarlin/src/HAL/LINUX/include build_unflags = -Wall lib_ldf_mode = off -lib_deps = build_src_filter = ${common.default_src_filter} + +# Environment specifically for unit testing through the Makefile +# This is somewhat unorthodox, in that it uses the PlatformIO Unity testing framework, +# but actual targets are dynamically generated during the build. This seems to prevent +# Unity from being automatically included, so it is added here. +[env:linux_native_test] +extends = env:linux_native +extra_scripts = ${common.extra_scripts} + post:buildroot/share/PlatformIO/scripts/collect-code-tests.py +build_src_filter = ${env:linux_native.build_src_filter} + +lib_deps = throwtheswitch/Unity@^2.5.2 +test_build_src = true + # # Native Simulation # Builds with a small subset of available features diff --git a/test/001-default.ini b/test/001-default.ini new file mode 100644 index 000000000000..b98042cc2dcb --- /dev/null +++ b/test/001-default.ini @@ -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 diff --git a/test/002-extruders_1_runout.ini b/test/002-extruders_1_runout.ini new file mode 100644 index 000000000000..74afa0f02f21 --- /dev/null +++ b/test/002-extruders_1_runout.ini @@ -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 diff --git a/test/003-extruders_3_runout.ini b/test/003-extruders_3_runout.ini new file mode 100644 index 000000000000..4bd91e8b7cae --- /dev/null +++ b/test/003-extruders_3_runout.ini @@ -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 diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000000..19b4cd7d5949 --- /dev/null +++ b/test/README.md @@ -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`. diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp new file mode 100644 index 000000000000..0d9e568760b2 --- /dev/null +++ b/test/unit_tests.cpp @@ -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 . + * + */ + +/** + * 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 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; +} diff --git a/test/unit_tests.h b/test/unit_tests.h new file mode 100644 index 000000000000..6f8387619a4b --- /dev/null +++ b/test/unit_tests.h @@ -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 . + * + */ +#pragma once + +#include +#include +#include + +// 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()