Skip to content

Commit bfa604b

Browse files
authored
Merge pull request #904 from david-cermak/feat/add_tests_v1.9
[mdns]: Add tests for recent feats/fixes
2 parents f5e62e8 + 92a3118 commit bfa604b

File tree

8 files changed

+149
-25
lines changed

8 files changed

+149
-25
lines changed

.github/workflows/mdns__build-target-test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ jobs:
2424
shell: bash
2525
run: |
2626
. ${IDF_PATH}/export.sh
27+
if [[ "${{ matrix.idf_ver }}" == "latest" ]]; then
28+
export EXPECTED_WARNING="warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_IP101'"
29+
else
30+
export EXPECTED_WARNING="warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_GENERIC'"
31+
fi
2732
python -m pip install idf-build-apps
2833
# Build default configs for all targets
2934
python ./ci/build_apps.py components/mdns/${{ matrix.test.path }} -r default -d
@@ -71,6 +76,22 @@ jobs:
7176
- name: Run ${{ matrix.test.app }} application on ${{ matrix.idf_target }}
7277
working-directory: components/mdns/${{ matrix.test.path }}
7378
run: |
79+
export PYENV_ROOT="$HOME/.pyenv"
80+
export PATH="$PYENV_ROOT/bin:$PATH"
81+
eval "$(pyenv init --path)"
82+
eval "$(pyenv init -)"
83+
if ! pyenv versions --bare | grep -q '^3\.12\.6$'; then
84+
echo "Installing Python 3.12.6..."
85+
pyenv install -s 3.12.6
86+
fi
87+
if ! pyenv virtualenvs --bare | grep -q '^myenv$'; then
88+
echo "Creating pyenv virtualenv 'myenv'..."
89+
pyenv virtualenv 3.12.6 myenv
90+
fi
91+
pyenv activate myenv
92+
python --version
93+
pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code esptool
94+
pip install --extra-index-url https://dl.espressif.com/pypi/ -r $GITHUB_WORKSPACE/ci/requirements.txt
7495
unzip ci/artifacts.zip -d ci
7596
for dir in `ls -d ci/build_*`; do
7697
rm -rf build sdkconfig.defaults

.github/workflows/mdns__host-tests.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,43 @@ jobs:
6868
diff -q $file /tmp/$file || exit 1
6969
echo "OK"
7070
done
71+
72+
fuzz_test:
73+
if: contains(github.event.pull_request.labels.*.name, 'mdns-fuzz') || github.event_name == 'push'
74+
name: Fuzzer tests for mdns lib
75+
strategy:
76+
matrix:
77+
idf_ver: ["latest"]
78+
79+
runs-on: ubuntu-22.04
80+
container: aflplusplus/aflplusplus
81+
steps:
82+
- name: Checkout esp-protocols
83+
uses: actions/checkout@v4
84+
85+
- name: Checkout ESP-IDF
86+
uses: actions/checkout@v4
87+
with:
88+
repository: espressif/esp-idf
89+
path: idf
90+
submodules: recursive
91+
92+
- name: Install Necessary Libs
93+
run: |
94+
apt-get update -y
95+
apt-get install -y libbsd-dev
96+
97+
- name: Run AFL++
98+
shell: bash
99+
run: |
100+
export IDF_PATH=$GITHUB_WORKSPACE/idf
101+
cd components/mdns/tests/test_afl_fuzz_host/
102+
make fuzz
103+
104+
- name: Upload Crash Artifacts
105+
if: failure()
106+
uses: actions/upload-artifact@v4
107+
with:
108+
name: fuzz-crashes
109+
path: components/mdns/tests/test_afl_fuzz_host/out/default/crashes.tar.gz
110+
if-no-files-found: ignore

components/mdns/examples/query_advertise/sdkconfig.ci.eth_custom_netif

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CONFIG_EXAMPLE_CONNECT_ETHERNET=y
1212
CONFIG_EXAMPLE_CONNECT_WIFI=n
1313
CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y
1414
CONFIG_EXAMPLE_ETH_PHY_IP101=y
15+
CONFIG_EXAMPLE_ETH_PHY_GENERIC=y
1516
CONFIG_EXAMPLE_ETH_MDC_GPIO=23
1617
CONFIG_EXAMPLE_ETH_MDIO_GPIO=18
1718
CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5

