From 13fce51d27d49e7ff7769c95e7aa4bc2fd4c9998 Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Thu, 27 Aug 2026 13:56:29 +0800 Subject: [PATCH] test(esp_http_client): keep-alive reuse and state sequence characterization Pin transport reuse across perform() calls, the Connection: close reconnect, and the exact get_state() sequence seen from the event handler (the getter lags its triggering events). --- .../main/test_http_client_lifecycle.c | 157 ++++++++++++++++++ .../test_apps/pytest_stage0_qemu.py | 2 +- 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 components/esp_http_client/test_apps/main/test_http_client_lifecycle.c diff --git a/components/esp_http_client/test_apps/main/test_http_client_lifecycle.c b/components/esp_http_client/test_apps/main/test_http_client_lifecycle.c new file mode 100644 index 00000000000..6d1da3ee559 --- /dev/null +++ b/components/esp_http_client/test_apps/main/test_http_client_lifecycle.c @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file test_http_client_lifecycle.c + * @brief P0 Critical Tests: keep-alive connection reuse and get_state sequence + * + * This file characterizes: + * - Connection reuse across two esp_http_client_perform() calls when the + * response carries "Connection: keep-alive" + * - Forced reconnect on the next perform() when the response carries + * "Connection: close" + * - The exact esp_http_client_get_state() sequence observed from inside the + * event handler during a simple GET, pinned as a regression baseline for + * the state-machine refactor. + */ + +#include +#include "esp_http_client.h" +#include "unity.h" +#include "sdkconfig.h" +#include "test_http_client_mock_transport.h" + +/* + * Every case in this file drives the client through a mock transport injected + * via esp_http_client_config_t::transport. Without custom transport support + * the clients would fall back to a real transport aimed at test-server.local, + * which does not exist, so the whole file compiles out. + */ +#if CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT + +static const char *resp_ka = + "HTTP/1.1 200 OK\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 2\r\n" + "\r\n" + "ok"; + +static const char *resp_close = + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Length: 2\r\n" + "\r\n" + "ok"; + +TEST_CASE("keep-alive reuses connection across two performs", "[esp_http_client][lifecycle][p0]") +{ + mock_http_transport_config_t mc = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG(); + mc.response_data = resp_ka; + esp_transport_handle_t mock = mock_http_transport_create(&mc); + TEST_ASSERT_NOT_NULL(mock); + mock_http_transport_queue_response(mock, resp_ka, 0); + + esp_http_client_config_t cfg = { + .url = "http://test-server.local/a", + .transport = mock, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + TEST_ASSERT_NOT_NULL(client); + + TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client)); + /* characterization: parked state between requests is CONNECTED */ + TEST_ASSERT_EQUAL(HTTP_STATE_CONNECTED, esp_http_client_get_state(client)); + + TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client)); + + mock_http_transport_stats_t stats; + mock_http_transport_get_stats(mock, &stats); + TEST_ASSERT_EQUAL(1, stats.connect_calls); /* the whole point of keep-alive */ + + esp_http_client_cleanup(client); + mock_http_transport_destroy(mock); +} + +TEST_CASE("Connection: close forces reconnect on next perform", "[esp_http_client][lifecycle][p0]") +{ + mock_http_transport_config_t mc = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG(); + mc.response_data = resp_close; + esp_transport_handle_t mock = mock_http_transport_create(&mc); + TEST_ASSERT_NOT_NULL(mock); + mock_http_transport_queue_response(mock, resp_close, 0); + + esp_http_client_config_t cfg = { + .url = "http://test-server.local/a", + .transport = mock, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + TEST_ASSERT_NOT_NULL(client); + + TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client)); + TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client)); + + mock_http_transport_stats_t stats; + mock_http_transport_get_stats(mock, &stats); + TEST_ASSERT_EQUAL(2, stats.connect_calls); + + esp_http_client_cleanup(client); + mock_http_transport_destroy(mock); +} + +/* state sequence observed via events — pins the getter mapping for the refactor */ +static esp_http_state_t s_state_log[16]; +static int s_state_log_n; + +static esp_err_t state_logger(esp_http_client_event_t *evt) +{ + if (s_state_log_n < 16) { + s_state_log[s_state_log_n++] = esp_http_client_get_state(evt->client); + } + return ESP_OK; +} + +TEST_CASE("get_state sequence during a simple GET", "[esp_http_client][lifecycle][p0]") +{ + mock_http_transport_config_t mc = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG(); + mc.response_data = resp_ka; + esp_transport_handle_t mock = mock_http_transport_create(&mc); + TEST_ASSERT_NOT_NULL(mock); + + s_state_log_n = 0; + esp_http_client_config_t cfg = { + .url = "http://test-server.local/a", + .event_handler = state_logger, + .transport = mock, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + TEST_ASSERT_NOT_NULL(client); + TEST_ASSERT_EQUAL(HTTP_STATE_INIT, esp_http_client_get_state(client)); + + TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client)); + + /* characterization: master behavior, see refactor spec + * exact sequence observed by the event handler, frozen as a regression + * baseline for the state-machine refactor. get_state() lags the event + * that triggered it in several places: it still reports + * REQ_COMPLETE_DATA at ON_STATUS_CODE and both ON_HEADER callbacks, and + * still reports RES_COMPLETE_HEADER at ON_HEADERS_COMPLETE and ON_DATA; + * it only advances to RES_ON_DATA_START at ON_FINISH, after all + * response data has already been delivered to the handler. */ + TEST_ASSERT_EQUAL(8, s_state_log_n); + TEST_ASSERT_EQUAL(HTTP_STATE_CONNECTED, s_state_log[0]); /* at ON_CONNECTED */ + TEST_ASSERT_EQUAL(HTTP_STATE_REQ_COMPLETE_HEADER, s_state_log[1]); /* at HEADERS_SENT */ + TEST_ASSERT_EQUAL(HTTP_STATE_REQ_COMPLETE_DATA, s_state_log[2]); /* at ON_STATUS_CODE */ + TEST_ASSERT_EQUAL(HTTP_STATE_REQ_COMPLETE_DATA, s_state_log[3]); /* at ON_HEADER (Connection) */ + TEST_ASSERT_EQUAL(HTTP_STATE_REQ_COMPLETE_DATA, s_state_log[4]); /* at ON_HEADER (Content-Length) */ + TEST_ASSERT_EQUAL(HTTP_STATE_RES_COMPLETE_HEADER, s_state_log[5]); /* at ON_HEADERS_COMPLETE */ + TEST_ASSERT_EQUAL(HTTP_STATE_RES_COMPLETE_HEADER, s_state_log[6]); /* at ON_DATA */ + TEST_ASSERT_EQUAL(HTTP_STATE_RES_ON_DATA_START, s_state_log[7]); /* at ON_FINISH */ + + esp_http_client_cleanup(client); + mock_http_transport_destroy(mock); +} + +#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT diff --git a/components/esp_http_client/test_apps/pytest_stage0_qemu.py b/components/esp_http_client/test_apps/pytest_stage0_qemu.py index a133c47c0ed..4f4179f5b19 100644 --- a/components/esp_http_client/test_apps/pytest_stage0_qemu.py +++ b/components/esp_http_client/test_apps/pytest_stage0_qemu.py @@ -8,4 +8,4 @@ from pytest_embedded_idf.utils import idf_parametrize @pytest.mark.qemu @idf_parametrize('target', ['esp32c3'], indirect=['target']) def test_http_client_mock(dut: Dut) -> None: - dut.run_all_single_board_cases(group=['basic', 'async'], timeout=120) + dut.run_all_single_board_cases(group=['basic', 'async', 'lifecycle'], timeout=120)