cmake: Add support for test build

This commit is contained in:
Renz Bagaporo 2018-08-27 10:48:16 +08:00
parent 129d32772e
commit cc774111bf
54 changed files with 1290 additions and 66 deletions

View File

@ -24,6 +24,7 @@ variables:
GIT_STRATEGY: fetch
GIT_SUBMODULE_STRATEGY: none
UNIT_TEST_BUILD_SYSTEM: make
# IDF environment
IDF_PATH: "$CI_PROJECT_DIR"
@ -172,11 +173,24 @@ build_esp_idf_tests:
- components/idf_test/unit_test/CIConfigs/*.yml
expire_in: 2 days
script:
- cd tools/unit-test-app
- MAKEFLAGS= make help # make sure kconfig tools are built in single process
- make ut-clean-all-configs
- export PATH="$IDF_PATH/tools:$PATH"
- cd $CI_PROJECT_DIR/tools/unit-test-app
- export EXTRA_CFLAGS="-Werror -Werror=deprecated-declarations"
- export EXTRA_CXXFLAGS=${EXTRA_CFLAGS}
# Build with CMake first
- idf.py ut-clean-all-configs
- idf.py ut-build-all-configs
- python tools/UnitTestParser.py
# Check if test demands CMake or Make built binaries. If CMake leave the built artifacts as is then exit.
- if [ "$UNIT_TEST_BUILD_SYSTEM" == "cmake" ]; then exit 0; fi
# If Make, delete the CMake built artifacts
- rm -rf builds output sdkconfig
- rm -rf components/idf_test/unit_test/TestCaseAll.yml
- rm -rf components/idf_test/unit_test/CIConfigs/*.yml
# Then build with Make
- cd $CI_PROJECT_DIR/tools/unit-test-app
- MAKEFLAGS= make help # make sure kconfig tools are built in single process
- make ut-clean-all-configs
- make ut-build-all-configs
- python tools/UnitTestParser.py
@ -623,6 +637,20 @@ check_examples_cmake_make:
script:
- tools/ci/check_examples_cmake_make.sh
check_ut_cmake_make:
stage: check
image: $CI_DOCKER_REGISTRY/esp32-ci-env$BOT_DOCKER_IMAGE_TAG
tags:
- build
except:
- master
- /^release\/v/
- /^v\d+\.\d+(\.\d+)?($|-)/
dependencies: []
before_script: *do_nothing_before
script:
- tools/ci/check_ut_cmake_make.sh
check_submodule_sync:
<<: *check_job_template
variables:
@ -1577,4 +1605,4 @@ IT_015_01:
<<: *test_template
tags:
- ESP32_IDF
- SSC_T2_4
- SSC_T2_4

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity app_update bootloader_support nvs_flash)
register_component()

View File

@ -17,7 +17,8 @@ externalproject_add(bootloader
# TODO: support overriding the bootloader in COMPONENT_PATHS
SOURCE_DIR "${IDF_PATH}/components/bootloader/subproject"
BINARY_DIR "${bootloader_build_dir}"
CMAKE_ARGS -DSDKCONFIG=${SDKCONFIG} -DIDF_PATH=${IDF_PATH}
CMAKE_ARGS -DSDKCONFIG=${SDKCONFIG} -DIDF_PATH=${IDF_PATH} -DEXTRA_COMPONENT_DIRS=${COMPONENT_DIRS}
-DTESTS_ALL=0 -DTEST_COMPONENTS=""
INSTALL_COMMAND ""
BUILD_ALWAYS 1 # no easy way around this...
BUILD_BYPRODUCTS ${bootloader_binary_files}

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity bootloader_support app_update)
register_component()

View File

@ -0,0 +1,8 @@
if(CONFIG_BT_ENABLED)
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
endif()
set(COMPONENT_REQUIRES unity nvs_flash bt)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity driver nvs_flash)
register_component()

View File

@ -144,4 +144,8 @@ else()
endif()
# Enable dynamic esp_timer overflow value if building unit tests
if(NOT "${BUILD_TEST_COMPONENTS}" EQUAL "")
add_definitions(-DESP_TIMER_DYNAMIC_OVERFLOW_VAL)
endif()
endif()

View File

@ -0,0 +1,30 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ". ${CMAKE_CURRENT_BINARY_DIR}")
set(COMPONENT_REQUIRES unity nvs_flash ulp)
register_component()
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/test_tjpgd_logo.h"
COMMAND xxd -i "logo.jpg" "${CMAKE_CURRENT_BINARY_DIR}/test_tjpgd_logo.h"
WORKING_DIRECTORY ${COMPONENT_PATH}
DEPENDS "${CMAKE_CURRENT_LIST_DIR}/logo.jpg")
# Calculate MD5 value of header file esp_wifi_os_adapter.h
execute_process(COMMAND md5sum ${IDF_PATH}/components/esp32/include/esp_wifi_os_adapter.h
COMMAND cut -c 1-7
OUTPUT_VARIABLE WIFI_OS_ADAPTER_MD5
OUTPUT_STRIP_TRAILING_WHITESPACE)
# Calculate MD5 value of header file esp_wifi_crypto_types.h
execute_process(COMMAND md5sum ${IDF_PATH}/components/esp32/include/esp_wifi_crypto_types.h
COMMAND cut -c 1-7
OUTPUT_VARIABLE WIFI_CRYPTO_MD5
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_definitions(-DWIFI_OS_ADAPTER_MD5=\"${WIFI_OS_ADAPTER_MD5}\")
add_definitions(-DWIFI_CRYPTO_MD5=\"${WIFI_CRYPTO_MD5}\")
add_custom_target(esp32_test_logo DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/test_tjpgd_logo.h")
add_dependencies(${COMPONENT_NAME} esp32_test_logo)

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,5 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity ethernet)
register_component()

View File

@ -0,0 +1,5 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity expat)
register_component()

View File

@ -0,0 +1,8 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity vfs fatfs)
set(COMPONENT_EMBED_TXTFILES fatfs.img)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity http_server)
register_component()

View File

@ -166,4 +166,10 @@ if(GCC_NOT_5_2_0)
PROPERTIES COMPILE_FLAGS
-Wno-implicit-fallthrough
)
endif()
endif()
set_source_files_properties(
${SRC}/randombytes/randombytes.c
PROPERTIES COMPILE_FLAGS
-DRANDOMBYTES_DEFAULT_IMPLEMENTATION
)

View File

@ -0,0 +1,40 @@
if(TESTS_ALL EQUAL 1)
message("not linking libsodium tests, use '-T libsodium' to test it")
else()
get_filename_component(LS_TESTDIR "${CMAKE_CURRENT_LIST_DIR}/../libsodium/test/default" ABSOLUTE)
set(COMPONENT_ADD_INCLUDEDIRS "." "${LS_TESTDIR}/../quirks")
set(COMPONENT_REQUIRES unity libsodium)
set(TEST_CASES "chacha20;aead_chacha20poly1305;box;box2;ed25519_convert;sign;hash")
foreach(test_case ${TEST_CASES})
file(GLOB test_case_file "${LS_TESTDIR}/${test_case}.c")
list(APPEND TEST_CASES_FILES ${test_case_file})
endforeach()
set(COMPONENT_SRCS "${TEST_CASES_FILES};test_sodium.c")
register_component()
# The libsodium test suite is designed to be run each test case as an executable on a desktop computer and uses
# filesytem to write & then compare contents of each file.
#
# For now, use their "BROWSER_TEST" mode with these hacks so that
# multiple test cases can be combined into one ELF file.
#
# Run each test case from test_sodium.c as CASENAME_xmain().
foreach(test_case_file ${TEST_CASES_FILES})
get_filename_component(test_case ${test_case_file} NAME_WE)
set_source_files_properties(${test_case_file}
PROPERTIES COMPILE_FLAGS
# This would generate 'warning "main" redefined' warnings at runtime, which are
# silenced here. Only other solution involves patching libsodium's cmptest.h.
"-Dxmain=${test_case}_xmain -Dmain=${test_case}_main -Wp,-w")
endforeach()
# this seems odd, but it prevents the libsodium test harness from
# trying to write to a file!
add_definitions(-DBROWSER_TESTS)
endif()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity mbedtls)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity nvs_flash bootloader_support)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,7 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_PRIV_INCLUDEDIRS "../proto-c/")
set(COMPONENT_REQUIRES unity mbedtls protocomm protobuf-c)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity pthread)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity sdmmc)
register_component()

View File

@ -11,4 +11,9 @@ set(SOC_SRCS "cpu_util.c"
"sdio_slave_periph.c"
"sdmmc_periph.c"
"soc_memory_layout.c"
"spi_periph.c")
"spi_periph.c")
if(NOT CMAKE_BUILD_EARLY_EXPANSION)
set_source_files_properties("esp32/rtc_clk.c" PROPERTIES
COMPILE_FLAGS "-fno-jump-tables -fno-tree-switch-conversion")
endif()

View File

@ -0,0 +1,7 @@
set(SOC_NAME esp32)
set(COMPONENT_SRCDIRS "../${SOC_NAME}/test")
set(COMPONENT_ADD_INCLUDEDIRS "../${SOC_NAME}/test")
set(COMPONENT_REQUIRES unity)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity spi_flash bootloader_support app_update)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity spiffs)
register_component()

View File

@ -0,0 +1,11 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity ulp soc)
register_component()
set(ULP_APP_NAME ulp_test_app)
set(ULP_S_SOURCES "ulp/test_jumps.S")
set(ULP_EXP_DEP_SRCS "test_ulp_as.c")
include(${IDF_PATH}/components/ulp/component_ulp_common.cmake)

View File

@ -1,4 +1,4 @@
ULP_APP_NAME = ulp_test
ULP_APP_NAME = ulp_test_app
ULP_S_SOURCES = $(addprefix $(COMPONENT_PATH)/ulp/, \
test_jumps.S \

View File

@ -2,17 +2,17 @@
#include "unity.h"
#include "soc/rtc_cntl_reg.h"
#include "esp32/ulp.h"
#include "ulp_test.h"
#include "ulp_test_app.h"
extern const uint8_t ulp_test_bin_start[] asm("_binary_ulp_test_bin_start");
extern const uint8_t ulp_test_bin_end[] asm("_binary_ulp_test_bin_end");
extern const uint8_t ulp_test_app_bin_start[] asm("_binary_ulp_test_app_bin_start");
extern const uint8_t ulp_test_app_bin_end[] asm("_binary_ulp_test_app_bin_end");
TEST_CASE("jumps condition", "[ulp]")
{
esp_err_t err = ulp_load_binary(0, ulp_test_bin_start,
(ulp_test_bin_end - ulp_test_bin_start) / sizeof(uint32_t));
esp_err_t err = ulp_load_binary(0, ulp_test_app_bin_start,
(ulp_test_app_bin_end - ulp_test_app_bin_start) / sizeof(uint32_t));
TEST_ESP_OK(err);
REG_CLR_BIT(RTC_CNTL_INT_RAW_REG, RTC_CNTL_ULP_CP_INT_RAW);

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity vfs fatfs spiffs)
register_component()

View File

@ -0,0 +1,8 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity wear_levelling)
set(COMPONENT_EMBED_FILES test_partition_v1.bin)
register_component()

View File

@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity wpa_supplicant mbedtls)
register_component()

View File

@ -219,6 +219,7 @@ Project CMakeLists File
Each project has a single top-level ``CMakeLists.txt`` file that contains build settings for the entire project. By default, the project CMakeLists can be quite minimal.
Minimal Example CMakeLists
--------------------------
@ -796,6 +797,7 @@ Here is an example minimal "pure CMake" component CMakeLists file for a componen
- This file is quite simple as there are not a lot of source files. For components with a large number of files, the globbing behaviour of ESP-IDF's component logic can make the component CMakeLists style simpler.)
- Any time a component adds a library target with the component name, the ESP-IDF build system will automatically add this to the build, expose public include directories, etc. If a component wants to add a library target with a different name, dependencies will need to be added manually via CMake commands.
.. _cmake-file-globbing:
File Globbing & Incremental Builds
==================================

View File

@ -22,6 +22,7 @@ API Guides
ULP Coprocessor <ulp>
ULP Coprocessor (CMake) <ulp-cmake>
Unit Testing <unit-tests>
Unit Testing (CMake) <unit-tests-cmake>
Application Level Tracing <app_trace>
Console Component <console>
ROM debug console <romconsole>

View File

@ -0,0 +1,211 @@
Unit Testing in ESP32 (CMake)
=============================
.. include:: ../cmake-warning.rst
ESP-IDF comes with a unit test app based on Unity - unit test framework. Unit tests are integrated in the ESP-IDF repository and are placed in ``test`` subdirectory of each component respectively.
Add normal test cases
---------------------
Unit tests are added in the ``test`` subdirectory of the respective component.
Tests are added in C files, a single C file can include multiple test cases.
Test files start with the word "test".
The test file should include unity.h and the header for the C module to be tested.
Tests are added in a function in the C file as follows::
TEST_CASE("test name", "[module name]"
{
// Add test here
}
First argument is a descriptive name for the test, second argument is an identifier in square brackets.
Identifiers are used to group related test, or tests with specific properties.
There is no need to add a main function with ``UNITY_BEGIN()`` and ``UNITY_END()`` in each test case.
``unity_platform.c`` will run ``UNITY_BEGIN()``, run the tests cases, and then call ``UNITY_END()``.
The ``test`` subdirectory should contain a :ref:`component CMakeLists.txt <component-directories-cmake>`, since they are themselves,
components. ESP-IDF uses the test framework ``unity`` and should be specified as a requirement for the component. Normally, components
:ref:`should list their sources manually <cmake-file-globbing>`; for component tests however, this requirement is relaxed and the
use of ``COMPONENT_SRCDIRS`` is advised.
Overall, the minimal ``test`` subdirectory CMakeLists.txt file may look like as follows:
.. code:: cmake
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()
See http://www.throwtheswitch.org/unity for more information about writing tests in Unity.
Add multiple devices test cases
-------------------------------
The normal test cases will be executed on one DUT (Device Under Test). Components need to communicate with each other (like GPIO, SPI ...) can't be tested with normal test cases.
Multiple devices test cases support writing and running test with multiple DUTs.
Here's an example of multiple devices test case::
void gpio_master_test()
{
gpio_config_t slave_config = {
.pin_bit_mask = 1 << MASTER_GPIO_PIN,
.mode = GPIO_MODE_INPUT,
};
gpio_config(&slave_config);
unity_wait_for_signal("output high level");
TEST_ASSERT(gpio_get_level(MASTER_GPIO_PIN) == 1);
}
void gpio_slave_test()
{
gpio_config_t master_config = {
.pin_bit_mask = 1 << SLAVE_GPIO_PIN,
.mode = GPIO_MODE_OUTPUT,
};
gpio_config(&master_config);
gpio_set_level(SLAVE_GPIO_PIN, 1);
unity_send_signal("output high level");
}
TEST_CASE_MULTIPLE_DEVICES("gpio multiple devices test example", "[driver]", gpio_master_test, gpio_slave_test);
The macro ``TEST_CASE_MULTIPLE_DEVICES`` is used to declare multiple devices test cases.
First argument is test case name, second argument is test case description.
From the third argument, upto 5 test functions can be defined, each function will be the entry point of tests running on each DUT.
Running test cases from different DUTs could require synchronizing between DUTs. We provide ``unity_wait_for_signal`` and ``unity_send_signal`` to support synchronizing with UART.
As the secnario in the above example, slave should get GPIO level after master set level. DUT UART console will prompt and requires user interaction:
DUT1 (master) console::
Waiting for signal: [output high level]!
Please press "Enter" key to once any board send this signal.
DUT2 (slave) console::
Send signal: [output high level]!
Once the signal is set from DUT2, you need to press "Enter" on DUT1, then DUT1 unblocks from ``unity_wait_for_signal`` and starts to change GPIO level.
Add multiple stages test cases
-------------------------------
The normal test cases are expected to finish without reset (or only need to check if reset happens). Sometimes we want to run some specific test after certain kinds of reset.
For example, we want to test if reset reason is correct after wakeup from deep sleep. We need to create deep sleep reset first and then check the reset reason.
To support this, we can define multiple stages test case, to group a set of test functions together::
static void trigger_deepsleep(void)
{
esp_sleep_enable_timer_wakeup(2000);
esp_deep_sleep_start();
}
void check_deepsleep_reset_reason()
{
RESET_REASON reason = rtc_get_reset_reason(0);
TEST_ASSERT(reason == DEEPSLEEP_RESET);
}
TEST_CASE_MULTIPLE_STAGES("reset reason check for deepsleep", "[esp32]", trigger_deepsleep, check_deepsleep_reset_reason);
Multiple stages test cases present a group of test functions to users. It need user interactions (select case and select different stages) to run the case.
Building unit test app
----------------------
Follow the setup instructions in the top-level esp-idf README.
Make sure that IDF_PATH environment variable is set to point to the path of esp-idf top-level directory.
Change into tools/unit-test-app directory to configure and build it:
* `idf.py menuconfig` - configure unit test app.
* `idf.py build -T all` - build unit test app with tests for each component having tests in the ``test`` subdirectory.
* `idf.py build -T xxx` - build unit test app with tests for specific components.
* `idf.py build -T all -E xxx` - build unit test app with all unit tests, except for unit tests of some components. (For instance: `idf.py build -T all -E ulp mbedtls` - build all unit tests exludes ulp and mbedtls components).
When the build finishes, it will print instructions for flashing the chip. You can simply run ``idf.py flash`` to flash all build output.
You can also run ``idf.py flash -T all`` or ``idf.py flash -T xxx`` to build and flash. Everything needed will be rebuilt automatically before flashing.
Use menuconfig to set the serial port for flashing.
Running unit tests
------------------
After flashing reset the ESP32 and it will boot the unit test app.
When unit test app is idle, press "Enter" will make it print test menu with all available tests::
Here's the test menu, pick your combo:
(1) "esp_ota_begin() verifies arguments" [ota]
(2) "esp_ota_get_next_update_partition logic" [ota]
(3) "Verify bootloader image in flash" [bootloader_support]
(4) "Verify unit test app image" [bootloader_support]
(5) "can use new and delete" [cxx]
(6) "can call virtual functions" [cxx]
(7) "can use static initializers for non-POD types" [cxx]
(8) "can use std::vector" [cxx]
(9) "static initialization guards work as expected" [cxx]
(10) "global initializers run in the correct order" [cxx]
(11) "before scheduler has started, static initializers work correctly" [cxx]
(12) "adc2 work with wifi" [adc]
(13) "gpio master/slave test example" [ignore][misc][test_env=UT_T2_1][multi_device]
(1) "gpio_master_test"
(2) "gpio_slave_test"
(14) "SPI Master clockdiv calculation routines" [spi]
(15) "SPI Master test" [spi][ignore]
(16) "SPI Master test, interaction of multiple devs" [spi][ignore]
(17) "SPI Master no response when switch from host1 (HSPI) to host2 (VSPI)" [spi]
(18) "SPI Master DMA test, TX and RX in different regions" [spi]
(19) "SPI Master DMA test: length, start, not aligned" [spi]
(20) "reset reason check for deepsleep" [esp32][test_env=UT_T2_1][multi_stage]
(1) "trigger_deepsleep"
(2) "check_deepsleep_reset_reason"
Normal case will print the case name and description. Master slave cases will also print the sub-menu (the registered test function names).
Test cases can be run by inputting one of the following:
- Test case name in quotation marks to run a single test case
- Test case index to run a single test case
- Module name in square brackets to run all test cases for a specific module
- An asterisk to run all test cases
``[multi_device]`` and ``[multi_stage]`` tags tell the test runner whether a test case is a multiple devices or multiple stages test case.
These tags are automatically added by ```TEST_CASE_MULTIPLE_STAGES`` and ``TEST_CASE_MULTIPLE_DEVICES`` macros.
After you select a multiple devices test case, it will print sub menu::
Running gpio master/slave test example...
gpio master/slave test example
(1) "gpio_master_test"
(2) "gpio_slave_test"
You need to input number to select the test running on the DUT.
Similar to multiple devices test cases, multiple stages test cases will also print sub menu::
Running reset reason check for deepsleep...
reset reason check for deepsleep
(1) "trigger_deepsleep"
(2) "check_deepsleep_reset_reason"
First time you execute this case, input ``1`` to run first stage (trigger deepsleep).
After DUT is rebooted and able to run test cases, select this case again and input ``2`` to run the second stage.
The case only passes if the last stage passes and all previous stages trigger reset.

View File

@ -22,6 +22,7 @@ API 指南
ULP Coprocessor <ulp>
ULP Coprocessor (CMake) <ulp-cmake>
单元测试 <unit-tests>
单元测试 (CMake) <unit-tests-cmake>
Application Level Tracing <app_trace>
Console Component <console>
ROM debug console <romconsole>

View File

@ -0,0 +1,248 @@
ESP32 中的单元测试 (CMake)
==========================
ESP-IDF
中附带了一个基于 ``Unity`` 的单元测试应用程序框架,且所有的单元测试用例分别保存在
ESP-IDF 仓库中每个组件的 ``test`` 子目录中。
添加常规测试用例
----------------
单元测试被添加在相应组件的 ``test`` 子目录中,测试用例写在 C 文件中,一个
C 文件可以包含多个测试用例。测试文件的名字要以 “test” 开头。
测试文件需要包含 ``unity.h`` 头文件,此外还需要包含待测试 C
模块需要的头文件。
测试用例需要通过 C 文件中特定的函数来添加,如下所示:
.. code:: c
TEST_CASE("test name", "[module name]"
{
// 在这里添加测试用例
}
- 第一个参数是字符串,用来描述当前测试。
- 第二个参数是字符串,用方括号中的标识符来表示,标识符用来对相关测试或具有特定属性的测试进行分组。
没有必要在每个测试用例中使用 ``UNITY_BEGIN()````UNITY_END()``
来声明主函数的区域, ``unity_platform.c`` 会自动调用
``UNITY_BEGIN()``\ 然后运行测试用例,最后调用 ``UNITY_END()``\ 。
``test`` 子目录需要包含 ref`组件 CMakeLists.txt <component-directories-cmake>`因为他们本身就是一种组件。ESP-IDF 使用了
``unity`` 测试框架,需要将其指定为组件的依赖项。通常,组件
ref`需要手动指定待编译的源文件 <cmake-file-globbing>`;但是,对于测试组件来说,这个要求被放宽了,仅仅是建议使用 “COMPONENT_SRCDIRS”。
总的来说,``test`` 子目录下最简单的 CMakeLists.txt 文件可能如下所示:
.. code:: cmake
set(COMPONENT_SRCDIRS ".")
set(COMPONENT_ADD_INCLUDEDIRS ".")
set(COMPONENT_REQUIRES unity)
register_component()
更多关于如何在 Unity 下编写测试用例的信息,请查阅
http://www.throwtheswitch.org/unity 。
添加多设备测试用例
------------------
常规测试用例会在一个 DUTDevice Under
Test在试设备上执行那些需要互相通信的组件比如
GPIOSPI...)不能使用常规测试用例进行测试。多设备测试用例支持使用多个
DUT 进行写入和运行测试。
以下是一个多设备测试用例:
.. code:: c
void gpio_master_test()
{
gpio_config_t slave_config = {
.pin_bit_mask = 1 << MASTER_GPIO_PIN,
.mode = GPIO_MODE_INPUT,
};
gpio_config(&slave_config);
unity_wait_for_signal("output high level");
TEST_ASSERT(gpio_get_level(MASTER_GPIO_PIN) == 1);
}
void gpio_slave_test()
{
gpio_config_t master_config = {
.pin_bit_mask = 1 << SLAVE_GPIO_PIN,
.mode = GPIO_MODE_OUTPUT,
};
gpio_config(&master_config);
gpio_set_level(SLAVE_GPIO_PIN, 1);
unity_send_signal("output high level");
}
TEST_CASE_MULTIPLE_DEVICES("gpio multiple devices test example", "[driver]", gpio_master_test, gpio_slave_test);
``TEST_CASE_MULTIPLE_DEVICES`` 用来声明多设备测试用例,
- 第一个参数指定测试用例的名字。
- 第二个参数是测试用例的描述。
- 从第三个参数开始可以指定最多5个测试函数每个函数都是单独运行在一个
DUT 上的测试入口点。
在不同的 DUT 上运行的测试用例,通常会要求它们之间进行同步。我们提供
``unity_wait_for_signal````unity_send_signal`` 这两个函数来使用 UART
去支持同步操作。如上例中的场景slave 应该在在 master 设置好 GPIO
电平后再去读取 GPIO 电平DUT 的 UART
终端会打印提示信息,并要求用户进行交互。
DUT1master终端
.. code:: bash
Waiting for signal: [output high level]!
Please press "Enter" key once any board send this signal.
DUT2slave终端
.. code:: bash
Send signal: [output high level]!
一旦 DUT2 发送了该信号,您需要在 DUT2 的终端输入回车,然后 DUT1 会从
``unity_wait_for_signal`` 函数中解除阻塞,并开始更改 GPIO 的电平。
添加多阶段测试用例
------------------
常规的测试用例无需重启就会结束(或者仅需要检查是否发生了重启),可有些时候我们想在某些特定类型的重启事件后运行指定的测试代码,例如,我们想在深度睡眠唤醒后检查复位的原因是否正确。首先我们需要出发深度睡眠复位事件,然后检查复位的原因。为了实现这一点,我们可以定义多阶段测试用例来将这些测试函数组合在一起。
.. code:: c
static void trigger_deepsleep(void)
{
esp_sleep_enable_timer_wakeup(2000);
esp_deep_sleep_start();
}
void check_deepsleep_reset_reason()
{
RESET_REASON reason = rtc_get_reset_reason(0);
TEST_ASSERT(reason == DEEPSLEEP_RESET);
}
TEST_CASE_MULTIPLE_STAGES("reset reason check for deepsleep", "[esp32]", trigger_deepsleep, check_deepsleep_reset_reason);
多阶段测试用例向用户呈现了一组测试函数,它需要用户进行交互(选择用例并选择不同的阶段)来运行。
编译单元测试程序
----------------
按照 esp-idf 顶层目录的 README 文件中的说明进行操作,请确保 ``IDF_PATH``
环境变量已经被设置指向了 esp-idf 的顶层目录。
切换到 ``tools/unit-test-app`` 目录下进行配置和编译:
- ``idf.py menuconfig`` - 配置单元测试程序。
- ``idf.py build -T all`` - 编译单元测试程序,测试每个组件 ``test``
子目录下的用例。
- ``idf.py build -T xxx`` - 编译单元测试程序,测试指定的组件。
- ``idf.py build -T all -E xxx`` -
编译单元测试程序,测试所有(除开指定)的组件。例如
``idf.py build -T all -E ulp mbedtls`` -
编译所有的单元测试,不包括 ``ulp````mbedtls``\ 组件。
当编译完成时,它会打印出烧写芯片的指令。您只需要运行 ``idf.py flash``
即可烧写所有编译输出的文件。
您还可以运行 ``idf.py flash -T all`` 或者
``idf.py flash -T xxx``
来编译并烧写,所有需要的文件都会在烧写之前自动重新编译。
使用 ``menuconfig`` 可以设置烧写测试程序所使用的串口。
运行单元测试
------------
烧写完成后重启 ESP32 它将启动单元测试程序。
当单元测试应用程序空闲时,输入回车键,它会打印出测试菜单,其中包含所有的测试项目。
.. code:: bash
Here's the test menu, pick your combo:
(1) "esp_ota_begin() verifies arguments" [ota]
(2) "esp_ota_get_next_update_partition logic" [ota]
(3) "Verify bootloader image in flash" [bootloader_support]
(4) "Verify unit test app image" [bootloader_support]
(5) "can use new and delete" [cxx]
(6) "can call virtual functions" [cxx]
(7) "can use static initializers for non-POD types" [cxx]
(8) "can use std::vector" [cxx]
(9) "static initialization guards work as expected" [cxx]
(10) "global initializers run in the correct order" [cxx]
(11) "before scheduler has started, static initializers work correctly" [cxx]
(12) "adc2 work with wifi" [adc]
(13) "gpio master/slave test example" [ignore][misc][test_env=UT_T2_1][multi_device]
(1) "gpio_master_test"
(2) "gpio_slave_test"
(14) "SPI Master clockdiv calculation routines" [spi]
(15) "SPI Master test" [spi][ignore]
(16) "SPI Master test, interaction of multiple devs" [spi][ignore]
(17) "SPI Master no response when switch from host1 (HSPI) to host2 (VSPI)" [spi]
(18) "SPI Master DMA test, TX and RX in different regions" [spi]
(19) "SPI Master DMA test: length, start, not aligned" [spi]
(20) "reset reason check for deepsleep" [esp32][test_env=UT_T2_1][multi_stage]
(1) "trigger_deepsleep"
(2) "check_deepsleep_reset_reason"
常规测试用例会打印用例名字和描述,主从测试用例还会打印子菜单(已注册的测试函数的名字)。
可以输入以下任意一项来运行测试用例:
- 引号中的测试用例的名字,运行单个测试用例。
- 测试用例的序号,运行单个测试用例。
- 方括号中的模块名字,运行指定模块所有的测试用例。
- 星号,运行所有测试用例。
``[multi_device]````[multi_stage]``
标签告诉测试运行者该用例是多设备测试还是多阶段测试。这些标签由
``TEST_CASE_MULTIPLE_STAGES````TEST_CASE_MULTIPLE_DEVICES``
宏自动生成。
一旦选择了多设备测试用例,它会打印一个子菜单:
.. code:: bash
Running gpio master/slave test example...
gpio master/slave test example
(1) "gpio_master_test"
(2) "gpio_slave_test"
您需要输入数字以选择在 DUT 上运行的测试。
与多设备测试用例相似,多阶段测试用例也会打印子菜单:
.. code:: bash
Running reset reason check for deepsleep...
reset reason check for deepsleep
(1) "trigger_deepsleep"
(2) "check_deepsleep_reset_reason"
第一次执行此用例时,输入 ``1`` 来运行第一阶段(触发深度睡眠)。在重启
DUT 并再次选择运行此用例后,输入 ``2``
来运行第二阶段。只有在最后一个阶段通过并且之前所有的阶段都成功触发了复位的情况下,该测试才算通过。

19
tools/ci/check_ut_cmake_make.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# While we support GNU Make & CMake together, check that unit tests support both
CMAKE_UT_PATHS=$( find ${IDF_PATH}/components/ -type f -name CMakeLists.txt | grep "/test/" | grep -v "mbedtls/programs")
MAKE_UT_PATHS=$( find ${IDF_PATH}/components/ -type f -name component.mk | grep "/test/" )
CMAKE_UT_PATHS="$(/usr/bin/dirname $CMAKE_UT_PATHS | sort -n)"
MAKE_UT_PATHS="$(/usr/bin/dirname $MAKE_UT_PATHS | sort -n)"
MISMATCH=$(comm -3 <(echo "$MAKE_UT_PATHS") <(echo "$CMAKE_UT_PATHS"))
if [ -n "$MISMATCH" ]; then
echo "Some unit tests are not in both CMake and GNU Make:"
echo "$MISMATCH"
exit 1
fi
echo "Unit tests match"
exit 0

View File

@ -50,6 +50,7 @@ tools/windows/eclipse_make.sh
tools/ci/build_examples_cmake.sh
tools/ci/test_build_system_cmake.sh
tools/ci/check_examples_cmake_make.sh
tools/ci/check_ut_cmake_make.sh
tools/cmake/convert_to_cmake.py
tools/cmake/run_cmake_lint.sh
tools/idf.py

View File

@ -16,7 +16,7 @@ endfunction()
#
function(register_component)
get_filename_component(component_dir ${CMAKE_CURRENT_LIST_FILE} DIRECTORY)
get_filename_component(component ${component_dir} NAME)
set(component ${COMPONENT_NAME})
spaces2list(COMPONENT_SRCDIRS)
spaces2list(COMPONENT_ADD_INCLUDEDIRS)
@ -102,6 +102,11 @@ function(register_component)
endif()
target_include_directories(${component} PRIVATE ${abs_dir})
endforeach()
if(component IN_LIST BUILD_TEST_COMPONENTS)
target_link_libraries(${component} "-L${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(${component} "-Wl,--whole-archive -l${component} -Wl,--no-whole-archive")
endif()
endfunction()
function(register_config_only_component)
@ -153,7 +158,7 @@ function(components_finish_registration)
get_target_property(a_type ${a} TYPE)
if(${a_type} MATCHES .+_LIBRARY)
set(COMPONENT_LIBRARIES "${COMPONENT_LIBRARIES};${a}")
list(APPEND COMPONENT_LIBRARIES ${a})
endif()
endif()
endforeach()

View File

@ -255,4 +255,4 @@ function(idf_get_git_revision)
add_definitions(-DIDF_VER=\"${IDF_VER}\")
git_submodule_check("${IDF_PATH}")
set(IDF_VER ${IDF_VER} PARENT_SCOPE)
endfunction()
endfunction()

View File

@ -63,6 +63,10 @@ macro(project name)
execute_process(COMMAND "${CMAKE_COMMAND}"
-D "COMPONENTS=${COMPONENTS}"
-D "COMPONENT_REQUIRES_COMMON=${COMPONENT_REQUIRES_COMMON}"
-D "EXCLUDE_COMPONENTS=${EXCLUDE_COMPONENTS}"
-D "TEST_COMPONENTS=${TEST_COMPONENTS}"
-D "TEST_EXCLUDE_COMPONENTS=${TEST_EXCLUDE_COMPONENTS}"
-D "TESTS_ALL=${TESTS_ALL}"
-D "DEPENDENCIES_FILE=${CMAKE_BINARY_DIR}/component_depends.cmake"
-D "COMPONENT_DIRS=${COMPONENT_DIRS}"
-D "BOOTLOADER_BUILD=${BOOTLOADER_BUILD}"
@ -83,6 +87,14 @@ macro(project name)
unset(BUILD_COMPONENTS_SPACES)
message(STATUS "Component paths: ${BUILD_COMPONENT_PATHS}")
# Print list of test components
if(TESTS_ALL EQUAL 1 OR TEST_COMPONENTS)
string(REPLACE ";" " " BUILD_TEST_COMPONENTS_SPACES "${BUILD_TEST_COMPONENTS}")
message(STATUS "Test component names: ${BUILD_TEST_COMPONENTS_SPACES}")
unset(BUILD_TEST_COMPONENTS_SPACES)
message(STATUS "Test component paths: ${BUILD_TEST_COMPONENT_PATHS}")
endif()
kconfig_set_variables()
kconfig_process_config()
@ -124,7 +136,15 @@ macro(project name)
# Add each component to the build as a library
#
foreach(COMPONENT_PATH ${BUILD_COMPONENT_PATHS})
get_filename_component(COMPONENT_NAME ${COMPONENT_PATH} NAME)
list(FIND BUILD_TEST_COMPONENT_PATHS ${COMPONENT_PATH} idx)
if(NOT idx EQUAL -1)
list(GET BUILD_TEST_COMPONENTS ${idx} test_component)
set(COMPONENT_NAME ${test_component})
else()
get_filename_component(COMPONENT_NAME ${COMPONENT_PATH} NAME)
endif()
add_subdirectory(${COMPONENT_PATH} ${COMPONENT_NAME})
endforeach()
unset(COMPONENT_NAME)

View File

@ -82,14 +82,14 @@ endmacro()
# return the path to the component in 'variable'
#
# Fatal error is printed if the component is not found.
function(find_component_path find_name component_paths variable)
foreach(path ${component_paths})
get_filename_component(name "${path}" NAME)
if("${name}" STREQUAL "${find_name}")
set("${variable}" "${path}" PARENT_SCOPE)
return()
endif()
endforeach()
function(find_component_path find_name components component_paths variable)
list(FIND components ${find_name} idx)
if(NOT idx EQUAL -1)
list(GET component_paths ${idx} path)
set("${variable}" "${path}" PARENT_SCOPE)
return()
else()
endif()
# TODO: find a way to print the dependency chain that lead to this not-found component
message(WARNING "Required component ${find_name} is not found in any of the provided COMPONENT_DIRS")
endfunction()
@ -100,10 +100,11 @@ endfunction()
#
# component_paths contains only unique component names. Directories
# earlier in the component_dirs list take precedence.
function(components_find_all component_dirs component_paths component_names)
function(components_find_all component_dirs component_paths component_names test_component_names)
# component_dirs entries can be files or lists of files
set(paths "")
set(names "")
set(test_names "")
# start by expanding the component_dirs list with all subdirectories
foreach(dir ${component_dirs})
@ -123,10 +124,15 @@ function(components_find_all component_dirs component_paths component_names)
get_filename_component(component "${component}" DIRECTORY)
get_filename_component(name "${component}" NAME)
if(NOT name IN_LIST names)
set(names "${names};${name}")
set(paths "${paths};${component}")
endif()
list(APPEND names "${name}")
list(APPEND paths "${component}")
# Look for test component directory
file(GLOB test "${component}/test/CMakeLists.txt")
if(test)
list(APPEND test_names "${name}")
endif()
endif()
else() # no CMakeLists.txt file
# test for legacy component.mk and warn
file(GLOB legacy_component "${dir}/component.mk")
@ -136,25 +142,26 @@ function(components_find_all component_dirs component_paths component_names)
"Component will be skipped.")
endif()
endif()
endforeach()
set(${component_paths} ${paths} PARENT_SCOPE)
set(${component_names} ${names} PARENT_SCOPE)
set(${test_component_names} ${test_names} PARENT_SCOPE)
endfunction()
# expand_component_requirements: Recursively expand a component's requirements,
# setting global properties BUILD_COMPONENTS & BUILD_COMPONENT_PATHS and
# also invoking the components to call register_component() above,
# which will add per-component global properties with dependencies, etc.
function(expand_component_requirements component)
get_property(seen_components GLOBAL PROPERTY SEEN_COMPONENTS)
if(${component} IN_LIST seen_components)
if(component IN_LIST seen_components)
return() # already added, or in process of adding, this component
endif()
set_property(GLOBAL APPEND PROPERTY SEEN_COMPONENTS ${component})
find_component_path("${component}" "${ALL_COMPONENT_PATHS}" component_path)
find_component_path("${component}" "${ALL_COMPONENTS}" "${ALL_COMPONENT_PATHS}" component_path)
debug("Expanding dependencies of ${component} @ ${component_path}")
if(NOT component_path)
set_property(GLOBAL APPEND PROPERTY COMPONENTS_NOT_FOUND ${component})
@ -176,27 +183,99 @@ function(expand_component_requirements component)
expand_component_requirements(${req})
endforeach()
list(FIND TEST_COMPONENTS ${component} idx)
if(NOT idx EQUAL -1)
list(GET TEST_COMPONENTS ${idx} test_component)
list(GET TEST_COMPONENT_PATHS ${idx} test_component_path)
set_property(GLOBAL APPEND PROPERTY BUILD_TEST_COMPONENTS ${test_component})
set_property(GLOBAL APPEND PROPERTY BUILD_TEST_COMPONENT_PATHS ${test_component_path})
endif()
# Now append this component to the full list (after its dependencies)
set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENT_PATHS ${component_path})
set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENTS ${component})
endfunction()
# filter_components_list: Filter the components included in the build
# as specified by the user. Or, in the case of unit testing, filter out
# the test components to be built.
macro(filter_components_list)
spaces2list(COMPONENTS)
spaces2list(EXCLUDE_COMPONENTS)
spaces2list(TEST_COMPONENTS)
spaces2list(TEST_EXCLUDE_COMPONENTS)
list(LENGTH ALL_COMPONENTS all_components_length)
math(EXPR all_components_length "${all_components_length} - 1")
foreach(component_idx RANGE 0 ${all_components_length})
list(GET ALL_COMPONENTS ${component_idx} component)
list(GET ALL_COMPONENT_PATHS ${component_idx} component_path)
if(COMPONENTS)
if(${component} IN_LIST COMPONENTS)
set(add_component 1)
else()
set(add_component 0)
endif()
else()
set(add_component 1)
endif()
if(NOT ${component} IN_LIST EXCLUDE_COMPONENTS AND add_component EQUAL 1)
list(APPEND components ${component})
list(APPEND component_paths ${component_path})
if(TESTS_ALL EQUAL 1 OR TEST_COMPONENTS)
if(NOT TESTS_ALL EQUAL 1 AND TEST_COMPONENTS)
if(${component} IN_LIST TEST_COMPONENTS)
set(add_test_component 1)
else()
set(add_test_component 0)
endif()
else()
set(add_test_component 1)
endif()
if(${component} IN_LIST ALL_TEST_COMPONENTS)
if(NOT ${component} IN_LIST TEST_EXCLUDE_COMPONENTS AND add_test_component EQUAL 1)
list(APPEND test_components ${component}_test)
list(APPEND test_component_paths ${component_path}/test)
list(APPEND components ${component}_test)
list(APPEND component_paths ${component_path}/test)
endif()
endif()
endif()
endif()
endforeach()
set(COMPONENTS ${components})
set(TEST_COMPONENTS ${test_components})
set(TEST_COMPONENT_PATHS ${test_component_paths})
list(APPEND ALL_COMPONENTS "${TEST_COMPONENTS}")
list(APPEND ALL_COMPONENT_PATHS "${TEST_COMPONENT_PATHS}")
endmacro()
# Main functionality goes here
# Find every available component in COMPONENT_DIRS, save as ALL_COMPONENT_PATHS and ALL_COMPONENTS
components_find_all("${COMPONENT_DIRS}" ALL_COMPONENT_PATHS ALL_COMPONENTS)
components_find_all("${COMPONENT_DIRS}" ALL_COMPONENT_PATHS ALL_COMPONENTS ALL_TEST_COMPONENTS)
if(NOT COMPONENTS)
set(COMPONENTS "${ALL_COMPONENTS}")
endif()
spaces2list(COMPONENTS)
filter_components_list()
debug("ALL_COMPONENT_PATHS ${ALL_COMPONENT_PATHS}")
debug("ALL_COMPONENTS ${ALL_COMPONENTS}")
debug("ALL_TEST_COMPONENTS ${ALL_TEST_COMPONENTS}")
set_property(GLOBAL PROPERTY SEEN_COMPONENTS "") # anti-infinite-recursion
set_property(GLOBAL PROPERTY BUILD_COMPONENTS "")
set_property(GLOBAL PROPERTY BUILD_COMPONENT_PATHS "")
set_property(GLOBAL PROPERTY BUILD_TEST_COMPONENTS "")
set_property(GLOBAL PROPERTY BUILD_TEST_COMPONENT_PATHS "")
set_property(GLOBAL PROPERTY COMPONENTS_NOT_FOUND "")
# Indicate that the component CMakeLists.txt is being included in the early expansion phase of the build,
@ -210,6 +289,8 @@ unset(CMAKE_BUILD_EARLY_EXPANSION)
get_property(build_components GLOBAL PROPERTY BUILD_COMPONENTS)
get_property(build_component_paths GLOBAL PROPERTY BUILD_COMPONENT_PATHS)
get_property(build_test_components GLOBAL PROPERTY BUILD_TEST_COMPONENTS)
get_property(build_test_component_paths GLOBAL PROPERTY BUILD_TEST_COMPONENT_PATHS)
get_property(not_found GLOBAL PROPERTY COMPONENTS_NOT_FOUND)
debug("components in build: ${build_components}")
@ -223,6 +304,8 @@ endfunction()
file(WRITE "${DEPENDENCIES_FILE}.tmp" "# Component requirements generated by expand_requirements.cmake\n\n")
line("set(BUILD_COMPONENTS ${build_components})")
line("set(BUILD_COMPONENT_PATHS ${build_component_paths})")
line("set(BUILD_TEST_COMPONENTS ${build_test_components})")
line("set(BUILD_TEST_COMPONENT_PATHS ${build_test_component_paths})")
line("")
line("# get_component_requirements: Generated function to read the dependencies of a given component.")

View File

@ -84,7 +84,6 @@ def _run_tool(tool_name, args, cwd):
except subprocess.CalledProcessError as e:
raise FatalError("%s failed with exit code %d" % (tool_name, e.returncode))
def check_environment():
"""
Verify the environment contains the top-level tools we need to operate
@ -152,7 +151,7 @@ def _ensure_build_directory(args, always_run_cmake=False):
# Verify/create the build directory
build_dir = args.build_dir
if not os.path.isdir(build_dir):
os.mkdir(build_dir)
os.makedirs(build_dir)
cache_path = os.path.join(build_dir, "CMakeCache.txt")
if not os.path.exists(cache_path) or always_run_cmake:
if args.generator is None:
@ -163,7 +162,10 @@ def _ensure_build_directory(args, always_run_cmake=False):
cmake_args += [ "--warn-uninitialized" ]
if args.no_ccache:
cmake_args += [ "-DCCACHE_DISABLE=1" ]
if args.define_cache_entry:
cmake_args += ["-D" + d for d in args.define_cache_entry]
cmake_args += [ project_dir]
_run_tool("cmake", cmake_args, cwd=args.build_dir)
except:
# don't allow partially valid CMakeCache.txt files,
@ -220,6 +222,7 @@ def build_target(target_name, args):
"""
_ensure_build_directory(args)
generator_cmd = GENERATOR_CMDS[args.generator]
if not args.no_ccache:
# Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
# (this means ccache hits can be shared between different projects. It may mean that some debug information
@ -258,13 +261,11 @@ def flash(action, args):
esptool_args += [ "write_flash", "@"+flasher_args_path ]
_run_tool("esptool.py", esptool_args, args.build_dir)
def erase_flash(action, args):
esptool_args = _get_esptool_args(args)
esptool_args += [ "erase_flash" ]
_run_tool("esptool.py", esptool_args, args.build_dir)
def monitor(action, args):
"""
Run idf_monitor.py to watch build output
@ -331,7 +332,6 @@ def fullclean(action, args):
def print_closing_message(args):
# print a closing message of some kind
#
if "flash" in str(args.actions):
print("Done")
return
@ -380,29 +380,29 @@ def print_closing_message(args):
ACTIONS = {
# action name : ( function (or alias), dependencies, order-only dependencies )
"all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
"build": ( "all", [], [] ), # build is same as 'all' target
"clean": ( clean, [], [ "fullclean" ] ),
"fullclean": ( fullclean, [], [] ),
"reconfigure": ( reconfigure, [], [ "menuconfig" ] ),
"menuconfig": ( build_target, [], [] ),
"confserver": ( build_target, [], [] ),
"size": ( build_target, [ "app" ], [] ),
"size-components": ( build_target, [ "app" ], [] ),
"size-files": ( build_target, [ "app" ], [] ),
"bootloader": ( build_target, [], [] ),
"bootloader-clean": ( build_target, [], [] ),
"bootloader-flash": ( flash, [ "bootloader" ], [ "erase_flash"] ),
"app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
"app-flash": ( flash, [ "app" ], [ "erase_flash"]),
"partition_table": ( build_target, [], [ "reconfigure" ] ),
"partition_table-flash": ( flash, [ "partition_table" ], [ "erase_flash" ]),
"flash": ( flash, [ "all" ], [ "erase_flash" ] ),
"erase_flash": ( erase_flash, [], []),
"monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
"all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
"build": ( "all", [], [] ), # build is same as 'all' target
"clean": ( clean, [], [ "fullclean" ] ),
"fullclean": ( fullclean, [], [] ),
"reconfigure": ( reconfigure, [], [ "menuconfig" ] ),
"menuconfig": ( build_target, [], [] ),
"defconfig": ( build_target, [], [] ),
"confserver": ( build_target, [], [] ),
"size": ( build_target, [ "app" ], [] ),
"size-components": ( build_target, [ "app" ], [] ),
"size-files": ( build_target, [ "app" ], [] ),
"bootloader": ( build_target, [], [] ),
"bootloader-clean": ( build_target, [], [] ),
"bootloader-flash": ( flash, [ "bootloader" ], [ "erase_flash"] ),
"app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
"app-flash": ( flash, [ "app" ], [ "erase_flash"]),
"partition_table": ( build_target, [], [ "reconfigure" ] ),
"partition_table-flash": ( flash, [ "partition_table" ], [ "erase_flash" ]),
"flash": ( flash, [ "all" ], [ "erase_flash" ] ),
"erase_flash": ( erase_flash, [], []),
"monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
}
def get_commandline_options():
""" Return all the command line options up to but not including the action """
result = []
@ -427,6 +427,15 @@ def get_default_serial_port():
except IndexError:
raise RuntimeError("No serial ports found. Connect a device, or use '-p PORT' option to set a specific port.")
# Import the actions, arguments extension file
if os.path.exists(os.path.join(os.getcwd(), "idf_ext.py")):
sys.path.append(os.getcwd())
try:
from idf_ext import add_action_extensions, add_argument_extensions
except ImportError as e:
print("Error importing extension file idf_ext.py. Skipping.")
print("Please make sure that it contains implementations (even if they're empty implementations) of")
print("add_action_extensions and add_argument_extensions.")
def main():
if sys.version_info[0] != 2 or sys.version_info[1] != 7:
@ -434,6 +443,19 @@ def main():
"you encounter. Search for 'Setting the Python Interpreter' in the ESP-IDF docs if you want to use "
"Python 2.7." % sys.version_info[:3])
# Add actions extensions
try:
add_action_extensions({
"build_target": build_target,
"reconfigure" : reconfigure,
"flash" : flash,
"monitor" : monitor,
"clean" : clean,
"fullclean" : fullclean
}, ACTIONS)
except NameError:
pass
parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
parser.add_argument('-p', '--port', help="Serial port",
default=os.environ.get('ESPPORT', None))
@ -444,10 +466,17 @@ def main():
parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
parser.add_argument('-D', '--define-cache-entry', help="Create a cmake cache entry", nargs='+')
parser.add_argument('--no-ccache', help="Disable ccache. Otherwise, if ccache is available on the PATH then it will be used for faster builds.", action="store_true")
parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
choices=ACTIONS.keys())
# Add arguments extensions
try:
add_argument_extensions(parser)
except NameError:
pass
args = parser.parse_args()
check_environment()

View File

@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(unit-test-app)

View File

@ -4,6 +4,8 @@ ESP-IDF unit tests are run using Unit Test App. The app can be built with the un
# Building Unit Test App
## GNU Make
* Follow the setup instructions in the top-level esp-idf README.
* Set IDF_PATH environment variable to point to the path to the esp-idf top-level directory.
* Change into `tools/unit-test-app` directory
@ -12,11 +14,21 @@ ESP-IDF unit tests are run using Unit Test App. The app can be built with the un
* Follow the printed instructions to flash, or run `make flash`.
* Unit test have a few preset sdkconfigs. It provides command `make ut-clean-config_name` and `make ut-build-config_name` (where `config_name` is the file name under `unit-test-app/configs` folder) to build with preset configs. For example, you can use `make ut-build-default TESTS_ALL=1` to build with config file `unit-test-app/configs/default`. Built binary for this config will be copied to `unit-test-app/output/config_name` folder.
## CMake
* Follow the setup instructions in the top-level esp-idf README.
* Set IDF_PATH environment variable to point to the path to the esp-idf top-level directory.
* Change into `tools/unit-test-app` directory
* `idf.py menuconfig` to configure the Unit Test App.
* `idf.py build -T <component> <component> ...` with `component` set to names of the components to be included in the test app. Or `idf.py build -T all` to build the test app with all the tests for components having `test` subdirectory.
* Follow the printed instructions to flash, or run `idf.py flash -p PORT`.
* Unit test have a few preset sdkconfigs. It provides command `idf.py ut-clean-config_name` and `idf.py ut-build-config_name` (where `config_name` is the file name under `unit-test-app/configs` folder) to build with preset configs. For example, you can use `idf.py ut-build-default -T all` to build with config file `unit-test-app/configs/default`. Built binary for this config will be copied to `unit-test-app/output/config_name` folder.
# Flash Size
The unit test partition table assumes a 4MB flash size. When testing `TESTS_ALL=1`, this additional factory app partition size is required.
The unit test partition table assumes a 4MB flash size. When testing `TESTS_ALL=1` (Make) or `-T all` (CMake), this additional factory app partition size is required.
If building unit tests to run on a smaller flash size, edit `partition_table_unit_tests_app.csv` and use `TEST_COMPONENTS=` instead of `TESTS_ALL` if tests don't fit in a smaller factory app partition (exact size will depend on configured options).
If building unit tests to run on a smaller flash size, edit `partition_table_unit_tests_app.csv` and use `TEST_COMPONENTS=` (Make) or `-T <component> <component> ...` (CMake) instead of `TESTS_ALL` or `-T all` if tests don't fit in a smaller factory app partition (exact size will depend on configured options).
# Running Unit Tests

View File

@ -0,0 +1,10 @@
set(COMPONENT_SRCDIRS .)
set(COMPONENT_ADD_INCLUDEDIRS include)
set(COMPONENT_REQUIRES spi_flash idf_test)
register_component()
if(GCC_NOT_5_2_0)
component_compile_options(-Wno-unused-const-variable)
endif()

View File

@ -0,0 +1,279 @@
import sys
import glob
import tempfile
import os
import os.path
import re
import shutil
import argparse
import json
import copy
PROJECT_NAME = "unit-test-app"
PROJECT_PATH = os.getcwd()
# List of unit-test-app configurations.
# Each file in configs/ directory defines a configuration. The format is the
# same as sdkconfig file. Configuration is applied on top of sdkconfig.defaults
# file from the project directory
CONFIG_NAMES = os.listdir(os.path.join(PROJECT_PATH, "configs"))
# Build (intermediate) and output (artifact) directories
BUILDS_DIR = os.path.join(PROJECT_PATH, "builds")
BINARIES_DIR = os.path.join(PROJECT_PATH, "output")
# Convert the values passed to the -T parameter to corresponding cache entry definitions
# TESTS_ALL and TEST_COMPONENTS
class TestComponentAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
# Create a new of cache definition entry, adding previous elements
cache_entries = list()
existing_entries = getattr(namespace, "define_cache_entry", [])
if existing_entries:
cache_entries.extend(existing_entries)
# Form -D arguments
if "all" in values:
cache_entries.append("TESTS_ALL=1")
cache_entries.append("TEST_COMPONENTS=''")
else:
cache_entries.append("TESTS_ALL=0")
cache_entries.append("TEST_COMPONENTS='%s'" % " ".join(values))
setattr(namespace, "define_cache_entry", cache_entries)
# Brute force add reconfigure at the very beginning
existing_actions = getattr(namespace, "actions", [])
if not "reconfigure" in existing_actions:
existing_actions = ["reconfigure"] + existing_actions
setattr(namespace, "actions", existing_actions)
class TestExcludeComponentAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
# Create a new of cache definition entry, adding previous elements
cache_entries = list()
existing_entries = getattr(namespace, "define_cache_entry", [])
if existing_entries:
cache_entries.extend(existing_entries)
cache_entries.append("TEST_EXCLUDE_COMPONENTS='%s'" % " ".join(values))
setattr(namespace, "define_cache_entry", cache_entries)
# Brute force add reconfigure at the very beginning
existing_actions = getattr(namespace, "actions", [])
if not "reconfigure" in existing_actions:
existing_actions = ["reconfigure"] + existing_actions
setattr(namespace, "actions", existing_actions)
def add_argument_extensions(parser):
# For convenience, define a -T argument that gets converted to -D arguments
parser.add_argument('-T', '--test-component', help="Specify the components to test", nargs='+', action=TestComponentAction)
# For convenience, define a -T argument that gets converted to -D arguments
parser.add_argument('-E', '--test-exclude-components', help="Specify the components to exclude from testing", nargs='+', action=TestExcludeComponentAction)
def add_action_extensions(base_functions, base_actions):
def ut_apply_config(ut_apply_config_name, args):
config_name = re.match(r"ut-apply-config-(.*)", ut_apply_config_name).group(1)
def set_config_build_variables(prop, defval = None):
property_value = re.match(r"^%s=(.*)" % prop, config_file_content)
if (property_value):
property_value = property_value.group(1)
else:
property_value = defval
if (property_value):
try:
args.define_cache_entry.append("%s=" % prop + property_value)
except AttributeError:
args.define_cache_entry = ["%s=" % prop + property_value]
return property_value
sdkconfig_set = None
if args.define_cache_entry:
sdkconfig_set = filter(lambda s: "SDKCONFIG=" in s, args.define_cache_entry)
sdkconfig_path = os.path.join(args.project_dir, "sdkconfig")
if sdkconfig_set:
sdkconfig_path = sdkconfig_set[-1].split("=")[1]
sdkconfig_path = os.path.abspath(sdkconfig_path)
try:
os.remove(sdkconfig_path)
except OSError:
pass
if config_name in CONFIG_NAMES:
# Parse the sdkconfig for components to be included/excluded and tests to be run
config = os.path.join(PROJECT_PATH, "configs", config_name)
with open(config, "r") as config_file:
config_file_content = config_file.read()
set_config_build_variables("EXCLUDE_COMPONENTS", "''")
test_components = set_config_build_variables("TEST_COMPONENTS", "''")
tests_all = None
if test_components == "''":
tests_all = "TESTS_ALL=1"
else:
tests_all = "TESTS_ALL=0"
try:
args.define_cache_entry.append(tests_all)
except AttributeError:
args.define_cache_entry = [tests_all]
set_config_build_variables("TEST_EXCLUDE_COMPONENTS","''")
with tempfile.NamedTemporaryFile() as sdkconfig_temp:
# Use values from the combined defaults and the values from
# config folder to build config
sdkconfig_default = os.path.join(PROJECT_PATH, "sdkconfig.defaults")
with open(sdkconfig_default, "r") as sdkconfig_default_file:
sdkconfig_temp.write(sdkconfig_default_file.read())
sdkconfig_config = os.path.join(PROJECT_PATH, "configs", config_name)
with open(sdkconfig_config, "r") as sdkconfig_config_file:
sdkconfig_temp.write("\n")
sdkconfig_temp.write(sdkconfig_config_file.read())
sdkconfig_temp.flush()
try:
args.define_cache_entry.append("SDKCONFIG_DEFAULTS=" + sdkconfig_temp.name)
except AttributeError:
args.define_cache_entry = ["SDKCONFIG_DEFAULTS=" + sdkconfig_temp.name]
reconfigure = base_functions["reconfigure"]
reconfigure(None, args)
else:
if not config_name == "all-configs":
print("unknown unit test app config for action '%s'" % ut_apply_config_name)
# This target builds the configuration. It does not currently track dependencies,
# but is good enough for CI builds if used together with clean-all-configs.
# For local builds, use 'apply-config-NAME' target and then use normal 'all'
# and 'flash' targets.
def ut_build(ut_build_name, args):
# Create a copy of the passed arguments to prevent arg modifications to accrue if
# all configs are being built
build_args = copy.copy(args)
config_name = re.match(r"ut-build-(.*)", ut_build_name).group(1)
if config_name in CONFIG_NAMES:
build_args.build_dir = os.path.join(BUILDS_DIR, config_name)
src = os.path.join(BUILDS_DIR, config_name)
dest = os.path.join(BINARIES_DIR, config_name)
try:
os.makedirs(dest)
except OSError:
pass
# Build, tweaking paths to sdkconfig and sdkconfig.defaults
ut_apply_config("ut-apply-config-" + config_name, build_args)
build_target = base_functions["build_target"]
build_target("all", build_args)
# Copy artifacts to the output directory
shutil.copyfile(os.path.join(build_args.project_dir, "sdkconfig"), os.path.join(dest, "sdkconfig"))
binaries = [PROJECT_NAME + x for x in [".elf", ".bin", ".map"]]
for binary in binaries:
shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
try:
os.mkdir(os.path.join(dest, "bootloader"))
except OSError:
pass
shutil.copyfile(os.path.join(src, "bootloader", "bootloader.bin"), os.path.join(dest, "bootloader", "bootloader.bin"))
for partition_table in glob.glob(os.path.join(src, "partition_table", "partition-table*.bin")):
try:
os.mkdir(os.path.join(dest, "partition_table"))
except OSError:
pass
shutil.copyfile(partition_table, os.path.join(dest, "partition_table", os.path.basename(partition_table)))
shutil.copyfile(os.path.join(src, "flash_project_args"), os.path.join(dest, "flash_project_args"))
binaries = glob.glob(os.path.join(src, "*.bin"))
binaries = [os.path.basename(s) for s in binaries]
for binary in binaries:
shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
else:
if not config_name == "all-configs":
print("unknown unit test app config for action '%s'" % ut_build_name)
def ut_clean(ut_clean_name, args):
config_name = re.match(r"ut-clean-(.*)", ut_clean_name).group(1)
if config_name in CONFIG_NAMES:
shutil.rmtree(os.path.join(BUILDS_DIR, config_name), ignore_errors=True)
shutil.rmtree(os.path.join(BINARIES_DIR, config_name), ignore_errors=True)
else:
if not config_name == "all-configs":
print("unknown unit test app config for action '%s'" % ut_clean_name)
def ut_help(action, args):
HELP_STRING = """
Additional unit-test-app specific targets
idf.py ut-build-NAME - Build unit-test-app with configuration provided in configs/NAME.
Build directory will be builds/NAME/, output binaries will be
under output/NAME/
idf.py ut-clean-NAME - Remove build and output directories for configuration NAME.
idf.py ut-build-all-configs - Build all configurations defined in configs/ directory.
idf.py ut-apply-config-NAME - Generates configuration based on configs/NAME in sdkconfig
file. After this, normal all/flash targets can be used.
Useful for development/debugging.
"""
print(HELP_STRING)
# Build dictionary of action extensions
extensions = dict()
# This generates per-config targets (clean, build, apply-config).
build_all_config_deps = []
clean_all_config_deps = []
for config in CONFIG_NAMES:
config_build_action_name = "ut-build-" + config
config_clean_action_name = "ut-clean-" + config
config_apply_config_action_name = "ut-apply-config-" + config
extensions[config_build_action_name] = (ut_build, [], [])
extensions[config_clean_action_name] = (ut_clean, [], [])
extensions[config_apply_config_action_name] = (ut_apply_config, [], [])
build_all_config_deps.append(config_build_action_name)
clean_all_config_deps.append(config_clean_action_name)
extensions["ut-build-all-configs"] = (ut_build, build_all_config_deps, [])
extensions["ut-clean-all-configs"] = (ut_clean, clean_all_config_deps, [])
extensions["ut-help"] = (ut_help, [], [])
base_actions.update(extensions)

View File

@ -0,0 +1,4 @@
set(COMPONENT_SRCS "app_main.c")
set(COMPONENT_ADD_INCLUDEDIRS "")
register_component()