components/mdns/tests/host_test/dnsfixture.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Unlicense OR CC0-1.0
33
import logging
44
import re
@@ -92,10 +92,58 @@ def check_record(self, name, query_type, expected=True, expect=None):
9292
if expect is None:
9393
expect = name
9494
if expected:
95-
assert any(expect in answer for answer in answers), f"Expected record '{expect}' not found in answer section"
95+
assert any(expect in answer for answer in answers), f"Expected record '{expect}' not in answer section"
9696
else:
9797
assert not any(expect in answer for answer in answers), f"Unexpected record '{expect}' found in answer section"
9898

99+
def parse_section(self, response, section: str, rdtype_text: str):
100+
"""Parse a specific response section (answer, authority, additional) for given rdtype.
101+
102+
Returns list of textual records for that rdtype.
103+
"""
104+
out = []
105+
if not response:
106+
return out
107+
rrsets = []
108+
if section == 'answer':
109+
rrsets = response.answer
110+
elif section == 'authority':
111+
rrsets = response.authority
112+
elif section == 'additional':
113+
rrsets = response.additional
114+
else:
115+
raise ValueError('invalid section')
116+
for rr in rrsets:
117+
if dns.rdatatype.to_text(rr.rdtype) != rdtype_text:
118+
continue
119+
for item in rr.items:
120+
full = (
121+
f'{rr.name} {rr.ttl} '
122+
f'{dns.rdataclass.to_text(rr.rdclass)} '
123+
f'{dns.rdatatype.to_text(rr.rdtype)} '
124+
f'{item.to_text()}'
125+
)
126+
out.append(full)
127+
return out
128+
129+
def check_additional(self, response, rdtype_text: str, owner_contains: str, expected: bool = True, expect_substr: str | None = None):
130+
"""Check Additional section for an RR of type rdtype_text whose owner includes owner_contains.
131+
132+
If expect_substr is provided, also require it to appear in the textual RR.
133+
"""
134+
records = self.parse_section(response, 'additional', rdtype_text)
135+
logger.info(f'additional({rdtype_text}): {records}')
136+
137+
def _matches(line: str) -> bool:
138+
in_owner = owner_contains in line
139+
has_val = (expect_substr in line) if expect_substr else True
140+
return in_owner and has_val
141+
found = any(_matches(r) for r in records)
142+
if expected:
143+
assert found, f"Expected {rdtype_text} for {owner_contains} in Additional not found"
144+
else:
145+
assert not found, f"Unexpected {rdtype_text} for {owner_contains} found in Additional"
146+
99147

100148
if __name__ == '__main__':
101149
if len(sys.argv) < 3:

components/mdns/tests/host_test/pytest_mdns.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Unlicense OR CC0-1.0
33
import logging
44

@@ -65,6 +65,17 @@ def test_add_service(mdns_console, dig_app):
6565
dig_app.check_record('_http._tcp.local', query_type='PTR', expected=True)
6666

6767

68+
def test_ptr_additional_records_for_service(dig_app):
69+
# Query PTR for the service type and ensure SRV/TXT are in Additional (RFC 6763 §12.1)
70+
resp = dig_app.run_query('_http._tcp.local', query_type='PTR')
71+
# Answer section should have at least one PTR to the instance
72+
answers = dig_app.parse_answer_section(resp, 'PTR')
73+
assert any('test_service._http._tcp.local' in a for a in answers)
74+
# Additional section should include SRV and TXT for the same instance
75+
dig_app.check_additional(resp, 'SRV', 'test_service._http._tcp.local', expected=True)
76+
dig_app.check_additional(resp, 'TXT', 'test_service._http._tcp.local', expected=True)
77+
78+
6879
def test_remove_service(mdns_console, dig_app):
6980
mdns_console.send_input('mdns_service_remove _http _tcp')
7081
mdns_console.send_input('mdns_service_lookup _http _tcp')

components/mdns/tests/test_afl_fuzz_host/Makefile

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
#INSTR=off
12
TEST_NAME=test
23
FUZZ=afl-fuzz
34
COMPONENTS_DIR=$(IDF_PATH)/components
4-
COMPILER_ICLUDE_DIR=$(shell echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf)
5+
# Use ESP32 toolchain include path if available, otherwise fall back to system includes for host-based compilation
6+
COMPILER_INCLUDE_DIR=$(shell if command -v xtensa-esp32-elf-gcc >/dev/null 2>&1; then echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf; else echo /usr; fi)
57

