diff --git a/.clang-format b/.clang-format index 4fb95466896aeb..a1c292ac4afefa 100644 --- a/.clang-format +++ b/.clang-format @@ -106,7 +106,6 @@ SpacesInSquareBrackets: false Standard: Cpp11 TabWidth: 8 UseTab: Never -InsertNewlineAtEOF: true --- Language: ObjC BasedOnStyle: WebKit diff --git a/.github/workflows/qemu.yaml b/.github/workflows/qemu.yaml index 7c8564958d1350..9156d022e5667b 100644 --- a/.github/workflows/qemu.yaml +++ b/.github/workflows/qemu.yaml @@ -97,3 +97,68 @@ jobs: --target tizen-arm-tests-no-ble-no-thread \ build " + + qemu-linux: + name: ubuntu + + runs-on: ubuntu-latest + if: github.actor != 'restyled-io[bot]' + + container: + image: ghcr.io/project-chip/chip-build-linux-qemu:74 + volumes: + - "/tmp/log_output:/tmp/test_logs" + # Required for using KVM + options: --privileged + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Checkout submodules & Bootstrap + uses: ./.github/actions/checkout-submodules-and-bootstrap + with: + platform: linux + + - name: Build Apps + run: | + scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv' + ./scripts/run_in_build_env.sh \ + "./scripts/build/build_examples.py \ + --target linux-x64-chip-tool \ + --target linux-x64-all-clusters \ + build \ + --copy-artifacts-to objdir-clone \ + " + # There is no enough space for running the test withouth cleaning the environment + - name: Clean up + run: | + rm -rf out/*/obj + rm -rf out/*/lib + rm -rf out/*/*.map + rm -rf $PW_ENVIRONMENT_ROOT + git clean -fdx --exclude out + # Without all required apps paths provided as argument, script starts to search for them in the current directory and it takes a lot of time. + - name: Run ble commission test using the python parser sending commands to chip-tool + run: | + ./scripts/run_in_vm.sh \ + "scripts/run_in_build_env.sh 'pip3 install -r scripts/setup/requirements.ble-wifi-testing.txt' && \ + ./scripts/run_in_build_env.sh \ + \"./scripts/tests/run_test_suite.py \ + --runner chip_tool_python \ + --target TestCommissionerNodeId \ + --chip-tool ./out/linux-x64-chip-tool/chip-tool \ + run \ + --iterations 1 \ + --test-timeout-seconds 120 \ + --all-clusters-app ./out/linux-x64-all-clusters/chip-all-clusters-app \ + --lock-app ./out/linux-x64-lock/chip-lock-app \ + --ota-provider-app ./out/linux-x64-ota-provider/chip-ota-provider-app \ + --ota-requestor-app ./out/linux-x64-ota-requestor/chip-ota-requestor-app \ + --tv-app ./out/linux-x64-tv-app/chip-tv-app \ + --bridge-app ./out/linux-x64-bridge/chip-bridge-app \ + --lit-icd-app ./out/linux-x64-lit-icd/lit-icd-app \ + --microwave-oven-app .out/linux-x64-microwave-oven/chip-microwave-oven-app \ + --rvc-app .out/linux-x64-rvc/chip-rvc-app \ + --ble-wifi \ + \" \ + " diff --git a/.gitignore b/.gitignore index 1c2d1430263594..e0c11483daa7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ examples/*/esp32/dependencies.lock # jupyter temporary files .ipynb_checkpoints +/runner.sh +/runner_status diff --git a/integrations/docker/images/stage-2/chip-build-linux-qemu/Dockerfile b/integrations/docker/images/stage-2/chip-build-linux-qemu/Dockerfile index 02a65e01a7a829..3e68901772b898 100644 --- a/integrations/docker/images/stage-2/chip-build-linux-qemu/Dockerfile +++ b/integrations/docker/images/stage-2/chip-build-linux-qemu/Dockerfile @@ -124,7 +124,7 @@ RUN mkdir -p /tmp/workdir/linux \ # Download Ubuntu image for QEMU && curl https://cloud-images.ubuntu.com/minimal/releases/noble/release/ubuntu-24.04-minimal-cloudimg-amd64.img \ -o /tmp/workdir/ubuntu-24.04-minimal-cloudimg-amd64.img - # Prepare ubuntu image +# Prepare ubuntu image RUN qemu-img create -f qcow2 -o preallocation=off $UBUNTU_QEMU_IMG 10G \ && virt-resize --expand /dev/sda1 /tmp/workdir/ubuntu-24.04-minimal-cloudimg-amd64.img $UBUNTU_QEMU_IMG \ && guestfish -a $UBUNTU_QEMU_IMG \ @@ -209,13 +209,14 @@ RUN qemu-img create -f qcow2 -o preallocation=off $UBUNTU_QEMU_IMG 10G \ -append 'console=ttyS0 root=/dev/vda4' \ -netdev user,id=network0 \ -device e1000,netdev=network0,mac=52:54:00:12:34:56 \ - -virtfs "local,path=/tmp,mount_tag=host0,security_model=passthrough,id=host0" \ -# tmp folder is mounted only to preserve error during boot + -virtfs "local,path=/tmp,mount_tag=host0,security_model=passthrough,id=host0" \ + # tmp folder is mounted only to preserve error during boot && mkdir -p /chip \ && rm -rf /opt/ubuntu-qemu/rootfs \ && echo -n \ "#!/bin/bash\n" \ "grep -q 'rootshell' /proc/cmdline && exit\n" \ + "[[ -n \$SSH_CONNECTION ]] && exit \n" \ "if [[ -x /chip/runner.sh ]]; then\n" \ " echo '### RUNNER START ###'\n" \ " cd /chip\n" \ @@ -227,7 +228,7 @@ RUN qemu-img create -f qcow2 -o preallocation=off $UBUNTU_QEMU_IMG 10G \ " read -r -t 5 -p 'Press ENTER to access root shell...' && exit || echo ' timeout.'\n" \ "fi\n" \ "echo 'Shutting down emulated system...'\n" \ - "echo o > /proc/sysrq-trigger\n" \ + "systemctl poweroff\n" \ | guestfish --rw -a $UBUNTU_QEMU_IMG -m /dev/sda4:/ upload - /launcher.sh : chmod 0755 /launcher.sh \ && virt-sparsify --compress ${UBUNTU_QEMU_IMG} ${UBUNTU_QEMU_IMG}.compressed \ && mv ${UBUNTU_QEMU_IMG}.compressed ${UBUNTU_QEMU_IMG} \ diff --git a/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh b/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh index 43cc4f80a9effa..e089f396680fb7 100755 --- a/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh +++ b/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh @@ -1,7 +1,7 @@ #!/bin/bash KERNEL="/opt/ubuntu-qemu/bzImage" -IMG="/opt/ubuntu-qemu/ubuntu-20.04.img" +IMG="/opt/ubuntu-qemu/ubuntu-24.04.img" ADDITIONAL_ARGS=() PROJECT_PATH="$(realpath "$(dirname "$0")/../../../../..")" @@ -23,7 +23,7 @@ fi -device virtio-blk-pci,drive=virtio-blk1 \ -drive file="$IMG",id=virtio-blk1,if=none,format=qcow2,readonly=off \ -kernel "$KERNEL" \ - -append 'console=ttyS0 mac80211_hwsim.radios=2 root=/dev/vda3' \ + -append 'console=ttyS0 mac80211_hwsim.radios=2 root=/dev/vda4' \ -netdev user,id=network0,hostfwd=tcp::2222-:22 \ -device e1000,netdev=network0,mac=52:54:00:12:34:56 \ -virtfs "local,path=$PROJECT_PATH,mount_tag=host0,security_model=passthrough,id=host0" \ diff --git a/scripts/py_matter_yamltests/matter_yamltests/pseudo_clusters/clusters/accessory_server_bridge.py b/scripts/py_matter_yamltests/matter_yamltests/pseudo_clusters/clusters/accessory_server_bridge.py index 15ebdf46428830..c83f85323eaf60 100644 --- a/scripts/py_matter_yamltests/matter_yamltests/pseudo_clusters/clusters/accessory_server_bridge.py +++ b/scripts/py_matter_yamltests/matter_yamltests/pseudo_clusters/clusters/accessory_server_bridge.py @@ -22,7 +22,7 @@ _PORT = 9000 if sys.platform == 'linux': - _IP = '10.10.10.5' + _IP = "10.10.12.5" def _make_url(): diff --git a/scripts/run_in_vm.sh b/scripts/run_in_vm.sh new file mode 100755 index 00000000000000..7050dc7611f145 --- /dev/null +++ b/scripts/run_in_vm.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This script executes the command given as an argument after +# activating the given python virtual environment +set -e + +PROJECT_PATH=$(dirname "$(dirname "$(realpath "$0")")") + +echo "$@" >"$PROJECT_PATH/runner.sh" +chmod +x "$PROJECT_PATH/runner.sh" + +echo "CMD:" +cat "$PROJECT_PATH/runner.sh" + +"$PROJECT_PATH/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh" + +if [ -f "$PROJECT_PATH/runner_status" ]; then + exit "$(cat "$PROJECT_PATH/runner_status")" +else + exit 1 +fi diff --git a/scripts/setup/requirements.ble-wifi-testing.txt b/scripts/setup/requirements.ble-wifi-testing.txt new file mode 100644 index 00000000000000..69c66f7cd4c8de --- /dev/null +++ b/scripts/setup/requirements.ble-wifi-testing.txt @@ -0,0 +1 @@ +PyGObject diff --git a/scripts/tests/chiptest/accessories.py b/scripts/tests/chiptest/accessories.py index 99433d7ed88660..b35ba3a7e57c9b 100644 --- a/scripts/tests/chiptest/accessories.py +++ b/scripts/tests/chiptest/accessories.py @@ -28,7 +28,7 @@ PORT = 9000 if sys.platform == 'linux': - IP = '10.10.10.5' + IP = "10.10.12.5" class AppsRegister: diff --git a/scripts/tests/chiptest/linux.py b/scripts/tests/chiptest/linux.py index bf3e140981d3a7..0cb97ff40b2589 100644 --- a/scripts/tests/chiptest/linux.py +++ b/scripts/tests/chiptest/linux.py @@ -18,15 +18,22 @@ Handles linux-specific functionality for running test cases """ +import glob import logging import os +import shutil import subprocess import sys import time +from collections import namedtuple +from time import sleep +from typing import Optional from .test_definition import ApplicationPaths test_environ = os.environ.copy() +PW_PROJECT_ROOT = os.environ.get("PW_PROJECT_ROOT") +QEMU_CONFIG_FILES = "integrations/docker/images/stage-2/chip-build-linux-qemu/files" def EnsureNetworkNamespaceAvailability(): @@ -57,7 +64,7 @@ def EnsurePrivateState(): sys.exit(1) -def CreateNamespacesForAppTest(): +def CreateNamespacesForAppTest(ble_wifi: bool = False): """ Creates appropriate namespaces for a tool and app binaries in a simulated isolated network. @@ -88,23 +95,33 @@ def CreateNamespacesForAppTest(): "ip netns exec app ip link set dev lo up", "ip link set dev eth-app-switch up", - "ip netns exec tool ip addr add 10.10.10.2/24 dev eth-tool", + "ip netns exec tool ip addr add 10.10.12.2/24 dev eth-tool", "ip netns exec tool ip link set dev eth-tool up", "ip netns exec tool ip link set dev lo up", "ip link set dev eth-tool-switch up", - # Force IPv6 to use ULAs that we control - "ip netns exec tool ip -6 addr flush eth-tool", - "ip netns exec app ip -6 addr flush eth-app", - "ip netns exec tool ip -6 a add fd00:0:1:1::2/64 dev eth-tool", - "ip netns exec app ip -6 a add fd00:0:1:1::3/64 dev eth-app", - - # create link between virtual host 'tool' and the test runner "ip addr add 10.10.10.5/24 dev eth-ci", + "ip addr add 10.10.12.5/24 dev eth-ci", "ip link set dev eth-ci up", "ip link set dev eth-ci-switch up", ] + if not ble_wifi: + COMMANDS += [ + "ip link add eth-app-direct type veth peer name eth-tool-direct", + "ip link set eth-app-direct netns app", + "ip link set eth-tool-direct netns tool", + "ip netns exec app ip addr add 10.10.15.1/24 dev eth-app-direct", + "ip netns exec app ip link set dev eth-app-direct up", + "ip netns exec tool ip addr add 10.10.15.2/24 dev eth-tool-direct", + "ip netns exec tool ip link set dev eth-tool-direct up", + # Force IPv6 to use ULAs that we control + "ip netns exec tool ip -6 addr flush eth-tool", + "ip netns exec app ip -6 addr flush eth-app", + "ip netns exec tool ip -6 a add fd00:0:1:1::2/64 dev eth-tool-direct", + "ip netns exec app ip -6 a add fd00:0:1:1::3/64 dev eth-app-direct", + ] + for command in COMMANDS: logging.debug("Executing '%s'" % command) if os.system(command) != 0: @@ -156,19 +173,232 @@ def RemoveNamespaceForAppTest(): sys.exit(1) -def PrepareNamespacesForTestExecution(in_unshare: bool): +def PrepareNamespacesForTestExecution(in_unshare: bool, ble_wifi: bool = False): if not in_unshare: EnsureNetworkNamespaceAvailability() elif in_unshare: EnsurePrivateState() - CreateNamespacesForAppTest() + CreateNamespacesForAppTest(ble_wifi) def ShutdownNamespaceForTestExecution(): RemoveNamespaceForAppTest() +class DbusTest: + DBUS_SYSTEM_BUS_ADDRESS = "unix:path=/tmp/chip-dbus-test" + + def __init__(self, dry_run: bool = False): + self._dbus = None + self._dbus_proxy = None + self.dry_run = dry_run + + def start(self): + if self.dry_run: + logging.info("Would start dbus") + return + original_env = os.environ.copy() + os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = DbusTest.DBUS_SYSTEM_BUS_ADDRESS + dbus = shutil.which("dbus-daemon") + self._dbus = subprocess.Popen([dbus, "--session", "--address", self.DBUS_SYSTEM_BUS_ADDRESS]) + + self._dbus_proxy = subprocess.Popen( + ["python3", f"{PW_PROJECT_ROOT}/scripts/tools/dbus-proxy-bluez.py", "--bus-proxy", DbusTest.DBUS_SYSTEM_BUS_ADDRESS], + env=original_env, + ) + + def stop(self): + if self._dbus: + self._dbus_proxy.terminate() + self._dbus.terminate() + self._dbus.wait() + + +class VirtualWifi: + def __init__( + self, + hostapd_path: str, + dnsmasq_path: str, + wpa_supplicant_path: str, + wlan_app: Optional[str] = None, + wlan_tool: Optional[str] = None, + dry_run: bool = False, + ): + self.dry_run = dry_run + self._hostapd_path = hostapd_path + self._dnsmasq_path = dnsmasq_path + self._wpa_supplicant_path = wpa_supplicant_path + self._hostapd_conf = os.path.join(PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/hostapd.conf") + self._dnsmasq_conf = os.path.join(PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/dnsmasq.conf") + self._wpa_supplicant_conf = os.path.join(PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/wpa_supplicant.conf") + + if (wlan_app is None or wlan_tool is None) and not dry_run: + wlans = glob.glob("/sys/devices/virtual/mac80211_hwsim/hwsim*/net/*") + if len(wlans) < 2: + raise RuntimeError("Not enough wlan devices found") + + self._wlan_app = os.path.basename(wlans[0]) + self._wlan_tool = os.path.basename(wlans[1]) + else: + self._wlan_app = wlan_app + self._wlan_tool = wlan_tool + self._hostapd = None + self._dnsmasq = None + self._wpa_supplicant = None + self._dhclient = None + + @staticmethod + def _get_phy(dev: str) -> str: + output = subprocess.check_output(["iw", "dev", dev, "info"]) + for line in output.split(b"\n"): + if b"wiphy" in line: + wiphy = int(line.split(b" ")[1]) + return f"phy{wiphy}" + raise ValueError(f"No wiphy found for {dev}") + + @staticmethod + def _move_phy_to_netns(phy: str, netns: str): + subprocess.check_call(["iw", "phy", phy, "set", "netns", "name", netns]) + + @staticmethod + def _set_interface_ip_in_netns(netns: str, dev: str, ip: str): + subprocess.check_call(["ip", "netns", "exec", netns, "ip", "link", "set", "dev", dev, "up"]) + subprocess.check_call(["ip", "netns", "exec", netns, "ip", "addr", "add", ip, "dev", dev]) + + def start(self): + hostapd_cmd = ["ip", "netns", "exec", "tool", self._hostapd_path, self._hostapd_conf] + dnsmaq_cmd = ["ip", "netns", "exec", "tool", self._dnsmasq_path, "-d", "-C", self._dnsmasq_conf] + dhclient_cmd = ["ip", "netns", "exec", "app", "dhclient", self._wlan_app] + wpa_cmd = [ + "ip", + "netns", + "exec", + "app", + self._wpa_supplicant_path, + "-u", + "-s", + "-i", + self._wlan_app, + "-c", + self._wpa_supplicant_conf, + ] + if self.dry_run: + logging.info(f"Would run hostapd with {hostapd_cmd}") + logging.info(f"Would run dnsmasq with {dnsmaq_cmd}") + logging.info(f"Would run wpa_supplicant with {wpa_cmd}") + return + # Write clean configuration for wifi to prevent auto wifi connection during next test + with open(self._wpa_supplicant_conf, "w") as f: + f.write("ctrl_interface=DIR=/run/wpa_supplicant\nctrl_interface_group=root\nupdate_config=1\n") + self._move_phy_to_netns(self._get_phy(self._wlan_app), "app") + self._move_phy_to_netns(self._get_phy(self._wlan_tool), "tool") + self._set_interface_ip_in_netns("tool", self._wlan_tool, "192.168.200.1/24") + + self._hostapd = subprocess.Popen(hostapd_cmd, stdout=subprocess.DEVNULL) + self._dnsmasq = subprocess.Popen(dnsmaq_cmd, stdout=subprocess.DEVNULL) + self._dhclient = subprocess.Popen(dhclient_cmd, stdout=subprocess.DEVNULL) + print(f"DnsMasq started with {self._dnsmasq.pid}") + self._wpa_supplicant = subprocess.Popen(wpa_cmd, stdout=subprocess.DEVNULL) + + def stop(self): + if self.dry_run: + logging.info("Would stop hostapd, dnsmasq and wpa_supplicant") + return + if self._hostapd: + self._hostapd.terminate() + self._hostapd.wait() + if self._dnsmasq: + self._dnsmasq.terminate() + self._dnsmasq.wait() + if self._wpa_supplicant: + self._wpa_supplicant.terminate() + self._wpa_supplicant.wait() + if self._dhclient: + self._dhclient.terminate() + self._dhclient.wait() + + +class VirtualBle: + BleDevice = namedtuple("BleDevice", ["hci", "mac", "index"]) + + def __init__(self, btvirt_path: str, bluetoothctl_path: str, dry_run: bool = False): + self._btvirt_path = btvirt_path + self._bluetoothctl_path = bluetoothctl_path + self._btvirt = None + self._bluetoothctl = None + self._ble_app = None + self._ble_tool = None + self._dry_run = dry_run + + @property + def ble_app(self) -> Optional[BleDevice]: + if self._dry_run: + return self.BleDevice(hci="hci0", mac="00:11:22:33:44:55", index=0) + if not self._ble_app: + raise RuntimeError("Bluetooth not started") + return self._ble_app + + @property + def ble_tool(self) -> Optional[BleDevice]: + if self._dry_run: + return self.BleDevice(hci="hci1", mac="00:11:22:33:44:56", index=1) + if not self._ble_tool: + raise RuntimeError("Bluetooth not started") + return self._ble_tool + + def bletoothctl_cmd(self, cmd): + self._bluetoothctl.stdin.write(cmd) + self._bluetoothctl.stdin.flush() + + def _get_mac_address(self, hci_name): + result = subprocess.run(["hcitool", "dev"], capture_output=True, text=True) + lines = result.stdout.splitlines() + + for line in lines: + if hci_name in line: + mac_address = line.split()[1] + return mac_address + + raise RuntimeError(f"No MAC address found for device {hci_name}") + + def _get_ble_info(self): + ble_dev_paths = glob.glob("/sys/devices/virtual/bluetooth/hci*") + hci = [os.path.basename(path) for path in ble_dev_paths] + if len(hci) < 2: + raise RuntimeError("Not enough BLE devices found") + self._ble_app = self.BleDevice(hci=hci[0], mac=self._get_mac_address(hci[0]), index=int(hci[0].replace("hci", ""))) + self._ble_tool = self.BleDevice(hci=hci[1], mac=self._get_mac_address(hci[1]), index=int(hci[1].replace("hci", ""))) + + def _run_bluetoothctl(self): + self._bluetoothctl = subprocess.Popen( + [self._bluetoothctl_path], text=True, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL + ) + self.bletoothctl_cmd(f"select {self.ble_app.mac}\n") + self.bletoothctl_cmd("power on\n") + self.bletoothctl_cmd(f"select {self.ble_tool.mac}\n") + self.bletoothctl_cmd("power on\n") + self.bletoothctl_cmd("quit\n") + self._bluetoothctl.wait() + + def start(self): + if self._dry_run: + logging.info("Would start bluetooth") + return + self._btvirt = subprocess.Popen([self._btvirt_path, "-l2"]) + sleep(1) + self._get_ble_info() + self._run_bluetoothctl() + + def stop(self): + if self._dry_run: + logging.info("Would stop bluetooth") + return + if self._btvirt: + self._btvirt.terminate() + self._btvirt.wait() + + def PathsWithNetworkNamespaces(paths: ApplicationPaths) -> ApplicationPaths: """ Returns a copy of paths with updated command arrays to invoke the diff --git a/scripts/tests/chiptest/test_definition.py b/scripts/tests/chiptest/test_definition.py index 484f247f70b1b2..3689a152ed7491 100644 --- a/scripts/tests/chiptest/test_definition.py +++ b/scripts/tests/chiptest/test_definition.py @@ -291,7 +291,7 @@ def tags_str(self) -> str: return ", ".join([t.to_s() for t in self.tags]) def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str, - timeout_seconds: typing.Optional[int], dry_run=False, test_runtime: TestRunTime = TestRunTime.CHIP_TOOL_PYTHON): + timeout_seconds: typing.Optional[int], dry_run=False, test_runtime: TestRunTime = TestRunTime.CHIP_TOOL_PYTHON, app_hci_number: typing.Optional[int] = None, tool_hci_number: typing.Optional[int] = None): """ Executes the given test case using the provided runner for execution. """ @@ -342,8 +342,11 @@ def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str, key = 'default' else: key = os.path.basename(path[-1]) - - app = App(runner, path) + ble_wifi_cmd = [] + if app_hci_number is not None: + ble_wifi_cmd = ["--ble-device", + str(app_hci_number), "--wifi"] + app = App(runner, path + ble_wifi_cmd) # Add the App to the register immediately, so if it fails during # start() we will be able to clean things up properly. apps_register.add(key, app) @@ -383,13 +386,27 @@ def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str, runner.RunSubprocess(python_cmd, name='CHIP_REPL_YAML_TESTER', dependencies=[apps_register], timeout_seconds=timeout_seconds) else: - pairing_cmd = paths.chip_tool_with_python_cmd + ['pairing', 'code', TEST_NODE_ID, setupCode] + pairing_server_args = [] + if tool_hci_number is not None: + pairing_cmd = paths.chip_tool_with_python_cmd + [ + "pairing", + "code-wifi", + TEST_NODE_ID, + "Virtual_Wifi", + "ExamplePassword", + "MT:-24J042C00KA0648G00", + ] + pairing_server_args = [ + "--ble-adapter", str(tool_hci_number)] + else: + pairing_cmd = paths.chip_tool_with_python_cmd + ['pairing', 'code', TEST_NODE_ID, setupCode] if self.target == TestTarget.LIT_ICD and test_runtime == TestRunTime.CHIP_TOOL_PYTHON: pairing_cmd += ['--icd-registration', 'true'] test_cmd = paths.chip_tool_with_python_cmd + ['tests', self.run_name] + ['--PICS', pics_file] server_args = ['--server_path', paths.chip_tool[-1]] + \ ['--server_arguments', 'interactive server' + - (' ' if len(tool_storage_args) else '') + ' '.join(tool_storage_args)] + (' ' if len(tool_storage_args) else '') + ' '.join(tool_storage_args) + + (' ' if len(pairing_server_args) else '') + ' '.join(pairing_server_args)] pairing_cmd += server_args test_cmd += server_args diff --git a/scripts/tests/run_test_suite.py b/scripts/tests/run_test_suite.py index 41b7b540a6dd50..37c31bd80593bf 100755 --- a/scripts/tests/run_test_suite.py +++ b/scripts/tests/run_test_suite.py @@ -295,10 +295,16 @@ def cmd_list(context): default=0, show_default=True, help='Number of tests that are expected to fail in each iteration. Overall test will pass if the number of failures matches this. Nonzero values require --keep-going') +@click.option( + '--ble-wifi', + is_flag=True, + default=False, + show_default=True, + help='Use a virtual wifi and bluetooth to commission device') @click.pass_context def cmd_run(context, iterations, all_clusters_app, lock_app, ota_provider_app, ota_requestor_app, fabric_bridge_app, tv_app, bridge_app, lit_icd_app, microwave_oven_app, rvc_app, network_manager_app, chip_repl_yaml_tester, - chip_tool_with_python, pics_file, keep_going, test_timeout_seconds, expected_failures): + chip_tool_with_python, pics_file, keep_going, test_timeout_seconds, expected_failures, ble_wifi): if expected_failures != 0 and not keep_going: logging.exception(f"'--expected-failures {expected_failures}' used without '--keep-going'") sys.exit(2) @@ -368,8 +374,24 @@ def cmd_run(context, iterations, all_clusters_app, lock_app, ota_provider_app, o ) if sys.platform == 'linux': - chiptest.linux.PrepareNamespacesForTestExecution( - context.obj.in_unshare) + chiptest.linux.PrepareNamespacesForTestExecution(context.obj.in_unshare, ble_wifi) + if ble_wifi: + dbus = chiptest.linux.DbusTest() + dbus.start() + + virt_wifi = chiptest.linux.VirtualWifi( + "/usr/sbin/hostapd", + "/usr/sbin/dnsmasq", + "/usr/sbin/wpa_supplicant", + dry_run=context.obj.dry_run, + ) + virt_ble = chiptest.linux.VirtualBle( + "/usr/bin/btvirt", + "/usr/bin/bluetoothctl", + dry_run=context.obj.dry_run, + ) + virt_wifi.start() + virt_ble.start() paths = chiptest.linux.PathsWithNetworkNamespaces(paths) logging.info("Each test will be executed %d times" % iterations) @@ -380,10 +402,14 @@ def cmd_run(context, iterations, all_clusters_app, lock_app, ota_provider_app, o def cleanup(): apps_register.uninit() if sys.platform == 'linux': + if ble_wifi: + virt_wifi.stop() + virt_ble.stop() + dbus.stop() chiptest.linux.ShutdownNamespaceForTestExecution() for i in range(iterations): - logging.info("Starting iteration %d" % (i+1)) + logging.info("Starting iteration %d" % (i + 1)) observed_failures = 0 for test in context.obj.tests: if context.obj.include_tags: @@ -401,18 +427,35 @@ def cleanup(): if context.obj.dry_run: logging.info("Would run test: %s" % test.name) else: - logging.info('%-20s - Starting test' % (test.name)) - test.Run( - runner, apps_register, paths, pics_file, test_timeout_seconds, context.obj.dry_run, - test_runtime=context.obj.runtime) + logging.info("%-20s - Starting test" % (test.name)) + if ble_wifi: + test.Run( + runner, + apps_register, + paths, + pics_file, + test_timeout_seconds, + context.obj.dry_run, + test_runtime=context.obj.runtime, + app_hci_number=virt_ble.ble_app.index, + tool_hci_number=virt_ble.ble_tool.index, + ) + else: + test.Run( + runner, + apps_register, + paths, + pics_file, + test_timeout_seconds, + context.obj.dry_run, + test_runtime=context.obj.runtime, + ) if not context.obj.dry_run: test_end = time.monotonic() - logging.info('%-30s - Completed in %0.2f seconds' % - (test.name, (test_end - test_start))) + logging.info("%-30s - Completed in %0.2f seconds" % (test.name, (test_end - test_start))) except Exception: test_end = time.monotonic() - logging.exception('%-30s - FAILED in %0.2f seconds' % - (test.name, (test_end - test_start))) + logging.exception("%-30s - FAILED in %0.2f seconds" % (test.name, (test_end - test_start))) observed_failures += 1 if not keep_going: cleanup() diff --git a/scripts/tools/dbus-proxy-bluez.py b/scripts/tools/dbus-proxy-bluez.py new file mode 100755 index 00000000000000..3ff0bda943b0f7 --- /dev/null +++ b/scripts/tools/dbus-proxy-bluez.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import os.path +import typing +from argparse import ArgumentParser +from collections import namedtuple + +from gi.repository import Gio, GLib + + +def bus_get_connection(address: str): + """Get a connection object for a given D-Bus bus.""" + if address == "session": + address = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION) + elif address == "system": + address = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SYSTEM) + logging.info("Connecting to: %s", address) + conn = Gio.DBusConnection.new_for_address_sync( + address, + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | + Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION) + logging.info("Assigned unique name: %s", conn.get_unique_name()) + return conn + + +def bus_get_name_owner(conn, name: str): + """Get the unique name of a well known name on a D-Bus bus.""" + params = GLib.Variant("(s)", (name,)) + reply = conn.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "GetNameOwner", + params, None, Gio.DBusCallFlags.NONE, -1) + return reply.get_child_value(0).get_string() + + +def bus_introspect_path(conn, client: str, path: str): + """Introspect a D-Bus object path and return its node info.""" + reply = conn.call_sync(client, path, + "org.freedesktop.DBus.Introspectable", "Introspect", + None, None, Gio.DBusCallFlags.NONE, -1) + xml = reply.get_child_value(0).get_string() + return Gio.DBusNodeInfo.new_for_xml(xml) + + +class DBusServiceProxy: + + MappingKey = namedtuple("MappingKey", ["path", "iface"]) + + objects: typing.Dict[MappingKey, int] = {} + subscriptions: typing.Set[str] = set() + clients = {} + + def __init__(self, source: str, proxy: str, service: str): + self.source = bus_get_connection(source) + self.proxy = bus_get_connection(proxy) + self.service = service + Gio.bus_own_name_on_connection(self.proxy, self.service, + Gio.BusNameOwnerFlags.DO_NOT_QUEUE, + self.on_bus_name_acquired, + self.on_bus_name_lost) + + def on_bus_name_acquired(self, conn, name): + logging.info("Acquired name on proxy bus: %s", name) + self.mirror_source_on_proxy(self.service, "/") + + def on_bus_name_lost(self, conn, name): + logging.debug("Lost name on proxy bus: %s", name) + + def proxy_client_save(self, path, client): + self.clients[path] = client + + def proxy_client_load(self, path): + return self.clients[path] + + def register_object(self, conn, path, iface): + key = DBusServiceProxy.MappingKey(path, iface.name) + if key not in self.objects: + logging.debug("Registering: %s { %s }", path, iface.name) + id = conn.register_object(path, iface, self.on_method_call) + self.objects[key] = id + + def unregister_object(self, conn, path, iface_name): + key = DBusServiceProxy.MappingKey(path, iface_name) + if key in self.objects: + logging.debug("Removing: %s { %s }", path, iface_name) + conn.unregister_object(self.objects.pop(key)) + + def signal_subscribe(self, conn, client): + """Subscribe for signals from a D-Bus client.""" + if client not in self.subscriptions: + conn.signal_subscribe(client, None, None, None, None, + Gio.DBusSignalFlags.NONE, + self.on_signal_received) + self.subscriptions.add(client) + + def mirror_path(self, conn_src, conn_dest, client, path, save=False): + """Mirror all interfaces and nodes of a D-Bus client object path. + + Parameters: + conn_src -- source D-Bus connection + conn_dest -- proxy D-Bus connection + client -- name of the client on the source bus + path -- object path to mirror recursively + save -- save the client name for the path + + """ + info = bus_introspect_path(conn_src, client, path) + for iface in info.interfaces: + if save: + self.proxy_client_save(path, client) + self.register_object(conn_dest, path, iface) + for node in info.nodes: + self.mirror_path(conn_src, conn_dest, client, + os.path.join(path, node.path), save) + + def mirror_source_on_proxy(self, client, path): + """Mirror source bus objects on the proxy bus.""" + self.signal_subscribe(self.source, client) + self.mirror_path(self.source, self.proxy, client, path) + + def mirror_proxy_on_source(self, client, path): + """Mirror proxy bus objects on the source bus.""" + self.signal_subscribe(self.proxy, client) + self.mirror_path(self.proxy, self.source, client, path, True) + + def on_method_call(self, conn, sender, *args, **kwargs): + if conn == self.source: + return self.on_method_call_from_source(sender, *args, **kwargs) + return self.on_method_call_from_proxy(sender, *args, **kwargs) + + def on_signal_received(self, conn, sender, *args, **kwargs): + if conn == self.source: + return self.on_signal_from_source(sender, *args, **kwargs) + return self.on_signal_from_proxy(sender, *args, **kwargs) + + def on_method_call_from_source(self, sender, path, iface, method, + params, invocation): + logging.debug("Call from source: %s %s.%s()", path, iface, method) + self.proxy.call(self.proxy_client_load(path), path, iface, method, + params, None, Gio.DBusCallFlags.NONE, -1, None, + self.on_method_return, invocation) + + def on_method_call_from_proxy(self, sender, path, iface, method, + params, invocation): + logging.debug("Call from proxy: %s %s.%s()", path, iface, method) + self.source.call(self.service, path, iface, method, + params, None, Gio.DBusCallFlags.NONE, -1, None, + self.on_method_return, invocation) + + def on_method_return(self, conn, result, invocation): + try: + logging.debug("Finishing call: %s %s.%s()", + invocation.get_object_path(), + invocation.get_interface_name(), + invocation.get_method_name()) + reply = conn.call_with_unix_fd_list_finish(result) + invocation.return_value_with_unix_fd_list(reply[0], + reply.out_fd_list) + except GLib.Error as e: + _, name, message = e.message.split(":", 2) + invocation.return_dbus_error(name, message.strip()) + + def on_signal_from_source(self, sender, path, iface, signal, params): + logging.debug("Signal from source: %s %s.%s", path, iface, signal) + if iface == "org.freedesktop.DBus.ObjectManager": + if signal == "InterfacesAdded": + dest_path = params.get_child_value(0).get_string() + self.mirror_source_on_proxy(self.service, dest_path) + if signal == "InterfacesRemoved": + dest_path = params.get_child_value(0).get_string() + for dest_iface in params.get_child_value(1).get_strv(): + self.unregister_object(self.proxy, dest_path, dest_iface) + self.proxy.emit_signal(None, path, iface, signal, params) + + def on_signal_from_proxy(self, sender, path, iface, signal, params): + logging.debug("Signal from proxy: %s %s.%s", path, iface, signal) + self.source.emit_signal(None, path, iface, signal, params) + + +class BluezProxy(DBusServiceProxy): + + def on_method_call_from_proxy(self, sender, path, iface, method, + params, invocation): + + if (iface == "org.bluez.GattManager1" and + method == "RegisterApplication"): + app_path = params.get_child_value(0).get_string() + logging.info("Mirroring GATT application: %s %s", sender, app_path) + self.mirror_proxy_on_source(sender, app_path) + + if iface == "org.bluez.LEAdvertisingManager1": + if method == "RegisterAdvertisement": + app_path = params.get_child_value(0).get_string() + logging.info("Mirroring advertiser: %s %s", sender, app_path) + self.mirror_proxy_on_source(sender, app_path) + + super().on_method_call_from_proxy(sender, path, iface, method, + params, invocation) + + +parser = ArgumentParser(description="BlueZ D-Bus proxy") +parser.add_argument( + "-v", "--verbose", action="store_true", + help="enable debug output") +parser.add_argument( + "--bus-source", metavar="ADDRESS", default="system", + help="""address of the source D-Bus bus; it can be a bus address string or + 'session' or 'system' keywords; default is '%(default)s'""") +parser.add_argument( + "--bus-proxy", metavar="ADDRESS", required=True, + help="""address of the proxy D-Bus bus""") + +args = parser.parse_args() +logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + +BluezProxy(args.bus_source, args.bus_proxy, "org.bluez") +GLib.MainLoop().run() diff --git a/src/platform/Linux/ConnectivityUtils.cpp b/src/platform/Linux/ConnectivityUtils.cpp index 8084ceca43b55a..72148d75fa64fc 100644 --- a/src/platform/Linux/ConnectivityUtils.cpp +++ b/src/platform/Linux/ConnectivityUtils.cpp @@ -261,16 +261,32 @@ InterfaceTypeEnum ConnectivityUtils::GetInterfaceConnectionType(const char * ifn { ret = InterfaceTypeEnum::kWiFi; } - else if ((strncmp(ifname, "en", 2) == 0) || (strncmp(ifname, "eth", 3) == 0)) + else { - struct ethtool_cmd ecmd = {}; - ecmd.cmd = ETHTOOL_GSET; - struct ifreq ifr = {}; - ifr.ifr_data = reinterpret_cast(&ecmd); - Platform::CopyString(ifr.ifr_name, ifname); + // During tests in CI WiFi interfaces are created by mac80211_hwsim driver + // Unfortunately, this driver does not support SIOCGIWNAME so we need to check it in a different way. + struct ethtool_drvinfo drvinfo = {}; + struct ifreq ifr_driver = {}; + drvinfo.cmd = ETHTOOL_GDRVINFO; + ifr_driver.ifr_data = reinterpret_cast(&drvinfo); + Platform::CopyString(ifr_driver.ifr_name, ifname); + if (ioctl(sock, SIOCETHTOOL, &ifr_driver) == 0 && strcmp(drvinfo.driver, "mac80211_hwsim") == 0) + { + ret = InterfaceTypeEnum::kWiFi; + } + else if ((strncmp(ifname, "en", 2) == 0) || (strncmp(ifname, "eth", 3) == 0)) + { + struct ethtool_cmd ecmd = {}; + ecmd.cmd = ETHTOOL_GSET; + struct ifreq ifr = {}; + ifr.ifr_data = reinterpret_cast(&ecmd); + Platform::CopyString(ifr.ifr_name, ifname); - if (ioctl(sock, SIOCETHTOOL, &ifr) != -1) - ret = InterfaceTypeEnum::kEthernet; + if (ioctl(sock, SIOCETHTOOL, &ifr) != -1) + { + ret = InterfaceTypeEnum::kEthernet; + } + } } close(sock); diff --git a/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.cpp b/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.cpp index c97ff2590fa60b..c75f9a7a096612 100644 --- a/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.cpp +++ b/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.cpp @@ -6697,4 +6697,4 @@ char const * GeneratedCommandIdToText(chip::ClusterId cluster, chip::CommandId i default: return "Unknown"; } -} +} \ No newline at end of file diff --git a/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.h b/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.h index 27fa6512fbdd01..f310593ba9e3e2 100644 --- a/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.h +++ b/zzz_generated/chip-tool/zap-generated/cluster/logging/EntryToText.h @@ -27,4 +27,4 @@ char const * AttributeIdToText(chip::ClusterId cluster, chip::AttributeId id); char const * AcceptedCommandIdToText(chip::ClusterId cluster, chip::CommandId id); -char const * GeneratedCommandIdToText(chip::ClusterId cluster, chip::CommandId id); +char const * GeneratedCommandIdToText(chip::ClusterId cluster, chip::CommandId id); \ No newline at end of file