From 50d059af071e0538d1a8b1a95d594e122a24a604 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 19 Jul 2024 14:18:48 +0200 Subject: [PATCH] feat(mdns): Add linux console functional tests --- .github/workflows/mdns__host-tests.yml | 23 ++-- .../linux_compat/esp_ringbuf/CMakeLists.txt | 1 + .../linux_compat/freertos/freertos_linux.c | 102 ++++++++++---- .../freertos/include/freertos/list.h | 6 + .../freertos/include/freertos/task.h | 3 + components/mdns/CMakeLists.txt | 5 +- components/mdns/mdns.c | 9 +- components/mdns/mdns_console.c | 7 +- .../mdns/tests/host_test/CMakeLists.txt | 11 +- .../components/esp_netif_linux/CMakeLists.txt | 10 +- .../esp_netif_linux/esp_netif_linux.c | 13 +- components/mdns/tests/host_test/dnsfixture.py | 128 ++++++++++++++++++ .../mdns/tests/host_test/main/CMakeLists.txt | 2 +- .../tests/host_test/main/Kconfig.projbuild | 6 + .../tests/host_test/main/idf_component.yml | 7 + components/mdns/tests/host_test/main/main.c | 98 +++++++++++--- .../mdns/tests/host_test/pytest_mdns.py | 72 ++++++++++ .../mdns/tests/host_test/sdkconfig.ci.app | 4 + .../mdns/tests/host_test/sdkconfig.ci.console | 4 + .../mdns/tests/host_test/sdkconfig.ci.target | 1 + .../mdns/tests/host_test/sdkconfig.defaults | 7 +- 21 files changed, 444 insertions(+), 75 deletions(-) create mode 100644 common_components/linux_compat/esp_ringbuf/CMakeLists.txt create mode 100644 common_components/linux_compat/freertos/include/freertos/list.h create mode 100644 components/mdns/tests/host_test/dnsfixture.py create mode 100644 components/mdns/tests/host_test/main/idf_component.yml create mode 100644 components/mdns/tests/host_test/pytest_mdns.py create mode 100644 components/mdns/tests/host_test/sdkconfig.ci.app create mode 100644 components/mdns/tests/host_test/sdkconfig.ci.console create mode 100644 components/mdns/tests/host_test/sdkconfig.ci.target diff --git a/.github/workflows/mdns__host-tests.yml b/.github/workflows/mdns__host-tests.yml index 81bb3ef6887..5ef6c8adee8 100644 --- a/.github/workflows/mdns__host-tests.yml +++ b/.github/workflows/mdns__host-tests.yml @@ -10,27 +10,30 @@ on: jobs: host_test_mdns: if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' - name: Host test + name: Host test build runs-on: ubuntu-22.04 - container: espressif/idf:release-v5.1 + container: espressif/idf:release-v5.3 steps: - name: Checkout esp-protocols uses: actions/checkout@v4 with: - path: esp-protocols + path: protocols - name: Build and Test shell: bash run: | - apt-get update && apt-get install -y dnsutils gcc g++ . ${IDF_PATH}/export.sh - cd $GITHUB_WORKSPACE/esp-protocols/components/mdns/tests/host_test - idf.py build - ./build/mdns_host.elf & - dig +short -p 5353 @224.0.0.251 myesp.local > ip.txt - cat ip.txt | xargs dig +short -p 5353 @224.0.0.251 -x - cat ip.txt + python -m pip install idf-build-apps dnspython pytest pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf + cd $GITHUB_WORKSPACE/protocols + # Build host tests app (with all configs and targets supported) + python ./ci/build_apps.py components/mdns/tests/host_test/ + cd components/mdns/tests/host_test + # First run the linux_app and send a quick A query and a reverse query + ./build_linux_app/mdns_host.elf & + python dnsfixture.py A myesp.local --ip_only | xargs python dnsfixture.py X + # Next we run the pytest (using the console app) + pytest build_afl_host_test_mdns: if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' diff --git a/common_components/linux_compat/esp_ringbuf/CMakeLists.txt b/common_components/linux_compat/esp_ringbuf/CMakeLists.txt new file mode 100644 index 00000000000..a96dced59ee --- /dev/null +++ b/common_components/linux_compat/esp_ringbuf/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register() diff --git a/common_components/linux_compat/freertos/freertos_linux.c b/common_components/linux_compat/freertos/freertos_linux.c index 2778057600c..37b66e02a36 100644 --- a/common_components/linux_compat/freertos/freertos_linux.c +++ b/common_components/linux_compat/freertos/freertos_linux.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -12,8 +12,24 @@ #include #include #include "osal/osal_api.h" +#include + +typedef struct task_notifiers { + sem_t sem; + TaskHandle_t id; +} task_notifiers_t; + +typedef struct pthread_params { + void *const param; + TaskFunction_t task; + bool started; + TaskHandle_t handle; +} pthread_params_t; static uint64_t s_semaphore_data = 0; +static task_notifiers_t *s_notifiers; +static int s_threads = 0; +pthread_mutex_t s_mutex; typedef enum queue_type_tag { MUTEX_REC, @@ -89,6 +105,7 @@ BaseType_t xSemaphoreGiveRecursive( QueueHandle_t xQueue) } return pdFALSE; } + BaseType_t xSemaphoreTake( QueueHandle_t xQueue, TickType_t pvTask ) { struct generic_queue_handle *h = xQueue; @@ -99,7 +116,6 @@ BaseType_t xSemaphoreTake( QueueHandle_t xQueue, TickType_t pvTask ) return xQueueReceive(xQueue, &s_semaphore_data, portMAX_DELAY); } - BaseType_t xSemaphoreTakeRecursive( QueueHandle_t xQueue, TickType_t pvTask ) { struct generic_queue_handle *h = xQueue; @@ -110,9 +126,6 @@ BaseType_t xSemaphoreTakeRecursive( QueueHandle_t xQueue, TickType_t pvTask ) return pdFALSE; } - - - void vQueueDelete( QueueHandle_t xQueue ) { struct generic_queue_handle *h = xQueue; @@ -128,8 +141,7 @@ void vQueueDelete( QueueHandle_t xQueue ) QueueHandle_t xSemaphoreCreateBinary(void) { - QueueHandle_t sempaphore = xQueueCreate(1, 1); - return sempaphore; + return xQueueCreate(1, 1); } QueueHandle_t xSemaphoreCreateMutex(void) @@ -145,6 +157,13 @@ QueueHandle_t xSemaphoreCreateRecursiveMutex(void) void vTaskDelete(TaskHandle_t *task) { + for (int i = 0; i < s_threads; ++i) { + if (task == s_notifiers[i].id) { + sem_destroy(&s_notifiers[i].sem); + s_notifiers[i].id = 0; + } + } + if (task == NULL) { pthread_exit(0); } @@ -171,14 +190,21 @@ void vTaskDelay( const TickType_t xTicksToDelay ) void *pthread_task(void *params) { - struct { - void *const param; - TaskFunction_t task; - bool started; - } *pthread_params = params; + pthread_params_t *pthread_params = params; void *const param = pthread_params->param; TaskFunction_t task = pthread_params->task; + + pthread_params->handle = xTaskGetCurrentTaskHandle(); + if (s_threads == 0) { + pthread_mutex_init(&s_mutex, NULL); + } + pthread_mutex_lock(&s_mutex); + s_notifiers = realloc(s_notifiers, sizeof(struct task_notifiers) * (++s_threads)); + assert(s_notifiers); + s_notifiers[s_threads - 1].id = pthread_params->handle; + sem_init(&s_notifiers[s_threads - 1].sem, 0, 0); + pthread_mutex_unlock(&s_mutex); pthread_params->started = true; task(param); @@ -198,16 +224,12 @@ BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pvTaskCode, return pdTRUE; } - BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pvCreatedTask) { pthread_t new_thread = (pthread_t)NULL; pthread_attr_t attr; - struct { - void *const param; - TaskFunction_t task; - bool started; - } pthread_params = { .param = pvParameters, .task = pvTaskCode}; + pthread_params_t pthread_params = { .param = pvParameters, .task = pvTaskCode}; + int res = pthread_attr_init(&attr); assert(res == 0); res = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); @@ -215,20 +237,33 @@ BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, const char *const pcName, cons res = pthread_create(&new_thread, &attr, pthread_task, &pthread_params); assert(res == 0); - if (pvCreatedTask) { - *pvCreatedTask = (void *)new_thread; - } - // just wait till the task started so we can unwind params from the stack while (pthread_params.started == false) { usleep(1000); } + if (pvCreatedTask) { + *pvCreatedTask = pthread_params.handle; + } + return pdTRUE; } void xTaskNotifyGive(TaskHandle_t task) { - + int i = 0; + while (true) { + pthread_mutex_lock(&s_mutex); + if (task == s_notifiers[i].id) { + sem_post(&s_notifiers[i].sem); + pthread_mutex_unlock(&s_mutex); + return; + } + pthread_mutex_unlock(&s_mutex); + if (++i == s_threads) { + i = 0; + } + usleep(1000); + } } BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time ) @@ -238,7 +273,7 @@ BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, TaskHandle_t xTaskGetCurrentTaskHandle(void) { - return NULL; + return (TaskHandle_t)pthread_self(); } EventGroupHandle_t xEventGroupCreate( void ) @@ -270,3 +305,22 @@ EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits { return osal_signal_wait(xEventGroup, uxBitsToWaitFor, xWaitForAllBits, xTicksToWait); } + +void ulTaskNotifyTake(bool clear_on_exit, uint32_t xTicksToWait) +{ + TaskHandle_t task = xTaskGetCurrentTaskHandle(); + int i = 0; + while (true) { + pthread_mutex_lock(&s_mutex); + if (task == s_notifiers[i].id) { + pthread_mutex_unlock(&s_mutex); + sem_wait(&s_notifiers[i].sem); + return; + } + pthread_mutex_unlock(&s_mutex); + if (++i == s_threads) { + i = 0; + } + usleep(1000); + } +} diff --git a/common_components/linux_compat/freertos/include/freertos/list.h b/common_components/linux_compat/freertos/include/freertos/list.h new file mode 100644 index 00000000000..9ce2387f3e1 --- /dev/null +++ b/common_components/linux_compat/freertos/include/freertos/list.h @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once diff --git a/common_components/linux_compat/freertos/include/freertos/task.h b/common_components/linux_compat/freertos/include/freertos/task.h index 5fb518466b1..235f4676133 100644 --- a/common_components/linux_compat/freertos/include/freertos/task.h +++ b/common_components/linux_compat/freertos/include/freertos/task.h @@ -11,6 +11,7 @@ extern "C" { #endif +#define tskNO_AFFINITY ( ( BaseType_t ) 0x7FFFFFFF ) #define TaskHandle_t TaskHandle_t #define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) ) @@ -18,6 +19,8 @@ void vTaskDelay( const TickType_t xTicksToDelay ); void xTaskNotifyGive(TaskHandle_t task); +void ulTaskNotifyTake(bool stuff, uint32_t timeout); + TaskHandle_t xTaskGetCurrentTaskHandle(void); BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time ); diff --git a/components/mdns/CMakeLists.txt b/components/mdns/CMakeLists.txt index 53aae068484..e0b2a4d9e0f 100644 --- a/components/mdns/CMakeLists.txt +++ b/components/mdns/CMakeLists.txt @@ -12,8 +12,9 @@ endif() idf_build_get_property(target IDF_TARGET) if(${target} STREQUAL "linux") - set(dependencies esp_netif_linux esp_timer esp_system) - set(srcs "mdns.c" ${MDNS_NETWORKING}) + set(dependencies esp_netif_linux esp_event) + set(private_dependencies esp_timer console esp_system) + set(srcs "mdns.c" ${MDNS_NETWORKING} ${MDNS_CONSOLE}) else() set(dependencies lwip console esp_netif) set(private_dependencies esp_timer esp_wifi) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index e28b7964ada..9679f6245ae 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -29,9 +29,6 @@ static void _mdns_browse_send(mdns_browse_t *browse); #if CONFIG_ETH_ENABLED && CONFIG_MDNS_PREDEF_NETIF_ETH #include "esp_eth.h" #endif -#if CONFIG_MDNS_PREDEF_NETIF_STA || CONFIG_MDNS_PREDEF_NETIF_AP -#include "esp_wifi.h" -#endif #if ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(5, 1, 0) #define MDNS_ESP_WIFI_ENABLED CONFIG_SOC_WIFI_SUPPORTED @@ -39,6 +36,10 @@ static void _mdns_browse_send(mdns_browse_t *browse); #define MDNS_ESP_WIFI_ENABLED CONFIG_ESP_WIFI_ENABLED #endif +#if MDNS_ESP_WIFI_ENABLED && (CONFIG_MDNS_PREDEF_NETIF_STA || CONFIG_MDNS_PREDEF_NETIF_AP) +#include "esp_wifi.h" +#endif + #ifdef MDNS_ENABLE_DEBUG void mdns_debug_packet(const uint8_t *data, size_t len); #endif @@ -4411,7 +4412,7 @@ void mdns_preset_if_handle_system_event(void *arg, esp_event_base_t event_base, } esp_netif_dhcp_status_t dcst; -#if MDNS_ESP_WIFI_ENABLED +#if MDNS_ESP_WIFI_ENABLED && (CONFIG_MDNS_PREDEF_NETIF_STA || CONFIG_MDNS_PREDEF_NETIF_AP) if (event_base == WIFI_EVENT) { switch (event_id) { case WIFI_EVENT_STA_CONNECTED: diff --git a/components/mdns/mdns_console.c b/components/mdns/mdns_console.c index 075be0682dd..9fe6d4b3e1f 100644 --- a/components/mdns/mdns_console.c +++ b/components/mdns/mdns_console.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -9,6 +9,7 @@ #include "argtable3/argtable3.h" #include "mdns.h" #include "mdns_private.h" +#include "inttypes.h" static const char *ip_protocol_str[] = {"V4", "V6", "MAX"}; @@ -26,7 +27,7 @@ static void mdns_print_results(mdns_result_t *results) printf(" SRV : %s.local:%u\n", r->hostname, r->port); } if (r->txt_count) { - printf(" TXT : [%u] ", r->txt_count); + printf(" TXT : [%u] ", (int)r->txt_count); for (size_t t = 0; t < r->txt_count; t++) { printf("%s=%s; ", r->txt[t].key, r->txt[t].value); } @@ -516,7 +517,7 @@ static int cmd_mdns_init(int argc, char **argv) printf("MDNS: Hostname: %s\n", mdns_init_args.hostname->sval[0]); } - if (mdns_init_args.instance->sval[0]) { + if (mdns_init_args.instance->count) { ESP_ERROR_CHECK( mdns_instance_name_set(mdns_init_args.instance->sval[0]) ); printf("MDNS: Instance: %s\n", mdns_init_args.instance->sval[0]); } diff --git a/components/mdns/tests/host_test/CMakeLists.txt b/components/mdns/tests/host_test/CMakeLists.txt index 79ac7b3baf2..5f0d7bdd642 100644 --- a/components/mdns/tests/host_test/CMakeLists.txt +++ b/components/mdns/tests/host_test/CMakeLists.txt @@ -1,11 +1,16 @@ cmake_minimum_required(VERSION 3.5) -set(EXTRA_COMPONENT_DIRS "../.." "../../../../common_components/linux_compat") include($ENV{IDF_PATH}/tools/cmake/project.cmake) -set(COMPONENTS main esp_netif_linux) +if(${IDF_TARGET} STREQUAL "linux") + set(EXTRA_COMPONENT_DIRS "../../../../common_components/linux_compat") + set(COMPONENTS main) +endif() + project(mdns_host) -# Enable sanitizers for mdns implementation +# Enable sanitizers only without console (we'd see some leaks on argtable when console exits) +if(NOT CONFIG_TEST_CONSOLE AND CONFIG_IDF_TARGET_LINUX) idf_component_get_property(mdns mdns COMPONENT_LIB) target_link_options(${mdns} INTERFACE -fsanitize=address -fsanitize=undefined) +endif() diff --git a/components/mdns/tests/host_test/components/esp_netif_linux/CMakeLists.txt b/components/mdns/tests/host_test/components/esp_netif_linux/CMakeLists.txt index 44f6b4be0d9..6f8d51ec8c3 100644 --- a/components/mdns/tests/host_test/components/esp_netif_linux/CMakeLists.txt +++ b/components/mdns/tests/host_test/components/esp_netif_linux/CMakeLists.txt @@ -1,2 +1,8 @@ -idf_component_register(SRCS esp_netif_linux.c - INCLUDE_DIRS include $ENV{IDF_PATH}/components/esp_netif/include) +idf_build_get_property(idf_target IDF_TARGET) +if(${IDF_TARGET} STREQUAL "linux") + idf_component_register(SRCS esp_netif_linux.c + INCLUDE_DIRS include $ENV{IDF_PATH}/components/esp_netif/include + REQUIRES esp_event) +else() + idf_component_register() +endif() diff --git a/components/mdns/tests/host_test/components/esp_netif_linux/esp_netif_linux.c b/components/mdns/tests/host_test/components/esp_netif_linux/esp_netif_linux.c index 67837239ae0..d769ebbe06e 100644 --- a/components/mdns/tests/host_test/components/esp_netif_linux/esp_netif_linux.c +++ b/components/mdns/tests/host_test/components/esp_netif_linux/esp_netif_linux.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -15,9 +15,12 @@ #include #include #include "esp_netif_types.h" +#include "esp_log.h" #define MAX_NETIFS 4 +static const char *TAG = "esp_netif_linux"; + static esp_netif_t *s_netif_list[MAX_NETIFS] = { 0 }; struct esp_netif_obj { @@ -50,6 +53,7 @@ esp_err_t esp_netif_get_ip_info(esp_netif_t *esp_netif, esp_netif_ip_info_t *ip_ struct sockaddr_in *pAddr = (struct sockaddr_in *) tmp->ifa_addr; inet_ntop(AF_INET, &pAddr->sin_addr, addr, sizeof(addr) ); if (strcmp(esp_netif->if_desc, tmp->ifa_name) == 0) { + ESP_LOGD(TAG, "AF_INET4: %s: %s\n", tmp->ifa_name, addr); memcpy(&ip_info->ip.addr, &pAddr->sin_addr, 4); } } @@ -103,7 +107,7 @@ esp_err_t esp_netif_get_ip6_linklocal(esp_netif_t *esp_netif, esp_ip6_addr_t *if struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)tmp->ifa_addr; inet_ntop(AF_INET6, &pAddr->sin6_addr, addr, sizeof(addr) ); if (strcmp(esp_netif->if_desc, tmp->ifa_name) == 0) { - printf("AF_INET6: %s: %s\n", tmp->ifa_name, addr); + ESP_LOGD(TAG, "AF_INET6: %s: %s\n", tmp->ifa_name, addr); memcpy(if_ip6->addr, &pAddr->sin6_addr, 4 * 4); break; } @@ -173,3 +177,8 @@ void esp_netif_destroy(esp_netif_t *esp_netif) } free(esp_netif); } + +const char *esp_netif_get_ifkey(esp_netif_t *esp_netif) +{ + return esp_netif->if_key; +} diff --git a/components/mdns/tests/host_test/dnsfixture.py b/components/mdns/tests/host_test/dnsfixture.py new file mode 100644 index 00000000000..e3b40512338 --- /dev/null +++ b/components/mdns/tests/host_test/dnsfixture.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import logging +import re +import socket +import sys + +import dns.message +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.resolver + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class DnsPythonWrapper: + def __init__(self, server='224.0.0.251', port=5353, retries=3): + self.server = server + self.port = port + self.retries = retries + + def send_and_receive_query(self, query, timeout=3): + logger.info(f'Sending DNS query to {self.server}:{self.port}') + try: + # Create a UDP socket + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(timeout) + + # Send the DNS query + query_data = query.to_wire() + sock.sendto(query_data, (self.server, self.port)) + + # Receive the DNS response + response_data, _ = sock.recvfrom(512) # 512 bytes is the typical size for a DNS response + + # Parse the response + response = dns.message.from_wire(response_data) + + return response + + except socket.timeout as e: + logger.warning(f'DNS query timed out: {e}') + return None + except dns.exception.DNSException as e: + logger.error(f'DNS query failed: {e}') + return None + + def run_query(self, name, query_type='PTR', timeout=3): + logger.info(f'Running DNS query for {name} with type {query_type}') + query = dns.message.make_query(name, dns.rdatatype.from_text(query_type), dns.rdataclass.IN) + + # Print the DNS question section + logger.info(f'DNS question section: {query.question}') + + # Send and receive the DNS query + response = None + for attempt in range(1, self.retries + 1): + logger.info(f'Attempt {attempt}/{self.retries}') + response = self.send_and_receive_query(query, timeout) + if response: + break + + if response: + logger.info(f'DNS query response:\n{response}') + else: + logger.warning('No response received or response was invalid.') + + return response + + def parse_answer_section(self, response, query_type): + answers = [] + if response: + for answer in response.answer: + if dns.rdatatype.to_text(answer.rdtype) == query_type: + for item in answer.items: + full_answer = ( + f'{answer.name} {answer.ttl} ' + f'{dns.rdataclass.to_text(answer.rdclass)} ' + f'{dns.rdatatype.to_text(answer.rdtype)} ' + f'{item.to_text()}' + ) + answers.append(full_answer) + return answers + + def check_record(self, name, query_type, expected=True): + output = self.run_query(name, query_type=query_type) + answers = self.parse_answer_section(output, query_type) + logger.info(f'answers: {answers}') + if expected: + assert any(name in answer for answer in answers), f"Expected service '{name}' not found in answer section" + else: + assert not any(name in answer for answer in answers), f"Unexpected service '{name}' found in answer section" + + +if __name__ == '__main__': + if len(sys.argv) < 3: + print('Usage: python dns_fixture.py ') + sys.exit(1) + + query_type = sys.argv[1] + name = sys.argv[2] + ip_only = len(sys.argv) > 3 and sys.argv[3] == '--ip_only' + if ip_only: + logger.setLevel(logging.WARNING) + + dns_wrapper = DnsPythonWrapper() + if query_type == 'X' and '.' in name: + # Sends an IPv4 reverse query + reversed_ip = '.'.join(reversed(name.split('.'))) + name = f'{reversed_ip}.in-addr.arpa' + query_type = 'PTR' + response = dns_wrapper.run_query(name, query_type=query_type) + answers = dns_wrapper.parse_answer_section(response, query_type) + + if answers: + for answer in answers: + logger.info(f'DNS query response: {answer}') + if ip_only: + ipv4_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b') + ipv4_addresses = ipv4_pattern.findall(answer) + if ipv4_addresses: + print(f"{', '.join(ipv4_addresses)}") + else: + logger.info(f'No response for {name} with query type {query_type}') + exit(9) # Same as dig timeout diff --git a/components/mdns/tests/host_test/main/CMakeLists.txt b/components/mdns/tests/host_test/main/CMakeLists.txt index 8d5202d6471..0e6f226ab7e 100644 --- a/components/mdns/tests/host_test/main/CMakeLists.txt +++ b/components/mdns/tests/host_test/main/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - REQUIRES mdns) + REQUIRES mdns console nvs_flash) diff --git a/components/mdns/tests/host_test/main/Kconfig.projbuild b/components/mdns/tests/host_test/main/Kconfig.projbuild index f19001f8721..9bf4bba235d 100644 --- a/components/mdns/tests/host_test/main/Kconfig.projbuild +++ b/components/mdns/tests/host_test/main/Kconfig.projbuild @@ -12,4 +12,10 @@ menu "Test Configuration" help Name/ID if the network interface on which we run the mDNS host test + config TEST_CONSOLE + bool "Start console" + default n + help + Test uses esp_console for interactive testing. + endmenu diff --git a/components/mdns/tests/host_test/main/idf_component.yml b/components/mdns/tests/host_test/main/idf_component.yml new file mode 100644 index 00000000000..e2d4fe9ba21 --- /dev/null +++ b/components/mdns/tests/host_test/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + idf: ">=5.0" + espressif/mdns: + version: "^1.0.0" + override_path: "../../.." + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/components/mdns/tests/host_test/main/main.c b/components/mdns/tests/host_test/main/main.c index 0a1fa2522de..581231f18ee 100644 --- a/components/mdns/tests/host_test/main/main.c +++ b/components/mdns/tests/host_test/main/main.c @@ -1,16 +1,30 @@ /* - * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include -#include "mdns.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "esp_console.h" +#include "mdns.h" +#include "mdns_console.h" static const char *TAG = "mdns-test"; +static void mdns_test_app(esp_netif_t *interface); + +#ifdef CONFIG_TEST_CONSOLE +static EventGroupHandle_t s_exit_signal = NULL; + +static int exit_console(int argc, char **argv) +{ + xEventGroupSetBits(s_exit_signal, 1); + return 0; +} + +#else static void query_mdns_host(const char *host_name) { ESP_LOGI(TAG, "Query A: %s.local", host_name); @@ -30,37 +44,83 @@ static void query_mdns_host(const char *host_name) ESP_LOGI(TAG, "Query A: %s.local resolved to: " IPSTR, host_name, IP2STR(&addr)); } +#endif // TEST_CONSOLE -int main(int argc, char *argv[]) +#ifndef CONFIG_IDF_TARGET_LINUX +#include "protocol_examples_common.h" +#include "esp_event.h" +#include "nvs_flash.h" + +/** + * @brief This is an entry point for the real target device, + * need to init few components and connect to a network interface + */ +void app_main(void) { + 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()); + + mdns_test_app(EXAMPLE_INTERFACE); + + ESP_ERROR_CHECK(example_disconnect()); +} +#else +/** + * @brief This is an entry point for the linux target (simulator on host) + * need to create a dummy WiFi station and use it as mdns network interface + */ +int main(int argc, char *argv[]) +{ setvbuf(stdout, NULL, _IONBF, 0); const esp_netif_inherent_config_t base_cg = { .if_key = "WIFI_STA_DEF", .if_desc = CONFIG_TEST_NETIF_NAME }; esp_netif_config_t cfg = { .base = &base_cg }; esp_netif_t *sta = esp_netif_new(&cfg); + + mdns_test_app(sta); + + esp_netif_destroy(sta); + return 0; +} +#endif + +static void mdns_test_app(esp_netif_t *interface) +{ ESP_ERROR_CHECK(mdns_init()); ESP_ERROR_CHECK(mdns_hostname_set(CONFIG_TEST_HOSTNAME)); ESP_LOGI(TAG, "mdns hostname set to: [%s]", CONFIG_TEST_HOSTNAME); - ESP_ERROR_CHECK(mdns_register_netif(sta)); - ESP_ERROR_CHECK(mdns_netif_action(sta, MDNS_EVENT_ENABLE_IP4 | MDNS_EVENT_IP4_REVERSE_LOOKUP | MDNS_EVENT_IP6_REVERSE_LOOKUP)); - -#ifdef REGISTER_SERVICE - //set default mDNS instance name - mdns_instance_name_set("myesp-inst"); - //structure with TXT records - mdns_txt_item_t serviceTxtData[3] = { - {"board", "esp32"}, - {"u", "user"}, - {"p", "password"} + ESP_ERROR_CHECK(mdns_register_netif(interface)); + ESP_ERROR_CHECK(mdns_netif_action(interface, MDNS_EVENT_ENABLE_IP4 /*| MDNS_EVENT_ENABLE_IP6 */ | MDNS_EVENT_IP4_REVERSE_LOOKUP | MDNS_EVENT_IP6_REVERSE_LOOKUP)); + +#ifdef CONFIG_TEST_CONSOLE + esp_console_repl_t *repl = NULL; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + s_exit_signal = xEventGroupCreate(); + + repl_config.prompt = "mdns>"; + // init console REPL environment + ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); + + const esp_console_cmd_t cmd_exit = { + .command = "exit", + .help = "exit mDNS console application", + .hint = NULL, + .func = exit_console, + .argtable = NULL }; - vTaskDelay(pdMS_TO_TICKS(10000)); - ESP_ERROR_CHECK(mdns_service_add("myesp-service2", "_http", "_tcp", 80, serviceTxtData, 3)); -#endif + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_exit) ); + mdns_console_register(); + ESP_ERROR_CHECK(esp_console_start_repl(repl)); + xEventGroupWaitBits(s_exit_signal, 1, pdTRUE, pdFALSE, portMAX_DELAY); + repl->del(repl); +#else vTaskDelay(pdMS_TO_TICKS(10000)); query_mdns_host("david-work"); vTaskDelay(pdMS_TO_TICKS(1000)); - esp_netif_destroy(sta); +#endif mdns_free(); ESP_LOGI(TAG, "Exit"); - return 0; } diff --git a/components/mdns/tests/host_test/pytest_mdns.py b/components/mdns/tests/host_test/pytest_mdns.py new file mode 100644 index 00000000000..6f2a9763b45 --- /dev/null +++ b/components/mdns/tests/host_test/pytest_mdns.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import logging + +import pexpect +import pytest +from dnsfixture import DnsPythonWrapper + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +ipv6_enabled = False + + +class MdnsConsole: + def __init__(self, command): + self.process = pexpect.spawn(command, encoding='utf-8') + self.process.logfile = open('mdns_interaction.log', 'w') # Log all interactions + self.process.expect('mdns> ', timeout=10) + + def send_input(self, input_data): + logger.info(f'Sending to stdin: {input_data}') + self.process.sendline(input_data) + + def get_output(self, expected_data): + logger.info(f'Expecting: {expected_data}') + self.process.expect(expected_data, timeout=10) + output = self.process.before.strip() + logger.info(f'Received from stdout: {output}') + return output + + def terminate(self): + self.send_input('exit') + self.get_output('Exit') + self.process.wait() + self.process.close() + assert self.process.exitstatus == 0 + + +@pytest.fixture(scope='module') +def mdns_console(): + app = MdnsConsole('./build_linux_console/mdns_host.elf') + yield app + app.terminate() + + +@pytest.fixture(scope='module') +def dig_app(): + return DnsPythonWrapper() + + +def test_mdns_init(mdns_console, dig_app): + mdns_console.send_input('mdns_init -h hostname') + mdns_console.get_output('MDNS: Hostname: hostname') + dig_app.check_record('hostname.local', query_type='A', expected=True) + if ipv6_enabled: + dig_app.check_record('hostname.local', query_type='AAAA', expected=True) + + +def test_add_service(mdns_console, dig_app): + mdns_console.send_input('mdns_service_add _http _tcp 80 -i test_service') + mdns_console.get_output('MDNS: Service Instance: test_service') + dig_app.check_record('_http._tcp.local', query_type='PTR', expected=True) + + +def test_remove_service(mdns_console, dig_app): + mdns_console.send_input('mdns_service_remove _http _tcp') + dig_app.check_record('_http._tcp.local', query_type='PTR', expected=False) + + +if __name__ == '__main__': + pytest.main(['-s', 'test_mdns.py']) diff --git a/components/mdns/tests/host_test/sdkconfig.ci.app b/components/mdns/tests/host_test/sdkconfig.ci.app new file mode 100644 index 00000000000..809c518a194 --- /dev/null +++ b/components/mdns/tests/host_test/sdkconfig.ci.app @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_TEST_HOSTNAME="myesp" +CONFIG_MDNS_ENABLE_DEBUG_PRINTS=y +CONFIG_MDNS_RESPOND_REVERSE_QUERIES=y diff --git a/components/mdns/tests/host_test/sdkconfig.ci.console b/components/mdns/tests/host_test/sdkconfig.ci.console new file mode 100644 index 00000000000..3b6dcb67465 --- /dev/null +++ b/components/mdns/tests/host_test/sdkconfig.ci.console @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_ESP_EVENT_POST_FROM_ISR=n +CONFIG_MDNS_ENABLE_CONSOLE_CLI=y +CONFIG_TEST_CONSOLE=y diff --git a/components/mdns/tests/host_test/sdkconfig.ci.target b/components/mdns/tests/host_test/sdkconfig.ci.target new file mode 100644 index 00000000000..2a43df1a395 --- /dev/null +++ b/components/mdns/tests/host_test/sdkconfig.ci.target @@ -0,0 +1 @@ +CONFIG_IDF_TARGET="esp32" diff --git a/components/mdns/tests/host_test/sdkconfig.defaults b/components/mdns/tests/host_test/sdkconfig.defaults index 621659fac86..7e79ce497b9 100644 --- a/components/mdns/tests/host_test/sdkconfig.defaults +++ b/components/mdns/tests/host_test/sdkconfig.defaults @@ -1,9 +1,6 @@ -CONFIG_IDF_TARGET="linux" +CONFIG_TEST_NETIF_NAME="eth0" +CONFIG_ESP_EVENT_POST_FROM_ISR=n CONFIG_MDNS_NETWORKING_SOCKET=y CONFIG_MDNS_SKIP_SUPPRESSING_OWN_QUERIES=y -CONFIG_TEST_NETIF_NAME="eth0" -CONFIG_TEST_HOSTNAME="myesp" CONFIG_MDNS_PREDEF_NETIF_STA=n CONFIG_MDNS_PREDEF_NETIF_AP=n -CONFIG_MDNS_ENABLE_DEBUG_PRINTS=y -CONFIG_MDNS_RESPOND_REVERSE_QUERIES=y