OVMS3-idf/tools/unit-test-app
KonstantinKondrashov 5ed8388f6b mbedtls: Add Montgomery exponentiation implementation (HAC 14.94)
It gave us a better performance of RSA operations. (2~11 times)

The old modexp implementation (Z = X ^ Y mod M) loaded all the data into
the hw registers and was waiting for completion, but due to
the hardware RSA implementation, the calculations always started with 4096 bit,
which took a lot of time.
Measurement results (measurements were made for keys: 2048, 3072 and 4096 bits)
(Old) - Sliding-window exponentiation (HAC 14.85):
keysize = 2048 bits
RSA key operation (performance): public [93206 us], private [280189 us]
keysize = 3072 bits
RSA key operation (performance): public [293614 us], private [858157 us]
 keysize = 4096 bits
RSA key operation (performance): public [653192 us], private [1912126 us]

Instead (Old) - Sliding-window exponentiation (HAC 14.85) was implemented
(New) - Montgomery exponentiation (HAC 14.94) which showed
better performance on private and public keys.
keysize = 2048 bits
RSA key operation (performance): public [14504 us], private [149456 us]
keysize = 3072 bits
RSA key operation (performance): public [35073 us], private [392743 us]
keysize = 4096 bits
RSA key operation (performance): public [58650 us], private [787186 us]

For this reason, the old implementation was removed
and the MBEDTLS_HARDWARE_MPI option was turned on by default.

Why the MPI_INTERRUPT option is removed:
the old implementation used calculations on the hardware and
it took a lot of time (10ms - 500ms). And in order not to stand idle
while waiting for completion, an interrupt option was added.
This made it possible to carry out other tasks during the calculation,
and this one to block. The new method is free from such a drawback and
the maximum duration of one RSA HW operation does not exceed 70us (usually 2-70 μs).
This option is no longer needed.

Closes: IDF-965
2019-11-05 16:33:11 +08:00
..
components/test_utils Support ELF files loadable with gdb 2019-09-24 07:19:50 +00:00
configs spi_flash: remove duplicate definition of spi_flash_unlock 2019-08-23 12:37:55 +08:00
main tools: Mass fixing of empty prototypes (for -Wstrict-prototypes) 2019-08-01 16:28:56 +07:00
tools add wifi init unit test cases 2019-10-11 13:00:57 +08:00
CMakeLists.txt cmake: Add support for test build 2018-10-20 12:07:24 +08:00
idf_ext.py idf_ext.py: don't rerun reconfig for tests when possible 2019-07-23 16:15:33 +02:00
Makefile make: Add feature to cmd 'make flash' - reset ota_data partition 2018-07-25 11:22:15 +00:00
partition_table_unit_test_app.csv global: update note in the partition tables 2019-07-27 10:28:16 +02:00
partition_table_unit_test_two_ota.csv app_update: Add unit tt pests for ota, factory, test partitions 2018-08-16 09:57:32 +05:00
README.md docs: Make CMake build system default, mark GNU Make as legacy option 2019-07-09 14:32:26 +10:00
sdkconfig.defaults mbedtls: Add Montgomery exponentiation implementation (HAC 14.94) 2019-11-05 16:33:11 +08:00
unit_test.py ci: fix random unit test CI failure: 2019-06-18 12:22:00 +08:00

Unit Test App

ESP-IDF unit tests are run using Unit Test App. The app can be built with the unit tests for a specific component. Unit tests are in test subdirectories of respective components.

Building Unit Test App

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 -T <component> -T <component> ... build with component set to names of the components to be included in the test app. Or idf.py -T all build to build the test app with all the tests for components having test subdirectory.
  • Follow the printed instructions to flash, or run idf.py -p PORT flash.
  • 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 -T all ut-build-default 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.

Legacy 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
  • make menuconfig to configure the Unit Test App.
  • make TEST_COMPONENTS= with TEST_COMPONENTS set to names of the components to be included in the test app. Or make TESTS_ALL=1 to build the test app with all the tests for components having test subdirectory.
  • 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.

Flash Size

The unit test partition table assumes a 4MB flash size. When testing -T all or TESTS_ALL=1 (Legacy GNU Make) or, 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 -T <component> <component> ... or TEST_COMPONENTS= (Legacy GNU Make) or instead of -T all or TESTS_ALL if tests don't fit in a smaller factory app partition (exact size will depend on configured options).

Running Unit Tests

The unit test loader will prompt by showing a menu of available tests to run:

  • Type a number to run a single test.
  • * to run all tests.
  • [tagname] to run tests with "tag"
  • ![tagname] to run tests without "tag" (![ignore] is very useful as it runs all CI-enabled tests.)
  • "test name here" to run test with given name

Testing Unit Tests with CI

CI Test Flow for Unit Test

Unit test uses 3 stages in CI: build, assign_test, unit_test.

Build Stage:

build_esp_idf_tests job will build all UT configs and parse test cases form built elf files. Built binary (tools/unit-test-app/output) and parsed cases (components/idf_test/unit_test/TestCaseAll.yml) will be saved as artifacts.

