feat(usb_host): Host tests for individual USB Host layers

- Public API CMock based Host tests
    - USB Host layer: driver install/uninstall unit test
    - USBH layer: driver install/uninstall unit test
    - Host tests are run in CI
This commit is contained in:
peter.marcisovsky
2025-03-25 09:43:47 +01:00
parent 18ae6c38a9
commit 6dfb7269a1
17 changed files with 567 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/usb/host_test:
enable:
- if: IDF_TARGET == "linux"
depends_components:
- usb

View File

@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
list(APPEND EXTRA_COMPONENT_DIRS
"$ENV{IDF_PATH}/tools/mocks/usb/usb_host_layer_mock/usb"
# The following line would be needed to include the freertos mock component if this test used mocked FreeRTOS.
#"$ENV{IDF_PATH}/tools/mocks/freertos/"
)
project(host_test_usb_host_layer)

View File

@@ -0,0 +1,37 @@
| Supported Targets | Linux |
| ----------------- | ----- |
# Description
This directory contains test code for `USB Host layer` of USB Host stack. Namely:
* USB Host public API calls to install and uninstall the USB Host driver with partially mocked USB Host stack to test Linux build and Cmock run for this partial Mock
* Mocked are all layers of the USB Host stack below the USB Host layer, which is used as a real component
Tests are written using [Catch2](https://github.com/catchorg/Catch2) test framework, use CMock, so you must install Ruby on your machine to run them.
This test directory uses freertos as real component
# Build
Tests build regularly like an idf project. Currently only working on Linux machines.
```
idf.py --preview set-target linux
idf.py build
```
# Run
The build produces an executable in the build folder.
Just run:
```
idf.py monitor
```
or run the executable directly:
```
./build/host_test_usb_host_layer.elf
```

View File

@@ -0,0 +1,12 @@
set(srcs)
list(APPEND srcs "test_main.cpp"
"usb_host_install_unit_test.cpp"
)
idf_component_register(SRCS ${srcs}
REQUIRES cmock usb
WHOLE_ARCHIVE)
# The following line would be needed to provide the 'main' function if this test used mocked FreeRTOS.
# As this test uses the real FreeRTOS implementation, we don't need Catch2 to provide 'main'.
#target_link_libraries(${COMPONENT_LIB} PRIVATE Catch2WithMain)

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/catch2: "^3.4.0"

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <catch2/catch_session.hpp>
#include <catch2/catch_test_macros.hpp>
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");
}
fflush(stdout);
exit(result);
}

View File

