diff --git a/.build-test-rules.yml b/.build-test-rules.yml index c3a6ba60ec..b3b253a365 100644 --- a/.build-test-rules.yml +++ b/.build-test-rules.yml @@ -31,3 +31,17 @@ esp_jpeg/examples/get_started: enable: - if: IDF_VERSION_MAJOR > 4 and IDF_TARGET in ["esp32", "esp32s2", "esp32s3"] reason: Example depends on BSP, which is supported only for IDF >= 5.0 and limited targets + +catch2/examples/catch2-test: + enable: + - if: INCLUDE_DEFAULT == 1 or IDF_TARGET == "linux" + disable: + - if: IDF_VERSION_MAJOR < 5 + reason: Example relies on WHOLE_ARCHIVE component property which was introduced in IDF v5.0 + - if: IDF_VERSION_MAJOR == 5 and IDF_VERSION_MINOR < 1 and IDF_TARGET == "linux" + reason: Docker container for release/v5.0 doesn't contain a C++ compiler + +catch2/examples/catch2-console: + disable: + - if: IDF_VERSION_MAJOR < 5 + reason: Example relies on WHOLE_ARCHIVE component property which was introduced in IDF v5.0 diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 78003a02c9..c84cfc5b8f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -21,6 +21,7 @@ body: multiple: false options: - bdc_motor + - catch2 - cbor - coap - coremark diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index d4a7f33484..00ae7ec2ee 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -26,6 +26,7 @@ jobs: # directories: > bdc_motor; + catch2; cbor; ccomp_timer; coap; diff --git a/.gitmodules b/.gitmodules index 29e2c91068..028effd1b1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -100,6 +100,7 @@ [submodule "coremark/coremark"] path = coremark/coremark url = https://github.com/eembc/coremark.git + [submodule "freetype/freetype"] path = freetype/freetype url = https://github.com/freetype/freetype.git @@ -109,3 +110,14 @@ sbom-url = https://github.com/freetype/freetype.git sbom-description = FreeType is a software font engine sbom-hash = de8b92dd7ec634e9e2b25ef534c54a3537555c11 + +[submodule "catch2/Catch2"] + path = catch2/Catch2 + url = https://github.com/catchorg/Catch2.git + sbom-version = 3.4.0 + sbom-cpe = cpe:2.3:a:catchorg:catch2:{}:*:*:*:*:*:*:* + sbom-supplier = Organization: catchorg + sbom-url = https://github.com/catchorg/Catch2 + sbom-description = A modern, C++-native, test framework for unit-tests, TDD and BDD - using C++14, C++17 and later + sbom-hash = 6e79e682b726f524310d55dec8ddac4e9c52fb5f + diff --git a/catch2/CMakeLists.txt b/catch2/CMakeLists.txt new file mode 100644 index 0000000000..21d31de121 --- /dev/null +++ b/catch2/CMakeLists.txt @@ -0,0 +1,25 @@ +idf_component_register(SRCS cmd_catch2.cpp + INCLUDE_DIRS include) + +set(CATCH_CONFIG_NO_POSIX_SIGNALS 1 CACHE BOOL OFF FORCE) +add_subdirectory(Catch2) + +target_link_libraries(${COMPONENT_LIB} PUBLIC Catch2::Catch2) +get_target_property(catch_target Catch2::Catch2 ALIASED_TARGET) + +# Silence a warning in catch_exception_translator_registry.cpp +target_compile_options(${catch_target} PRIVATE -Wno-unused-function) + +# Link to pthreads to avoid issues with STL headers +idf_build_get_property(target IDF_TARGET) +if(NOT target STREQUAL "linux") + target_link_libraries(${catch_target} PUBLIC idf::pthread) +endif() + +# If console component is present in the build, include the console +# command feature. +idf_build_get_property(build_components BUILD_COMPONENTS) +if("console" IN_LIST build_components) + target_compile_definitions(${COMPONENT_LIB} PRIVATE WITH_CONSOLE) + target_link_libraries(${COMPONENT_LIB} PUBLIC idf::console) +endif() diff --git a/catch2/Catch2 b/catch2/Catch2 new file mode 160000 index 0000000000..6e79e682b7 --- /dev/null +++ b/catch2/Catch2 @@ -0,0 +1 @@ +Subproject commit 6e79e682b726f524310d55dec8ddac4e9c52fb5f diff --git a/catch2/LICENSE.txt b/catch2/LICENSE.txt new file mode 100644 index 0000000000..36b7cd93cd --- /dev/null +++ b/catch2/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/catch2/README.md b/catch2/README.md new file mode 100644 index 0000000000..d65c9021bc --- /dev/null +++ b/catch2/README.md @@ -0,0 +1,94 @@ +# Catch2 unit testing library + +This component is an ESP-IDF wrapper around [Catch2 unit testing library](https://github.com/catchorg/Catch2). + +Tests written using this component can be executed both on real hardware and on the host. + +## Using Catch2 in ESP-IDF + +### Example + +To get started with Catch2 quickly, use the example provided along with this component: + +```bash +idf.py create-project-from-example "espressif/catch:catch2-test" +cd catch2-test +idf.py set-target esp32 +idf.py build flash monitor +``` + +The example can also be used on host (Linux): +```bash +idf.py --preview set-target linux +idf.py build monitor +``` + +### Writing tests + +Tests and assertions are written as usual for Catch2, refer to the [official documentation](https://github.com/catchorg/Catch2/tree/devel/docs) for more details. Here is a simple example: + +```c++ +#include + +TEST_CASE("Test case 1") +{ + REQUIRE(1 == 1); +} +``` + +To ensure the test cases are linked into the application, use `WHOLE_ARCHIVE` argument when calling `idf_component_register()` in CMake. For example: + +```cmake +idf_component_register(SRCS "test_main.cpp" + "test_cases.cpp" + INCLUDE_DIRS "." + WHOLE_ARCHIVE) + +``` + +### Enabling C++ exceptions + +Catch2 relies on C++ exceptions for assertion handling. To get most benefits out of Catch2, it is recommended to have exceptions enabled in the project (`CONFIG_COMPILER_CXX_EXCEPTIONS=y`). + +### Stack size + +Catch2 uses significant amount of stack space — around 8kB, plus the stack space used by the test cases themselves. Therefore it is necessary to increase the stack size of the `main` task using the `CONFIG_ESP_MAIN_TASK_STACK_SIZE` option, or to invoke Catch from a new task with sufficient stack size. It is also recommended to keep `CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK` or `CONFIG_ESP_SYSTEM_HW_STACK_GUARD` options enabled to detect stack overflows. + +### Invoking the test runner + +Catch2 test framework can implement the application entry point (`main(int, char**)`) which calls the test runner. This functionality is typically used via the `CATCH_CONFIG_MAIN` macro. However ESP-IDF applications use `app_main(void)` function as an entry point, so the approach with `CATCH_CONFIG_MAIN` doesn't work. + + +Instead, invoke Catch2 in your `app_main` as follows: + +```c++ +#include + +extern "C" void app_main(void) +{ + // prepare command line arguments to Catch2 + const int argc = 1; + const char* argv[2] = {"target_test_main", NULL}; + + // run the tests + int result = Catch::Session().run(argc, argv); + // ... handle the result +``` + +### Integration with ESP-IDF `console` component + +This component provides a function to register an ESP-IDF console command to invoke Catch2 test cases: +```c++ +esp_err_t register_catch2(const char* cmd_name); +``` + +This function registers a command with the specified name (for example, "test") with ESP-IDF `console` component. The command passes all the arguments to Catch2 test runner. This makes it possible to invoke tests from an interactive console running on an ESP chip. + +To try this functionality, use `catch2-console` example: + +```bash +idf.py create-project-from-example "espressif/catch:catch2-console" +cd catch2-console +idf.py set-target esp32 +idf.py build flash monitor +``` diff --git a/catch2/cmd_catch2.cpp b/catch2/cmd_catch2.cpp new file mode 100644 index 0000000000..380a42c15d --- /dev/null +++ b/catch2/cmd_catch2.cpp @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: BSL-1.0 + * Note: same license as Catch2 + */ +#if WITH_CONSOLE +#include "Catch2/src/catch2/catch_config.hpp" +#include "esp_console.h" +#include "catch2/catch_session.hpp" +#include "cmd_catch2.h" + +static int cmd_catch2(int argc, char **argv) +{ + static auto session = Catch::Session(); + Catch::ConfigData configData; + session.useConfigData(configData); + return session.run(argc, argv); +} + +extern "C" esp_err_t register_catch2(const char *cmd_name) +{ + const esp_console_cmd_t cmd = { + .command = cmd_name, + .help = "Run tests", + .hint = NULL, + .func = &cmd_catch2, + .argtable = NULL + }; + return esp_console_cmd_register(&cmd); +} + +#endif // WITH_CONSOLE diff --git a/catch2/examples/catch2-console/CMakeLists.txt b/catch2/examples/catch2-console/CMakeLists.txt new file mode 100644 index 0000000000..37dc7bab5e --- /dev/null +++ b/catch2/examples/catch2-console/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) + +project(catch2-console) + diff --git a/catch2/examples/catch2-console/README.md b/catch2/examples/catch2-console/README.md new file mode 100644 index 0000000000..c45f243f26 --- /dev/null +++ b/catch2/examples/catch2-console/README.md @@ -0,0 +1,23 @@ +# Catch2 console example + +This example shows how to combine Catch2 test framework with ESP-IDF `console` component. + +In this example, you can execute test cases from an interactive console on an ESP chip. + +## Using the example + +To run the example, build and flash the project as usual. For example, with an ESP32 chip: + +```bash +idf.py set-target esp32 +idf.py build flash monitor +``` + +In the console, use `test` command to invoke Catch2. `test` accepts the same command line arguments as Catch2 tests on the host: + +- `test -h` — prints command line argument reference +- `test --list-tests` — lists all the registered tests +- `test` — runs all the registered tests +- `test ` — runs specific tests + +[See Catch2 documentation](https://github.com/catchorg/Catch2/blob/devel/docs/command-line.md) for the complete command line argument reference. diff --git a/catch2/examples/catch2-console/main/CMakeLists.txt b/catch2/examples/catch2-console/main/CMakeLists.txt new file mode 100644 index 0000000000..8e8df3a7f5 --- /dev/null +++ b/catch2/examples/catch2-console/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "test_main.cpp" + "test_cases.cpp" + INCLUDE_DIRS "." + WHOLE_ARCHIVE + PRIV_REQUIRES console) diff --git a/catch2/examples/catch2-console/main/idf_component.yml b/catch2/examples/catch2-console/main/idf_component.yml new file mode 100644 index 0000000000..3649e3c50c --- /dev/null +++ b/catch2/examples/catch2-console/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/catch2: + version: "*" + override_path: "../../../" diff --git a/catch2/examples/catch2-console/main/test_cases.cpp b/catch2/examples/catch2-console/main/test_cases.cpp new file mode 100644 index 0000000000..8cdaee220a --- /dev/null +++ b/catch2/examples/catch2-console/main/test_cases.cpp @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include + +TEST_CASE("Test case 1") +{ + REQUIRE(1 == 1); +} diff --git a/catch2/examples/catch2-console/main/test_main.cpp b/catch2/examples/catch2-console/main/test_main.cpp new file mode 100644 index 0000000000..725880de7a --- /dev/null +++ b/catch2/examples/catch2-console/main/test_main.cpp @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "esp_console.h" +#include "cmd_catch2.h" + +#if SOC_USB_SERIAL_JTAG_SUPPORTED +#if !CONFIG_ESP_CONSOLE_SECONDARY_NONE +#warning "A secondary serial console is not useful when using the console component. Please disable it in menuconfig." +#endif +#endif + +extern "C" void app_main(void) +{ + esp_console_repl_t *repl = NULL; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + repl_config.prompt = "catch2>"; + repl_config.task_stack_size = 10000; + + /* Register commands */ + esp_console_register_help_command(); + register_catch2("test"); + +#if defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM) + esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl)); + +#elif defined(CONFIG_ESP_CONSOLE_USB_CDC) + esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl)); + +#elif defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) + esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl)); + +#else +#error Unsupported console type +#endif + + ESP_ERROR_CHECK(esp_console_start_repl(repl)); +} diff --git a/catch2/examples/catch2-console/sdkconfig.defaults b/catch2/examples/catch2-console/sdkconfig.defaults new file mode 100644 index 0000000000..75ebeae1c7 --- /dev/null +++ b/catch2/examples/catch2-console/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y diff --git a/catch2/examples/catch2-test/CMakeLists.txt b/catch2/examples/catch2-test/CMakeLists.txt new file mode 100644 index 0000000000..9808d6ea5c --- /dev/null +++ b/catch2/examples/catch2-test/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) + +project(catch2-test) + diff --git a/catch2/examples/catch2-test/README.md b/catch2/examples/catch2-test/README.md new file mode 100644 index 0000000000..6cf3f4373d --- /dev/null +++ b/catch2/examples/catch2-test/README.md @@ -0,0 +1,36 @@ +# Catch2 example + +This example should help you get started with Catch2 test framework. + +## Using the example + +To run the example on an ESP32, build and flash the project as usual: + +```bash +idf.py set-target esp32 +idf.py build flash monitor +``` + +The example can also be used on Linux host: +```bash +idf.py --preview set-target linux +idf.py build monitor +``` + +## Example structure + +- [main/idf_component.yml](main/idf_component.yml) adds a dependency on `espressif/catch2` component. +- [main/CMakeLists.txt](main/CMakeLists.txt) specifies the source files and registers the `main` component with `WHOLE_ARCHIVE` option enabled. +- [main/test_main.cpp](main/test_main.cpp) implements the application entry point which calls the test runner. +- [main/test_cases.cpp](main/test_cases.cpp) implements one trivial test case. +- [sdkconfig.defaults](sdkconfig.defaults) sets the options required to run the example: enables C++ exceptions and increases the size of the `main` task stack. + +## Expected output + +``` +Randomness seeded to: 3499211612 +=============================================================================== +All tests passed (1 assertion in 1 test case) + +Test passed. +``` diff --git a/catch2/examples/catch2-test/main/CMakeLists.txt b/catch2/examples/catch2-test/main/CMakeLists.txt new file mode 100644 index 0000000000..9f8b8cd187 --- /dev/null +++ b/catch2/examples/catch2-test/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "test_main.cpp" + "test_cases.cpp" + INCLUDE_DIRS "." + WHOLE_ARCHIVE) diff --git a/catch2/examples/catch2-test/main/idf_component.yml b/catch2/examples/catch2-test/main/idf_component.yml new file mode 100644 index 0000000000..3649e3c50c --- /dev/null +++ b/catch2/examples/catch2-test/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/catch2: + version: "*" + override_path: "../../../" diff --git a/catch2/examples/catch2-test/main/test_cases.cpp b/catch2/examples/catch2-test/main/test_cases.cpp new file mode 100644 index 0000000000..8cdaee220a --- /dev/null +++ b/catch2/examples/catch2-test/main/test_cases.cpp @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include + +TEST_CASE("Test case 1") +{ + REQUIRE(1 == 1); +} diff --git a/catch2/examples/catch2-test/main/test_main.cpp b/catch2/examples/catch2-test/main/test_main.cpp new file mode 100644 index 0000000000..68ff756aa0 --- /dev/null +++ b/catch2/examples/catch2-test/main/test_main.cpp @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include + +extern "C" void app_main(void) +{ + int argc = 1; + const char *argv[2] = { + "target_test_main", + NULL + }; + + auto result = Catch::Session().run(argc, argv); + if (result != 0) { + printf("Test failed with result %d\n", result); + } else { + printf("Test passed.\n"); + } +} diff --git a/catch2/examples/catch2-test/sdkconfig.defaults b/catch2/examples/catch2-test/sdkconfig.defaults new file mode 100644 index 0000000000..02cf678d77 --- /dev/null +++ b/catch2/examples/catch2-test/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=10000 diff --git a/catch2/idf_component.yml b/catch2/idf_component.yml new file mode 100644 index 0000000000..25ab3412c2 --- /dev/null +++ b/catch2/idf_component.yml @@ -0,0 +1,10 @@ +version: "3.4.0" +description: A modern, C++-native, test framework for unit-tests, TDD and BDD - using C++14, C++17 and later +url: https://github.com/espressif/idf-extra-components/tree/master/catch2 +repository: https://github.com/espressif/idf-extra-components.git +issues: https://github.com/espressif/idf-extra-components/issues +documentation: https://github.com/catchorg/Catch2/tree/devel/docs +dependencies: + # Mostly because in older IDF versions there was no WHOLE_ARCHIVE component property, + # so linking all the test cases in a component was a bit hard. + idf: ">=5.0.0" diff --git a/catch2/include/cmd_catch2.h b/catch2/include/cmd_catch2.h new file mode 100644 index 0000000000..3fdaeea07d --- /dev/null +++ b/catch2/include/cmd_catch2.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: BSL-1.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" + +/** + * @brief Register a command to run Catch2 tests. + * + * @param cmd_name Name of the command to use. For example, "test". + * @return esp_err_t ESP_OK on success, otherwise an error code. + */ +esp_err_t register_catch2(const char *cmd_name); + +#ifdef __cplusplus +} +#endif diff --git a/test_app/CMakeLists.txt b/test_app/CMakeLists.txt index 6fbad01755..b27297b994 100644 --- a/test_app/CMakeLists.txt +++ b/test_app/CMakeLists.txt @@ -14,7 +14,7 @@ endif() # 3. Add here if the component is compatible with IDF >= v5.0 if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0") - list(APPEND EXTRA_COMPONENT_DIRS ../bdc_motor ../led_strip ../sh2lib ../nghttp ../esp_serial_slave_link ../onewire_bus ../ccomp_timer) + list(APPEND EXTRA_COMPONENT_DIRS ../bdc_motor ../led_strip ../sh2lib ../nghttp ../esp_serial_slave_link ../onewire_bus ../ccomp_timer ../catch2) endif() # !This section should NOT be touched when adding new component!