test(esp_http_client): extend mock transport with async, EAGAIN injection, response queue

This commit is contained in:
Ashish Sharma
2026-09-05 16:46:49 +08:00
parent c57e06b6a9
commit a30250930b
2 changed files with 171 additions and 0 deletions
@@ -22,10 +22,20 @@ typedef struct {
size_t read_offset; /*!< Current position in response data */
size_t bytes_processed; /*!< Bytes processed (for error injection) */
char *response_buffer; /*!< Internal copy of response data */
int async_polls_left; /*!< Remaining "in progress" returns from mock_connect_async() */
int wb_reads_left; /*!< Remaining EAGAIN injections for mock_read() */
int wb_writes_left; /*!< Remaining EAGAIN injections for mock_write() */
const char *resp_queue[8]; /*!< FIFO of queued response pointers (caller-owned) */
size_t resp_queue_len[8]; /*!< Lengths matching resp_queue entries */
int resp_queue_count; /*!< Number of entries queued */
int resp_queue_next; /*!< Index of the next entry to pop */
char req_capture[2048]; /*!< Bytes written since the last request boundary */
size_t req_capture_len; /*!< Number of valid bytes in req_capture */
} mock_http_transport_ctx_t;
// Forward declarations of transport function implementations
static int mock_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms);
static int mock_connect_async(esp_transport_handle_t t, const char *host, int port, int timeout_ms);
static int mock_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms);
static int mock_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms);
static int mock_close(esp_transport_handle_t t);
@@ -99,6 +109,45 @@ static int mock_connect(esp_transport_handle_t t, const char *host, int port, in
return 0;
}
/**
* @brief Mock async connect implementation
*
* Simulates a non-blocking connect(): returns "in progress" for
* `async_connect_polls` calls, then reports success. Honors
* MOCK_TRANSPORT_MODE_CONNECT_FAIL to simulate an async connect failure.
*/
static int mock_connect_async(esp_transport_handle_t t, const char *host, int port, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx) {
ESP_LOGE(TAG, "Invalid transport context");
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.connect_calls++;
}
if (ctx->config.mode == MOCK_TRANSPORT_MODE_CONNECT_FAIL) {
ESP_LOGD(TAG, "Mock connect_async failed (simulated)");
return -1; /* ASYNC_TRANS_CONNECT_FAIL */
}
if (ctx->async_polls_left > 0) {
ctx->async_polls_left--;
ESP_LOGD(TAG, "Mock connect_async: still connecting (%d polls left)", ctx->async_polls_left);
return 0; /* ASYNC_TRANS_CONNECTING */
}
ctx->is_connected = true;
ctx->read_offset = 0;
ctx->bytes_processed = 0;
ESP_LOGI(TAG, "Mock connect_async succeeded");
return 1; /* ASYNC_TRANS_CONNECT_PASS */
}
/**
* @brief Mock read implementation
*/
@@ -110,6 +159,14 @@ static int mock_read(esp_transport_handle_t t, char *buffer, int len, int timeou
return -1;
}
// Would-block injection: simulate a non-blocking socket returning EAGAIN
if (ctx->wb_reads_left > 0) {
ctx->wb_reads_left--;
ESP_LOGD(TAG, "Mock read: EAGAIN (simulated would-block, %d left)", ctx->wb_reads_left);
errno = EAGAIN;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.read_calls++;
}
@@ -190,6 +247,14 @@ static int mock_write(esp_transport_handle_t t, const char *buffer, int len, int
return -1;
}
// Would-block injection: simulate a non-blocking socket returning EAGAIN
if (ctx->wb_writes_left > 0) {
ctx->wb_writes_left--;
ESP_LOGD(TAG, "Mock write: EAGAIN (simulated would-block, %d left)", ctx->wb_writes_left);
errno = EAGAIN;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.write_calls++;
}
@@ -229,6 +294,24 @@ static int mock_write(esp_transport_handle_t t, const char *buffer, int len, int
}
}
// Request boundary: previous response fully consumed and a new request starts.
// Pop the next queued response into the active buffer via set_response()
// (which owns free/realloc + read_offset reset) and start a fresh capture.
if (ctx->resp_queue_next < ctx->resp_queue_count &&
ctx->response_buffer && ctx->read_offset >= ctx->config.response_len) {
mock_http_transport_set_response(t, ctx->resp_queue[ctx->resp_queue_next],
ctx->resp_queue_len[ctx->resp_queue_next]);
ctx->resp_queue_next++;
ctx->req_capture_len = 0;
}
// Capture written bytes (request content) for test assertions, capped to
// avoid overflowing the fixed-size buffer.
size_t space = sizeof(ctx->req_capture) - ctx->req_capture_len;
size_t copy = (size_t)len < space ? (size_t)len : space;
memcpy(ctx->req_capture + ctx->req_capture_len, buffer, copy);
ctx->req_capture_len += copy;
// Normal write (just track it, don't actually store)
ctx->bytes_processed += len;
@@ -388,6 +471,11 @@ esp_transport_handle_t mock_http_transport_create(const mock_http_transport_conf
ctx->config.track_calls = true;
}
// Initialize error-injection countdown counters from config
ctx->async_polls_left = ctx->config.async_connect_polls;
ctx->wb_reads_left = ctx->config.would_block_reads;
ctx->wb_writes_left = ctx->config.would_block_writes;
// Set context
esp_transport_set_context_data(transport, ctx);
@@ -400,6 +488,7 @@ esp_transport_handle_t mock_http_transport_create(const mock_http_transport_conf
mock_poll_read,
mock_poll_write,
mock_destroy);
esp_transport_set_async_connect_func(transport, mock_connect_async);
ESP_LOGI(TAG, "Mock HTTP transport created (mode=%d)", ctx->config.mode);
return transport;
@@ -452,6 +541,11 @@ esp_err_t mock_http_transport_set_config(esp_transport_handle_t transport,
ctx->read_offset = 0;
ctx->bytes_processed = 0;
// Re-initialize error-injection countdown counters from the new config
ctx->async_polls_left = ctx->config.async_connect_polls;
ctx->wb_reads_left = ctx->config.would_block_reads;
ctx->wb_writes_left = ctx->config.would_block_writes;
ESP_LOGD(TAG, "Mock transport config updated (mode=%d)", ctx->config.mode);
return ESP_OK;
}
@@ -527,3 +621,35 @@ esp_err_t mock_http_transport_set_response(esp_transport_handle_t transport,
ESP_LOGD(TAG, "Mock transport response updated (%zu bytes)", ctx->config.response_len);
return ESP_OK;
}
esp_err_t mock_http_transport_queue_response(esp_transport_handle_t t,
const char *data, size_t len)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx || ctx->resp_queue_count >= 8 || !data) {
return ESP_ERR_INVALID_ARG;
}
// Queue entries are caller-owned (e.g. string literals); store the pointer only.
ctx->resp_queue[ctx->resp_queue_count] = data;
ctx->resp_queue_len[ctx->resp_queue_count] = len > 0 ? len : strlen(data);
ctx->resp_queue_count++;
return ESP_OK;
}
esp_err_t mock_http_transport_get_last_request(esp_transport_handle_t t,
char *buf, size_t buf_len, size_t *out_len)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx || !buf || buf_len == 0) {
return ESP_ERR_INVALID_ARG;
}
size_t n = ctx->req_capture_len < buf_len - 1 ? ctx->req_capture_len : buf_len - 1;
memcpy(buf, ctx->req_capture, n);
buf[n] = '\0';
if (out_len) {
*out_len = n;
}
return ESP_OK;
}
@@ -35,6 +35,9 @@ typedef struct {
int bytes_before_error; /*!< Number of bytes to process before injecting error (-1 = no limit) */
int connect_delay_ms; /*!< Delay before connect succeeds (0 = immediate) */
bool track_calls; /*!< Enable call tracking for verification */
int async_connect_polls; /*!< connect_async returns "in progress" this many times, then succeeds */
int would_block_reads; /*!< First N reads return -1 with errno = EAGAIN */
int would_block_writes; /*!< First N writes return -1 with errno = EAGAIN */
} mock_http_transport_config_t;
/**
@@ -47,6 +50,9 @@ typedef struct {
.bytes_before_error = -1, \
.connect_delay_ms = 0, \
.track_calls = true, \
.async_connect_polls = 0, \
.would_block_reads = 0, \
.would_block_writes = 0, \
}
/**
@@ -153,6 +159,45 @@ esp_err_t mock_http_transport_set_response(esp_transport_handle_t transport,
const char *response_data,
size_t response_len);
/**
* @brief Queue a response to be served after the next request boundary
*
* Appends to a FIFO of up to 8 responses. When mock_write() detects a request
* boundary (the previously active response has been fully read and a new
* write arrives), the next queued response is popped into the active buffer
* via mock_http_transport_set_response(). Useful for testing keep-alive
* request/response sequences on a single connection.
*
* @param[in] t Mock transport handle
* @param[in] data Response data to queue (caller-owned, must outlive its use - e.g. a string literal)
* @param[in] len Length of data (0 = use strlen)
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if t is NULL, data is NULL, or the queue is full (8 entries)
*/
esp_err_t mock_http_transport_queue_response(esp_transport_handle_t t,
const char *data, size_t len);
/**
* @brief Retrieve the bytes captured from the most recent request
*
* mock_write() appends every written byte (capped at 2048 bytes) into an
* internal capture buffer, reset at each request boundary. This lets tests
* assert on the serialized request content (headers, body).
*
* @param[in] t Mock transport handle
* @param[out] buf Buffer to receive the captured request bytes, NUL-terminated
* @param[in] buf_len Size of buf (including space for the NUL terminator)
* @param[out] out_len Optional, number of bytes copied (excluding NUL terminator)
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if t is NULL, buf is NULL, or buf_len is 0
*/
esp_err_t mock_http_transport_get_last_request(esp_transport_handle_t t,
char *buf, size_t buf_len, size_t *out_len);
#ifdef __cplusplus
}
#endif