@@ -0,0 +1,254 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <catch2/catch_test_macros.hpp>
#include "usb_host.h" // Real implementation of usb_host.h
// Test all the mocked headers defined for this mock
extern "C" {
#include "Mockusb_phy.h"
#include "Mockhcd.h"
#include "Mockusbh.h"
#include "Mockenum.h"
#include "Mockhub.h"
}
SCENARIO("USB Host pre-uninstall")
{
// No USB Host driver previously installed
GIVEN("No USB Host previously installed") {
// Uninstall not-installed USB Host
SECTION("Uninstalling not installed USB Host") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_uninstall());
}
}
}
SCENARIO("USB Host install")
{
// USB Host config is not valid, USB Host driver is not installed from previous test case
GIVEN("No USB Host config, USB Host driver not installed") {
// Try to install the USB Host driver with usb_host_config set to nullptr
SECTION("USB Host config is nullptr") {
// Call the DUT function, expect ESP_ERR_INVALID_ARG
REQUIRE(ESP_ERR_INVALID_ARG == usb_host_install(nullptr));
}
}
// USB Host config struct
usb_host_config_t usb_host_config = {
.skip_phy_setup = false,
.root_port_unpowered = false,
.intr_flags = 1,
.enum_filter_cb = nullptr,
};
// USB host config is valid, USB Host driver is not installed from previous test case
GIVEN("USB Host config, USB Host driver not installed") {
// Try to install the USB Host driver, with PHY install error
SECTION("Fail to install USB Host - PHY Install error") {
// Make the PHY install to fail
usb_new_phy_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Try to install the USB Host driver, with HCD Install error, use internal PHY
SECTION("Fail to install USB Host - HCD Install error (internal PHY)") {
// Make the PHY install to pass
usb_phy_handle_t phy_handle;
usb_new_phy_ExpectAnyArgsAndReturn(ESP_OK);
usb_new_phy_ReturnThruPtr_handle_ret(&phy_handle);
// make the HCD port install to fail
hcd_install_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// goto hcd_err: We must uninstall the PHY
usb_del_phy_ExpectAndReturn(phy_handle, ESP_OK);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Try to install the USB Host driver, with HCD Install error, use external PHY
SECTION("Fail to install USB Host - HCD Install error (external PHY)") {
// Skip the PHY Setup (external phy)
usb_host_config.skip_phy_setup = true;
// make the HCD port install to fail
hcd_install_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Try to install the USB Host driver, with USBH Install error, use internal PHY
SECTION("Fail to install USB Host - USBH install error") {
// Make the PHY install to pass
usb_phy_handle_t phy_handle;
usb_new_phy_ExpectAnyArgsAndReturn(ESP_OK);
usb_new_phy_ReturnThruPtr_handle_ret(&phy_handle);
// make the HCD port install to pass
hcd_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the USBH install to fail
usbh_install_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// goto usbh_err: We must uninstall HCD port and PHY
hcd_uninstall_ExpectAndReturn(ESP_OK);
usb_del_phy_ExpectAndReturn(phy_handle, ESP_OK);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Try to install the USB Host driver, with Enum driver Install error, use internal PHY
SECTION("Fail to install USB Host - Enum driver install error") {
// Make the PHY install to pass
usb_phy_handle_t phy_handle;
usb_new_phy_ExpectAnyArgsAndReturn(ESP_OK);
usb_new_phy_ReturnThruPtr_handle_ret(&phy_handle);
// make the HCD port install to pass
hcd_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the USBH install to pass
usbh_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the ENUM Driver to fail
enum_install_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// goto enum_err: We must uninstall USBH, HCD port and PHY
usbh_uninstall_ExpectAndReturn(ESP_OK);
hcd_uninstall_ExpectAndReturn(ESP_OK);
usb_del_phy_ExpectAndReturn(phy_handle, ESP_OK);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Try to install the USB Host driver, with Hub driver Install error, use internal PHY
SECTION("Fail to install USB Host - Hub driver install error") {
// Make the PHY install to pass
usb_phy_handle_t phy_handle;
usb_new_phy_ExpectAnyArgsAndReturn(ESP_OK);
usb_new_phy_ReturnThruPtr_handle_ret(&phy_handle);
// make the HCD port install to pass
hcd_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the USBH install to pass
usbh_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the ENUM Driver to pass
enum_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the HUB Driver to fail
hub_install_ExpectAnyArgsAndReturn(ESP_ERR_INVALID_STATE);
// goto hub_err: We must uninstall Enum driver, USBH, HCD port and PHY
enum_uninstall_ExpectAndReturn(ESP_OK);
usbh_uninstall_ExpectAndReturn(ESP_OK);
hcd_uninstall_ExpectAndReturn(ESP_OK);
usb_del_phy_ExpectAndReturn(phy_handle, ESP_OK);
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
// Successfully install the USB Host driver
SECTION("Successfully install the USB Host driver") {
// Make the PHY install to pass
usb_phy_handle_t phy_handle;
usb_new_phy_ExpectAnyArgsAndReturn(ESP_OK);
usb_new_phy_ReturnThruPtr_handle_ret(&phy_handle);
// make the HCD port install to pass
hcd_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the USBH install to pass
usbh_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the ENUM Driver to pass
enum_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make the HUB Driver to pass
hub_install_ExpectAnyArgsAndReturn(ESP_OK);
// Make hub_root_start() to pass
hub_root_start_ExpectAndReturn(ESP_OK);
// Call the DUT function, expect ESP_OK
REQUIRE(ESP_OK == usb_host_install(&usb_host_config));
}
}
// USB Host config is valid, USB Host driver was successfully installed in previous test case
GIVEN("USB Host config, USB Host driver previously installed") {
// Try to install USB Host driver again, after it has been successfully installed
SECTION("Fail to install already installed USB Host driver") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_install(&usb_host_config));
}
}
}
SCENARIO("USB Host post-uninstall")
{
// USB Host driver successfully installed from previous test case
GIVEN("USB Host previously installed") {
// Uninstall successfully installed USB Host driver
SECTION("Successfully uninstall the USB Host driver") {
// Make the hub_root_stop() to pass
hub_root_stop_ExpectAndReturn(ESP_OK);
// Make uninstalling of all the drivers to pass
hub_uninstall_ExpectAndReturn(ESP_OK);
enum_uninstall_ExpectAndReturn(ESP_OK);
usbh_uninstall_ExpectAndReturn(ESP_OK);
hcd_uninstall_ExpectAndReturn(ESP_OK);
// Make the usb_del_phy() to pass
usb_del_phy_ExpectAnyArgsAndReturn(ESP_OK);
// Call the DUT function, expect ESP_OK
REQUIRE(ESP_OK == usb_host_uninstall());
}
}
// USB Host driver successfully uninstalled from previous test case
GIVEN("USB Host successfully uninstalled") {
// USB Host successfully uninstalled, try to uninstall it again
SECTION("Uninstall already uninstalled USB Host driver") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usb_host_uninstall());
}
}
}

View File

@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_usb_host_host_layer_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=5)

View File

@@ -0,0 +1,4 @@
CONFIG_IDF_TARGET="linux"
CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n

View File

@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
list(APPEND EXTRA_COMPONENT_DIRS
"$ENV{IDF_PATH}/tools/mocks/usb/usbh_layer_mock/usb"
# The following line would be needed to include the freertos mock component if this test used mocked FreeRTOS.
#"$ENV{IDF_PATH}/tools/mocks/freertos/"
)
project(host_test_usbh_layer)

View File

@@ -0,0 +1,37 @@
| Supported Targets | Linux |
| ----------------- | ----- |
# Description
This directory contains test code for `USBH layer` of USB Host stack. Namely:
* USBH public API calls to install and uninstall the USBH driver with partially mocked USB Host stack to test Linux build and Cmock run for this partial Mock
* Mocked are all layers of the USB Host stack below the USBH layer, which is used as a real component
Tests are written using [Catch2](https://github.com/catchorg/Catch2) test framework, use CMock, so you must install Ruby on your machine to run them.
This test directory uses freertos as a real component
# Build
Tests build regularly like an idf project. Currently only working on Linux machines.
```
idf.py --preview set-target linux
idf.py build
```
# Run
The build produces an executable in the build folder.
Just run:
```
idf.py monitor
```
or run the executable directly:
```
./build/host_test_usbh_layer.elf
```

View File

@@ -0,0 +1,12 @@
set(srcs)
list(APPEND srcs "test_main.cpp"
"usbh_install_unit_test.cpp"
)
idf_component_register(SRCS ${srcs}
REQUIRES cmock usb
WHOLE_ARCHIVE)
# The following line would be needed to provide the 'main' function if this test used mocked FreeRTOS.
# As this test uses the real FreeRTOS implementation, we don't need Catch2 to provide 'main'.
#target_link_libraries(${COMPONENT_LIB} PRIVATE Catch2WithMain)

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/catch2: "^3.4.0"

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <catch2/catch_session.hpp>
#include <catch2/catch_test_macros.hpp>
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");
}
fflush(stdout);
exit(result);
}

