Skip to content

Commit 3195025

Browse files
committed
Add qemu testing to CI
1 parent 4d05420 commit 3195025

File tree

7 files changed

+325
-9
lines changed

7 files changed

+325
-9
lines changed

.github/workflows/qemu.yaml

+63
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,66 @@ jobs:
9797
--target tizen-arm-tests-no-ble-no-thread \
9898
build
9999
"
100+
101+
qemu-linux:
102+
name: ubuntu
103+
104+
runs-on: ubuntu-latest
105+
if: github.actor != 'restyled-io[bot]'
106+
107+
container:
108+
image: ghcr.io/jlatusek/chip-build-linux-qemu:latest
109+
volumes:
110+
- "/tmp/log_output:/tmp/test_logs"
111+
# Required for using KVM
112+
options: --privileged
113+
114+
steps:
115+
- name: Checkout
116+
uses: actions/checkout@v4
117+
- name: Checkout submodules & Bootstrap
118+
uses: ./.github/actions/checkout-submodules-and-bootstrap
119+
with:
120+
platform: linux
121+
122+
- name: Build Apps
123+
run: |
124+
scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv'
125+
./scripts/run_in_build_env.sh \
126+
"./scripts/build/build_examples.py \
127+
--target linux-x64-chip-tool \
128+
--target linux-x64-all-clusters \
129+
build \
130+
--copy-artifacts-to objdir-clone \
131+
"
132+
# There is no enough space for running the test withouth cleaning the environment
133+
- name: Clean up
134+
run: |
135+
rm -rf out/*/obj
136+
rm -rf out/*/lib
137+
rm -rf $PW_ENVIRONMENT_ROOT
138+
git clean -fdx --exclude out
139+
# 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.
140+
- name: Run ble commission test using the python parser sending commands to chip-tool
141+
run: |
142+
./scripts/run_in_vm.sh \
143+
"./scripts/run_in_build_env.sh \
144+
\"./scripts/tests/run_test_suite.py \
145+
--runner chip_tool_python \
146+
--target TestCommissionerNodeId \
147+
--chip-tool ./out/linux-x64-chip-tool/chip-tool \
148+
run \
149+
--iterations 1 \
150+
--test-timeout-seconds 120 \
151+
--all-clusters-app ./out/linux-x64-all-clusters/chip-all-clusters-app \
152+
--lock-app ./out/linux-x64-lock/chip-lock-app \
153+
--ota-provider-app ./out/linux-x64-ota-provider/chip-ota-provider-app \
154+
--ota-requestor-app ./out/linux-x64-ota-requestor/chip-ota-requestor-app \
155+
--tv-app ./out/linux-x64-tv-app/chip-tv-app \
156+
--bridge-app ./out/linux-x64-bridge/chip-bridge-app \
157+
--lit-icd-app ./out/linux-x64-lit-icd/lit-icd-app \
158+
--microwave-oven-app .out/linux-x64-microwave-oven/chip-microwave-oven-app \
159+
--rvc-app .out/linux-x64-rvc/chip-rvc-app \
160+
--ble-wifi \
161+
\" \
162+
"

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ examples/*/esp32/dependencies.lock
8484
# jupyter temporary files
8585
.ipynb_checkpoints
8686

87+
/runner.sh
88+
/runner_status

scripts/run_in_vm.sh

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright (c) 2024 Project CHIP Authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# This script executes the command given as an argument after
19+
# activating the given python virtual environment
20+
set -e
21+
22+
PROJECT_PATH=$(dirname "$(dirname "$(realpath "$0")")")
23+
24+
echo "$@" >"$PROJECT_PATH/runner.sh"
25+
chmod +x "$PROJECT_PATH/runner.sh"
26+
27+
echo "CMD:"
28+
cat "$PROJECT_PATH/runner.sh"
29+
30+
"$PROJECT_PATH/integrations/docker/images/stage-2/chip-build-linux-qemu/run-img.sh"
31+
32+
if [ -f "$PROJECT_PATH/runner_status" ]; then
33+
exit "$(cat "$PROJECT_PATH/runner_status")"
34+
else
35+
exit 1
36+
fi

scripts/setup/requirements.all.txt

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mypy==0.971
4242
mypy-protobuf==3.5.0
4343
protobuf==4.24.4
4444
types-protobuf==4.24.0.2
45+
PyGObject
4546

4647
cryptography
4748

scripts/tests/chiptest/linux.py

+175
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,22 @@
1818
Handles linux-specific functionality for running test cases
1919
"""
2020

21+
import glob
2122
import logging
2223
import os
24+
import shutil
2325
import subprocess
2426
import sys
2527
import time
28+
from collections import namedtuple
29+
from time import sleep
30+
from typing import Optional
2631

2732
from .test_definition import ApplicationPaths
2833

2934
test_environ = os.environ.copy()
35+
PW_PROJECT_ROOT = os.environ.get("PW_PROJECT_ROOT")
36+
QEMU_CONFIG_FILES = "integrations/docker/images/stage-2/chip-build-linux-qemu/files"
3037

3138

3239
def EnsureNetworkNamespaceAvailability():
@@ -169,6 +176,174 @@ def ShutdownNamespaceForTestExecution():
169176
RemoveNamespaceForAppTest()
170177

171178

179+
class DbusTest:
180+
DBUS_SYSTEM_BUS_ADDRESS = "unix:path=/tmp/chip-dbus-test"
181+
182+
def __init__(self):
183+
self._dbus = None
184+
self._dbus_proxy = None
185+
186+
def start(self):
187+
original_env = os.environ.copy()
188+
os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = DbusTest.DBUS_SYSTEM_BUS_ADDRESS
189+
dbus = shutil.which("dbus-daemon")
190+
self._dbus = subprocess.Popen(
191+
[dbus, "--session", "--address", self.DBUS_SYSTEM_BUS_ADDRESS])
192+
193+
self._dbus_proxy = subprocess.Popen(
194+
["python3", f"{PW_PROJECT_ROOT}/scripts/tools/dbus-proxy-bluez.py", "--bus-proxy", DbusTest.DBUS_SYSTEM_BUS_ADDRESS], env=original_env)
195+
196+
def stop(self):
197+
if self._dbus:
198+
self._dbus_proxy.terminate()
199+
self._dbus.terminate()
200+
self._dbus.wait()
201+
202+
203+
class VirtualWifi:
204+
def __init__(self, hostapd_path: str, dnsmasq_path: str, wpa_supplicant_path: str, wlan_app: Optional[str] = None, wlan_tool: Optional[str] = None):
205+
self._hostapd_path = hostapd_path
206+
self._dnsmasq_path = dnsmasq_path
207+
self._wpa_supplicant_path = wpa_supplicant_path
208+
self._hostapd_conf = os.path.join(
209+
PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/hostapd.conf")
210+
self._dnsmasq_conf = os.path.join(
211+
PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/dnsmasq.conf")
212+
self._wpa_supplicant_conf = os.path.join(
213+
PW_PROJECT_ROOT, QEMU_CONFIG_FILES, "wifi/wpa_supplicant.conf")
214+
215+
if wlan_app is None or wlan_tool is None:
216+
wlans = glob.glob(
217+
"/sys/devices/virtual/mac80211_hwsim/hwsim*/net/*")
218+
if len(wlans) < 2:
219+
raise RuntimeError("Not enough wlan devices found")
220+
221+
self._wlan_app = os.path.basename(wlans[0])
222+
self._wlan_tool = os.path.basename(wlans[1])
223+
else:
224+
self._wlan_app = wlan_app
225+
self._wlan_tool = wlan_tool
226+
self._hostapd = None
227+
self._dnsmasq = None
228+
self._wpa_supplicant = None
229+
230+
@staticmethod
231+
def _get_phy(dev: str) -> str:
232+
output = subprocess.check_output(['iw', 'dev', dev, 'info'])
233+
for line in output.split(b'\n'):
234+
if b'wiphy' in line:
235+
wiphy = int(line.split(b' ')[1])
236+
return f"phy{wiphy}"
237+
raise ValueError(f'No wiphy found for {dev}')
238+
239+
@staticmethod
240+
def _move_phy_to_netns(phy: str, netns: str):
241+
subprocess.check_call(
242+
["iw", "phy", phy, "set", "netns", "name", netns])
243+
244+
@staticmethod
245+
def _set_interface_ip_in_netns(netns: str, dev: str, ip: str):
246+
subprocess.check_call(
247+
["ip", "netns", "exec", netns, "ip", "link", "set", "dev", dev, "up"])
248+
subprocess.check_call(
249+
["ip", "netns", "exec", netns, "ip", "addr", "add", ip, "dev", dev])
250+
251+
def start(self):
252+
self._move_phy_to_netns(self._get_phy(self._wlan_app), 'app')
253+
self._move_phy_to_netns(self._get_phy(self._wlan_tool), 'tool')
254+
self._set_interface_ip_in_netns(
255+
'tool', self._wlan_tool, '192.168.200.1/24')
256+
257+
self._hostapd = subprocess.Popen(["ip", "netns", "exec", "tool", self._hostapd_path,
258+
self._hostapd_conf], stdout=subprocess.DEVNULL)
259+
self._dnsmasq = subprocess.Popen(["ip", "netns", "exec", "tool", self._dnsmasq_path,
260+
'-d', '-C', self._dnsmasq_conf], stdout=subprocess.DEVNULL)
261+
self._wpa_supplicant = subprocess.Popen(
262+
["ip", "netns", "exec", "app", self._wpa_supplicant_path, "-u", '-s', '-c', self._wpa_supplicant_conf], stdout=subprocess.DEVNULL)
263+
264+
def stop(self):
265+
if self._hostapd:
266+
self._hostapd.terminate()
267+
self._hostapd.wait()
268+
if self._dnsmasq:
269+
self._dnsmasq.terminate()
270+
self._dnsmasq.wait()
271+
if self._wpa_supplicant:
272+
self._wpa_supplicant.terminate()
273+
self._wpa_supplicant.wait()
274+
275+
276+
class VirtualBle:
277+
BleDevice = namedtuple('BleDevice', ['hci', 'mac', 'index'])
278+
279+
def __init__(self, btvirt_path: str, bluetoothctl_path: str):
280+
self._btvirt_path = btvirt_path
281+
self._bluetoothctl_path = bluetoothctl_path
282+
self._btvirt = None
283+
self._bluetoothctl = None
284+
self._ble_app = None
285+
self._ble_tool = None
286+
287+
@property
288+
def ble_app(self) -> Optional[BleDevice]:
289+
if not self._ble_app:
290+
raise RuntimeError("Bluetooth not started")
291+
return self._ble_app
292+
293+
@property
294+
def ble_tool(self) -> Optional[BleDevice]:
295+
if not self._ble_tool:
296+
raise RuntimeError("Bluetooth not started")
297+
return self._ble_tool
298+
299+
def bletoothctl_cmd(self, cmd):
300+
self._bluetoothctl.stdin.write(cmd)
301+
self._bluetoothctl.stdin.flush()
302+
303+
def _get_mac_address(self, hci_name):
304+
result = subprocess.run(
305+
['hcitool', 'dev'], capture_output=True, text=True)
306+
lines = result.stdout.splitlines()
307+
308+
for line in lines:
309+
if hci_name in line:
310+
mac_address = line.split()[1]
311+
return mac_address
312+
313+
raise RuntimeError(f"No MAC address found for device {hci_name}")
314+
315+
def _get_ble_info(self):
316+
ble_dev_paths = glob.glob("/sys/devices/virtual/bluetooth/hci*")
317+
hci = [os.path.basename(path) for path in ble_dev_paths]
318+
if len(hci) < 2:
319+
raise RuntimeError("Not enough BLE devices found")
320+
self._ble_app = self.BleDevice(
321+
hci=hci[0], mac=self._get_mac_address(hci[0]), index=int(hci[0].replace('hci', '')))
322+
self._ble_tool = self.BleDevice(
323+
hci=hci[1], mac=self._get_mac_address(hci[1]), index=int(hci[1].replace('hci', '')))
324+
325+
def _run_bluetoothctl(self):
326+
self._bluetoothctl = subprocess.Popen([self._bluetoothctl_path], text=True,
327+
stdin=subprocess.PIPE, stdout=subprocess.DEVNULL)
328+
self.bletoothctl_cmd(f"select {self.ble_app.mac}\n")
329+
self.bletoothctl_cmd("power on\n")
330+
self.bletoothctl_cmd(f"select {self.ble_tool.mac}\n")
331+
self.bletoothctl_cmd("power on\n")
332+
self.bletoothctl_cmd("quit\n")
333+
self._bluetoothctl.wait()
334+
335+
def start(self):
336+
self._btvirt = subprocess.Popen([self._btvirt_path, '-l2'])
337+
sleep(1)
338+
self._get_ble_info()
339+
self._run_bluetoothctl()
340+
341+
def stop(self):
342+
if self._btvirt:
343+
self._btvirt.terminate()
344+
self._btvirt.wait()
345+
346+
172347
def PathsWithNetworkNamespaces(paths: ApplicationPaths) -> ApplicationPaths:
173348
"""
174349
Returns a copy of paths with updated command arrays to invoke the

scripts/tests/chiptest/test_definition.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ def tags_str(self) -> str:
284284
return ", ".join([t.to_s() for t in self.tags])
285285

286286
def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str,
287-
timeout_seconds: typing.Optional[int], dry_run=False, test_runtime: TestRunTime = TestRunTime.CHIP_TOOL_PYTHON):
287+
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):
288288
"""
289289
Executes the given test case using the provided runner for execution.
290290
"""
@@ -331,8 +331,11 @@ def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str,
331331
key = 'default'
332332
else:
333333
key = os.path.basename(path[-1])
334-
335-
app = App(runner, path)
334+
ble_wifi_cmd = []
335+
if app_hci_number is not None:
336+
ble_wifi_cmd = ["--ble-device",
337+
str(app_hci_number), "--wifi"]
338+
app = App(runner, path + ble_wifi_cmd)
336339
# Add the App to the register immediately, so if it fails during
337340
# start() we will be able to clean things up properly.
338341
apps_register.add(key, app)
@@ -372,11 +375,19 @@ def Run(self, runner, apps_register, paths: ApplicationPaths, pics_file: str,
372375
runner.RunSubprocess(python_cmd, name='CHIP_REPL_YAML_TESTER',
373376
dependencies=[apps_register], timeout_seconds=timeout_seconds)
374377
else:
375-
pairing_cmd = paths.chip_tool_with_python_cmd + ['pairing', 'code', TEST_NODE_ID, setupCode]
378+
pairing_server_args = []
379+
if tool_hci_number is not None:
380+
pairing_cmd = paths.chip_tool_with_python_cmd + [
381+
"pairing", "ble-wifi", TEST_NODE_ID, "Virtual_Wifi", "ExamplePassword", "20202021", "3840", ]
382+
pairing_server_args = [
383+
"--ble-adapter", str(tool_hci_number)]
384+
else:
385+
pairing_cmd = paths.chip_tool_with_python_cmd + ['pairing', 'code', TEST_NODE_ID, setupCode]
376386
test_cmd = paths.chip_tool_with_python_cmd + ['tests', self.run_name] + ['--PICS', pics_file]
377387
server_args = ['--server_path', paths.chip_tool[-1]] + \
378388
['--server_arguments', 'interactive server' +
379-
(' ' if len(tool_storage_args) else '') + ' '.join(tool_storage_args)]
389+
(' ' if len(tool_storage_args) else '') + ' '.join(tool_storage_args) +
390+
(' ' if len(pairing_server_args) else '') + ' '.join(pairing_server_args)]
380391
pairing_cmd += server_args
381392
test_cmd += server_args
382393

0 commit comments

Comments
 (0)