diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 990b1495f0..1cc046d2c9 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -155,6 +155,14 @@ build_pytest_examples_esp32c2: IDF_TARGET: esp32c2 TEST_DIR: examples +build_pytest_examples_esp32h2: + extends: + - .build_pytest_template + - .rules:build:example_test-esp32h2 + variables: + IDF_TARGET: esp32h2 + TEST_DIR: examples + build_pytest_components_esp32: extends: - .build_pytest_template diff --git a/.gitlab/ci/dependencies/dependencies.yml b/.gitlab/ci/dependencies/dependencies.yml index 71dbe58a7f..7e0f261dad 100644 --- a/.gitlab/ci/dependencies/dependencies.yml +++ b/.gitlab/ci/dependencies/dependencies.yml @@ -157,6 +157,19 @@ build:integration_test: - "build:example_test" - build:target_test +# For i154 runners +"test:example_test-i154": + patterns: + - "target_test-i154" + labels: + - target_test + - example_test + included_in: + - "build:example_test-esp32h2" + - "build:example_test-esp32s3" + - "build:example_test" + - build:target_test + # due to the lack of runners, c2 tests will only be triggered by label "test:{0}-esp32c2": matrix: diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index 70ab941386..706f4b3819 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -79,6 +79,14 @@ - "components/**/*" +.patterns-target_test-i154: &patterns-target_test-i154 + - "components/esp_netif/**/*" + - "components/esp_phy/**/*" + - "components/ieee802154/**/*" + - "components/lwip/**/*" + - "examples/common_components/iperf/**/*" + - "examples/openthread/**/*" + .patterns-integration_test: &patterns-integration_test - "tools/ci/python_packages/gitlab_api.py" - "tools/ci/integration_test/**/*" @@ -781,6 +789,8 @@ changes: *patterns-example_test-usb - <<: *if-dev-push changes: *patterns-example_test-wifi + - <<: *if-dev-push + changes: *patterns-target_test-i154 .rules:build:example_test-esp32: rules: @@ -884,6 +894,8 @@ changes: *patterns-example_test-usb - <<: *if-dev-push changes: *patterns-example_test-wifi + - <<: *if-dev-push + changes: *patterns-target_test-i154 .rules:build:example_test-esp32s2: rules: @@ -936,6 +948,8 @@ changes: *patterns-example_test-usb - <<: *if-dev-push changes: *patterns-example_test-wifi + - <<: *if-dev-push + changes: *patterns-target_test-i154 .rules:build:integration_test: rules: @@ -1025,6 +1039,8 @@ changes: *patterns-example_test-wifi - <<: *if-dev-push changes: *patterns-integration_test + - <<: *if-dev-push + changes: *patterns-target_test-i154 - <<: *if-dev-push changes: *patterns-unit_test - <<: *if-dev-push @@ -2718,6 +2734,18 @@ - <<: *if-dev-push changes: *patterns-example_test-wifi +.rules:test:example_test-i154: + rules: + - <<: *if-revert-branch + when: never + - <<: *if-protected + - <<: *if-label-build-only + when: never + - <<: *if-label-example_test + - <<: *if-label-target_test + - <<: *if-dev-push + changes: *patterns-target_test-i154 + .rules:test:host_test: rules: - <<: *if-revert-branch diff --git a/.gitlab/ci/target-test.yml b/.gitlab/ci/target-test.yml index cc588b04a7..096ac51479 100644 --- a/.gitlab/ci/target-test.yml +++ b/.gitlab/ci/target-test.yml @@ -356,6 +356,17 @@ component_ut_pytest_esp32c3_flash_multi: - build_pytest_components_esp32c3 tags: [ esp32c3, flash_mutli ] +example_test_pytest_openthread_br: + extends: + - .pytest_examples_dir_template + - .rules:test:example_test-i154 + needs: + - build_pytest_examples_esp32s3 + - build_pytest_examples_esp32h2 + tags: + - esp32h2 + - i154_multi_dut + .pytest_test_apps_dir_template: extends: .pytest_template variables: diff --git a/examples/openthread/.build-test-rules.yml b/examples/openthread/.build-test-rules.yml index 374b79845a..c357600b06 100644 --- a/examples/openthread/.build-test-rules.yml +++ b/examples/openthread/.build-test-rules.yml @@ -5,6 +5,10 @@ examples/openthread/ot_br: - if: IDF_TARGET == "esp32c2" temporary: true reason: target esp32c2 is not supported yet + disable_test: + - if: IDF_TARGET in ["esp32", "esp32c3", "esp32s2"] + temporary: true + reason: only test on esp32s3 examples/openthread/ot_cli: enable: diff --git a/examples/openthread/ot_br/sdkconfig.ci.br b/examples/openthread/ot_br/sdkconfig.ci.br new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/openthread/ot_br/sdkconfig.defaults b/examples/openthread/ot_br/sdkconfig.defaults index e8dddfb481..369bdb00f6 100644 --- a/examples/openthread/ot_br/sdkconfig.defaults +++ b/examples/openthread/ot_br/sdkconfig.defaults @@ -17,7 +17,6 @@ CONFIG_PARTITION_TABLE_MD5=y # # mbedTLS # - CONFIG_MBEDTLS_CMAC_C=y CONFIG_MBEDTLS_SSL_PROTO_DTLS=y CONFIG_MBEDTLS_KEY_EXCHANGE_ECJPAKE=y diff --git a/examples/openthread/ot_ci_function.py b/examples/openthread/ot_ci_function.py new file mode 100644 index 0000000000..b71e7f19e7 --- /dev/null +++ b/examples/openthread/ot_ci_function.py @@ -0,0 +1,222 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +# !/usr/bin/env python3 +# this file defines some functions for testing cli and br under pytest framework + +import re +import subprocess +import time +from typing import Tuple, Union + +import netifaces +import pexpect +from pytest_embedded_idf.dut import IdfDut + + +def reset_thread(dut:IdfDut) -> None: + time.sleep(1) + dut.write('factoryreset') + time.sleep(3) + dut.expect('OpenThread attached to netif', timeout=10) + dut.write(' ') + dut.write('state') + + +# config thread +def config_thread(dut:IdfDut, model:str, dataset:str='0') -> Union[str, None]: + if model == 'random': + dut.write('dataset init new') + dut.expect('Done', timeout=2) + dut.write('dataset commit active') + dut.expect('Done', timeout=2) + dut.write('ifconfig up') + dut.expect('Done', timeout=2) + dut.write('dataset active -x') # get dataset + dut_data = dut.expect(r'\n(\w{212})\r', timeout=5)[1].decode() + return str(dut_data) + if model == 'appointed': + tmp = 'dataset set active ' + str(dataset) + dut.write(tmp) + dut.expect('Done', timeout=2) + dut.write('ifconfig up') + dut.expect('Done', timeout=2) + return None + return None + + +# get the mleid address of the thread +def get_mleid_addr(dut:IdfDut) -> str: + dut_adress = '' + clean_buffer(dut) + dut.write('ipaddr mleid') + dut_adress = dut.expect(r'\n((?:\w+:){7}\w+)\r', timeout=5)[1].decode() + return dut_adress + + +# get the rloc address of the thread +def get_rloc_addr(dut:IdfDut) -> str: + dut_adress = '' + clean_buffer(dut) + dut.write('ipaddr rloc') + dut_adress = dut.expect(r'\n((?:\w+:){7}\w+)\r', timeout=5)[1].decode() + return dut_adress + + +# get the linklocal address of the thread +def get_linklocal_addr(dut:IdfDut) -> str: + dut_adress = '' + clean_buffer(dut) + dut.write('ipaddr linklocal') + dut_adress = dut.expect(r'\n((?:\w+:){7}\w+)\r', timeout=5)[1].decode() + return dut_adress + + +# get the global unicast address of the thread: +def get_global_unicast_addr(dut:IdfDut, br:IdfDut) -> str: + dut_adress = '' + clean_buffer(br) + br.write('br omrprefix') + omrprefix = br.expect(r'\n((?:\w+:){4}):/\d+\r', timeout=5)[1].decode() + clean_buffer(dut) + dut.write('ipaddr') + dut_adress = dut.expect(r'(%s(?:\w+:){3}\w+)\r' % str(omrprefix), timeout=5)[1].decode() + return dut_adress + + +# start thread +def start_thread(dut:IdfDut) -> str: + role = '' + dut.write('thread start') + tmp = dut.expect(r'Role detached -> (\w+)\W', timeout=20)[0] + role = re.findall(r'Role detached -> (\w+)\W', str(tmp))[0] + return role + + +# config br and cli manually +def form_network_using_manual_configuration(leader:IdfDut, child:IdfDut, leader_name:str, thread_dataset_model:str, + thread_dataset:str, wifi:IdfDut, wifi_ssid:str, wifi_psk:str) -> str: + time.sleep(3) + leader.expect('OpenThread attached to netif', timeout=10) + leader.write(' ') + leader.write('state') + child.expect('OpenThread attached to netif', timeout=10) + child.write(' ') + child.write('state') + reset_thread(leader) + reset_thread(child) + leader.write('channel 12') + leader.expect('Done', timeout=2) + child.write('channel 12') + child.expect('Done', timeout=2) + res = '0000' + if wifi_psk != '0000': + res = connect_wifi(wifi, wifi_ssid, wifi_psk, 10)[0] + leader_data = '' + if thread_dataset_model == 'random': + leader_data = str(config_thread(leader, 'random')) + else: + config_thread(leader, 'appointed', thread_dataset) + if leader_name == 'br': + leader.write('bbr enable') + leader.expect('Done', timeout=2) + role = start_thread(leader) + assert role == 'leader' + if thread_dataset_model == 'random': + config_thread(child, 'appointed', leader_data) + else: + config_thread(child, 'appointed', thread_dataset) + if leader_name != 'br': + child.write('bbr enable') + child.expect('Done', timeout=2) + role = start_thread(child) + assert role == 'child' + return res + + +# ping of thread +def ot_ping(dut:IdfDut, target:str, times:int) -> Tuple[int, int]: + command = 'ping ' + str(target) + ' 0 ' + str(times) + dut.write(command) + transmitted = dut.expect(r'(\d+) packets transmitted', timeout=30)[1].decode() + tx_count = int(transmitted) + received = dut.expect(r'(\d+) packets received', timeout=30)[1].decode() + rx_count = int(received) + return tx_count, rx_count + + +# connect Wi-Fi +def connect_wifi(dut:IdfDut, ssid:str, psk:str, nums:int) -> Tuple[str, int]: + clean_buffer(dut) + ip_address = '' + information = '' + for order in range(1, nums): + dut.write('wifi connect -s ' + str(ssid) + ' -p ' + str(psk)) + tmp = dut.expect(pexpect.TIMEOUT, timeout=5) + ip_address = re.findall(r'sta ip: (\w+.\w+.\w+.\w+),', str(tmp))[0] + information = dut.expect(r'wifi sta (\w+ \w+ \w+)\W', timeout=5)[1].decode() + if information == 'is connected successfully': + break + assert information == 'is connected successfully' + return ip_address, order + + +def reset_host_interface() -> None: + interface_name = get_host_interface_name() + flag = False + try: + command = 'ifconfig ' + interface_name + ' down' + subprocess.call(command, shell=True, timeout=5) + time.sleep(10) + command = 'ifconfig ' + interface_name + ' up' + subprocess.call(command, shell=True, timeout=10) + time.sleep(20) + flag = True + finally: + time.sleep(10) + assert flag + + +def set_interface_sysctl_options() -> None: + interface_name = get_host_interface_name() + flag = False + try: + command = 'sysctl -w net/ipv6/conf/' + interface_name + '/accept_ra=2' + subprocess.call(command, shell=True, timeout=5) + time.sleep(1) + command = 'sysctl -w net/ipv6/conf/' + interface_name + '/accept_ra_rt_info_max_plen=128' + subprocess.call(command, shell=True, timeout=5) + time.sleep(5) + flag = True + finally: + time.sleep(5) + assert flag + + +def init_interface_ipv6_address() -> None: + interface_name = get_host_interface_name() + flag = False + try: + command = 'ip -6 route | grep ' + interface_name + " | grep ra | awk {'print $1'} | xargs -I {} ip -6 route del {}" + subprocess.call(command, shell=True, timeout=5) + time.sleep(0.5) + subprocess.call(command, shell=True, timeout=5) + time.sleep(1) + command = 'ip -6 address show dev ' + interface_name + \ + " scope global | grep 'inet6' | awk {'print $2'} | xargs -I {} ip -6 addr del {} dev " + interface_name + subprocess.call(command, shell=True, timeout=5) + time.sleep(1) + flag = True + finally: + time.sleep(5) + assert flag + + +def get_host_interface_name() -> str: + interfaces = netifaces.interfaces() + interface_name = [s for s in interfaces if 'wl' in s][0] + return str(interface_name) + + +def clean_buffer(dut:IdfDut) -> None: + str_length = str(len(dut.expect(pexpect.TIMEOUT, timeout=0.1))) + dut.expect(r'[\s\S]{%s}' % str(str_length), timeout=10) diff --git a/examples/openthread/ot_cli/sdkconfig.ci.cli b/examples/openthread/ot_cli/sdkconfig.ci.cli new file mode 100644 index 0000000000..3151ae4eaf --- /dev/null +++ b/examples/openthread/ot_cli/sdkconfig.ci.cli @@ -0,0 +1,3 @@ +CONFIG_IDF_TARGET="esp32h2" +CONFIG_IDF_TARGET_ESP32H2=y +CONFIG_IDF_TARGET_ESP32H2_BETA_VERSION_2=y diff --git a/examples/openthread/ot_rcp/sdkconfig.ci.rcp b/examples/openthread/ot_rcp/sdkconfig.ci.rcp new file mode 100644 index 0000000000..98376e6808 --- /dev/null +++ b/examples/openthread/ot_rcp/sdkconfig.ci.rcp @@ -0,0 +1,3 @@ +CONFIG_OPENTHREAD_UART_PIN_MANUAL=y +CONFIG_OPENTHREAD_UART_RX_PIN=4 +CONFIG_OPENTHREAD_UART_TX_PIN=5 diff --git a/examples/openthread/pytest_otbr.py b/examples/openthread/pytest_otbr.py new file mode 100644 index 0000000000..2130183641 --- /dev/null +++ b/examples/openthread/pytest_otbr.py @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +# !/usr/bin/env python3 + + +import os.path +import re +import socket +import struct +import subprocess +import time +from typing import Tuple + +import ot_ci_function as ocf +import pytest +from pytest_embedded_idf.dut import IdfDut + +# This file contains the test scripts for Thread: + +# Case 1: Thread network formation and attaching +# A Thread Border Router forms a Thread network, a Thread device attaches to it, then test ping connection between them. + +# Case 2: Bidirectional IPv6 connectivity +# Test IPv6 ping connection between Thread device and Linux Host (via Thread Border Router). + +# Case 3: Multicast forwarding from Wi-Fi to Thread network +# Thread device joins the multicast group, then test group communication from Wi-Fi to Thread network. + +# Case 4: Multicast forwarding from Thread to Wi-Fi network +# Linux Host joins the multicast group, test group communication from Thread to Wi-Fi network. + + +@pytest.fixture(name='Init_interface') +def fixture_Init_interface() -> bool: + ocf.init_interface_ipv6_address() + ocf.reset_host_interface() + ocf.set_interface_sysctl_options() + return True + + +# Case 1: Thread network formation and attaching +@pytest.mark.esp32s3 +@pytest.mark.esp32h2 +@pytest.mark.i154_multi_dut +@pytest.mark.flaky(reruns=2, reruns_delay=10) +@pytest.mark.parametrize( + 'port, config, count, app_path, beta_target, target', [ + ('/dev/USB_BR|/dev/USB_CLI|/dev/USB_RCP', 'br|cli|rcp', 3, + f'{os.path.join(os.path.dirname(__file__), "ot_br")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_cli")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_rcp")}', + 'esp32s3|esp32h2beta2|esp32h2beta2', 'esp32s3|esp32h2|esp32h2'), + ], + indirect=True, +) +def test_thread_connect(dut:Tuple[IdfDut, IdfDut]) -> None: + br = dut[0] + cli = dut[1] + + dataset = '-1' + ocf.form_network_using_manual_configuration(br, cli, 'br', 'random', dataset, br, 'OTCITE', '0000') + time.sleep(1) + flag = False + try: + cli_mleid_addr = ocf.get_mleid_addr(cli) + br_mleid_addr = ocf.get_mleid_addr(br) + rx_nums = ocf.ot_ping(cli, br_mleid_addr, 5)[1] + assert rx_nums != 0 + rx_nums = ocf.ot_ping(br, cli_mleid_addr, 5)[1] + assert rx_nums != 0 + flag = True + finally: + br.write('factoryreset') + cli.write('factoryreset') + time.sleep(3) + assert flag + + +# Case 2: Bidirectional IPv6 connectivity +@pytest.mark.esp32s3 +@pytest.mark.esp32h2 +@pytest.mark.i154_multi_dut +@pytest.mark.flaky(reruns=5, reruns_delay=10) +@pytest.mark.parametrize( + 'port, config, count, app_path, beta_target, target', [ + ('/dev/USB_BR|/dev/USB_CLI|/dev/USB_RCP', 'br|cli|rcp', 3, + f'{os.path.join(os.path.dirname(__file__), "ot_br")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_cli")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_rcp")}', + 'esp32s3|esp32h2beta2|esp32h2beta2', 'esp32s3|esp32h2|esp32h2'), + ], + indirect=True, +) +def test_Bidirectional_IPv6_connectivity(Init_interface:bool, dut: Tuple[IdfDut, IdfDut]) -> None: + br = dut[0] + cli = dut[1] + assert Init_interface + + dataset = '-1' + ocf.form_network_using_manual_configuration(br, cli, 'br', 'random', dataset, br, 'OTCITE', 'otcitest888') + time.sleep(5) + cli_global_unicast_addr = ocf.get_global_unicast_addr(cli, br) + flag = False + try: + command = 'ping ' + str(cli_global_unicast_addr) + ' -c 10' + out_bytes = subprocess.check_output(command, shell=True, timeout=60) + out_str = out_bytes.decode('utf-8') + role = re.findall(r' (\d+)%', str(out_str))[0] + assert role != '100' + interface_name = ocf.get_host_interface_name() + command = 'ifconfig ' + interface_name + out_bytes = subprocess.check_output(command, shell=True, timeout=5) + out_str = out_bytes.decode('utf-8') + host_global_unicast_addr = re.findall(r'inet6 ((?:\w+:){7}\w+) prefixlen 64 scopeid 0x0', str(out_str)) + rx_nums = 0 + for ip_addr in host_global_unicast_addr: + txrx_nums = ocf.ot_ping(cli, str(ip_addr), 5) + rx_nums = rx_nums + int(txrx_nums[1]) + assert rx_nums != 0 + flag = True + finally: + br.write('factoryreset') + cli.write('factoryreset') + time.sleep(3) + assert flag + + +# Case 3: Multicast forwarding from Wi-Fi to Thread network +@pytest.mark.esp32s3 +@pytest.mark.esp32h2 +@pytest.mark.i154_multi_dut +@pytest.mark.flaky(reruns=5, reruns_delay=10) +@pytest.mark.parametrize( + 'port, config, count, app_path, beta_target, target', [ + ('/dev/USB_BR|/dev/USB_CLI|/dev/USB_RCP', 'br|cli|rcp', 3, + f'{os.path.join(os.path.dirname(__file__), "ot_br")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_cli")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_rcp")}', + 'esp32s3|esp32h2beta2|esp32h2beta2', 'esp32s3|esp32h2|esp32h2'), + ], + indirect=True, +) +def test_multicast_forwarding_A(Init_interface:bool, dut: Tuple[IdfDut, IdfDut]) -> None: + br = dut[0] + cli = dut[1] + assert Init_interface + + dataset = '-1' + ocf.form_network_using_manual_configuration(br, cli, 'br', 'random', dataset, br, 'OTCITE', 'otcitest888') + time.sleep(5) + flag = False + try: + br.write('bbr') + br.expect('server16', timeout=2) + cli.write('mcast join ff04::125') + cli.expect('Done', timeout=2) + time.sleep(1) + interface_name = ocf.get_host_interface_name() + command = 'ping -I ' + str(interface_name) + ' -t 64 ff04::125 -c 10' + out_bytes = subprocess.check_output(command, shell=True, timeout=60) + out_str = out_bytes.decode('utf-8') + role = re.findall(r' (\d+)%', str(out_str))[0] + assert role != '100' + flag = True + finally: + br.write('factoryreset') + cli.write('factoryreset') + time.sleep(3) + assert flag + + +# Case 4: Multicast forwarding from Thread to Wi-Fi network +@pytest.mark.esp32s3 +@pytest.mark.esp32h2 +@pytest.mark.i154_multi_dut +@pytest.mark.flaky(reruns=5, reruns_delay=5) +@pytest.mark.parametrize( + 'port, config, count, app_path, beta_target, target', [ + ('/dev/USB_BR|/dev/USB_CLI|/dev/USB_RCP', 'br|cli|rcp', 3, + f'{os.path.join(os.path.dirname(__file__), "ot_br")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_cli")}' + f'|{os.path.join(os.path.dirname(__file__), "ot_rcp")}', + 'esp32s3|esp32h2beta2|esp32h2beta2', 'esp32s3|esp32h2|esp32h2'), + ], + indirect=True, +) +def test_multicast_forwarding_B(Init_interface:bool, dut: Tuple[IdfDut, IdfDut]) -> None: + br = dut[0] + cli = dut[1] + assert Init_interface + + dataset = '-1' + ocf.form_network_using_manual_configuration(br, cli, 'br', 'random', dataset, br, 'OTCITE', 'otcitest888') + time.sleep(5) + br.write('bbr') + br.expect('server16', timeout=2) + interface_name = ocf.get_host_interface_name() + if_index = socket.if_nametoindex(interface_name) + sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + sock.bind(('::', 5090)) + sock.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, + struct.pack('16si', socket.inet_pton(socket.AF_INET6, 'ff04::125'), + if_index)) + time.sleep(1) + cli.write('udp open') + cli.expect('Done', timeout=2) + cli.write('udp send ff04::125 5090 hello') + cli.expect('Done', timeout=2) + data = b'' + try: + print('The host start to receive message!') + sock.settimeout(5) + data = (sock.recvfrom(1024))[0] + print('The host has received message!') + except socket.error: + print('The host did not received message!') + finally: + sock.close() + br.write('factoryreset') + cli.write('factoryreset') + time.sleep(3) + assert data == b'hello' diff --git a/pytest.ini b/pytest.ini index 18a3d1222c..7178904e65 100644 --- a/pytest.ini +++ b/pytest.ini @@ -64,6 +64,7 @@ markers = test_jtag_arm: runner where the chip is accessible through JTAG as well # multi-dut markers + i154_multi_dut: tests should be used for i154, such as openthread. multi_dut_generic: tests should be run on generic runners, at least have two duts connected. # host_test markers