View File

@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <catch2/catch_test_macros.hpp>
#include "usbh.h" // Real implementation of usbh.h
// Test all the mocked headers defined for this mock
extern "C" {
#include "Mockhcd.h"
#include "Mockusb_private.h"
}
SCENARIO("USBH pre-uninstall")
{
// No USBH driver previously installed
GIVEN("No USBH previously installed") {
// Uninstall not-installed USBH driver
SECTION("Uninstalling not installed USBH driver") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usbh_uninstall());
}
}
}
SCENARIO("USBH install")
{
// USBH config is not valid, USBH driver is not previously installed
GIVEN("No USBH config, USBH driver not installed") {
// Try to install the USBH driver with usbh_config set to nullptr
SECTION("USBH config is nullptr") {
// Call the DUT function, expect ESP_ERR_INVALID_ARG
REQUIRE(ESP_ERR_INVALID_ARG == usbh_install(nullptr));
}
}
// USBH config struct
usbh_config_t usbh_config = {};
// USBH config is valid, USBH driver is not previously installed
GIVEN("USBH config, USBH driver not installed") {
// Successfully install the USBH Driver
SECTION("Successfully install the USBH Driver") {
// Call the DUT function, expect ESP_OK
REQUIRE(ESP_OK == usbh_install(&usbh_config));
}
}
// USBH config is valid, USBH driver is previously installed
GIVEN("USBH config, USBH driver previously installed") {
SECTION("Fail to install already installed USBH driver") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usbh_install(&usbh_config));
}
}
}
SCENARIO("USBH post-uninstall")
{
// USBH driver successfully installed from previous test case
GIVEN("USBH driver previously installed") {
// Uninstall successfully installed USBH driver
SECTION("Successfully uninstall the USBH driver") {
// Call the DUT function, expect ESP_OK
REQUIRE(ESP_OK == usbh_uninstall());
}
}
// USBH driver successfully uninstalled from previous test case
GIVEN("USBH successfully uninstalled") {
// USBH successfully uninstalled, try to uninstall it again
SECTION("Uninstall already uninstalled USBH driver") {
// Call the DUT function, expect ESP_ERR_INVALID_STATE
REQUIRE(ESP_ERR_INVALID_STATE == usbh_uninstall());
}
}
}

View File

@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_usbh_layer_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=5)

View File

@@ -0,0 +1,4 @@
CONFIG_IDF_TARGET="linux"
CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n