68
CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversion -Wno-macro-redefined -Wno-int-to-void-pointer-cast -DHOOK_MALLOC_FAILED -DESP_EVENT_H_ -D__ESP_LOG_H__ \
79
-I. -I../.. -I../../include -I../../private_include -I ./build/config \
@@ -35,7 +37,7 @@ CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversi
3537
-I$(COMPONENTS_DIR)/xtensa/include \
3638
-I$(COMPONENTS_DIR)/xtensa/esp32/include \
3739
-I$(COMPONENTS_DIR)/esp_hw_support/etm/include \
38-
-I$(COMPILER_ICLUDE_DIR)/include
40+
-I$(COMPILER_INCLUDE_DIR)/include
3941

4042

4143
MDNS_C_DEPENDENCY_INJECTION=-include mdns_di.h
@@ -77,7 +79,18 @@ $(TEST_NAME): $(OBJECTS)
7779
@$(LD) $(OBJECTS) -o $@ $(LDLIBS)
7880

7981
fuzz: $(TEST_NAME)
80-
@$(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME)
82+
# timeout returns 124 if time limit is reached, original return code otherwise
83+
# pass only if: fuzzing was running smoothly until timeout AND no crash found
84+
@timeout 10m $(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME) || \
85+
if [ $$? -eq 124 ]; then \
86+
if [ -n "$$(find out/default/crashes -type f 2>/dev/null)" ]; then \
87+
echo "Crashes found!"; \
88+
tar -czf out/default/crashes.tar.gz -C out/default crashes; \
89+
exit 1; \
90+
fi \
91+
else \
92+
exit 1; \
93+
fi
8194

8295
clean:
8396
@rm -rf *.o *.SYM $(TEST_NAME) out

components/mdns/tests/test_afl_fuzz_host/esp32_mock.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@
5555

5656
#define pdMS_TO_TICKS(a) a
5757
#define xSemaphoreTake(s,d) true
58-
#define xTaskDelete(a)
59-
#define vTaskDelete(a) free(a)
58+
#define vTaskDelete(a) free(NULL)
6059
#define xSemaphoreGive(s)
6160
#define xQueueCreateMutex(s)
6261
#define _mdns_pcb_init(a,b) true
@@ -66,7 +65,7 @@
6665
#define vSemaphoreDelete(s) free(s)
6766
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U
6867
#define xTaskCreatePinnedToCore(a,b,c,d,e,f,g) *(f) = malloc(1)
69-
#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) true
68+
#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) ((void*)1)
7069
#define vTaskDelay(m) usleep((m)*0)
7170
#define esp_random() (rand()%UINT32_MAX)
7271

@@ -139,4 +138,8 @@ TaskHandle_t xTaskGetCurrentTaskHandle(void);
139138
void xTaskNotifyGive(TaskHandle_t task);
140139
BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time);
141140

141+
static inline void xTaskGetStaticBuffers(void *pvTaskBuffer, void *pvStackBuffer, void *pvTaskTCB)
142+
{
143+
}
144+
142145
#endif //_ESP32_COMPAT_H_

components/mdns/tests/test_afl_fuzz_host/test.c

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,30 +78,20 @@ static int mdns_test_service_txt_set(const char *service, const char *proto, ui
7878
static int mdns_test_sub_service_add(const char *sub_name, const char *service_name, const char *proto, uint32_t port)
7979
{
8080
if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) {
81-
// This is expected failure as the service thread is not running
81+
return ESP_FAIL;
8282
}
83-
mdns_action_t *a = NULL;
84-
GetLastItem(&a);
85-
mdns_test_execute_action(a);
8683

8784
if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) {
8885
return ESP_FAIL;
8986
}
90-
int ret = mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name);
91-
a = NULL;
92-
GetLastItem(&a);
93-
mdns_test_execute_action(a);
94-
return ret;
87+
return mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name);
9588
}
9689

9790
static int mdns_test_service_add(const char *service_name, const char *proto, uint32_t port)
9891
{
9992
if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) {
100-
// This is expected failure as the service thread is not running
93+
return ESP_FAIL;
10194
}
102-
mdns_action_t *a = NULL;
103-
GetLastItem(&a);
104-
mdns_test_execute_action(a);
10595

10696
if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) {
10797
return ESP_FAIL;
@@ -266,9 +256,6 @@ int main(int argc, char **argv)
266256
}
267257
#ifndef MDNS_NO_SERVICES
268258
mdns_service_remove_all();
269-
mdns_action_t *a = NULL;
270-
GetLastItem(&a);
271-
mdns_test_execute_action(a);
272259
#endif
273260
ForceTaskDelete();
274261
mdns_free();

0 commit comments

Comments
 (0)