From 6cf077169e7af25d47547cdc03e55fbb53d5c362 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 19 May 2021 12:36:36 +0200 Subject: [PATCH] tcp_transport: Rework test to split test code and use fixtures --- .../test/tcp_transport_fixtures.h | 41 ++ .../tcp_transport/test/test_transport.c | 455 ------------------ .../tcp_transport/test/test_transport_basic.c | 44 ++ .../test/test_transport_connect.c | 178 +++++++ .../test/test_transport_fixtures.c | 311 ++++++++++++ components/tcp_transport/transport_ssl.c | 18 +- components/tcp_transport/transport_ws.c | 2 +- 7 files changed, 587 insertions(+), 462 deletions(-) create mode 100644 components/tcp_transport/test/tcp_transport_fixtures.h delete mode 100644 components/tcp_transport/test/test_transport.c create mode 100644 components/tcp_transport/test/test_transport_basic.c create mode 100644 components/tcp_transport/test/test_transport_connect.c create mode 100644 components/tcp_transport/test/test_transport_fixtures.c diff --git a/components/tcp_transport/test/tcp_transport_fixtures.h b/components/tcp_transport/test/tcp_transport_fixtures.h new file mode 100644 index 0000000000..dc93f89b92 --- /dev/null +++ b/components/tcp_transport/test/tcp_transport_fixtures.h @@ -0,0 +1,41 @@ +#ifndef _TCP_TRANSPORT_FIXTURES_H_ +#define _TCP_TRANSPORT_FIXTURES_H_ + +/** + * @brief Structures and types for passing socket options + */ +enum expected_sock_option_types { + SOCK_OPT_TYPE_BOOL, + SOCK_OPT_TYPE_INT, +}; +struct expected_sock_option { + int level; + int optname; + int optval; + enum expected_sock_option_types opttype; +}; + +/** + * @brief Helper test functions for timeout connection tests + * + * This case simulates connection timeout running tcp connect asynchronously with other socket connection + * consuming entire socket listener backlog. + * Important: Both tasks must run on the same core, with listener's prio higher to make sure that + * 1) first the localhost_listener() creates and connects all sockets until the last one blocks + * 2) before the tcp_connect_task() attempts to connect and thus fails with connection timeout + */ +void tcp_transport_test_connection_timeout(esp_transport_handle_t transport_under_test); + + +/** + * @brief Helper test function to check socket options configured separately by transports + * + * This sets up the connection test to start two tasks, but unlike tcp_transport_test_connection_timeout, + * here we just let the connection to happen or at least open on TCP layer so we get the internal socket + * descriptor. While the connection is in progress or connected, we can check the socket options configured + * by the tcp_transport API. + */ +void tcp_transport_test_socket_options(esp_transport_handle_t transport_under_test, bool async, + const struct expected_sock_option *expected_opts, size_t sock_options_len); + +#endif //_TCP_TRANSPORT_FIXTURES_H_ diff --git a/components/tcp_transport/test/test_transport.c b/components/tcp_transport/test/test_transport.c deleted file mode 100644 index 505ec29507..0000000000 --- a/components/tcp_transport/test/test_transport.c +++ /dev/null @@ -1,455 +0,0 @@ -#include "unity.h" - -#include "esp_transport.h" -#include "esp_transport_tcp.h" -#include "esp_transport_ssl.h" -#include "esp_transport_ws.h" -#include "test_utils.h" -#include "esp_log.h" -#include "lwip/err.h" -#include "lwip/sockets.h" -#include "lwip/sys.h" -#include -#include "freertos/event_groups.h" - -#define TCP_CONNECT_DONE (1) -#define TCP_LISTENER_DONE (2) -#define TCP_ACCEPTOR_DONE (4) -#define TCP_LISTENER_ACCEPTED (8) -#define TCP_LISTENER_READY (16) - -struct tcp_connect_task_params { - int timeout_ms; - int port; - EventGroupHandle_t tcp_connect_done; - int ret; - int listen_sock; - int accepted_sock; - int last_connect_sock; - bool tcp_listener_failed; - esp_transport_handle_t transport_under_test; - bool accept_connection; - bool consume_sock_backlog; -}; - -#define TEST_TRANSPORT_BIND_IFNAME() \ - struct ifreq ifr; \ - ifr.ifr_name[0] = 'l'; \ - ifr.ifr_name[1] = 'o'; \ - ifr.ifr_name[2] = '\0'; - -/** - * @brief Recursively connects with a new socket to loopback interface until the last one blocks. - * The last socket is closed upon test teardown, that initiates recursive cleanup (close) for all - * active/connected sockets. - */ -static void connect_once(struct tcp_connect_task_params *params) -{ - struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_LOOPBACK), - .sin_family = AF_INET, - .sin_port = htons(params->port) }; - int connect_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); - if (connect_sock < 0) { - params->tcp_listener_failed = true; - return; - } - params->last_connect_sock = connect_sock; - int err = connect(connect_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4)); - if (err != 0) { - // The last connection is expected to fail here, since the both sockets get closed on test cleanup - return; - } - connect_once(params); - close(connect_sock); -} - -/** - * @brief creates a listener (and an acceptor if configured) - * - * if consume_sock_backlog set: connect as many times as possible to prepare an endpoint which - * would make the client block but not complete TCP handshake - * - * if accept_connection set: waiting normally for connection creating an acceptor to mimic tcp-transport endpoint - */ -static void localhost_listener(void *pvParameters) -{ - const char* TAG = "tcp_transport_test"; - struct tcp_connect_task_params *params = pvParameters; - struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_ANY), - .sin_family = AF_INET, - .sin_port = htons(params->port) }; - // Create listener socket and bind it to ANY address - params->listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); - int opt = 1; - setsockopt(params->listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - if (params->listen_sock < 0) { - ESP_LOGE(TAG, "Unable to create socket"); - params->tcp_listener_failed = true; - goto failed; - } - int err = bind(params->listen_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4)); - if (err != 0) { - ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - params->tcp_listener_failed = true; - goto failed; - } - - // Listen with backlog set to a low number - err = listen(params->listen_sock, 4); - if (err != 0) { - ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); - params->tcp_listener_failed = true; - goto failed; - } - - // Listener is ready at this point - xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_READY); - - if (params->consume_sock_backlog) { - // Ideally we would set backlog to 0, but since this is an implementation specific recommendation parameter, - // we recursively create sockets and try to connect to this listener in order to consume the backlog. After - // the backlog is consumed, the last connection blocks (waiting for accept), but at that point we are sure - // that any other connection would also block - connect_once(params); - } else if (params->accept_connection) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - params->accepted_sock = accept(params->listen_sock, (struct sockaddr *)&source_addr, &addr_len); - if (params->accepted_sock < 0) { - ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); - goto failed; - } - xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_ACCEPTED); // Mark the socket as accepted - // ...and wait for the "acceptor" tests to finish - xEventGroupWaitBits(params->tcp_connect_done, TCP_ACCEPTOR_DONE, true, true, params->timeout_ms * 10); - } - -failed: - xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_DONE); - vTaskSuspend(NULL); -} - -static void tcp_connect_task(void *pvParameters) -{ - struct tcp_connect_task_params *params = pvParameters; - - params->ret = esp_transport_connect(params->transport_under_test, "localhost", params->port, params->timeout_ms); - if (params->accept_connection) { - // If we test the accepted connection, need to wait until the test completes - xEventGroupWaitBits(params->tcp_connect_done, TCP_ACCEPTOR_DONE, true, true, params->timeout_ms * 10); - } - xEventGroupSetBits(params->tcp_connect_done, TCP_CONNECT_DONE); - vTaskSuspend(NULL); -} - - -TEST_CASE("tcp_transport: init and deinit transport list", "[tcp_transport][leaks=0]") -{ - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_list_add(transport_list, tcp, "tcp"); - TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list)); -} - -TEST_CASE("tcp_transport: using ssl transport separately", "[tcp_transport][leaks=0]") -{ - esp_transport_handle_t h = esp_transport_ssl_init(); - TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(h)); -} - -TEST_CASE("tcp_transport: using ws transport separately", "[tcp_transport][leaks=0]") -{ - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_handle_t ws = esp_transport_ws_init(tcp); - TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(ws)); - TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(tcp)); -} - -static void transport_connection_timeout_test(esp_transport_handle_t transport_under_test) -{ - // This case simulates connection timeout running tcp connect asynchronously with other socket connection - // consuming entire socket listener backlog. - // Important: Both tasks must run on the same core, with listener's prio higher to make sure that - // 1) first the localhost_listener() creates and connects all sockets until the last one blocks - // 2) before the tcp_connect_task() attempts to connect and thus fails with connection timeout - - struct tcp_connect_task_params params = { .tcp_connect_done = xEventGroupCreate(), - .timeout_ms = 200, - .port = 80, - .consume_sock_backlog = true, - .transport_under_test = transport_under_test }; - TickType_t max_wait = pdMS_TO_TICKS(params.timeout_ms * 10); - TaskHandle_t localhost_listener_task_handle = NULL; - TaskHandle_t tcp_connect_task_handle = NULL; - - test_case_uses_tcpip(); - - // Create listener and connect it with as many sockets until the last one blocks - xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, (void*)¶ms, 5, &localhost_listener_task_handle, 0); - - // Perform tcp-connect in a separate task to check asynchronously for the timeout - xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, (void*)¶ms, 4, &tcp_connect_task_handle, 0); - - // Roughly measure tick-time spent while trying to connect - TickType_t start = xTaskGetTickCount(); - EventBits_t bits = xEventGroupWaitBits(params.tcp_connect_done, TCP_CONNECT_DONE, true, true, max_wait); - TickType_t end = xTaskGetTickCount(); - - TEST_ASSERT_EQUAL(TCP_CONNECT_DONE, TCP_CONNECT_DONE & bits); // Connection has finished - TEST_ASSERT_EQUAL(-1, params.ret); // Connection failed with -1 - - // Test connection attempt took expected timeout value - TEST_ASSERT_INT_WITHIN(pdMS_TO_TICKS(params.timeout_ms/5), pdMS_TO_TICKS(params.timeout_ms), end-start); - - // Closing both parties of the last "blocking" connection to unwind localhost_listener() and let other connected sockets closed - close(params.listen_sock); - close(params.last_connect_sock); - - // Cleanup - xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_DONE, true, true, max_wait); - TEST_ASSERT_EQUAL(false, params.tcp_listener_failed); - vEventGroupDelete(params.tcp_connect_done); - test_utils_task_delete(localhost_listener_task_handle); - test_utils_task_delete(tcp_connect_task_handle); -} - -TEST_CASE("tcp_transport: connect timeout", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_list_add(transport_list, tcp, "tcp"); - - transport_connection_timeout_test(tcp); - esp_transport_close(tcp); - esp_transport_list_destroy(transport_list); -} - -TEST_CASE("ssl_transport: connect timeout", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_list_add(transport_list, tcp, "tcp"); - esp_transport_handle_t ssl = esp_transport_ssl_init(); - esp_transport_list_add(transport_list, ssl, "ssl"); - - transport_connection_timeout_test(ssl); - esp_transport_close(tcp); - esp_transport_close(ssl); - esp_transport_list_destroy(transport_list); -} - -TEST_CASE("transport: init and deinit multiple transport items", "[tcp_transport][leaks=0]") -{ - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_list_add(transport_list, tcp, "tcp"); - esp_transport_handle_t ssl = esp_transport_ssl_init(); - esp_transport_list_add(transport_list, ssl, "ssl"); - esp_transport_handle_t ws = esp_transport_ws_init(tcp); - esp_transport_list_add(transport_list, ws, "ws"); - esp_transport_handle_t wss = esp_transport_ws_init(ssl); - esp_transport_list_add(transport_list, wss, "wss"); - TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list)); -} - -// This is a private API of the tcp transport, but needed for socket operation tests -int esp_transport_get_socket(esp_transport_handle_t t); - -// Structures and types for passing socket options -enum expected_sock_option_types { - SOCK_OPT_TYPE_BOOL, - SOCK_OPT_TYPE_INT, -}; - -struct expected_sock_option { - int level; - int optname; - int optval; - enum expected_sock_option_types opttype; -}; - -static void socket_operation_test(esp_transport_handle_t transport_under_test, - const struct expected_sock_option expected_opts[], size_t sock_options_len) -{ - struct tcp_connect_task_params params = { .tcp_connect_done = xEventGroupCreate(), - .timeout_ms = 200, - .port = 80, - .accept_connection = true, - .transport_under_test = transport_under_test }; - TickType_t max_wait = pdMS_TO_TICKS(params.timeout_ms * 10); - TaskHandle_t localhost_listener_task_handle = NULL; - TaskHandle_t tcp_connect_task_handle = NULL; - - test_case_uses_tcpip(); - - // Create a listener and wait for it to be ready - xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, (void*)¶ms, 5, &localhost_listener_task_handle, 0); - xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_READY, true, true, max_wait); - // Perform tcp-connect in a separate task - xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, (void*)¶ms, 6, &tcp_connect_task_handle, 0); - - // Wait till the connection gets accepted to get the client's socket - xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_ACCEPTED, true, true, max_wait); - int sock = esp_transport_get_socket(params.transport_under_test); - for (int i=0; ikeep_alive_idle; - expected_opts[2].optname = TCP_KEEPINTVL; - expected_opts[2].optval = config->keep_alive_interval; - expected_opts[3].optname = TCP_KEEPCNT; - expected_opts[3].optval = config->keep_alive_count; - - socket_operation_test(transport_under_test, expected_opts, sizeof(expected_opts)/sizeof(struct expected_sock_option)); -} - -TEST_CASE("tcp_transport: Keep alive test", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t tcp = esp_transport_tcp_init(); - esp_transport_list_add(transport_list, tcp, "tcp"); - - // Perform the test - esp_transport_keep_alive_t keep_alive_cfg = { - .keep_alive_interval = 5, - .keep_alive_idle = 4, - .keep_alive_enable = true, - .keep_alive_count = 3 }; - esp_transport_tcp_set_keep_alive(tcp, &keep_alive_cfg); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_tcp_set_interface_name(tcp, &ifr); - - tcp_transport_keepalive_test(tcp, &keep_alive_cfg); - - // Cleanup - esp_transport_close(tcp); - esp_transport_list_destroy(transport_list); -} - -TEST_CASE("ssl_transport: Keep alive test", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t ssl = esp_transport_ssl_init(); - esp_transport_list_add(transport_list, ssl, "ssl"); - esp_tls_init_global_ca_store(); - esp_transport_ssl_enable_global_ca_store(ssl); - - // Perform the test - esp_transport_keep_alive_t keep_alive_cfg = { - .keep_alive_interval = 2, - .keep_alive_idle = 3, - .keep_alive_enable = true, - .keep_alive_count = 4 }; - esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - tcp_transport_keepalive_test(ssl, &keep_alive_cfg); - - // Cleanup - esp_transport_close(ssl); - esp_transport_list_destroy(transport_list); -} - -TEST_CASE("ws_transport: Keep alive test", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_list_handle_t transport_list = esp_transport_list_init(); - esp_transport_handle_t ssl = esp_transport_ssl_init(); - esp_transport_list_add(transport_list, ssl, "ssl"); - esp_tls_init_global_ca_store(); - esp_transport_ssl_enable_global_ca_store(ssl); - esp_transport_handle_t ws = esp_transport_ws_init(ssl); - esp_transport_list_add(transport_list, ws, "wss"); - - // Perform the test - esp_transport_keep_alive_t keep_alive_cfg = { - .keep_alive_interval = 1, - .keep_alive_idle = 2, - .keep_alive_enable = true, - .keep_alive_count = 3 }; - esp_transport_tcp_set_keep_alive(ssl, &keep_alive_cfg); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - tcp_transport_keepalive_test(ws, &keep_alive_cfg); - - // Cleanup - esp_transport_close(ssl); - esp_transport_list_destroy(transport_list); -} - -// Note: This functionality is tested and kept only for compatibility reasons with IDF <= 4.x -// It is strongly encouraged to use transport within lists only -TEST_CASE("ssl_transport: Check that parameters (keepalive) are set independently on the list", "[tcp_transport]") -{ - // Init the transport under test - esp_transport_handle_t ssl = esp_transport_ssl_init(); - esp_tls_init_global_ca_store(); - esp_transport_ssl_enable_global_ca_store(ssl); - - // Perform the test - esp_transport_keep_alive_t keep_alive_cfg = { - .keep_alive_interval = 2, - .keep_alive_idle = 4, - .keep_alive_enable = true, - .keep_alive_count = 3 }; - esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - tcp_transport_keepalive_test(ssl, &keep_alive_cfg); - - // Cleanup - esp_transport_close(ssl); - esp_transport_destroy(ssl); -} diff --git a/components/tcp_transport/test/test_transport_basic.c b/components/tcp_transport/test/test_transport_basic.c new file mode 100644 index 0000000000..9e7f92a661 --- /dev/null +++ b/components/tcp_transport/test/test_transport_basic.c @@ -0,0 +1,44 @@ +#include "unity.h" + +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "esp_transport_ssl.h" +#include "esp_transport_ws.h" +#include "esp_log.h" + + +TEST_CASE("tcp_transport: init and deinit transport list", "[tcp_transport][leaks=0]") +{ + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list)); +} + +TEST_CASE("tcp_transport: using ssl transport separately", "[tcp_transport][leaks=0]") +{ + esp_transport_handle_t h = esp_transport_ssl_init(); + TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(h)); +} + +TEST_CASE("tcp_transport: using ws transport separately", "[tcp_transport][leaks=0]") +{ + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(ws)); + TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(tcp)); +} + +TEST_CASE("transport: init and deinit multiple transport items", "[tcp_transport][leaks=0]") +{ + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + esp_transport_handle_t ssl = esp_transport_ssl_init(); + esp_transport_list_add(transport_list, ssl, "ssl"); + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + esp_transport_list_add(transport_list, ws, "ws"); + esp_transport_handle_t wss = esp_transport_ws_init(ssl); + esp_transport_list_add(transport_list, wss, "wss"); + TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list)); +} diff --git a/components/tcp_transport/test/test_transport_connect.c b/components/tcp_transport/test/test_transport_connect.c new file mode 100644 index 0000000000..514c8cfdf8 --- /dev/null +++ b/components/tcp_transport/test/test_transport_connect.c @@ -0,0 +1,178 @@ +#include "unity.h" +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "esp_transport_ssl.h" +#include "esp_transport_ws.h" +#include "esp_log.h" +#include "lwip/sockets.h" +#include "tcp_transport_fixtures.h" + + +#define TEST_TRANSPORT_BIND_IFNAME() \ + struct ifreq ifr; \ + ifr.ifr_name[0] = 'l'; \ + ifr.ifr_name[1] = 'o'; \ + ifr.ifr_name[2] = '\0'; + + +static void tcp_transport_keepalive_test(esp_transport_handle_t transport_under_test, bool async, esp_transport_keep_alive_t *config) +{ + static struct expected_sock_option expected_opts[4] = { + { .level = SOL_SOCKET, .optname = SO_KEEPALIVE, .optval = 1, .opttype = SOCK_OPT_TYPE_BOOL }, + { .level = IPPROTO_TCP }, + { .level = IPPROTO_TCP }, + { .level = IPPROTO_TCP } + }; + + expected_opts[1].optname = TCP_KEEPIDLE; + expected_opts[1].optval = config->keep_alive_idle; + expected_opts[2].optname = TCP_KEEPINTVL; + expected_opts[2].optval = config->keep_alive_interval; + expected_opts[3].optname = TCP_KEEPCNT; + expected_opts[3].optval = config->keep_alive_count; + + tcp_transport_test_socket_options(transport_under_test, async, expected_opts, + sizeof(expected_opts) / sizeof(struct expected_sock_option)); +} + +TEST_CASE("tcp_transport: connect timeout", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + + tcp_transport_test_connection_timeout(tcp); + esp_transport_close(tcp); + esp_transport_list_destroy(transport_list); +} + +TEST_CASE("ssl_transport: connect timeout", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + esp_transport_handle_t ssl = esp_transport_ssl_init(); + esp_transport_list_add(transport_list, ssl, "ssl"); + + tcp_transport_test_connection_timeout(ssl); + esp_transport_close(tcp); + esp_transport_close(ssl); + esp_transport_list_destroy(transport_list); +} + +TEST_CASE("tcp_transport: Keep alive test", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + + // Perform the test + esp_transport_keep_alive_t keep_alive_cfg = { + .keep_alive_interval = 5, + .keep_alive_idle = 4, + .keep_alive_enable = true, + .keep_alive_count = 3 }; + esp_transport_tcp_set_keep_alive(tcp, &keep_alive_cfg); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_tcp_set_interface_name(tcp, &ifr); + + // Run the test for both sync and async_connect + tcp_transport_keepalive_test(tcp, true, &keep_alive_cfg); + tcp_transport_keepalive_test(tcp, false, &keep_alive_cfg); + + // Cleanup + esp_transport_close(tcp); + esp_transport_list_destroy(transport_list); +} + +TEST_CASE("ssl_transport: Keep alive test", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t ssl = esp_transport_ssl_init(); + esp_transport_list_add(transport_list, ssl, "ssl"); + esp_tls_init_global_ca_store(); + esp_transport_ssl_enable_global_ca_store(ssl); + + // Perform the test + esp_transport_keep_alive_t keep_alive_cfg = { + .keep_alive_interval = 2, + .keep_alive_idle = 3, + .keep_alive_enable = true, + .keep_alive_count = 4 }; + esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(ssl, &ifr); + + // Run the test for async_connect only + // - TLS connection would connect on socket level only, returning tls-handshake in progress + tcp_transport_keepalive_test(ssl, true, &keep_alive_cfg); + + // Cleanup + esp_transport_close(ssl); + esp_transport_list_destroy(transport_list); +} + +TEST_CASE("ws_transport: Keep alive test", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_list_handle_t transport_list = esp_transport_list_init(); + esp_transport_handle_t tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + esp_transport_list_add(transport_list, ws, "ws"); + + // Perform the test + esp_transport_keep_alive_t keep_alive_cfg = { + .keep_alive_interval = 11, + .keep_alive_idle = 22, + .keep_alive_enable = true, + .keep_alive_count = 33 }; + esp_transport_tcp_set_keep_alive(tcp, &keep_alive_cfg); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(tcp, &ifr); + + // Run the test for sync_connect only (ws doesn't support async) + tcp_transport_keepalive_test(ws, false, &keep_alive_cfg); + + // Cleanup + esp_transport_close(tcp); + esp_transport_list_destroy(transport_list); +} + +// Note: This functionality is tested and kept only for compatibility reasons with IDF <= 4.x +// It is strongly encouraged to use transport within lists only +TEST_CASE("ssl_transport: Check that parameters (keepalive) are set independently on the list", "[tcp_transport]") +{ + // Init the transport under test + esp_transport_handle_t ssl = esp_transport_ssl_init(); + esp_tls_init_global_ca_store(); + esp_transport_ssl_enable_global_ca_store(ssl); + + // Perform the test + esp_transport_keep_alive_t keep_alive_cfg = { + .keep_alive_interval = 2, + .keep_alive_idle = 4, + .keep_alive_enable = true, + .keep_alive_count = 3 }; + esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(ssl, &ifr); + + tcp_transport_keepalive_test(ssl, true, &keep_alive_cfg); + + // Cleanup + esp_transport_close(ssl); + esp_transport_destroy(ssl); +} diff --git a/components/tcp_transport/test/test_transport_fixtures.c b/components/tcp_transport/test/test_transport_fixtures.c new file mode 100644 index 0000000000..1ad70ddbd7 --- /dev/null +++ b/components/tcp_transport/test/test_transport_fixtures.c @@ -0,0 +1,311 @@ +#include "unity.h" + +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "test_utils.h" +#include "esp_log.h" +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "freertos/event_groups.h" +#include "tcp_transport_fixtures.h" + +// This is a private API of the tcp transport, but needed for socket operation tests +int esp_transport_get_socket(esp_transport_handle_t t); + +/** + * @brief Event flags for synchronization between the listener task, the connection task and the test task + */ +enum { + TCP_CONNECT_DONE = 1 << 0, /*!< Indicates that the connection task has finished, so the transport_connect() exited */ + TCP_LISTENER_DONE = 1 << 1, /*!< Indicates that the listener task has finished either with success for failure */ + TCP_TEST_DONE = 1 << 2, /*!< Indicates that the test case finished, test tear-down() called */ + TCP_LISTENER_READY = 1 << 3, /*!< Indicates that the listener task is ready to accept connections */ + TCP_LISTENER_ACCEPTED = 1 << 4, /*!< Indicates that the listener task has accepted a connection (from transport_connect()) */ +}; + +/** + * @brief Connection test configuration parameters + */ +struct tcp_connect_test_config { + esp_transport_handle_t transport_under_test; + bool accept_connection; + bool consume_sock_backlog; + bool connect_async; + int timeout_ms; + int port; + bool listener_task_prio_higher; +}; + +/** + * @brief Test setup structure containing all the info needed for the connection tests + */ +struct tcp_connect_test_storage { + struct tcp_connect_test_config config; + TickType_t max_wait; + EventGroupHandle_t tcp_connect_done; + int connect_return_value; + int listen_sock; + int accepted_sock; + int last_connect_sock; + bool tcp_listener_failed; + TaskHandle_t listener_task; + TaskHandle_t tcp_connect_task; +}; + +typedef struct tcp_connect_test_storage *tcp_connect_test_t; + +/** + * @brief Recursively connects with a new socket to loopback interface until the last one blocks. + * The last socket is closed upon test teardown, that initiates recursive cleanup (close) for all + * active/connected sockets. + */ +static void connect_once(struct tcp_connect_test_storage *storage) +{ + struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + .sin_family = AF_INET, + .sin_port = htons(storage->config.port) }; + int connect_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + if (connect_sock < 0) { + storage->tcp_listener_failed = true; + return; + } + storage->last_connect_sock = connect_sock; + int err = connect(connect_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4)); + if (err != 0) { + // The last connection is expected to fail here, since the both sockets get closed on test cleanup + return; + } + connect_once(storage); + close(connect_sock); +} + +/** + * @brief creates a listener (and an acceptor if configured) + * + * if consume_sock_backlog set: connect as many times as possible to prepare an endpoint which + * would make the client block but not complete TCP handshake + * + * if accept_connection set: waiting normally for connection creating an acceptor to mimic tcp-transport endpoint + */ +static void localhost_listener(void *pvParameters) +{ + const char* TAG = "tcp_transport_test"; + struct tcp_connect_test_storage *storage = pvParameters; + struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_family = AF_INET, + .sin_port = htons(storage->config.port) }; + // Create listener socket and bind it to ANY address + storage->listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + int opt = 1; + setsockopt(storage->listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + if (storage->listen_sock < 0) { + ESP_LOGE(TAG, "Unable to create socket"); + storage->tcp_listener_failed = true; + goto failed; + } + int err = bind(storage->listen_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4)); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + storage->tcp_listener_failed = true; + goto failed; + } + + // Listen with backlog set to a low number + err = listen(storage->listen_sock, 4); + if (err != 0) { + ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); + storage->tcp_listener_failed = true; + goto failed; + } + + // Listener is ready at this point + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_READY); + + if (storage->config.consume_sock_backlog) { + // Ideally we would set backlog to 0, but since this is an implementation specific recommendation parameter, + // we recursively create sockets and try to connect to this listener in order to consume the backlog. After + // the backlog is consumed, the last connection blocks (waiting for accept), but at that point we are sure + // that any other connection would also block + connect_once(storage); + } else if (storage->config.accept_connection) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + storage->accepted_sock = accept(storage->listen_sock, (struct sockaddr *)&source_addr, &addr_len); + if (storage->accepted_sock < 0) { + ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); + goto failed; + } + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_ACCEPTED); // Mark the socket as accepted + // ...and wait for the "acceptor" tests to finish + xEventGroupWaitBits(storage->tcp_connect_done, TCP_TEST_DONE, true, true, storage->config.timeout_ms * 10); + } + +failed: + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_DONE); + vTaskSuspend(NULL); +} + +/** + * @brief This task simply tries to connect to localhost (server provided by listner's task) using tcp_transport + */ +static void tcp_connect_task(void *pvParameters) +{ + struct tcp_connect_test_storage *storage = pvParameters; + + int (*connect_fn)(esp_transport_handle_t, const char *, int, int) = + storage->config.connect_async ? esp_transport_connect_async : esp_transport_connect; + + storage->connect_return_value = connect_fn(storage->config.transport_under_test, "localhost", storage->config.port, storage->config.timeout_ms); + + if (storage->config.accept_connection) { + // If we test the accepted connection, need to wait until the test completes + xEventGroupWaitBits(storage->tcp_connect_done, TCP_TEST_DONE, true, true, storage->config.timeout_ms * 10); + } + xEventGroupSetBits(storage->tcp_connect_done, TCP_CONNECT_DONE); + vTaskSuspend(NULL); +} + +static inline void close_if_valid(int *s) +{ + if (*s >= 0) { + close(*s); + *s = -1; + } +} + +/** + * @brief Connect test setup function + * + * Creates the Test storage, configures it accordingly and starts two tasks + * * localhost_listener -- to provide a simple server endpoint for the transport layers to connect to + * * tcp_connect_task -- to perform the connection + */ +static tcp_connect_test_t connect_test_setup(struct tcp_connect_test_config *config) +{ + tcp_connect_test_t t = calloc(1, sizeof(struct tcp_connect_test_storage)); + if (!t) { + return NULL; + } + memcpy(&t->config, config, sizeof(struct tcp_connect_test_config)); + t->tcp_connect_done = xEventGroupCreate(); + if (!t->tcp_connect_done) { + return NULL; + } + t->max_wait = pdMS_TO_TICKS(config->timeout_ms * 10); + + t->listen_sock = t->last_connect_sock = t->accepted_sock = -1; // mark all sockets invalid + + test_case_uses_tcpip(); + + // Create listener task + xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, t, 5, &t->listener_task, 0); + xEventGroupWaitBits(t->tcp_connect_done, TCP_LISTENER_READY, true, true, t->max_wait); + + // Perform tcp-connect in a separate task to check asynchronously for the timeout or to connect (depends on the test config) + xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, t, + config->listener_task_prio_higher? 4 : 6, &t->tcp_connect_task, 0); + + return t; +} + +/** + * @brief Destroys and cleans out the test environment + */ +static void connect_test_teardown(tcp_connect_test_t t) +{ + // Mark the test done and wait for the listener to check if finished with no issues + xEventGroupSetBits(t->tcp_connect_done, TCP_TEST_DONE); + xEventGroupWaitBits(t->tcp_connect_done, TCP_LISTENER_DONE, true, true, t->max_wait); + TEST_ASSERT_EQUAL(false, t->tcp_listener_failed); + + // Closing both parties of the last "blocking" connection to unwind localhost_listener() and let other connected sockets closed + close_if_valid(&t->listen_sock); + close_if_valid(&t->last_connect_sock); + close_if_valid(&t->accepted_sock); + + // Cleanup + vTaskSuspend(t->tcp_connect_task); + vTaskSuspend(t->listener_task); + vEventGroupDelete(t->tcp_connect_done); + test_utils_task_delete(t->tcp_connect_task); + test_utils_task_delete(t->listener_task); + free(t); +} + +/** + * @brief Utility function for testing timeouts for different transports + */ +void tcp_transport_test_connection_timeout(esp_transport_handle_t transport_under_test) +{ + + struct tcp_connect_test_config params = { + .timeout_ms = 200, + .port = 80, + .consume_sock_backlog = true, + .connect_async = false, + .transport_under_test = transport_under_test, + .listener_task_prio_higher = true + }; + + tcp_connect_test_t test = connect_test_setup(¶ms); + TEST_ASSERT_NOT_NULL(test); + + // Roughly measure tick-time spent while trying to connect + TickType_t start = xTaskGetTickCount(); + EventBits_t bits = xEventGroupWaitBits(test->tcp_connect_done, TCP_CONNECT_DONE, true, true, test->max_wait); + TickType_t end = xTaskGetTickCount(); + + TEST_ASSERT_EQUAL(TCP_CONNECT_DONE, TCP_CONNECT_DONE & bits); // Connection has finished + TEST_ASSERT_EQUAL(-1, test->connect_return_value); // Connection failed with -1 + + // Test connection attempt took expected timeout value + TEST_ASSERT_INT_WITHIN(pdMS_TO_TICKS(params.timeout_ms/5), pdMS_TO_TICKS(params.timeout_ms), end-start); + + // Close the last bound connection, to recursively unwind the consumed backlog + close_if_valid(&test->last_connect_sock); + + connect_test_teardown(test); +} + +/** + * @brief Utility function for testing timeouts for different transports, options and both sync and async connection + */ +void tcp_transport_test_socket_options(esp_transport_handle_t transport_under_test, bool async, + const struct expected_sock_option *expected_opts, size_t sock_options_len) +{ + struct tcp_connect_test_config params = { + .timeout_ms = 200, + .port = 80, + .accept_connection = true, + .consume_sock_backlog = false, + .transport_under_test = transport_under_test, + .connect_async = async, + .listener_task_prio_higher = false + }; + + tcp_connect_test_t test = connect_test_setup(¶ms); + TEST_ASSERT_NOT_NULL(test); + + // Wait till the connection gets accepted to get the client's socket + xEventGroupWaitBits(test->tcp_connect_done, TCP_LISTENER_ACCEPTED, true, true, test->max_wait); + int sock = esp_transport_get_socket(params.transport_under_test); + for (int i=0; iconn_state = TRANS_SSL_CONNECTING; + ssl->sockfd = -1; } if (ssl->conn_state == TRANS_SSL_CONNECTING) { - return esp_tls_conn_new_async(host, strlen(host), port, &ssl->cfg, ssl->tls); + int progress = esp_tls_conn_new_async(host, strlen(host), port, &ssl->cfg, ssl->tls); + if (progress >= 0) { + ssl->sockfd = ssl->tls->sockfd; + } + return progress; + } return 0; } @@ -161,7 +167,7 @@ static int ssl_poll_read(esp_transport_handle_t t, int timeout_ms) uint32_t optlen = sizeof(sock_errno); getsockopt(ssl->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); esp_transport_capture_errno(t, sock_errno); - ESP_LOGE(TAG, "poll_read select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->tls->sockfd); + ESP_LOGE(TAG, "poll_read select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->sockfd); ret = -1; } return ret; @@ -184,7 +190,7 @@ static int ssl_poll_write(esp_transport_handle_t t, int timeout_ms) uint32_t optlen = sizeof(sock_errno); getsockopt(ssl->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); esp_transport_capture_errno(t, sock_errno); - ESP_LOGE(TAG, "poll_write select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->tls->sockfd); + ESP_LOGE(TAG, "poll_write select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->sockfd); ret = -1; } return ret; @@ -196,7 +202,7 @@ static int ssl_write(esp_transport_handle_t t, const char *buffer, int len, int transport_esp_tls_t *ssl = ssl_get_context_data(t); if ((poll = esp_transport_poll_write(t, timeout_ms)) <= 0) { - ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->tls->sockfd, timeout_ms); + ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->sockfd, timeout_ms); return poll; } int ret = esp_tls_conn_write(ssl->tls, (const unsigned char *) buffer, len); @@ -213,7 +219,7 @@ static int tcp_write(esp_transport_handle_t t, const char *buffer, int len, int transport_esp_tls_t *ssl = ssl_get_context_data(t); if ((poll = esp_transport_poll_write(t, timeout_ms)) <= 0) { - ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->tls->sockfd, timeout_ms); + ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->sockfd, timeout_ms); return poll; } int ret = send(ssl->sockfd,(const unsigned char *) buffer, len, 0); @@ -392,7 +398,7 @@ static int ssl_get_socket(esp_transport_handle_t t) { transport_esp_tls_t *ssl = ssl_get_context_data(t); if (ssl && ssl->tls) { - return ssl->tls->sockfd; + return ssl->sockfd; } return -1; } diff --git a/components/tcp_transport/transport_ws.c b/components/tcp_transport/transport_ws.c index 75d984739b..4906a44a64 100644 --- a/components/tcp_transport/transport_ws.c +++ b/components/tcp_transport/transport_ws.c @@ -602,7 +602,7 @@ esp_transport_handle_t esp_transport_ws_init(esp_transport_handle_t parent_handl }); esp_transport_set_func(t, ws_connect, ws_read, ws_write, ws_close, ws_poll_read, ws_poll_write, ws_destroy); - // webocket underlying transfer is the payload transfer handle + // websocket underlying transfer is the payload transfer handle esp_transport_set_parent_transport_func(t, ws_get_payload_transport_handle); esp_transport_set_context_data(t, ws);