mirror of
https://github.com/espressif/esp-mqtt.git
synced 2026-06-05 21:04:46 +00:00
feat: add mqtt conformance test app
Adds a conformance test app based on paho test suite. This introduce the basis infrastructure and initial tests.
This commit is contained in:
@@ -98,6 +98,16 @@ test/apps/publish_connect_test:
|
|||||||
temporary: false
|
temporary: false
|
||||||
reason: Only esp32 target has ethernet runners for integration tests
|
reason: Only esp32 target has ethernet runners for integration tests
|
||||||
|
|
||||||
|
# MQTT conformance integration test
|
||||||
|
test/apps/mqtt_conformance:
|
||||||
|
enable:
|
||||||
|
- if: IDF_TARGET in ["esp32"]
|
||||||
|
reason: Integration test for conformance scenarios
|
||||||
|
disable_test:
|
||||||
|
- if: IDF_TARGET != "esp32"
|
||||||
|
temporary: false
|
||||||
|
reason: Only esp32 target has ethernet runners for integration tests
|
||||||
|
|
||||||
# Host tests (unit tests with mocks)
|
# Host tests (unit tests with mocks)
|
||||||
test/host:
|
test/host:
|
||||||
enable:
|
enable:
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ test/host/managed_components/
|
|||||||
# idf-ci generated files
|
# idf-ci generated files
|
||||||
app_info_*.txt
|
app_info_*.txt
|
||||||
size_info_*.txt
|
size_info_*.txt
|
||||||
|
test_child_pipeline.yml
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,12 @@ Warning: Deprecated: Option '--flash_freq' is deprecated. Use '--flash-freq' ins
|
|||||||
Warning: Deprecated: Command 'sign_data' is deprecated. Use 'sign-data' instead.
|
Warning: Deprecated: Command 'sign_data' is deprecated. Use 'sign-data' instead.
|
||||||
Warning: Deprecated: Command 'extract_public_key' is deprecated. Use 'extract-public-key' instead.
|
Warning: Deprecated: Command 'extract_public_key' is deprecated. Use 'extract-public-key' instead.
|
||||||
CryptographyDeprecationWarning
|
CryptographyDeprecationWarning
|
||||||
|
warning: unknown kconfig symbol 'EXAMPLE_USE_INTERNAL_ETHERNET' assigned to 'y'
|
||||||
warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_IP101' assigned to 'y'
|
warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_IP101' assigned to 'y'
|
||||||
|
warning: unknown kconfig symbol 'EXAMPLE_ETH_MDC_GPIO' assigned
|
||||||
|
warning: unknown kconfig symbol 'EXAMPLE_ETH_MDIO_GPIO' assigned
|
||||||
|
warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_RST_GPIO' assigned
|
||||||
|
warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_ADDR' assigned
|
||||||
WARNING: The following Kconfig variables were used in "if" clauses
|
WARNING: The following Kconfig variables were used in "if" clauses
|
||||||
|
Kconfig variables were used in .* "if" clauses
|
||||||
|
Missing kconfig option
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "test/tools/paho.mqtt.testing"]
|
||||||
|
path = test/tools/paho.mqtt.testing
|
||||||
|
url = https://github.com/eclipse-paho/paho.mqtt.testing.git
|
||||||
@@ -50,3 +50,9 @@ job_template_jinja = """
|
|||||||
git worktree remove -f mqtt || rm -rf mqtt
|
git worktree remove -f mqtt || rm -rf mqtt
|
||||||
"""
|
"""
|
||||||
runs_per_job = 15
|
runs_per_job = 15
|
||||||
|
|
||||||
|
[gitlab.test_pipeline]
|
||||||
|
pre_yaml_jinja = """
|
||||||
|
variables:
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# The following boilerplate must appear in this order.
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
set(EXTRA_COMPONENT_DIRS "../common")
|
||||||
|
idf_build_set_property(MINIMAL_BUILD ON)
|
||||||
|
|
||||||
|
project(mqtt_conformance)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# MQTT conformance app (HIL)
|
||||||
|
|
||||||
|
This app exposes a console API for pytest-embedded HIL tests that target MQTT conformance behavior.
|
||||||
|
|
||||||
|
## Console commands
|
||||||
|
|
||||||
|
- `init`: Create and configure MQTT client
|
||||||
|
- `set_uri <uri>`: Override broker URI before `start`
|
||||||
|
- `start`: Start MQTT client
|
||||||
|
- `stop`: Stop MQTT client
|
||||||
|
- `destroy`: Destroy MQTT client
|
||||||
|
- `subscribe <topic> <qos>`: Subscribe to topic
|
||||||
|
- `publish <topic> <pattern> <pattern_repetitions> <qos> <retain> <enqueue>`: Publish payload
|
||||||
|
|
||||||
|
## Conformance mapping
|
||||||
|
|
||||||
|
Each pytest case should document the MQTT specification section it validates where practical.
|
||||||
|
|
||||||
|
The paho reference suite is integrated as git submodule at:
|
||||||
|
|
||||||
|
`test/tools/paho.mqtt.testing`
|
||||||
|
|
||||||
|
## Running tests locally
|
||||||
|
|
||||||
|
From the repository root (or the mqtt worktree root if using worktrees):
|
||||||
|
|
||||||
|
1. Ensure the environment is active (e.g. `direnv allow` at repo root so IDF and pytest-embedded are available).
|
||||||
|
|
||||||
|
2. Initialize the paho.mqtt.testing submodule:
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive test/tools/paho.mqtt.testing
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the conformance tests (connect a board with Ethernet, or use the same target/port as in CI):
|
||||||
|
```bash
|
||||||
|
pytest test/apps/mqtt_conformance/ -v
|
||||||
|
```
|
||||||
|
To run a single test or filter by keyword, add e.g. `-k test_mqtt_v311` or the test path.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
idf_component_register(SRCS "mqtt_conformance.c" "mqtt_conformance_console.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES mqtt nvs_flash console esp_netif)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
dependencies:
|
||||||
|
protocol_examples_common:
|
||||||
|
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
|
||||||
|
mqtt:
|
||||||
|
version: "*"
|
||||||
|
override_path: "../../../.."
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_random.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "mqtt_client.h"
|
||||||
|
#if CONFIG_MQTT_PROTOCOL_5
|
||||||
|
#include "mqtt5_client.h"
|
||||||
|
#endif
|
||||||
|
#include "mqtt_conformance.h"
|
||||||
|
|
||||||
|
static const char *TAG = "mqtt_conformance";
|
||||||
|
|
||||||
|
#define CLIENT_ID_SIZE 20
|
||||||
|
|
||||||
|
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
|
||||||
|
{
|
||||||
|
esp_mqtt_event_handle_t event = event_data;
|
||||||
|
|
||||||
|
switch (event->event_id) {
|
||||||
|
case MQTT_EVENT_CONNECTED:
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_DISCONNECTED:
|
||||||
|
#if CONFIG_MQTT_PROTOCOL_5
|
||||||
|
if (event->error_handle) {
|
||||||
|
ESP_LOGW(TAG, "DISCONNECT_REASON=%d", event->error_handle->disconnect_return_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_SUBSCRIBED:
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
|
||||||
|
|
||||||
|
if (event->data_len > 0 && event->data) {
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED data_len=%d return_code=0x%02x",
|
||||||
|
event->data_len, (unsigned int)(uint8_t)event->data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_UNSUBSCRIBED:
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
|
||||||
|
|
||||||
|
if (event->data_len > 0 && event->data) {
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED data_len=%d reason_code=0x%02x",
|
||||||
|
event->data_len, (unsigned int)(uint8_t)event->data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_PUBLISHED:
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_DATA:
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_DATA topic=%.*s qos=%d len=%d offset=%d total=%d", event->topic_len, event->topic,
|
||||||
|
event->qos, event->data_len, event->current_data_offset, event->total_data_len);
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_DATA_PAYLOAD %.*s", event->data_len, event->data ? event->data : "");
|
||||||
|
|
||||||
|
if (event->current_data_offset + event->data_len == event->total_data_len) {
|
||||||
|
ESP_LOGI(TAG, "MQTT_EVENT_DATA_COMPLETE msg_id=%d total=%d", event->msg_id, event->total_data_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MQTT_EVENT_ERROR:
|
||||||
|
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
|
||||||
|
|
||||||
|
if (event->error_handle) {
|
||||||
|
ESP_LOGE(TAG, "error_type=%" PRId32 " connect_return_code=%" PRId32,
|
||||||
|
(int32_t)event->error_handle->error_type,
|
||||||
|
(int32_t)event->error_handle->connect_return_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->data_len > 0 && event->data) {
|
||||||
|
ESP_LOGE(TAG, "MQTT_EVENT_ERROR data_len=%d data=%.*s",
|
||||||
|
event->data_len, event->data_len, event->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void conformance_register_event_handlers(command_context_t *ctx)
|
||||||
|
{
|
||||||
|
esp_mqtt_client_register_event(ctx->mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void conformance_unregister_event_handlers(command_context_t *ctx)
|
||||||
|
{
|
||||||
|
esp_mqtt_client_unregister_event(ctx->mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void conformance_set_broker_uri(command_context_t *ctx, const char *uri)
|
||||||
|
{
|
||||||
|
esp_mqtt_client_config_t config = {0};
|
||||||
|
config.broker.address.uri = uri;
|
||||||
|
esp_mqtt_set_config(ctx->mqtt_client, &config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void conformance_configure_client(command_context_t *ctx)
|
||||||
|
{
|
||||||
|
static char client_id[CLIENT_ID_SIZE];
|
||||||
|
snprintf(client_id, sizeof(client_id), "esp-%08" PRIx32, esp_random());
|
||||||
|
esp_mqtt_client_config_t config = {0};
|
||||||
|
config.credentials.client_id = client_id;
|
||||||
|
ESP_LOGI(TAG, "Client configured, client_id=%s (broker URI set via set_uri command)", client_id);
|
||||||
|
esp_mqtt_set_config(ctx->mqtt_client, &config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mqtt_client.h"
|
||||||
|
|
||||||
|
struct arg_int;
|
||||||
|
struct arg_str;
|
||||||
|
struct arg_end;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
esp_mqtt_client_handle_t mqtt_client;
|
||||||
|
} command_context_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct arg_str *uri;
|
||||||
|
struct arg_end *end;
|
||||||
|
} set_uri_args_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct arg_str *topic;
|
||||||
|
struct arg_int *qos;
|
||||||
|
struct arg_end *end;
|
||||||
|
} subscribe_args_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct arg_str *topic;
|
||||||
|
struct arg_str *pattern;
|
||||||
|
struct arg_int *pattern_repetitions;
|
||||||
|
struct arg_int *qos;
|
||||||
|
struct arg_int *retain;
|
||||||
|
struct arg_int *enqueue;
|
||||||
|
struct arg_end *end;
|
||||||
|
} publish_args_t;
|
||||||
|
|
||||||
|
void conformance_register_event_handlers(command_context_t *ctx);
|
||||||
|
void conformance_unregister_event_handlers(command_context_t *ctx);
|
||||||
|
void conformance_configure_client(command_context_t *ctx);
|
||||||
|
void conformance_set_broker_uri(command_context_t *ctx, const char *uri);
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "mqtt_client.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "protocol_examples_common.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "argtable3/argtable3.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "mqtt_conformance.h"
|
||||||
|
|
||||||
|
static const char *TAG = "mqtt_conformance";
|
||||||
|
|
||||||
|
static command_context_t command_context;
|
||||||
|
static set_uri_args_t set_uri_args;
|
||||||
|
static subscribe_args_t subscribe_args;
|
||||||
|
static publish_args_t publish_args;
|
||||||
|
|
||||||
|
#define RETURN_ON_PARSE_ERROR(args) do { \
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **) &(args)); \
|
||||||
|
if (nerrors != 0) { \
|
||||||
|
arg_print_errors(stderr, (args).end, argv[0]); \
|
||||||
|
return 1; \
|
||||||
|
}} while(0)
|
||||||
|
|
||||||
|
static int require_client(void)
|
||||||
|
{
|
||||||
|
if (!command_context.mqtt_client) {
|
||||||
|
ESP_LOGE(TAG, "MQTT client not initialized, call init first");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_init(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (command_context.mqtt_client) {
|
||||||
|
ESP_LOGW(TAG, "MQTT client already initialized");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_mqtt_client_config_t mqtt_cfg = {
|
||||||
|
.broker.address.uri = "mqtt://127.0.0.1:1234",
|
||||||
|
.network.disable_auto_reconnect = true,
|
||||||
|
#if CONFIG_MQTT_PROTOCOL_5
|
||||||
|
.session.protocol_ver = MQTT_PROTOCOL_V_5,
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
command_context.mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
|
||||||
|
|
||||||
|
if (!command_context.mqtt_client) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize client");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
conformance_configure_client(&command_context);
|
||||||
|
conformance_register_event_handlers(&command_context);
|
||||||
|
ESP_LOGI(TAG, "Mqtt client initialized");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_set_uri(int argc, char **argv)
|
||||||
|
{
|
||||||
|
RETURN_ON_PARSE_ERROR(set_uri_args);
|
||||||
|
|
||||||
|
if (require_client() != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
conformance_set_broker_uri(&command_context, set_uri_args.uri->sval[0]);
|
||||||
|
ESP_LOGI(TAG, "Broker URI updated to %s", set_uri_args.uri->sval[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_start(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (require_client() != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_mqtt_client_start(command_context.mqtt_client) != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start mqtt client task");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Mqtt client started");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_stop(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (require_client() != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_mqtt_client_stop(command_context.mqtt_client) != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to stop mqtt client task");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Mqtt client stopped");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_destroy(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (!command_context.mqtt_client) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
conformance_unregister_event_handlers(&command_context);
|
||||||
|
esp_mqtt_client_destroy(command_context.mqtt_client);
|
||||||
|
command_context.mqtt_client = NULL;
|
||||||
|
ESP_LOGI(TAG, "mqtt client for tests destroyed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_subscribe(int argc, char **argv)
|
||||||
|
{
|
||||||
|
RETURN_ON_PARSE_ERROR(subscribe_args);
|
||||||
|
|
||||||
|
if (require_client() != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int msg_id = esp_mqtt_client_subscribe(command_context.mqtt_client, subscribe_args.topic->sval[0],
|
||||||
|
subscribe_args.qos->ival[0]);
|
||||||
|
|
||||||
|
if (msg_id < 0) {
|
||||||
|
ESP_LOGE(TAG, "Subscribe failed, msg_id=%d", msg_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Subscribe requested, msg_id=%d", msg_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_publish(int argc, char **argv)
|
||||||
|
{
|
||||||
|
RETURN_ON_PARSE_ERROR(publish_args);
|
||||||
|
|
||||||
|
if (require_client() != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *pattern = publish_args.pattern->sval[0];
|
||||||
|
int repetitions = publish_args.pattern_repetitions->ival[0];
|
||||||
|
size_t pattern_len = strlen(pattern);
|
||||||
|
size_t payload_len = pattern_len * (size_t)repetitions;
|
||||||
|
char *payload = NULL;
|
||||||
|
|
||||||
|
if (repetitions < 0) {
|
||||||
|
ESP_LOGE(TAG, "Invalid pattern repetitions");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload_len > 0) {
|
||||||
|
payload = malloc(payload_len);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate payload");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < repetitions; i++) {
|
||||||
|
memcpy(payload + (size_t)i * pattern_len, pattern, pattern_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int msg_id;
|
||||||
|
|
||||||
|
if (publish_args.enqueue->ival[0]) {
|
||||||
|
msg_id = esp_mqtt_client_enqueue(command_context.mqtt_client, publish_args.topic->sval[0], payload, payload_len,
|
||||||
|
publish_args.qos->ival[0], publish_args.retain->ival[0], true);
|
||||||
|
} else {
|
||||||
|
msg_id = esp_mqtt_client_publish(command_context.mqtt_client, publish_args.topic->sval[0], payload, payload_len,
|
||||||
|
publish_args.qos->ival[0], publish_args.retain->ival[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(payload);
|
||||||
|
|
||||||
|
if (msg_id < 0) {
|
||||||
|
ESP_LOGE(TAG, "Publish failed, msg_id=%d", msg_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Publish requested, msg_id=%d", msg_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void register_common_commands(void)
|
||||||
|
{
|
||||||
|
const esp_console_cmd_t init = {
|
||||||
|
.command = "init",
|
||||||
|
.help = "Initialize mqtt client",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_init,
|
||||||
|
};
|
||||||
|
const esp_console_cmd_t set_uri = {
|
||||||
|
.command = "set_uri",
|
||||||
|
.help = "Set broker URI",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_set_uri,
|
||||||
|
.argtable = &set_uri_args,
|
||||||
|
};
|
||||||
|
const esp_console_cmd_t start = {
|
||||||
|
.command = "start",
|
||||||
|
.help = "Start mqtt client",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_start,
|
||||||
|
};
|
||||||
|
const esp_console_cmd_t stop = {
|
||||||
|
.command = "stop",
|
||||||
|
.help = "Stop mqtt client",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_stop,
|
||||||
|
};
|
||||||
|
const esp_console_cmd_t destroy = {
|
||||||
|
.command = "destroy",
|
||||||
|
.help = "Destroy mqtt client",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_destroy,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&init));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&set_uri));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&start));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&stop));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&destroy));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void register_pubsub_commands(void)
|
||||||
|
{
|
||||||
|
set_uri_args.uri = arg_str1(NULL, NULL, "<uri>", "Broker URI");
|
||||||
|
set_uri_args.end = arg_end(1);
|
||||||
|
subscribe_args.topic = arg_str1(NULL, NULL, "<topic>", "Subscribe topic");
|
||||||
|
subscribe_args.qos = arg_int1(NULL, NULL, "<qos>", "Subscribe qos");
|
||||||
|
subscribe_args.end = arg_end(1);
|
||||||
|
publish_args.topic = arg_str1(NULL, NULL, "<topic>", "Publish topic");
|
||||||
|
publish_args.pattern = arg_str1(NULL, NULL, "<pattern>", "Payload pattern");
|
||||||
|
publish_args.pattern_repetitions = arg_int1(NULL, NULL, "<pattern repetitions>", "Number of pattern repetitions");
|
||||||
|
publish_args.qos = arg_int1(NULL, NULL, "<qos>", "Publish qos");
|
||||||
|
publish_args.retain = arg_int1(NULL, NULL, "<retain>", "Publish retain flag");
|
||||||
|
publish_args.enqueue = arg_int1(NULL, NULL, "<enqueue>", "0=publish,1=enqueue");
|
||||||
|
publish_args.end = arg_end(1);
|
||||||
|
const esp_console_cmd_t subscribe = {
|
||||||
|
.command = "subscribe",
|
||||||
|
.help = "Subscribe to a topic",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_subscribe,
|
||||||
|
.argtable = &subscribe_args,
|
||||||
|
};
|
||||||
|
const esp_console_cmd_t publish = {
|
||||||
|
.command = "publish",
|
||||||
|
.help = "Publish a message",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &do_publish,
|
||||||
|
.argtable = &publish_args,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&subscribe));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&publish));
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_main(void)
|
||||||
|
{
|
||||||
|
static const size_t max_line = 256;
|
||||||
|
ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
|
||||||
|
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
|
||||||
|
esp_log_level_set("*", ESP_LOG_INFO);
|
||||||
|
esp_log_level_set("wifi", ESP_LOG_ERROR);
|
||||||
|
esp_log_level_set("mqtt_client", ESP_LOG_INFO);
|
||||||
|
esp_log_level_set("outbox", ESP_LOG_INFO);
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
ESP_ERROR_CHECK(example_connect());
|
||||||
|
esp_console_repl_t *repl = NULL;
|
||||||
|
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||||
|
repl_config.prompt = "mqtt>";
|
||||||
|
repl_config.max_cmdline_length = max_line;
|
||||||
|
esp_console_register_help_command();
|
||||||
|
register_pubsub_commands();
|
||||||
|
register_common_commands();
|
||||||
|
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
|
||||||
|
ESP_ERROR_CHECK(esp_console_start_repl(repl));
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Generator, Protocol
|
||||||
|
|
||||||
|
import pexpect
|
||||||
|
import pytest
|
||||||
|
from pytest_embedded import Dut
|
||||||
|
from pytest_embedded_idf.utils import idf_parametrize
|
||||||
|
|
||||||
|
TOPIC_SIZE = 16
|
||||||
|
DUT_READY_TIMEOUT = 30
|
||||||
|
DUT_CONNECT_TIMEOUT = 30
|
||||||
|
DUT_SUBSCRIBE_TIMEOUT = 60
|
||||||
|
DUT_TEST_TIMEOUT = 60
|
||||||
|
PAHO_BROKER_PORT = int(os.getenv("MQTT_CONFORMANCE_PAHO_BROKER_PORT", "18883"))
|
||||||
|
CONNECT_RETRIES = int(os.getenv("MQTT_CONFORMANCE_CONNECT_RETRIES", "3"))
|
||||||
|
RETRY_BACKOFF_SEC = float(os.getenv("MQTT_CONFORMANCE_RETRY_BACKOFF_SEC", "2"))
|
||||||
|
|
||||||
|
PAHO_SPEC_FILE = (
|
||||||
|
Path(__file__).resolve().parents[3]
|
||||||
|
/ "test"
|
||||||
|
/ "tools"
|
||||||
|
/ "paho.mqtt.testing"
|
||||||
|
/ "interoperability"
|
||||||
|
/ "specifications"
|
||||||
|
/ "MQTTV311.py"
|
||||||
|
)
|
||||||
|
PAHO_INTEROP_DIR = PAHO_SPEC_FILE.parent.parent
|
||||||
|
|
||||||
|
# Add paho interoperability directory so we can import the broker at fixture time.
|
||||||
|
if str(PAHO_INTEROP_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PAHO_INTEROP_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
def build_topic() -> str:
|
||||||
|
suffix = "".join(random.choice(string.ascii_letters) for _ in range(TOPIC_SIZE))
|
||||||
|
return f"test/conformance/{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def require_paho_testing_checked_out() -> None:
|
||||||
|
"""Hard requirement: fail the test if the paho.mqtt.testing submodule is not available."""
|
||||||
|
if not PAHO_SPEC_FILE.exists():
|
||||||
|
pytest.fail(
|
||||||
|
"paho.mqtt.testing submodule is not available (required for mqtt conformance tests). "
|
||||||
|
"Run: git submodule update --init --recursive test/tools/paho.mqtt.testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_host_ip4_by_dest_ip(dest_ip: str = "8.8.8.8") -> str:
|
||||||
|
"""Return the primary host IPv4 used to reach dest_ip (e.g. for DUT to reach host broker)."""
|
||||||
|
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as sock:
|
||||||
|
sock.connect((dest_ip, 80))
|
||||||
|
return sock.getsockname()[0]
|
||||||
|
|
||||||
|
|
||||||
|
class _BrokerHandle(Protocol):
|
||||||
|
uri: str
|
||||||
|
|
||||||
|
def shutdown(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _start_paho_broker(port: int, host_ip: str) -> _BrokerHandle:
|
||||||
|
"""Start paho V311+V5 broker in-process; return object with .uri and .shutdown().
|
||||||
|
Imports deferred so idf-ci collection-time mocking does not replace paho.
|
||||||
|
"""
|
||||||
|
from mqtt.brokers.V311 import MQTTBrokers as MQTTV3Brokers
|
||||||
|
from mqtt.brokers.V5 import MQTTBrokers as MQTTV5Brokers
|
||||||
|
from mqtt.brokers.listeners import TCPListeners
|
||||||
|
|
||||||
|
lock = threading.RLock()
|
||||||
|
shared_data: dict = {}
|
||||||
|
options = {
|
||||||
|
"visual": False,
|
||||||
|
"persistence": False,
|
||||||
|
"overlapping_single": True,
|
||||||
|
"dropQoS0": True,
|
||||||
|
"zero_length_clientids": True,
|
||||||
|
"publish_on_pubrel": False,
|
||||||
|
"topicAliasMaximum": 2,
|
||||||
|
"maximumPacketSize": 16384,
|
||||||
|
"receiveMaximum": 2,
|
||||||
|
"serverKeepAlive": 60,
|
||||||
|
"maximum_qos": 2,
|
||||||
|
"retain_available": True,
|
||||||
|
"subscription_identifier_available": True,
|
||||||
|
"shared_subscription_available": True,
|
||||||
|
"server_keep_alive": None,
|
||||||
|
}
|
||||||
|
broker3 = MQTTV3Brokers(options=options.copy(), lock=lock, sharedData=shared_data)
|
||||||
|
broker5 = MQTTV5Brokers(options=options.copy(), lock=lock, sharedData=shared_data)
|
||||||
|
broker3.setBroker5(broker5)
|
||||||
|
broker5.setBroker3(broker3)
|
||||||
|
TCPListeners.setBrokers(broker3, broker5)
|
||||||
|
server = TCPListeners.create(port=port, host="", serve_forever=False)
|
||||||
|
|
||||||
|
class _Broker:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.uri = f"mqtt://{host_ip}:{port}"
|
||||||
|
self._broker3 = broker3
|
||||||
|
self._broker5 = broker5
|
||||||
|
self._server = server
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
self._broker3.shutdown()
|
||||||
|
self._broker5.shutdown()
|
||||||
|
if self._server:
|
||||||
|
self._server.shutdown()
|
||||||
|
|
||||||
|
return _Broker()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def broker() -> Generator[_BrokerHandle, None, None]:
|
||||||
|
"""Start paho MQTT broker in-process for the smoke test. No subclass, just V311+V5 + TCP listener."""
|
||||||
|
require_paho_testing_checked_out()
|
||||||
|
host_ip = os.getenv("MQTT_CONFORMANCE_HOST_IP", "").strip() or get_host_ip4_by_dest_ip()
|
||||||
|
b = _start_paho_broker(port=PAHO_BROKER_PORT, host_ip=host_ip)
|
||||||
|
yield b
|
||||||
|
b.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def broker_uri(broker: _BrokerHandle) -> str:
|
||||||
|
return broker.uri
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mqtt_client(dut: Dut, broker_uri: str):
|
||||||
|
require_paho_testing_checked_out()
|
||||||
|
dut.expect(re.compile(rb"mqtt>"), timeout=DUT_READY_TIMEOUT)
|
||||||
|
dut.write("init")
|
||||||
|
dut.write(f"set_uri {broker_uri}")
|
||||||
|
yield dut
|
||||||
|
dut.write("destroy")
|
||||||
|
|
||||||
|
|
||||||
|
def start_client(dut: Dut) -> None:
|
||||||
|
for attempt in range(1, CONNECT_RETRIES + 1):
|
||||||
|
dut.write("start")
|
||||||
|
try:
|
||||||
|
dut.expect(re.compile(rb"MQTT_EVENT_CONNECTED"), timeout=DUT_CONNECT_TIMEOUT)
|
||||||
|
return
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
dut.write("stop")
|
||||||
|
if attempt == CONNECT_RETRIES:
|
||||||
|
raise
|
||||||
|
time.sleep(RETRY_BACKOFF_SEC)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_client(dut: Dut) -> None:
|
||||||
|
dut.write("stop")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.eth_ip101
|
||||||
|
@idf_parametrize("target", ["esp32"], indirect=["target"])
|
||||||
|
def test_mqtt_v311_subscribe_and_qos1_publish__sec_3_8_4_and_4_3(mqtt_client: Dut) -> None:
|
||||||
|
"""
|
||||||
|
MQTT v3.1.1 conformance smoke case:
|
||||||
|
- section 3.8.4: SUBSCRIBE/SUBACK interaction
|
||||||
|
- section 4.3: QoS 1 publish flow (at least once semantics)
|
||||||
|
|
||||||
|
Reference suite integrated from:
|
||||||
|
test/tools/paho.mqtt.testing/interoperability/specifications/MQTTV311.py
|
||||||
|
"""
|
||||||
|
topic = build_topic()
|
||||||
|
|
||||||
|
start_client(mqtt_client)
|
||||||
|
mqtt_client.write(f"subscribe {topic} 1")
|
||||||
|
mqtt_client.expect(re.compile(rb"MQTT_EVENT_SUBSCRIBED"), timeout=DUT_SUBSCRIBE_TIMEOUT)
|
||||||
|
|
||||||
|
mqtt_client.write(f"publish {topic} qos1 4 1 0 1")
|
||||||
|
# DUT may emit DATA_COMPLETE (incoming) before PUBLISHED (outgoing ack); accept either order.
|
||||||
|
mqtt_client.expect(
|
||||||
|
[re.compile(rb"MQTT_EVENT_PUBLISHED"), re.compile(rb"MQTT_EVENT_DATA_COMPLETE")],
|
||||||
|
timeout=DUT_TEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
mqtt_client.expect(
|
||||||
|
[re.compile(rb"MQTT_EVENT_PUBLISHED"), re.compile(rb"MQTT_EVENT_DATA_COMPLETE")],
|
||||||
|
timeout=DUT_TEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
stop_client(mqtt_client)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CONFIG_MQTT_PROTOCOL_5=y
|
||||||
|
CONFIG_EXAMPLE_CONNECT_ETHERNET=y
|
||||||
|
CONFIG_EXAMPLE_CONNECT_WIFI=n
|
||||||
|
CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS=y
|
||||||
|
# CONFIG_EXAMPLE_* names work on IDF 5.x; IDF 6.x maps them via protocol_examples_common sdkconfig.rename.
|
||||||
|
CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y
|
||||||
|
CONFIG_EXAMPLE_ETH_PHY_IP101=y
|
||||||
|
CONFIG_EXAMPLE_ETH_MDC_GPIO=23
|
||||||
|
CONFIG_EXAMPLE_ETH_MDIO_GPIO=18
|
||||||
|
CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5
|
||||||
|
CONFIG_EXAMPLE_ETH_PHY_ADDR=1
|
||||||
|
CONFIG_EXAMPLE_CONNECT_IPV6=y
|
||||||
|
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
||||||
Submodule
+1
Submodule test/tools/paho.mqtt.testing added at 9d7bb80bb8
Reference in New Issue
Block a user