When we add new test case, it will construct a structure to save case data during build. We'll parse the test case from this structure. The description (defined in test case: TEST_CASE("name", "description")) is used to extend test case definition. The format of test description is a list of tags:

  1. first tag is always group of test cases, it's mandatory
  2. the rest tags should be [type=value]. Tags could have default value and omitted value. For example, reset tag default value is "POWERON_RESET", omitted value is "" (do not reset) :
    • "[reset]" equal to [reset=POWERON_RESET]
    • if reset tag doesn't exist, then it equals to [reset=""]
  3. the [leaks] tag is used to disable the leak checking. A specific maximum memory leakage can be set as follows: [leaks=500]. This allows no more than 500 bytes of heap to be leaked. Also there is a special function to set the critical level of leakage not through a tag, just directly in the test code test_utils_set_critical_leak_level().

The priority of using leakage level is as follows:

  1. Setting by tag [leaks=500].
  2. Setting by test_utils_set_critical_leak_level() function.
  3. Setting by default leakage in Kconfig CONFIG_UNITY_CRITICAL_LEAK_LEVEL_GENERAL.

Tests marked as [leaks] or [leaks=xxx] reset the device after completion (or after each stage in multistage tests).

TagDefinition.yml defines how we should parse the description. In TagDefinition.yml, we declare the tags we are interested in, their default value and omitted value. Parser will parse the properities of test cases according to this file, and add them as test case attributes.

We will build unit-test-app with different sdkconfigs. Some config items requires specific board to run. For example, if CONFIG_ESP32_SPIRAM_SUPPORT is enabled, then unit test app must run on board supports PSRAM. ConfigDependency.yml is used to define the mapping between sdkconfig items and tags. The tags will be saved as case attributes, used to select jobs and runners. In the previous example, psram tag is generated, will only select jobs and runners also contains psram tag.

Assign Test Stage:

assign_test job will try to assign all cases to test jobs defined in .gitlab-ci.yml, according to test environment and tags. For each job, one config file with same name of test job will be generated in components/idf_test/unit_test/CIConfigs/(this folder will be passed to test jobs as artifacts). These config files will tell test jobs which cases it need to run, and pass some extra configs (like if the case will reset) of test case to runner.

Please check related document in tiny-test-fw for details.

Unit Test Stage:

All jobs in unit_test stage will run job according to unit test configs. Then unit test jobs will use tiny-test-fw runner to run the test cases. The test logs will be saved as artifacts.

Unit test jobs will do reset before running each case (because some cases do not cleanup when failed). This makes test cases independent with each other during execution.

Handle Unit Test CI Issues

1. Assign Test Failures

Gitlab CI do not support create jobs at runtime. We must maunally add all jobs to CI config file. To make test running in parallel, we limit the number of cases running on each job. When add new unit test cases, it could exceed the limitation that current unit test jobs support. In this case, assign test job will raise error, remind you to add jobs to .gitlab-ci.yml.

Too many test cases vs jobs to run. Please add the following jobs to .gitlab-ci.yml with specific tags:
* Add job with: UT_T1_1, ESP32_IDF, psram
* Add job with: UT_T1_1, ESP32_IDF

The above is an example of error message in assign test job. In this case, please add the following jobs in .gitlab-ci.yml:

UT_001_25:
  <<: *unit_test_template
  tags:
    - ESP32_IDF
    - UT_T1_1

UT_004_09:
  <<: *unit_test_template
  tags:
    - ESP32_IDF
    - UT_T1_1
    - psram

The naming rule of jobs are UT + job type index + job index. Each combination of tags is a different job type.

2. Debugging Failed Cases

First you can check the logs. It's saved as unit test job artifacts. You can download from the test job page.

If you want to reproduce locally, you need to:

  1. Download artifacts of build_esp_idf_tests. The built binary is in tools/unit-test-app/output folder.
    • Built binary in CI could be slightly different from locally built binary with the same revision, some cases might only fails with CI built binary.
  2. Check the following print in CI job to get the config name: Running unit test for config: config_name. Then flash the binary of this config to your board.
  3. Run the failed case on your board (refer to Running Unit Tests section).
    • There're some special UT cases (multiple stages case, multiple devices cases) which requires user interaction:
      • You can refer to unit test document to run test manually.
      • Or, you can use tools/unit-test-app/unit_test.py to run the test cases:
        • read document of tiny-test-fw, set correct TEST_FW_PATH and IDF_PATH
        • run unit_test.py (see examples below)
    • You can also use tools/tiny-test-fw/Runner.py to run test cases (it will be the same as what Runner do). Please use python Runner.py -c $CONFIG_FILE $IDF_PATH/tools/unit-test-app command, where CONFIG_FILE is a YAML file with same name with CI job in components/idf_test/unit_test/CIConfigs (artifacts, need to be download from assign_test job).

Running unit tests on local machine by unit_test.py

A couple of examples follow for running unit tests on local machine.

# run a simple unit test
./unit_test.py "UART can do select()"
# repeat the tests two times
./unit_test.py -r 2 "UART can do select()"
# use custom environment config file
./unit_test.py -e /tmp/EnvConfigTemplate.yml "UART can do select()"
# use custom application binary
./unit_test.py -b /tmp/app.bin "UART can do select()"
# run a list of unit tests
./unit_test.py "UART can do select()" "concurent selects work"
# add some options for unit tests
./unit_test.py "UART can do select()",timeout:10 "concurent selects work",config:release,env_tag:UT_T2_1
# run a multi stage test (type of test and child case numbers are autodetected)
./unit_test.py "check a time after wakeup from deep sleep"
# run a list of different unit tests (one simple and one multi stage test)
./unit_test.py "concurent selects work" "NOINIT attributes behavior"