Functional/integration testing
Subsystems (2)
v3 cover Functional/integration testing
Add framework for writing tests simulating "real" environments where BlueZ and other parts of the stack run on different virtual machine hosts that communicate with each other.
*** v3 ***
https://github.com/pv/bluez/compare/func-test-v2-r..func-test-v3
* fix configure.ac openpty() detection to match TOOLS conditional, to fix make distcheck
* properly retry virtio RPC connection if it fails initially
* properly restart VM if previous test hangs
* allow custom parent host side proxy objects, use them for pexpect
* improve --list with out-of-tree test files
* fix missing bus.set_exit_on_disconnect(False) for obex tests
* have --vm-timeout etc. change values also on VM host side
* use larger-memory VM instances for Pipewire, in case ASAN enabled
* set reasonable inside-VM ASAN_OPTION default values
* don't run btvirt under stdbuf, since not compatible with ASAN
*** v2 ***
https://github.com/pv/bluez/compare/func-test-v1-r..func-test-v2
* move unit/func_test -> test/functional & test/pytest_bluez
The pytest_bluez plugin is in principle reusable for other projects, so we can eg. have more complete Pipewire integration tests that can live in Pipewire repository.
* openpty() is in -lutil on some platforms, detect this in autoconf
* more emulator adjustments:
- fix SCO data packet support in btvirt
- more complete Reset command
* improve logging: get timestamps from kernel, and reorder logs to timestamp order, so that lines from different hosts, btmon, and parent tester appear in right order regardless of whether VM console / btmon is lagging
- this requires accurate clock sync in the VM, so enable KVM PTP in config and run chronyd inside the VMs
- use virtio port instead of qemu console to export logs, since the console has fixed baud rate and is too slow
* add --btmon & export btsnoop dumps from VM hosts
* fix compatibility with older Python versions
* add parametrized_host_config()
* split Pipewire test to A2DP/BAP/HFP and really stream audio. These catch the 5.86 regression fixed in 066a164a524e498 and the 5.84 one in 6b0a08776a
* add support for tests that reuse tester environment, so they can run faster without needing Bluetoothd teardown/setup in between
* add HostPlugin.presetup (mainly for test skipping)
* deal with RPC virtio port buffer possibly containing unflushed commands from previous failed test
* add some Agent1 interface tests
* add basic Obex file transfer tests
* add support for logging in to a running test instance (for gdb etc)
* export any core dumps out from test environ
Some bells & whistles:
* add --kernel-build for kernel image build
* test suite Python code formatting checks
***
Implements:
- RPC communication with tester instances running each of the VM hosts. Tests run on parent host, which instructs VM hosts what to do.
- Extensible way to add stateful test-specific code inside the VM instances
- Logging control: output from different processes running inside the VM are separated and can be filtered.
- Test runner framework with Pytest (more convenient than Python/unittest)
- Automatic grouping of tests to minimize VM reboots
- Redirecting USB controllers to use for testing in addition to btvirt
- Fairly straightforward, ~1600 sloc for the framework
There is no requirement that the tests spawn VM instances, the test runner can be used for any tests written in Python.
See doc/test-functional.rst for various examples.
Also test/functional/test_bluetoothctl_vm.py has some simple cases, and test/functional/test_pipewire.py for a more complicated setup
host0(qemu): Pipewire <-> BlueZ <-> kernel
<-> btvirt
host1(qemu): kernel <-> BlueZ <-> PipewireThe framework allows easily passing any data and code between the parent and VM hosts, so writing tests is straightforward.
***
Some examples:
$ test/test-functional --list -q
test/functional/lib/tests/test_rpc.py::test_basic test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2]
$ test/test-functional -v --no-header ======================================= test session starts ======================================== collected 5 items
test/functional/lib/tests/test_rpc.py::test_basic PASSED [ 20%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1] SKIPPED [ 40%] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image) [ 60%]
test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] SKIPPED (No k...) [ 80%]
test/functional/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image) [100%]
=================================== 1 passed, 4 skipped in 0.19s ===================================
$ test/test-functional --kernel=../linux ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items
test/functional/lib/tests/test_rpc.py . [ 20%] test/functional/test_bluetoothctl_vm.py . [ 40%] test/functional/test_btmgmt_vm.py . [ 60%] test/functional/test_bluetoothctl_vm.py . [ 80%] test/functional/test_pipewire.py . [100%]
============================== 5 passed in 41.92s ==============================
$ test/test-functional --kernel=../linux -k test_btmgmt ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py . [100%]
======================= 1 passed, 4 deselected in 9.15s ========================
$ grep btmgmt test-functional.log
13:15:42 INFO rpc.host.0.0 : client: call_plugin ('call', '__call__', <function run at 0x7f27b81ce140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'}
13:15:42 INFO host.0.0.rpc : server: call_plugin ('call', '__call__', <function run at 0x7fd5e35a1010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'}
13:15:42 INFO host.0.0.run : $ /home/pauli/prj/external/bluez/build/tools/btmgmt --index 0 info$ test/test-functional --kernel=../linux -k test_btmgmt --log-cli-level=0 ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] -------------------------------- live log setup --------------------------------
13:00:31 INFO func_test.lib.env : Starting btvirt: /usr/bin/stdbuf -o L -e L /home/pauli/prj/external/bluez/build/emulator/btvirt --server=/tmp/bluez-func-test-8t6ychy8
13:00:31 OUT btvirt : Bluetooth emulator ver 5.86
13:00:31 INFO func_test.lib.env : Starting host: /home/pauli/prj/external/bluez/build/tools/test-runner --kernel=../linux/arch/x86/boot/bzImage -u/tmp/bluez-func-test-8t6ychy8/bt-server-bredrle -o -chardev -o socket,id=ser0,path=/tmp/bluez-func-test-8t6ychy8/bluez-func-test-rpc-0,server=on,wait=off -o -device -o virtio-serial -o -device -o virtserialport,chardev=ser0,name=bluez-func-test-rpc -H -- /usr/bin/python3 -P /home/pauli/prj/external/bluez/test/functional/lib/runner.py /dev/ttyS2
13:00:31 OUT btvirt : Request for /tmp/bluez-func-test-8t6ychy8/bt-server-bredrle
13:00:32 OUT host.0.0 : early console in extract_kernel
13:00:32 OUT host.0.0 : input_data: 0x000000000425c2c4
...
13:00:39 INFO rpc.host.0.0 : client: call_plugin ('call', '__call__', <function run at 0x7f7547472140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'}
13:00:39 DEBUG host.0.0.rpc : server: done
13:00:39 INFO host.0.0.rpc : server: call_plugin ('call', '__call__', <function run at 0x7f77dcc81010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'}
13:00:39 INFO host.0.0.run : $ /home/pauli/prj/external/bluez/build/tools/btmgmt --index 0 info
13:00:40 OUT host.0.0.run.out : hci0: Primary controller
13:00:40 OUT host.0.0.run.out : addr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000
13:00:40 OUT host.0.0.run.out : supported settings: powered connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past-sender past-receiver
13:00:40 OUT host.0.0.run.out : current settings: br/edr
13:00:40 OUT host.0.0.run.out : name
13:00:40 OUT host.0.0.run.out : short name
13:00:40 INFO host.0.0.run : (return code 0)
13:00:40 DEBUG rpc.host.0.0 : client-replyPASSED [100%]
13:00:40 OUT host.0.0 : qemu-system-x86_64: terminating on signal 15 from pid 149047 (python3)
======================= 1 passed, 4 deselected in 8.84s ========================
$ test/test-functional --kernel=../linux -k test_bluetoothctl_pair --log-cli-level=0 --log-filter=*.bluetoothctl,rpc.* --force-usb ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] -------------------------------- live log setup --------------------------------
13:03:20 INFO rpc.host.0.0 : client: start_load (<func_test.lib.host_plugins.Bdaddr object at 0x7f268712d160>,) {}
13:03:20 INFO rpc.host.0.0 : client: start_load (<func_test.lib.host_plugins.Call object at 0x7f268712d2b0>,) {}
13:03:20 INFO rpc.host.0.0 : client: start_load (<func_test.lib.host_plugins.DbusSystem object at 0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.0 : client: start_load (<func_test.lib.host_plugins.Bluetoothd object at 0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.0 : client: start_load (<func_test.lib.host_plugins.Bluetoothctl object at 0x7f268712d010>,) {}
13:03:20 INFO rpc.host.0.1 : client: start_load (<func_test.lib.host_plugins.Bdaddr object at 0x7f26871542d0>,) {}
13:03:20 INFO rpc.host.0.1 : client: start_load (<func_test.lib.host_plugins.Call object at 0x7f2687154410>,) {}
13:03:20 INFO rpc.host.0.1 : client: start_load (<func_test.lib.host_plugins.DbusSystem object at 0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.1 : client: start_load (<func_test.lib.host_plugins.Bluetoothd object at 0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.1 : client: start_load (<func_test.lib.host_plugins.Bluetoothctl object at 0x7f2687154190>,) {}
13:03:20 INFO rpc.host.0.0 : client: wait_load () {}
13:03:21 DEBUG rpc.host.0.0 : client-reply
13:03:21 INFO rpc.host.0.1 : client: wait_load () {}
13:03:21 DEBUG rpc.host.0.1 : client-reply
-------------------------------- live log call ---------------------------------
13:03:21 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'send', 'show\n') {}
13:03:21 DEBUG rpc.host.0.0 : client-reply
13:03:21 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'expect', 'Powered: yes') {}
...
13:03:23 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'send', 'pair 70:1a:b8:73:99:bb\n') {}
13:03:23 OUT host.0.0.bluetoothctl: pair 70:1a:b8:73:99:bb
13:03:23 DEBUG rpc.host.0.0 : client-reply
13:03:23 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'expect', 'Confirm passkey (\\d+).*:') {}
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> pair 70:1a:b8:73:99:bb
13:03:23 OUT host.0.0.bluetoothctl: Attempting to pair with 70:1A:B8:73:99:BB
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 device_flags_changed: 70:1A:B8:73:99:BB (BR/EDR)
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> supp: 0x00000007 curr: 0x00000000
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 type 7 discovering off
13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 70:1A:B8:73:99:BB type BR/EDR connected eir_len 12
13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Connected: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> Request confirmation
13:03:25 DEBUG rpc.host.0.0 : client-reply
13:03:25 INFO rpc.host.0.1 : client: call_plugin ('bluetoothctl', 'expect', 'Confirm passkey 237345') {}
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> hci0 84:5C:F3:77:31:19 type BR/EDR connected eir_len 12
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [NEW] Device 84:5C:F3:77:31:19 BlueZ 5.86
13:03:25 DEBUG rpc.host.0.1 : client-reply
13:03:25 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> Request confirmation
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.0 : client-reply
13:03:25 INFO rpc.host.0.1 : client: call_plugin ('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.1 : client-reply
13:03:25 INFO rpc.host.0.0 : client: call_plugin ('bluetoothctl', 'expect', 'Pairing successful') {}
13:03:25 OUT host.0.0.bluetoothctl: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> hci0 new_link_key 70:1A:B8:73:99:BB type 0x08 pin_len 0 store_hint 1
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Bonded: yes
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB AddressType: public
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110c-0000-1000-8000-00805f9b34fb
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110e-0000-1000-8000-00805f9b34fb
13:03:26 DEBUG rpc.host.0.0 : client-replyPASSED [100%]
------------------------------ live log teardown -------------------------------
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:98:FF:qemu-system-x86_64: terminating on signal 15 from pid 149357 (python3)
======================= 1 passed, 4 deselected in 13.22s =======================
$ test/test-functional -k test_btmgmt --kernel=../linux --trace ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py
Quoted 2 lines
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
Quoted 1 line
PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(19)test_btmgmt_info()
-> (host,) = hosts
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21)test_btmgmt_info()
-> result = host.call(
(Pdb) p host.bdaddr
'00:aa:01:00:00:42'
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(22)test_btmgmt_info()
-> run,
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(23)test_btmgmt_info()
-> [btmgmt, "--index", "0", "info"],
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(24)test_btmgmt_info()
-> stdout=subprocess.PIPE,
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(25)test_btmgmt_info()
-> stdin=subprocess.DEVNULL,
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(26)test_btmgmt_info()
-> encoding="utf-8",
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21)test_btmgmt_info()
-> result = host.call(
(Pdb) n
Quoted 1 line
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(28)test_btmgmt_info()
-> assert result.returncode == 0
(Pdb) p result
CompletedProcess(args=['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info'], returncode=0, stdout='hci0:\tPrimary controller\n\taddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000\n\tsupported settings: powered connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past-sender past-receiver \n\tcurrent settings: br/edr \n\tname \n\tshort name \n') (Pdb) print(result.stdout)
hci0: Primary controller
addr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000
supported settings: powered connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past-sender past-receiver
current settings: br/edr
name
short name
(Pdb) q
!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!! ======================= 4 deselected in 75.91s (0:01:15) =======================
***
Pauli Virtanen (20):
emulator: btvirt: check pkt lengths, don't get stuck on malformed
emulator: btvirt: allow specifying where server unix sockets are made
emulator: btvirt: support SCO data packets
emulator: btdev: clear more state on Reset
test-runner: enable path argument for --unix
test-runner: Add -o/--option option
test-runner: allow source tree root for -k
doc: enable CONFIG_VIRTIO_CONSOLE in tester config
test-runner: use virtio-serial for implementing -u device forwarding
doc: enable KVM paravirtualization & clock support in tester kernel
config
doc: add functional/integration testing documentation
test: add functional/integration testing framework
test: functional: add Pipewire-using audio streaming tests
test: functional: add --btmon option to start btmon
build: add functional testing target
test: functional: impose Python code formatting
test: functional: add option for building kernel image first
test: functional: add custom Agent1 implementation for testing
test: functional: warn on kernel warning/bug messages
test: functional: add basic obex file transfer tests
Makefile.am | 9 + Makefile.tools | 2 + configure.ac | 31 + doc/ci.config | 9 + doc/test-functional.rst | 949 ++++++++++++++++++++++++ doc/test-runner.rst | 17 + doc/tester.config | 9 + emulator/btdev.c | 117 +-- emulator/main.c | 37 +- emulator/server.c | 21 + test/functional/__init__.py | 2 + test/functional/requirements.txt | 4 + test/functional/test_agent.py | 46 ++ test/functional/test_bluetoothctl_vm.py | 152 ++++ test/functional/test_btmgmt_vm.py | 30 + test/functional/test_obex.py | 285 +++++++ test/functional/test_pipewire.py | 539 ++++++++++++++ test/functional/test_tests.py | 23 + test/pytest.ini | 18 + test/pytest_bluez/__init__.py | 12 + test/pytest_bluez/agent.py | 371 +++++++++ test/pytest_bluez/btmon.py | 116 +++ test/pytest_bluez/build_kernel.py | 43 ++ test/pytest_bluez/env.py | 803 ++++++++++++++++++++ test/pytest_bluez/host_plugins.py | 627 ++++++++++++++++ test/pytest_bluez/plugin.py | 637 ++++++++++++++++ test/pytest_bluez/rpc.py | 385 ++++++++++ test/pytest_bluez/runner.py | 20 + test/pytest_bluez/tests/__init__.py | 2 + test/pytest_bluez/tests/test_rpc.py | 62 ++ test/pytest_bluez/tests/test_utils.py | 16 + test/pytest_bluez/utils.py | 706 ++++++++++++++++++ test/test-functional | 7 + test/test-functional-attach | 52 ++ tools/test-runner.c | 376 +++++++--- 35 files changed, 6383 insertions(+), 152 deletions(-) create mode 100644 doc/test-functional.rst create mode 100644 test/functional/__init__.py create mode 100644 test/functional/requirements.txt create mode 100644 test/functional/test_agent.py create mode 100644 test/functional/test_bluetoothctl_vm.py create mode 100644 test/functional/test_btmgmt_vm.py create mode 100644 test/functional/test_obex.py create mode 100644 test/functional/test_pipewire.py create mode 100644 test/functional/test_tests.py create mode 100644 test/pytest.ini create mode 100644 test/pytest_bluez/__init__.py create mode 100644 test/pytest_bluez/agent.py create mode 100644 test/pytest_bluez/btmon.py create mode 100644 test/pytest_bluez/build_kernel.py create mode 100644 test/pytest_bluez/env.py create mode 100644 test/pytest_bluez/host_plugins.py create mode 100644 test/pytest_bluez/plugin.py create mode 100644 test/pytest_bluez/rpc.py create mode 100644 test/pytest_bluez/runner.py create mode 100644 test/pytest_bluez/tests/__init__.py create mode 100644 test/pytest_bluez/tests/test_rpc.py create mode 100644 test/pytest_bluez/tests/test_utils.py create mode 100644 test/pytest_bluez/utils.py create mode 100755 test/test-functional create mode 100755 test/test-functional-attach
Signature · 1 line
2.53.0
PATCH v3 1/20 emulator: btvirt: check pkt lengths, don't get stuck on malformed
Don't try to parse packet before whole header is received.
If received data has unknown packet type, reset buffer so that we don't get stuck.
--- emulator/server.c | 9 +++++++++ 1 file changed, 9 insertions(+)
emulator/server.c +9 -0
@@ -136,12 +136,20 @@ again: client->pkt_len =0 ;break ;case HCI_ACLDATA_PKT : +if (count <HCI_ACL_HDR_SIZE +1 ) { + client->pkt_offset += len; +return ; + } acl_hdr = (hci_acl_hdr *)(ptr +1 ); client->pkt_expect =HCI_ACL_HDR_SIZE + acl_hdr->dlen +1 ; client->pkt_data =malloc (client->pkt_expect); client->pkt_len =0 ;break ;case HCI_ISODATA_PKT : +if (count <HCI_ISO_HDR_SIZE +1 ) { + client->pkt_offset += len; +return ; + } iso_hdr = (hci_iso_hdr *)(ptr +1 ); client->pkt_expect =HCI_ISO_HDR_SIZE + iso_hdr->dlen +1 ;
@@ -151,6 +159,7 @@ again:default :printf ("packet error, unknown type: %d\n" , client->pkt_type); + client->pkt_offset =0 ;return ; }
Signature · 1 line
2.53.0
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list. This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1070560
---Test result---
Test Summary:
CheckPatch PENDING 0.39 seconds GitLint PENDING 0.35 seconds BuildEll PASS 21.16 seconds BluezMake PASS 648.87 seconds MakeCheck PASS 19.44 seconds MakeDistcheck PASS 252.06 seconds CheckValgrind PASS 295.83 seconds CheckSmatch WARNING 357.92 seconds bluezmakeextell PASS 184.14 seconds IncrementalBuild PENDING 0.29 seconds ScanBuild PASS 1015.16 seconds
Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script Output:
##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:
##############################
Test: CheckSmatch - WARNING
Desc: Run smatch tool with source
Output:emulator/btdev.c:480:29: warning: Variable length array is used.
##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series Output:
https://github.com/bluez/bluez/pull/1981/checks
---
Regards,
Linux Bluetooth
PATCH v3 2/20 emulator: btvirt: allow specifying where server unix sockets are made
In reply to Pauli VirtanenMake --server to take optional path name where to create the various server sockets.
--- emulator/main.c | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-)
emulator/main.c +24 -13
@@ -18,6 +18,7 @@#include <stdbool.h> #include <getopt.h> #include <sys/uio.h> +#include <limits.h> #include "src/shared/mainloop.h" #include "src/shared/util.h"
@@ -46,7 +47,7 @@ static void usage(void)printf ("options:\n" "\t-d Enable debug\n" "\t-S Create local serial port\n" -"\t-s Create local server sockets\n" +"\t-s[path=/tmp] Create local server sockets\n" "\t-t[port=45550] Create a TCP server\n" "\t-l[num] Number of local controllers\n" "\t-L Create LE only controller\n"
@@ -60,7 +61,7 @@ static void usage(void)static const struct option main_options[] = { {"debug" , no_argument,NULL ,'d' }, {"serial" , no_argument,NULL ,'S' }, - {"server" , no_argument,NULL ,'s' }, + {"server" , optional_argument,NULL ,'s' }, {"tcp" , optional_argument,NULL ,'t' }, {"local" , optional_argument,NULL ,'l' }, {"le" , no_argument,NULL ,'L' },
@@ -88,6 +89,7 @@ int main(int argc, char *argv[])struct server *server5;bool debug_enabled = false;bool server_enabled = false; +const char *server_path ="/tmp" ;uint16_t tcp_port =0 ;bool serial_enabled = false;int letest_count =0 ;
@@ -100,7 +102,7 @@ int main(int argc, char *argv[])for (;;) {int opt; - opt =getopt_long (argc, argv,"dSst::l::LBAU::T::vh" , + opt =getopt_long (argc, argv,"dSs::t::l::LBAU::T::vh" , main_options,NULL );if (opt <0 )break ;
@@ -114,6 +116,8 @@ int main(int argc, char *argv[])break ;case 's' : server_enabled = true; +if (optarg) + server_path = optarg;break ;case 't' :if (optarg)
@@ -196,28 +200,35 @@ int main(int argc, char *argv[]) }if (server_enabled) { - server1 =server_open_unix (SERVER_TYPE_BREDRLE , -"/tmp/bt-server-bredrle" ); +char path[PATH_MAX ]; + +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server-bredrle" ); + server1 =server_open_unix (SERVER_TYPE_BREDRLE , path);if (!server1)fprintf (stderr,"Failed to open BR/EDR/LE server\n" ); - server2 =server_open_unix (SERVER_TYPE_BREDR , -"/tmp/bt-server-bredr" ); +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server-bredr" ); + server2 =server_open_unix (SERVER_TYPE_BREDR , path);if (!server2)fprintf (stderr,"Failed to open BR/EDR server\n" ); - server3 =server_open_unix (SERVER_TYPE_AMP , -"/tmp/bt-server-amp" ); +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server-amp" ); + server3 =server_open_unix (SERVER_TYPE_AMP , path);if (!server3)fprintf (stderr,"Failed to open AMP server\n" ); - server4 =server_open_unix (SERVER_TYPE_LE , -"/tmp/bt-server-le" ); +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server-le" ); + server4 =server_open_unix (SERVER_TYPE_LE , path);if (!server4)fprintf (stderr,"Failed to open LE server\n" ); - server5 =server_open_unix (SERVER_TYPE_MONITOR , -"/tmp/bt-server-mon" ); +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server-mon" ); + server5 =server_open_unix (SERVER_TYPE_MONITOR , path);if (!server5)fprintf (stderr,"Failed to open monitor server\n" ); }
Signature · 1 line
2.53.0
Re: [PATCH BlueZ v3 02/20] emulator: btvirt: allow specifying where server unix sockets are made
Quoted 81 lines · Pauli Virtanen
Make --server to take optional path name where to create the various server sockets.
---
emulator/main.c | 37 ++++++++++++++++++++++++-------------1 file changed, 24 insertions(+), 13 deletions(-)emulator/main.c +1
-0@@ -18,6 +18,7 @@ #include <stdbool.h> #include <getopt.h> #include <sys/uio.h> +#include <limits.h>
#include "src/shared/mainloop.h"
#include "src/shared/util.h"emulator/main.c +0
-0@@ -46,7 +47,7 @@ static void usage(void)printf("options:\n"
"\t-d Enable debug\n" "\t-S Create local serial port\n"emulator/main.c +0
-1- "\t-s Create local serversockets\n"
emulator/main.c +1
-0+ "\t-s[path=/tmp] Create local serversockets\n"
"\t-t[port=45550] Create a TCP server\n" "\t-l[num] Number of local controllers\n"
"\t-L Create LE only controller\n"emulator/main.c +2
-1@@ -60,7 +61,7 @@ static void usage(void) static const struct option main_options[] = { { "debug", no_argument, NULL, 'd' }, { "serial", no_argument, NULL, 'S' }, - { "server", no_argument, NULL, 's' }, + { "server", optional_argument, NULL, 's' }, { "tcp", optional_argument, NULL, 't' }, { "local", optional_argument, NULL, 'l' }, { "le", no_argument, NULL, 'L' },@@ -88,6 +89,7 @@ int main(int argc, char *argv[]) struct server *server5; bool debug_enabled = false; bool server_enabled = false; +const char *server_path ="/tmp" ; uint16_t tcp_port = 0; bool serial_enabled = false; int letest_count = 0;@@ -100,7 +102,7 @@ int main(int argc, char *argv[]) for (;;) { int opt;emulator/main.c +0
-1- opt = getopt_long(argc, argv,"
dSst::l::LBAU::T::vh",emulator/main.c +1
-0+ opt = getopt_long(argc, argv,"
dSs::t::l::LBAU::T::vh",
main_options, NULL); if (opt < 0)
break;emulator/main.c +0
-0@@ -114,6 +116,8 @@ int main(int argc, char *argv[]) break;case 's':
server_enabled= true;emulator/main.c +2
-0+if (optarg) + server_path = optarg;break;
case 't':
if (optarg)emulator/main.c +0
-0@@ -196,28 +200,35 @@ int main(int argc, char *argv[]) }
if (server_enabled) {emulator/main.c +0
-2- server1 = server_open_unix(SERVER_TYPE_BREDRLE, - "/tmp/bt-server-bredrle");
emulator/main.c +4
-0+char path[PATH_MAX ]; + + snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-bredrle");
emulator/main.c +1
-0+ server1 = server_open_unix(SERVER_TYPE_BREDRLE,
Quoted 1 line
path);
If we were allowed to use new glib calls, this hand-rolled code should probably use like g_build_filename():
https://docs.gtk.org/glib/func.build_filename.html
and g_auto:
https://docs.gtk.org/glib/auto-cleanup.html
to simplify the code.
Luiz, could we have have an update to maintainer-guidelines.rst to reflect the current guidelines for using or adding glib calls to the code?
Pauli, do you think we could have helpers for this sort of path manipulation in src/shared? Not saying it needs to be to the level of complexity of the g_build_filename() implementation, but snprintf()...:
https://github.com/GNOME/glib/blob/main/glib/gfileutils.c#L1926
Luiz, another question, could we start using __attribute__(cleanup()) in bluez?
Consider the following example, adapted from systemd:
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static inline void freep(void *p) {
free(*(void**) p);
}#define _cleanup_(f) __attribute__((cleanup(f)))
#define _cleanup_free_ _cleanup_(freep)
int main(int argc, char **argv)
{
_cleanup_free_ char *foo = NULL;
foo = strdup("my string");
printf ("%s\n", foo);
return 0;
}Is that something we could use? Happy to send a patchset to implement the basics of it, and cleanup a few places where that would make the code more readable.
As the systemd coding style says in
https://systemd.io/CODING_STYLE/#memory-allocation
"Make use of _cleanup_free_ and friends. It makes your code much nicer to read (and shorter)!"
Quoted 49 lines
if (!server1)
fprintf(stderr, "Failed to open BR/EDR/LE server\n");emulator/main.c +0
-2- server2 = server_open_unix(SERVER_TYPE_BREDR, - "/tmp/bt-server-bredr");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-bredr");
emulator/main.c +1
-0+ server2 =server_open_unix (SERVER_TYPE_BREDR , path);if (!server2)
fprintf(stderr, "Failed to open BR/EDR server\n");emulator/main.c +0
-2- server3 = server_open_unix(SERVER_TYPE_AMP, - "/tmp/bt-server-amp");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-amp");
emulator/main.c +1
-0+ server3 =server_open_unix (SERVER_TYPE_AMP , path);if (!server3)
fprintf(stderr, "Failed to open AMP server\n");emulator/main.c +0
-2- server4 = server_open_unix(SERVER_TYPE_LE, - "/tmp/bt-server-le");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-le");
emulator/main.c +1
-0+ server4 =server_open_unix (SERVER_TYPE_LE , path);if (!server4)
fprintf(stderr, "Failed to open LE server\n");emulator/main.c +0
-2- server5 = server_open_unix(SERVER_TYPE_MONITOR, - "/tmp/bt-server-mon");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-mon");
emulator/main.c +1
-0+ server5 = server_open_unix(SERVER_TYPE_MONITOR,path);
if (!server5)
fprintf(stderr, "Failed to open monitor server\n");
}
Luiz Augusto von Dentz
maintainerRe: [PATCH BlueZ v3 02/20] emulator: btvirt: allow specifying where server unix sockets are made
Hi Bastien,
Quoted 83 lines · Bastien Nocera
Quoted 81 lines · Pauli Virtanen
Make --server to take optional path name where to create the various server sockets.
--- emulator/main.c | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-)emulator/main.c +1
-1@@ -18,6 +18,7 @@#include <stdbool.h> #include <getopt.h> #include <sys/uio.h> +#include <limits.h> #include "src/shared/mainloop.h" #include "src/shared/util.h" @@ -46,7 +47,7 @@ static void usage(void)printf ("options:\n" "\t-d Enable debug\n" "\t-S Create local serial port\n" - "\t-s Create local serversockets\n"
+ "\t-s[path=/tmp] Create local server
sockets\n"
"\t-t[port=45550] Create a TCP server\n"
"\t-l[num] Number of local
controllers\n"
"\t-L Create LE only
controller\n"emulator/main.c +3
-2@@ -60,7 +61,7 @@ static void usage(void)static const struct option main_options[] = { {"debug" , no_argument,NULL ,'d' }, {"serial" , no_argument,NULL ,'S' }, - {"server" , no_argument,NULL ,'s' }, + {"server" , optional_argument,NULL ,'s' }, {"tcp" , optional_argument,NULL ,'t' }, {"local" , optional_argument,NULL ,'l' }, {"le" , no_argument,NULL ,'L' },@@ -88,6 +89,7 @@ int main(int argc, char *argv[])struct server *server5;bool debug_enabled = false;bool server_enabled = false; +const char *server_path ="/tmp" ;uint16_t tcp_port =0 ;bool serial_enabled = false;int letest_count =0 ;@@ -100,7 +102,7 @@ int main(int argc, char *argv[])for (;;) {int opt; - opt =getopt_long (argc, argv,"dSst::l::LBAU::T::vh" , + opt =getopt_long (argc, argv,"dSs::t::l::LBAU::T::vh" , main_options,NULL );if (opt < 0) break;emulator/main.c +7
-2@@ -114,6 +116,8 @@ int main(int argc, char *argv[])break ;case 's' : server_enabled = true; +if (optarg) + server_path = optarg;break ;case 't' :if (optarg)@@ -196,28 +200,35 @@ int main(int argc, char *argv[]) }if (server_enabled) { - server1 =server_open_unix (SERVER_TYPE_BREDRLE , -"/tmp/bt-server- bredrle"); + char path[PATH_MAX ]; + +snprintf (path,sizeof (path),"%s/%s" , server_path, +"bt-server- bredrle"); + server1 = server_open_unix(SERVER_TYPE_BREDRLE,
Quoted 8 lines
emulator/main.c int main(int argc, char *argv[]) · line 210
Quoted 1 line
emulator/main.c +0
-0path);If we were allowed to use new glib calls, this hand-rolled code should probably use like
g_build_filename():
https://docs.gtk.org/glib/func.build_filename.html
andg_auto:
https://docs.gtk.org/glib/auto-cleanup.html
to simplify the code.
Don't really want to drag new dependecies.
Quoted 8 lines
Luiz, could we have have an update to
maintainer-guidelines.rstto reflect the current guidelines for using or adding glib calls to the code?Pauli, do you think we could have helpers for this sort of path manipulation in src/shared? Not saying it needs to be to the level of complexity of the
g_build_filename()implementation, butsnprintf()...:
https://github.com/GNOME/glib/blob/main/glib/gfileutils.c#L1926
Perhaps something like a util_filename or something simpler.
Quoted 27 lines
Luiz, another question, could we start using __attribute__(cleanup()) in bluez?
Consider the following example, adapted from systemd:
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>static inline void freep(void *p) { free(*(void**) p); }#define
_cleanup_(f)__attribute__((cleanup(f)))
#define _cleanup_free__cleanup_(freep)int
main(int argc, char **argv){ _cleanup_free_ char *foo = NULL; foo = strdup("my string"); printf ("%s\n", foo); return 0; }
Quoted 6 lines
Is that something we could use? Happy to send a patchset to implement the basics of it, and cleanup a few places where that would make the code more readable.
As the systemd coding style says in
https://systemd.io/CODING_STYLE/#memory-allocation
Yes, that's probably a good idea. I need to check what the kernel is doing in this regard as well since I recall they were introducing some support for auto freeing stuff.
Quoted 53 lines
"Make use of _cleanup_free_ and friends. It makes your code much nicer to read (and shorter)!"
Quoted 49 lines
emulator/main.c +6
-6if (!server1)fprintf (stderr,"Failed to open BR/EDR/LE server\n"); - server2 =server_open_unix (SERVER_TYPE_BREDR , -"/tmp/bt-server- bredr"); + snprintf(path,sizeof (path),"%s/%s" , server_path, +"bt-server- bredr"); + server2 =server_open_unix (SERVER_TYPE_BREDR , path);if (!server2)fprintf (stderr,"Failed to open BR/EDR server\n"); - server3 =server_open_unix (SERVER_TYPE_AMP , -"/tmp/bt-server- amp"); + snprintf(path,sizeof (path),"%s/%s" , server_path, +"bt-server- amp"); + server3 =server_open_unix (SERVER_TYPE_AMP , path);if (!server3)fprintf (stderr,"Failed to open AMP server\n"); - server4 = server_open_unix(SERVER_TYPE_LE, - "/tmp/bt-server-le");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-le");
+ server4 =server_open_unix(SERVER_TYPE_LE, path);
if (!server4)
fprintf(stderr, "Failed to open LE
server\n");emulator/main.c +0
-2- server5 = server_open_unix(SERVER_TYPE_MONITOR, - "/tmp/bt-server-mon");
emulator/main.c +2
-0+ snprintf(path, sizeof(path), "%s/%s", server_path, + "bt-server-mon");
+ server5 = server_open_unix(SERVER_TYPE_MONITOR,
path);
if (!server5)
fprintf(stderr, "Failed to open monitorserver\n"); }
Signature · 1 line
Luiz Augusto von Dentz
Re: [PATCH BlueZ v3 02/20] emulator: btvirt: allow specifying where server unix sockets are made
Quoted 9 lines · Luiz Augusto von Dentz
Quoted 7 lines
If we were allowed to use new glib calls, this hand-rolled code should
probably use likeg_build_filename():
https://docs.gtk.org/glib/func.build_filename.html
andg_auto:
https://docs.gtk.org/glib/auto-cleanup.html
to simplify the code.Don't really want to drag new dependecies.
btvirt depended on glib's headers. I've sent a patch to remove that use.
Quoted 5 lines
Quoted 5 lines
Luiz, could we have have an update to
maintainer-guidelines.rstto reflect the current guidelines for using or adding glib calls to the
code?
Do you want me to send a patch to the maintainer-guidelines to explain that glib usage is fine as long as the binary already links to it? (or needs a robust mainloop, as we've seen recently with btmgmt?)
PATCH v3 3/20 emulator: btvirt: support SCO data packets
In reply to Pauli VirtanenSupport also SCO data packets in btvirt.
--- emulator/server.c | 12 ++++++++++++ 1 file changed, 12 insertions(+)
emulator/server.c +12 -0
@@ -119,6 +119,7 @@ again:hci_command_hdr *cmd_hdr;hci_acl_hdr *acl_hdr;hci_iso_hdr *iso_hdr; +hci_sco_hdr *sco_hdr;if (!client->pkt_data) { client->pkt_type = ptr[0 ];
@@ -156,6 +157,17 @@ again: client->pkt_data =malloc (client->pkt_expect); client->pkt_len =0 ;break ; +case HCI_SCODATA_PKT : +if (count <HCI_SCO_HDR_SIZE +1 ) { + client->pkt_offset += len; +return ; + } + sco_hdr = (hci_sco_hdr *)(ptr +1 ); + client->pkt_expect =HCI_SCO_HDR_SIZE + + sco_hdr->dlen +1 ; + client->pkt_data =malloc (client->pkt_expect); + client->pkt_len =0 ; +break ;default :printf ("packet error, unknown type: %d\n" , client->pkt_type);
Signature · 1 line
2.53.0
PATCH v3 4/20 emulator: btdev: clear more state on Reset
In reply to Pauli VirtanenOn controller Reset command, initialize most fields in struct btdev to zero, similarly to the state just after btdev_create().
This excludes some fields like command bitmasks, which hciemu may have adjusted.
To make this easier, add struct_group() macro similar to what kernel uses.
--- emulator/btdev.c | 117 ++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 47 deletions(-)
emulator/btdev.c +70 -47
@@ -52,6 +52,12 @@#define has_bredr (btdev) (!((btdev)->features[4] & 0x20))#define has_le (btdev) (!!((btdev)->features[4] & 0x40)) +#define struct_group(NAME, MEMBERS...) \ + union { \ + struct { MEMBERS }; \ + struct { MEMBERS } NAME; \ + } +#define ACL_HANDLE BIT(0)#define SCO_HANDLE BIT(8)#define CIS_HANDLE SCO_HANDLE
@@ -151,15 +157,6 @@ struct btdev {struct queue *conns; -bool auth_init; -uint8_t link_key[16 ]; -uint16_t pin[16 ]; -uint8_t pin_len; -uint8_t io_cap; -uint8_t auth_req; -bool ssp_auth_complete; -uint8_t ssp_status; -btdev_command_func command_handler;void *command_data;
@@ -198,6 +195,18 @@ struct btdev {const struct btdev_cmd *emu_cmds;bool aosp_capable; +/* State zeroed on reset */ +struct_group (reset_group, + + bool auth_init; + uint8_t link_key[16]; +uint16_t pin[16 ]; +uint8_t pin_len; +uint8_t io_cap; +uint8_t auth_req; +bool ssp_auth_complete; +uint8_t ssp_status; +uint16_t default_link_policy;uint8_t event_mask[8 ];uint8_t event_mask_page2[8 ];
@@ -251,25 +260,26 @@ struct btdev {struct le_cig le_cig[CIG_SIZE ];uint8_t le_iso_path[2 ]; -/* Real time length of AL array */ -uint8_t le_al_len; -/* Real time length of RL array */ -uint8_t le_rl_len; -struct btdev_al le_al[AL_SIZE ]; -struct btdev_rl le_rl[RL_SIZE ];uint8_t le_rl_enable; -uint16_t le_rl_timeout;struct pending_conn pending_conn[MAX_PENDING_CONN ]; -uint8_t le_local_sk256[32 ]; -uint16_t sync_train_interval;uint32_t sync_train_timeout;uint8_t sync_train_service_data;uint16_t le_ext_adv_type; + ); /* reset_group */ + +/* Real time length of AL array */ +uint8_t le_al_len; +/* Real time length of RL array */ +uint8_t le_rl_len; +struct btdev_al le_al[AL_SIZE ]; +struct btdev_rl le_rl[RL_SIZE ]; +uint16_t le_rl_timeout; +struct queue *le_ext_adv;struct queue *le_per_adv;struct queue *le_big;
@@ -619,15 +629,52 @@ static void le_big_free(void *data)free (big); } +static void btdev_init_param (struct btdev *btdev) +{ +unsigned int i; + + btdev->page_scan_interval =0x0800 ; + btdev->page_scan_window =0x0012 ; + btdev->page_scan_type =0x00 ; + + btdev->sync_train_interval =0x0080 ; + btdev->sync_train_timeout =0x0002ee00 ; + btdev->sync_train_service_data =0x00 ; + + btdev->acl_mtu =192 ; + btdev->acl_max_pkt =1 ; + + btdev->sco_mtu =72 ; + btdev->sco_max_pkt =1 ; + + btdev->iso_mtu =251 ; + btdev->iso_max_pkt =1 ; + +for (i =0 ; i <ARRAY_SIZE (btdev->le_cig); ++i) + btdev->le_cig[i].params.cig_id =0xff ; + + btdev->country_code =0x00 ; +} +static void btdev_reset (struct btdev *btdev) {/* FIXME: include here clearing of all states that should be * cleared upon HCI_Reset */ - btdev->le_scan_enable =0x00 ; - btdev->le_adv_enable =0x00 ; - btdev->le_pa_enable =0x00 ; +if (btdev->inquiry_id >0 ) { +timeout_remove (btdev->inquiry_id); + btdev->inquiry_id =0 ; + } + +queue_remove_all (btdev->conns,NULL ,NULL , conn_remove); +queue_remove_all (btdev->le_ext_adv,NULL ,NULL , le_ext_adv_free); +queue_remove_all (btdev->le_per_adv,NULL ,NULL , free); +queue_remove_all (btdev->le_big,NULL ,NULL , le_big_free); + +memset (&btdev->reset_group,0 ,sizeof (btdev->reset_group)); + +btdev_init_param (btdev);al_clear (btdev);rl_clear (btdev);
@@ -635,10 +682,7 @@ static void btdev_reset(struct btdev *btdev) btdev->le_al_len =AL_SIZE ; btdev->le_rl_len =RL_SIZE ; -queue_remove_all (btdev->conns,NULL ,NULL , conn_remove); -queue_remove_all (btdev->le_ext_adv,NULL ,NULL , le_ext_adv_free); -queue_remove_all (btdev->le_per_adv,NULL ,NULL , free); -queue_remove_all (btdev->le_big,NULL ,NULL , le_big_free); + btdev->le_rl_timeout =0x0384 ; }static int cmd_reset (struct btdev *dev,const void *data,uint8_t len)
@@ -8132,7 +8176,6 @@ struct btdev *btdev_create(enum btdev_type type, uint16_t id) {struct btdev *btdev;int index; -unsigned int i; btdev =malloc (sizeof (*btdev));if (!btdev)
@@ -8197,27 +8240,7 @@ struct btdev *btdev_create(enum btdev_type type, uint16_t id)break ; } - btdev->page_scan_interval =0x0800 ; - btdev->page_scan_window =0x0012 ; - btdev->page_scan_type =0x00 ; - - btdev->sync_train_interval =0x0080 ; - btdev->sync_train_timeout =0x0002ee00 ; - btdev->sync_train_service_data =0x00 ; - - btdev->acl_mtu =192 ; - btdev->acl_max_pkt =1 ; - - btdev->sco_mtu =72 ; - btdev->sco_max_pkt =1 ; - - btdev->iso_mtu =251 ; - btdev->iso_max_pkt =1 ; - -for (i =0 ; i <ARRAY_SIZE (btdev->le_cig); ++i) - btdev->le_cig[i].params.cig_id =0xff ; - - btdev->country_code =0x00 ; +btdev_init_param (btdev); index =add_btdev (btdev);if (index <0 ) {
Signature · 1 line
2.53.0
PATCH v3 5/20 test-runner: enable path argument for --unix
In reply to Pauli VirtanenAllow specifying the path for the controller socket to be used.
--- tools/test-runner.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)
tools/test-runner.c +8 -6
@@ -54,6 +54,7 @@ static bool start_monitor = false;static bool qemu_host_cpu = false;static int num_devs =0 ;static int num_emulator =0 ; +static const char *device_path ="/tmp/bt-server-bredr" ;static const char *qemu_binary =NULL ;static const char *kernel_image =NULL ;static char *audio_server;
@@ -313,11 +314,10 @@ static void start_qemu(void) argv[pos++] = (char *) cmdline;for (i =0 ; i < num_devs; i++) { -const char *path ="/tmp/bt-server-bredr" ;char *chrdev, *serdev; - chrdev =alloca (48 +strlen (path)); -sprintf (chrdev,"socket,path=%s,id=bt%d" , path, i); + chrdev =alloca (48 +strlen (device_path)); +sprintf (chrdev,"socket,path=%s,id=bt%d" , device_path, i); serdev =alloca (48 );sprintf (serdev,"pci-serial,chardev=bt%d" , i);
@@ -1198,7 +1198,7 @@ static void usage(void)"\t-m, --monitor Start btmon\n" "\t-l, --emulator[=num] Start btvirt\n" "\t-A, --audio[=path] Start audio server\n" -"\t-u, --unix [path] Provide serial device\n" +"\t-u, --unix[=path] Provide serial device\n" "\t-U, --usb [qemu_args] Provide USB device\n" "\t-q, --qemu <path> QEMU binary\n" "\t-H, --qemu-host-cpu Use host CPU (requires KVM support)\n"
@@ -1211,7 +1211,7 @@ static const struct option main_options[] = { { "auto", no_argument, NULL, 'a' }, { "dbus", no_argument, NULL, 'b' }, { "dbus-session", no_argument, NULL, 's' }, - { "unix", no_argument, NULL, 'u' }, + { "unix", optional_argument, NULL, 'u' }, { "daemon", no_argument, NULL, 'd' }, { "emulator", no_argument, NULL, 'l' }, { "monitor", no_argument, NULL, 'm' },
@@ -1239,7 +1239,7 @@ int main(int argc, char *argv[])for (;;) {int opt; - opt =getopt_long (argc, argv,"aubdsl::mq:Hk:A::U:vh" , + opt =getopt_long (argc, argv,"au::bdsl::mq:Hk:A::U:vh" , main_options,NULL );if (opt <0 )break ;
@@ -1250,6 +1250,8 @@ int main(int argc, char *argv[])break ;case 'u' : num_devs =1 ; +if (optarg) + device_path = optarg;break ;case 'b' : start_dbus = true;
Signature · 1 line
2.53.0
PATCH v3 6/20 test-runner: Add -o/--option option
In reply to Pauli VirtanenAllow passing arbitrary arguments to QEMU.
--- tools/test-runner.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-)
tools/test-runner.c +19 -3
@@ -41,6 +41,7 @@#endif #define CMDLINE_MAX (2048 * 10) +#define EXTRA_OPT_MAX 64static const char *own_binary;static char **test_argv;
@@ -59,6 +60,8 @@ static const char *qemu_binary = NULL;static const char *kernel_image =NULL ;static char *audio_server;static char *usb_dev; +static char *extra_opts[EXTRA_OPT_MAX ]; +static int num_extra_opts;static const char *qemu_table[] = {"qemu-system-x86_64" ,
@@ -291,7 +294,8 @@ static void start_qemu(void) argv =alloca (sizeof (qemu_argv) + (sizeof (char *) * (6 + (num_devs *4 ))) + - (sizeof (char *) * (usb_dev ?4 :0 ))); + (sizeof (char *) * (usb_dev ?4 :0 )) + + (sizeof (char *) * num_extra_opts));memcpy (argv, qemu_argv,sizeof (qemu_argv)); pos = (sizeof (qemu_argv) /sizeof (char *)) -1 ;
@@ -335,6 +339,9 @@ static void start_qemu(void) argv[pos++] = usb_dev; } +for (i =0 ; i < num_extra_opts; ++i) + argv[pos++] = extra_opts[i]; + argv[pos] =NULL ;execve (argv[0 ], argv, qemu_envp);
@@ -1199,10 +1206,11 @@ static void usage(void)"\t-l, --emulator[=num] Start btvirt\n" "\t-A, --audio[=path] Start audio server\n" "\t-u, --unix[=path] Provide serial device\n" -"\t-U, --usb [qemu_args] Provide USB device\n" +"\t-U, --usb <qemu_args> Provide USB device\n" "\t-q, --qemu <path> QEMU binary\n" "\t-H, --qemu-host-cpu Use host CPU (requires KVM support)\n" "\t-k, --kernel <image> Kernel image (bzImage)\n" +"\t-o, --option <opt> Additional argument passed to QEMU\n" "\t-h, --help Show help options\n"); }
@@ -1220,6 +1228,7 @@ static const struct option main_options[] = { { "kernel", required_argument, NULL, 'k' }, { "audio", optional_argument, NULL, 'A' }, { "usb", required_argument, NULL, 'U' }, + { "option", required_argument, NULL, 'o' }, { "version", no_argument, NULL, 'v' }, { "help", no_argument, NULL, 'h' }, { }
@@ -1239,7 +1248,7 @@ int main(int argc, char *argv[])for (;;) {int opt; - opt =getopt_long (argc, argv,"au::bdsl::mq:Hk:A::U:vh" , + opt =getopt_long (argc, argv,"au::bdsl::mq:Hk:A::U:o:vh" , main_options,NULL );if (opt <0 )break ;
@@ -1284,6 +1293,13 @@ int main(int argc, char *argv[])case 'U' : usb_dev = optarg;break ; +case 'o' : +if (num_extra_opts >=EXTRA_OPT_MAX ) { +fprintf (stderr,"Too many -o\n" ); +return EXIT_FAILURE ; + } + extra_opts[num_extra_opts++] = optarg; +break ;case 'v' :printf ("%s\n" ,VERSION );return EXIT_SUCCESS ;
Signature · 1 line
2.53.0
PATCH v3 7/20 test-runner: allow source tree root for -k
In reply to Pauli VirtanenAllow passing source tree root for -k option, look up kernel below it.
--- tools/test-runner.c | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-)
tools/test-runner.c +28 -14
@@ -93,18 +93,31 @@ static const char *kernel_table[] = {NULL }; -static const char *find_kernel (void ) +static bool find_kernel (const char *root,char path[PATH_MAX ]) { +struct stat st;int i; -for (i =0 ; kernel_table[i]; i++) { -struct stat st; - -if (!stat (kernel_table[i], &st)) -return kernel_table[i]; +if (root) { +snprintf (path,PATH_MAX ,"%s" , root); +if (stat (path, &st)) +return false; +if (!(st.st_mode &S_IFDIR )) +return true; } -return NULL ; +for (i =0 ; kernel_table[i]; i++) { +if (root) +snprintf (path,PATH_MAX ,"%s/%s" , root, + kernel_table[i]); +else +snprintf (path,PATH_MAX ,"%s" , + kernel_table[i]); +if (!stat (path, &st)) +return true; + } + +return false; } static const struct {
@@ -1209,7 +1222,7 @@ static void usage(void)"\t-U, --usb <qemu_args> Provide USB device\n" "\t-q, --qemu <path> QEMU binary\n" "\t-H, --qemu-host-cpu Use host CPU (requires KVM support)\n" -"\t-k, --kernel <image> Kernel image (bzImage)\n" +"\t-k, --kernel <image> Kernel bzImage or source tree path\n" "\t-o, --option <opt> Additional argument passed to QEMU\n" "\t-h, --help Show help options\n"); }
@@ -1236,6 +1249,8 @@ static const struct option main_options[] = {int main (int argc,char *argv[]) { +char kernel_path[PATH_MAX ]; +if (getpid () ==1 &&getppid () ==0 ) {prepare_sandbox ();run_tests ();
@@ -1335,14 +1350,13 @@ int main(int argc, char *argv[]) } } -if (!kernel_image) { - kernel_image =find_kernel (); -if (!kernel_image) { -fprintf (stderr,"No default kernel image found\n" ); -return EXIT_FAILURE ; - } +if (!find_kernel (kernel_image, kernel_path)) { +fprintf (stderr,"No kernel image found\n" ); +return EXIT_FAILURE ; } + kernel_image = kernel_path; +printf ("Using QEMU binary %s\n" , qemu_binary);printf ("Using kernel image %s\n" , kernel_image);
Signature · 1 line
2.53.0
PATCH v3 8/20 doc: enable CONFIG_VIRTIO_CONSOLE in tester config
In reply to Pauli VirtanenEnable kernel option that allows using -device virtserialport in qemu. This is easier to make work reliably than pci-serial channel.
--- doc/ci.config | 1 + doc/test-runner.rst | 1 + doc/tester.config | 1 + 3 files changed, 3 insertions(+)
doc/ci.config +1 -0
@@ -6,6 +6,7 @@ CONFIG_VIRTIO=y CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_CONSOLE=y CONFIG_NET=y CONFIG_INET=y
doc/test-runner.rst +1 -0
@@ -45,6 +45,7 @@ option (like the Bluetooth subsystem) can be enabled on top of this. CONFIG_VIRTIO=y CONFIG_VIRTIO_PCI=y + CONFIG_VIRTIO_CONSOLE=y CONFIG_NET=y CONFIG_INET=y
doc/tester.config +1 -0
@@ -1,6 +1,7 @@ CONFIG_PCI=y CONFIG_VIRTIO=y CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_CONSOLE=y CONFIG_NET=y CONFIG_INET=y
Signature · 1 line
2.53.0
PATCH v3 9/20 test-runner: use virtio-serial for implementing -u device forwarding
In reply to Pauli VirtanenUsing pci-serial to forward eg. btvirt sockets is unreliable, as qemu or kernel seems to be sometimes dropping part of the sent data or insert spurious \0 bytes, leading to sporadic errors like:
kernel: Bluetooth: hci0: command 0x0c52 tx timeout
kernel: Bluetooth: hci0: Opcode 0x0c52 failed: -110
btvirt: packet error, unknown type: 0This appears to occur most often when host system is under load, e.g. due to multiple test-runners running at the same time. The problem is not specific to btvirt, but seems to be in the qemu serial device layer vs. kernel interaction.
Change test-runner to use virtserialport to forward the btvirt connection inside the VM, as virtio-serial doesn't appear to have these problems.
Since it's not a TTY device, we have to do vport <-> tty-with-hci-ldisc forwarding of the data in test-runner, so this becomes a bit more involved.
--- Makefile.tools | 2 + configure.ac | 9 ++ tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++----------- 3 files changed, 241 insertions(+), 70 deletions(-)
Makefile.tools +2 -0
@@ -321,6 +321,8 @@ tools_btgatt_server_SOURCES = tools/btgatt-server.c src/uuid-helper.c tools_btgatt_server_LDADD = src/libshared-mainloop.la \ lib/libbluetooth-internal.la +tools_test_runner_LDADD = $(OPENPTY_LIBS) + tools_rctest_LDADD = lib/libbluetooth-internal.la tools_l2test_LDADD = lib/libbluetooth-internal.la
configure.ac +9 -0
@@ -251,6 +251,15 @@ AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable-tools], [disable Bluetooth tools]), [enable_tools=${enableval}]) AM_CONDITIONAL(TOOLS, test "${enable_tools}" != "no") +openpty_libs= +if (test "${enable_tools}" != "no"); then + AC_CHECK_FUNCS([openpty], [openpty_libs=], + [AC_CHECK_LIB([util], [openpty], [openpty_libs=-lutil], + [AC_CHECK_LIB([bsd], [openpty], [openpty_libs=-lbsd], + [AC_MSG_ERROR([openpty not found])])])]) +fi +AC_SUBST(OPENPTY_LIBS, [${openpty_libs}]) + AC_ARG_ENABLE(monitor, AS_HELP_STRING([--disable-monitor], [disable Bluetooth monitor]), [enable_monitor=${enableval}]) AM_CONDITIONAL(MONITOR, test "${enable_monitor}" != "no")
tools/test-runner.c +230 -70
359 lines not on this page · raw message
Signature · 1 line
2.53.0
Luiz Augusto von Dentz
maintainerRe: [PATCH BlueZ v3 09/20] test-runner: use virtio-serial for implementing -u device forwarding
Hi Pauli,
Quoted 14 lines · Pauli Virtanen
Using pci-serial to forward eg. btvirt sockets is unreliable, as qemu or kernel seems to be sometimes dropping part of the sent data or insert spurious \0 bytes, leading to sporadic errors like:
kernel: Bluetooth: hci0: command 0x0c52 tx timeout kernel: Bluetooth: hci0: Opcode 0x0c52 failed: -110 btvirt: packet error, unknown type: 0This appears to occur most often when host system is under load, e.g. due to multiple test-runners running at the same time. The problem is not specific to btvirt, but seems to be in the qemu serial device layer vs. kernel interaction.
Quoted 7 lines
Change test-runner to use virtserialport to forward the btvirt connection inside the VM, as virtio-serial doesn't appear to have these problems.
Since it's not a TTY device, we have to do vport <-> tty-with-hci-ldisc forwarding of the data in test-runner, so this becomes a bit more involved.
Lets try to sort this out first before we go into the functional framework. Does the vport not working as tty perhaps require a kernel driver or something to enable direct operation?
Quoted 252 lines
--- Makefile.tools | 2 + configure.ac | 9 ++ tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++----------- 3 files changed, 241 insertions(+), 70 deletions(-)Makefile.tools +2
-0@@ -321,6 +321,8 @@ tools_btgatt_server_SOURCES = tools/btgatt-server.c src/uuid-helper.c tools_btgatt_server_LDADD = src/libshared-mainloop.la \ lib/libbluetooth-internal.la +tools_test_runner_LDADD = $(OPENPTY_LIBS) + tools_rctest_LDADD = lib/libbluetooth-internal.la tools_l2test_LDADD = lib/libbluetooth-internal.laconfigure.ac +9
-0@@ -251,6 +251,15 @@ AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable-tools], [disable Bluetooth tools]), [enable_tools=${enableval}]) AM_CONDITIONAL(TOOLS, test "${enable_tools}" != "no") +openpty_libs= +if (test "${enable_tools}" != "no"); then + AC_CHECK_FUNCS([openpty], [openpty_libs=], + [AC_CHECK_LIB([util], [openpty], [openpty_libs=-lutil], + [AC_CHECK_LIB([bsd], [openpty], [openpty_libs=-lbsd], + [AC_MSG_ERROR([openpty not found])])])]) +fi +AC_SUBST(OPENPTY_LIBS, [${openpty_libs}]) + AC_ARG_ENABLE(monitor, AS_HELP_STRING([--disable-monitor], [disable Bluetooth monitor]), [enable_monitor=${enableval}]) AM_CONDITIONAL(MONITOR, test "${enable_monitor}" != "no")tools/test-runner.c +109
-58@@ -24,6 +24,9 @@#include <getopt.h> #include <poll.h> #include <limits.h> +#include <dirent.h> +#include <pty.h> +#include <stdint.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/types.h> @@ -306,7 +309,7 @@ static void start_qemu(void) testargs); argv =alloca (sizeof (qemu_argv) + - (sizeof (char *) * (6 + (num_devs *4 ))) + + (sizeof (char *) * (8 + (num_devs *4 ))) + (sizeof (char *) * (usb_dev ?4 :0 )) + (sizeof (char *) * num_extra_opts));memcpy (argv, qemu_argv,sizeof (qemu_argv));@@ -330,14 +333,17 @@ static void start_qemu(void) argv[pos++] ="-append" ; argv[pos++] = (char *) cmdline; + argv[pos++] ="-device" ; + argv[pos++] ="virtio-serial" ; +for (i =0 ; i < num_devs; i++) {char *chrdev, *serdev; chrdev =alloca (48 +strlen (device_path));sprintf (chrdev,"socket,path=%s,id=bt%d" , device_path, i); - serdev =alloca (48 ); -sprintf (serdev,"pci-serial,chardev=bt%d" , i); + serdev =alloca (64 ); +sprintf (serdev,"virtserialport,chardev=bt%d,name=bt.%d" , i, i); argv[pos++] ="-chardev" ; argv[pos++] = chrdev;@@ -360,65 +366,12 @@ static void start_qemu(void)execve (argv[0 ], argv, qemu_envp); } -static int open_serial (const char *path) -{ -struct termios ti; -int fd, saved_ldisc, ldisc =N_HCI ; - - fd =open (path,O_RDWR |O_NOCTTY ); -if (fd <0 ) { -perror ("Failed to open serial port" ); -return -1 ; - } - -if (tcflush (fd,TCIOFLUSH ) <0 ) { -perror ("Failed to flush serial port" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { -perror ("Failed get serial line discipline" ); -close (fd); -return -1 ; - } - -/* Switch TTY to raw mode */ -memset (&ti,0 ,sizeof (ti)); -cfmakeraw (&ti); - - ti.c_cflag |= (B115200 |CLOCAL |CREAD ); - -/* Set flow control */ - ti.c_cflag |=CRTSCTS ; - -if (tcsetattr (fd,TCSANOW , &ti) <0 ) { -perror ("Failed to set serial port settings" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { -perror ("Failed set serial line discipline" ); -close (fd); -return -1 ; - } - -printf ("Switched line discipline from %d to %d\n" , saved_ldisc, ldisc); - -return fd; -} - -static int attach_proto (const char *path,unsigned int proto, +static int attach_proto (int fd,unsigned int proto,unsigned int mandatory_flags,unsigned int optional_flags) {unsigned int flags = mandatory_flags | optional_flags; -int fd, dev_id; - - fd =open_serial (path); -if (fd <0 ) -return -1 ; +int dev_id;if (ioctl (fd,HCIUARTSETFLAGS , flags) <0 ) {if (errno ==EINVAL ) {@@ -895,13 +848,222 @@ static int start_audio_server(pid_t pids[2])return 0 ; } +static bool find_attach_dev (char path[PATH_MAX ]) +{ +const char *vport_path ="/sys/class/virtio-ports" ; +struct dirent *entry; +DIR *dir; + + dir =opendir (vport_path); +if (!dir) +return false; + +while ((entry =readdir (dir)) !=NULL ) { +FILE *f; +char buf[64 ]; +size_t size; + +snprintf (path,PATH_MAX ,"%s/%s/name" , vport_path, + entry->d_name); + f =fopen (path,"r" ); +if (!f) +continue ; + + size =fread (buf,1 ,sizeof (buf) -1 , f); + buf[size] =0 ; + +fclose (f); + +if (strncmp (buf,"bt." ,3 ) ==0 ) { +snprintf (path,PATH_MAX ,"/dev/%s" , entry->d_name); +closedir (dir); +return true; + } + } + +closedir (dir); +return false; +} + +static void copy_fd_bidi (int src,int dst) +{ +fd_set rfds, wfds; +int fd[2 ] = { src, dst }; +uint8_t buf[2 ][4096 ]; +size_t size[2 ] = {0 ,0 }; +size_t pos[2 ] = {0 ,0 }; +int i, ret; + +/* Simple copying of data src <-> dst to both directions */ + +for (i =0 ; i <2 ; ++i) { +int flags =fcntl (fd[i],F_GETFL ); + +if (fcntl (fd[i],F_SETFL , flags |O_NONBLOCK ) <0 ) { +perror ("fcntl" ); + goto error; + } + } + +while (1 ) { +FD_ZERO (&rfds); +FD_ZERO (&wfds); + +for (i =0 ; i <2 ; ++i) { +if (size[i]) +FD_SET (fd[i], &wfds); +else +FD_SET (fd[1 - i], &rfds); + } + + ret =select (FD_SETSIZE , &rfds, &wfds,NULL ,NULL ); +if (ret <0 ) { +if (errno ==EINTR ) +continue ; +perror ("select" ); + goto error; + } + +for (i =0 ; i <2 ; ++i) { +ssize_t s; + +if (!size[i] &&FD_ISSET (fd[1 - i], &rfds)) { + s =read (fd[1 - i], buf[i],sizeof (buf[i])); +if (s >=0 ) { + size[i] = s; + pos[i] =0 ; + }else if (errno ==EINTR ) { +/* ok */ + }else { +perror ("read" ); + goto error; + } + + } + +if (size[i]) { + s =write (fd[i], buf[i] + pos[i], size[i]); +if (s >=0 ) { + size[i] -= s; + pos[i] += s;
tools/test-runner.c static int start_audio_server(pid_t pids[2]) · line 958
Quoted 10 lines
tools/test-runner.c +10
-0+ }else if (errno ==EINTR || errno ==EAGAIN + || errno ==EWOULDBLOCK ) { +/* ok */ + }else { +perror ("write" ); + goto error; + } + } + } + }
It doesn't look that great if you ask me. I think this requires us to copy significantly more data than before, likely resulting in worse performance, which could be detrimental in resource-limited environments like GitHub CI/CD.
Quoted 151 lines
tools/test-runner.c +111
-13+return ; + +error: +fprintf (stderr,"Bluetooth controller forward terminated with error\n" ); +exit (1 ); +} + +static int start_controller_forward (const char *path,pid_t *controller_pid) +{ +struct termios ti; +pid_t pid; +int src =-1 , dst =-1 , fd =-1 ; +int ret, saved_ldisc, ldisc =N_HCI ; + +/* virtio-serial ports cannot be used for HCI line disciple, so +* openpty() serial device and forward data to/from it. +*/ + + src =open (path,O_RDWR ); +if (src <0 ) + goto error; + +/* Raw mode TTY */ +memset (&ti,0 ,sizeof (ti)); +cfmakeraw (&ti); + ti.c_cflag |=B115200 |CLOCAL |CREAD ; + +/* With flow control */ + ti.c_cflag |=CRTSCTS ; + + ret =openpty (&dst, &fd,NULL , &ti,NULL ); +if (ret <0 ) + goto error; + +if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { +perror ("Failed get serial line discipline" ); + goto error; + } + +if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { +perror ("Failed set serial line discipline" ); + goto error; + } + +printf ("Switched line discipline from %d to %d\n" , saved_ldisc, ldisc); + + pid =fork (); +if (pid <0 ) { +perror ("Failed to fork new process" ); + goto error; + }else if (pid ==0 ) { +close (fd); +copy_fd_bidi (src, dst); +exit (0 ); + } + + *controller_pid = pid; + +close (src); +close (dst); +return fd; + +error: +if (src >=0 ) +close (src); +if (dst >=0 ) +close (dst); +if (fd >=0 ) +close (fd); +return -1 ; +} + +static int attach_controller (pid_t *controller_pid) +{ +unsigned int basic_flags, extra_flags; +char path[PATH_MAX ]; +int fd; + + *controller_pid =-1 ; + +if (!find_attach_dev (path)) { +printf ("Failed to find Bluetooth controller virtio\n" ); +return -1 ; + } + +printf ("Forwarding Bluetooth controller from %s\n" , path); + + fd =start_controller_forward (path, controller_pid); +if (fd <0 ) { +printf ("Failed to forward Bluetooth controller\n" ); +return -1 ; + } + + basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); + extra_flags = (1 <<HCI_UART_VND_DETECT ); + +printf ("Attaching Bluetooth controller\n" ); + +return attach_proto (fd,HCI_UART_H4 , basic_flags, extra_flags); +} +static void run_command (char *cmdname,char *home) {char *argv[9 ], *envp[3 ];int pos =0 , idx =0 ;int serial_fd;pid_t pid, dbus_pid, daemon_pid, monitor_pid, emulator_pid, - dbus_session_pid, audio_pid[2 ]; + dbus_session_pid, audio_pid[2 ], controller_pid;int i;if (!home) {@@ -910,18 +1072,11 @@ static void run_command(char *cmdname, char *home) }if (num_devs) { -const char *node ="/dev/ttyS1" ; -unsigned int basic_flags, extra_flags; - -printf ("Attaching BR/EDR controller to %s\n" , node); - - basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); - extra_flags = (1 <<HCI_UART_VND_DETECT ); - - serial_fd =attach_proto (node,HCI_UART_H4 , basic_flags, - extra_flags); - }else + serial_fd =attach_controller (&controller_pid); + }else { serial_fd =-1 ; + controller_pid =-1 ; + }if (start_dbus) {create_dbus_system_conf ();@@ -1063,6 +1218,11 @@ start_next: monitor_pid =-1 ; } +if (corpse == controller_pid) { +printf ("Controller terminated\n" ); + controller_pid =-1 ; + } +for (i =0 ; i <2 ; ++i) {if (corpse == audio_pid[i]) {printf ("Audio server %d terminated\n" , i); --2.53.0
Signature · 1 line
Luiz Augusto von Dentz
Re: [PATCH BlueZ v3 09/20] test-runner: use virtio-serial for implementing -u device forwarding
ti, 2026-03-24 kello 16:24 -0400, Luiz Augusto von Dentz kirjoitti:
Quoted 21 lines
Hi Pauli,
Quoted 18 lines · Pauli Virtanen
Using pci-serial to forward eg. btvirt sockets is unreliable, as qemu or kernel seems to be sometimes dropping part of the sent data or insert spurious \0 bytes, leading to sporadic errors like:
kernel: Bluetooth: hci0: command 0x0c52 tx timeout kernel: Bluetooth: hci0: Opcode 0x0c52 failed: -110 btvirt: packet error, unknown type: 0This appears to occur most often when host system is under load, e.g. due to multiple test-runners running at the same time. The problem is not specific to btvirt, but seems to be in the qemu serial device layer vs. kernel interaction.
Change test-runner to use virtserialport to forward the btvirt connection inside the VM, as virtio-serial doesn't appear to have these problems.
Quoted 7 lines
Quoted 3 lines
Since it's not a TTY device, we have to do vport <-> tty-with-hci-ldisc forwarding of the data in test-runner, so this becomes a bit more involved.
Lets try to sort this out first before we go into the functional framework. Does the vport not working as tty perhaps require a kernel driver or something to enable direct operation?
Looks like qemu has different option for the virtio TTY device. We can replace this patch with the following -> v4
I'll wait for other comments on the series before continuing.
tools/test-runner.c +6 -1
@@ -306,7 +306,7 @@ static void start_qemu(void) testargs); argv =alloca (sizeof (qemu_argv) + - (sizeof (char *) * (6 + (num_devs *4 ))) + + (sizeof (char *) * (8 + (num_devs *4 ))) + (sizeof (char *) * (usb_dev ?4 :0 )) + (sizeof (char *) * num_extra_opts));memcpy (argv, qemu_argv,sizeof (qemu_argv));
@@ -330,14 +330,19 @@ static void start_qemu(void) argv[pos++] ="-append" ; argv[pos++] = (char *) cmdline; +if (num_devs) { + argv[pos++] ="-device" ; + argv[pos++] ="virtio-serial" ; + } +for (i =0 ; i < num_devs; i++) {char *chrdev, *serdev; chrdev =alloca (48 +strlen (device_path)); sprintf(chrdev, "socket,path=%s,id=bt%d", device_path,
i);
- serdev = alloca(48); - sprintf(serdev, "pci-serial,chardev=bt%d", i); + serdev = alloca(64); + sprintf(serdev, "virtconsole,chardev=bt%d,name=bt.%d",
i, i);
argv[pos++] = "-chardev";
argv[pos++] = chrdev;
@@ -910,7 +915,7 @@ static void run_command(char *cmdname, char *home)
}
if (num_devs) {
- const char *node = "/dev/ttyS1";
+ const char *node = "/dev/hvc0";
unsigned int basic_flags, extra_flags;
printf("Attaching BR/EDR controller to %s\n", node);Quoted 421 lines
Quoted 253 lines
--- Makefile.tools | 2 + configure.ac | 9 ++ tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++----------- 3 files changed, 241 insertions(+), 70 deletions(-)Makefile.tools +2
-0@@ -321,6 +321,8 @@ tools_btgatt_server_SOURCES = tools/btgatt-server.c src/uuid-helper.c tools_btgatt_server_LDADD = src/libshared-mainloop.la \ lib/libbluetooth-internal.la +tools_test_runner_LDADD = $(OPENPTY_LIBS) + tools_rctest_LDADD = lib/libbluetooth-internal.la tools_l2test_LDADD = lib/libbluetooth-internal.laconfigure.ac +9
-0@@ -251,6 +251,15 @@ AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable-tools], [disable Bluetooth tools]), [enable_tools=${enableval}]) AM_CONDITIONAL(TOOLS, test "${enable_tools}" != "no") +openpty_libs= +if (test "${enable_tools}" != "no"); then + AC_CHECK_FUNCS([openpty], [openpty_libs=], + [AC_CHECK_LIB([util], [openpty], [openpty_libs=-lutil], + [AC_CHECK_LIB([bsd], [openpty], [openpty_libs=-lbsd], + [AC_MSG_ERROR([openpty not found])])])]) +fi +AC_SUBST(OPENPTY_LIBS, [${openpty_libs}]) + AC_ARG_ENABLE(monitor, AS_HELP_STRING([--disable-monitor], [disable Bluetooth monitor]), [enable_monitor=${enableval}]) AM_CONDITIONAL(MONITOR, test "${enable_monitor}" != "no")tools/test-runner.c +109
-58@@ -24,6 +24,9 @@#include <getopt.h> #include <poll.h> #include <limits.h> +#include <dirent.h> +#include <pty.h> +#include <stdint.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/types.h> @@ -306,7 +309,7 @@ static void start_qemu(void) testargs); argv =alloca (sizeof (qemu_argv) + - (sizeof (char *) * (6 + (num_devs *4 ))) + + (sizeof (char *) * (8 + (num_devs *4 ))) + (sizeof (char *) * (usb_dev ?4 :0 )) + (sizeof (char *) * num_extra_opts));memcpy (argv, qemu_argv,sizeof (qemu_argv));@@ -330,14 +333,17 @@ static void start_qemu(void) argv[pos++] ="-append" ; argv[pos++] = (char *) cmdline; + argv[pos++] ="-device" ; + argv[pos++] ="virtio-serial" ; +for (i =0 ; i < num_devs; i++) {char *chrdev, *serdev; chrdev =alloca (48 +strlen (device_path));sprintf (chrdev,"socket,path=%s,id=bt%d" , device_path, i); - serdev =alloca (48 ); -sprintf (serdev,"pci-serial,chardev=bt%d" , i); + serdev =alloca (64 ); +sprintf (serdev,"virtserialport,chardev=bt%d,name=bt.%d" , i, i); argv[pos++] ="-chardev" ; argv[pos++] = chrdev;@@ -360,65 +366,12 @@ static void start_qemu(void)execve (argv[0 ], argv, qemu_envp); } -static int open_serial (const char *path) -{ -struct termios ti; -int fd, saved_ldisc, ldisc =N_HCI ; - - fd =open (path,O_RDWR |O_NOCTTY ); -if (fd <0 ) { -perror ("Failed to open serial port" ); -return -1 ; - } - -if (tcflush (fd,TCIOFLUSH ) <0 ) { -perror ("Failed to flush serial port" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { -perror ("Failed get serial line discipline" ); -close (fd); -return -1 ; - } - -/* Switch TTY to raw mode */ -memset (&ti,0 ,sizeof (ti)); -cfmakeraw (&ti); - - ti.c_cflag |= (B115200 |CLOCAL |CREAD ); - -/* Set flow control */ - ti.c_cflag |=CRTSCTS ; - -if (tcsetattr (fd,TCSANOW , &ti) <0 ) { -perror ("Failed to set serial port settings" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { -perror ("Failed set serial line discipline" ); -close (fd); -return -1 ; - } - -printf ("Switched line discipline from %d to %d\n" , saved_ldisc, ldisc); - -return fd; -} - -static int attach_proto (const char *path,unsigned int proto, +static int attach_proto (int fd,unsigned int proto,unsigned int mandatory_flags,unsigned int optional_flags) {unsigned int flags = mandatory_flags | optional_flags; -int fd, dev_id; - - fd =open_serial (path); -if (fd <0 ) -return -1 ; +int dev_id;if (ioctl (fd,HCIUARTSETFLAGS , flags) <0 ) {if (errno ==EINVAL ) {@@ -895,13 +848,222 @@ static int start_audio_server(pid_t pids[2])return 0 ; } +static bool find_attach_dev (char path[PATH_MAX ]) +{ +const char *vport_path ="/sys/class/virtio-ports" ; +struct dirent *entry; +DIR *dir; + + dir =opendir (vport_path); +if (!dir) +return false; + +while ((entry =readdir (dir)) !=NULL ) { +FILE *f; +char buf[64 ]; +size_t size; + +snprintf (path,PATH_MAX ,"%s/%s/name" , vport_path, + entry->d_name); + f =fopen (path,"r" ); +if (!f) +continue ; + + size =fread (buf,1 ,sizeof (buf) -1 , f); + buf[size] =0 ; + +fclose (f); + +if (strncmp (buf,"bt." ,3 ) ==0 ) { +snprintf (path,PATH_MAX ,"/dev/%s" , entry->d_name); +closedir (dir); +return true; + } + } + +closedir (dir); +return false; +} + +static void copy_fd_bidi (int src,int dst) +{ +fd_set rfds, wfds; +int fd[2 ] = { src, dst }; +uint8_t buf[2 ][4096 ]; +size_t size[2 ] = {0 ,0 }; +size_t pos[2 ] = {0 ,0 }; +int i, ret; + +/* Simple copying of data src <-> dst to both directions */ + +for (i =0 ; i <2 ; ++i) { +int flags =fcntl (fd[i],F_GETFL ); + +if (fcntl (fd[i],F_SETFL , flags |O_NONBLOCK ) <0 ) { +perror ("fcntl" ); + goto error; + } + } + +while (1 ) { +FD_ZERO (&rfds); +FD_ZERO (&wfds); + +for (i =0 ; i <2 ; ++i) { +if (size[i]) +FD_SET (fd[i], &wfds); +else +FD_SET (fd[1 - i], &rfds); + } + + ret =select (FD_SETSIZE , &rfds, &wfds,NULL ,NULL ); +if (ret <0 ) { +if (errno ==EINTR ) +continue ; +perror ("select" ); + goto error; + } + +for (i =0 ; i <2 ; ++i) { +ssize_t s; + +if (!size[i] &&FD_ISSET (fd[1 - i], &rfds)) { + s =read (fd[1 - i], buf[i],sizeof (buf[i])); +if (s >=0 ) { + size[i] = s; + pos[i] =0 ; + }else if (errno ==EINTR ) { +/* ok */ + }else { +perror ("read" ); + goto error; + } + + } + +if (size[i]) { + s =write (fd[i], buf[i] + pos[i], size[i]); +if (s >=0 ) { + size[i] -= s; + pos[i] += s;tools/test-runner.c static int start_audio_server(pid_t pids[2]) · line 958
Quoted 10 lines
tools/test-runner.c +10
-0+ }else if (errno ==EINTR || errno ==EAGAIN + || errno ==EWOULDBLOCK ) { +/* ok */ + }else { +perror ("write" ); + goto error; + } + } + } + }It doesn't look that great if you ask me. I think this requires us to copy significantly more data than before, likely resulting in worse performance, which could be detrimental in resource-limited environments like GitHub CI/CD.
Quoted 151 lines
tools/test-runner.c +111
-13+return ; + +error: +fprintf (stderr,"Bluetooth controller forward terminated with error\n" ); +exit (1 ); +} + +static int start_controller_forward (const char *path,pid_t *controller_pid) +{ +struct termios ti; +pid_t pid; +int src =-1 , dst =-1 , fd =-1 ; +int ret, saved_ldisc, ldisc =N_HCI ; + +/* virtio-serial ports cannot be used for HCI line disciple, so +* openpty() serial device and forward data to/from it. +*/ + + src =open (path,O_RDWR ); +if (src <0 ) + goto error; + +/* Raw mode TTY */ +memset (&ti,0 ,sizeof (ti)); +cfmakeraw (&ti); + ti.c_cflag |=B115200 |CLOCAL |CREAD ; + +/* With flow control */ + ti.c_cflag |=CRTSCTS ; + + ret =openpty (&dst, &fd,NULL , &ti,NULL ); +if (ret <0 ) + goto error; + +if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { +perror ("Failed get serial line discipline" ); + goto error; + } + +if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { +perror ("Failed set serial line discipline" ); + goto error; + } + +printf ("Switched line discipline from %d to %d\n" , saved_ldisc, ldisc); + + pid =fork (); +if (pid <0 ) { +perror ("Failed to fork new process" ); + goto error; + }else if (pid ==0 ) { +close (fd); +copy_fd_bidi (src, dst); +exit (0 ); + } + + *controller_pid = pid; + +close (src); +close (dst); +return fd; + +error: +if (src >=0 ) +close (src); +if (dst >=0 ) +close (dst); +if (fd >=0 ) +close (fd); +return -1 ; +} + +static int attach_controller (pid_t *controller_pid) +{ +unsigned int basic_flags, extra_flags; +char path[PATH_MAX ]; +int fd; + + *controller_pid =-1 ; + +if (!find_attach_dev (path)) { +printf ("Failed to find Bluetooth controller virtio\n" ); +return -1 ; + } + +printf ("Forwarding Bluetooth controller from %s\n" , path); + + fd =start_controller_forward (path, controller_pid); +if (fd <0 ) { +printf ("Failed to forward Bluetooth controller\n" ); +return -1 ; + } + + basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); + extra_flags = (1 <<HCI_UART_VND_DETECT ); + +printf ("Attaching Bluetooth controller\n" ); + +return attach_proto (fd,HCI_UART_H4 , basic_flags, extra_flags); +} +static void run_command (char *cmdname,char *home) {char *argv[9 ], *envp[3 ];int pos =0 , idx =0 ;int serial_fd;pid_t pid, dbus_pid, daemon_pid, monitor_pid, emulator_pid, - dbus_session_pid, audio_pid[2 ]; + dbus_session_pid, audio_pid[2 ], controller_pid;int i;if (!home) {@@ -910,18 +1072,11 @@ static void run_command(char *cmdname, char *home) }if (num_devs) { -const char *node ="/dev/ttyS1" ; -unsigned int basic_flags, extra_flags; - -printf ("Attaching BR/EDR controller to %s\n" , node); - - basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); - extra_flags = (1 <<HCI_UART_VND_DETECT ); - - serial_fd =attach_proto (node,HCI_UART_H4 , basic_flags, - extra_flags); - }else + serial_fd =attach_controller (&controller_pid); + }else { serial_fd =-1 ; + controller_pid =-1 ; + }if (start_dbus) {create_dbus_system_conf ();@@ -1063,6 +1218,11 @@ start_next: monitor_pid =-1 ; } +if (corpse == controller_pid) { +printf ("Controller terminated\n" ); + controller_pid =-1 ; + } +for (i =0 ; i <2 ; ++i) {if (corpse == audio_pid[i]) {printf ("Audio server %d terminated\n" , i); --2.53.0
Signature · 1 line
Pauli Virtanen
Re: [PATCH BlueZ v3 09/20] test-runner: use virtio-serial for implementing -u device forwarding
In reply to Pauli VirtanenQuoted 57 lines · Pauli Virtanen
Using pci-serial to forward eg. btvirt sockets is unreliable, as qemu or
kernel seems to be sometimes dropping part of the sent data or insert spurious \0 bytes, leading to sporadic errors like:kernel: Bluetooth: hci0: command
0x0c52tx timeout
kernel: Bluetooth: hci0: Opcode0x0c52failed: -110
btvirt: packet error, unknown type: 0This appears to occur most often when host system is under load, e.g. due to multiple test-runners running at the same time. The problem is
not specific to btvirt, but seems to be in the qemu serial device layer
vs. kernel interaction.Change test-runner to use virtserialport to forward the btvirt connection inside the VM, as virtio-serial doesn't appear to have these
problems.Since it's not a TTY device, we have to do vport <-> tty-with-hci- ldisc
forwarding of the data in test-runner, so this becomes a bit more involved.
---
Makefile.tools | 2 +
configure.ac | 9 ++
tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++--------- --3 files changed, 241 insertions(+), 70 deletions(-)Makefile.tools +0
-0@@ -321,6 +321,8 @@ tools_btgatt_server_SOURCES = tools/btgatt-
server.csrc/uuid-helper.c
tools_btgatt_server_LDADD = src/libshared-mainloop.la \
lib/libbluetooth-internal.laMakefile.tools +2
-0+tools_test_runner_LDADD = $(OPENPTY_LIBS) +tools_rctest_LDADD =
lib/libbluetooth-internal.la
tools_l2test_LDADD =lib/libbluetooth-internal.laconfigure.ac +0
-0@@ -251,6 +251,15 @@ AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable- tools], [disable Bluetooth tools]), [enable_tools=${enableval}]) AM_CONDITIONAL(TOOLS, test "${enable_tools}" != "no")
configure.ac AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable-tools], · line 259
Quoted 6 lines
configure.ac +6
-0+openpty_libs= +if (test "${enable_tools}" != "no"); then + AC_CHECK_FUNCS([openpty], [openpty_libs=], + [AC_CHECK_LIB([util], [openpty], [openpty_libs=-lutil], + [AC_CHECK_LIB([bsd], [openpty], [openpty_libs=-lbsd], + [AC_MSG_ERROR([openpty not found])])])])
Why check for -lbsd? The openpty(3) man page just says "-lutil". Is this maybe copy/pasted from software that can also run on BSD?
Quoted 398 lines
configure.ac +3
-0+fi +AC_SUBST(OPENPTY_LIBS, [${openpty_libs}]) +AC_ARG_ENABLE(monitor,
AS_HELP_STRING([--disable-monitor], [disable Bluetooth monitor]), [enable_monitor=${enableval}])AM_CONDITIONAL(MONITOR, test "${enable_monitor}" != "no")tools/test-runner.c +3
-0@@ -24,6 +24,9 @@ #include <getopt.h> #include <poll.h> #include <limits.h> +#include <dirent.h> +#include <pty.h> +#include <stdint.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/types.h>@@ -306,7 +309,7 @@ static void start_qemu(void) testargs);
argv = alloca(sizeof(qemu_argv) +tools/test-runner.c +1
-1- (sizeof(char *) * (6 + (num_devs * 4))) + + (sizeof(char *) * (8 + (num_devs * 4))) +(
sizeof(char *)* (usb_dev? 4 : 0)) + (sizeof(char *)*num_extra_opts));memcpy(argv, qemu_argv, sizeof(qemu_argv));tools/test-runner.c +0
-0@@ -330,14 +333,17 @@ static void start_qemu(void) argv[pos++] = "-append"; argv[pos++] = (char *) cmdline;tools/test-runner.c +3
-0+ argv[pos++] ="-device" ; + argv[pos++] ="virtio-serial" ; +for (i = 0; i <
num_devs; i++) {
char *chrdev, *serdev;
chrdev =alloca(48 + strlen(device_path));sprintf(chrdev, "socket,path=%s,id=bt%d", device_path, i);tools/test-runner.c +2
-2- serdev =alloca (48 ); -sprintf (serdev,"pci-serial,chardev=bt%d" , i); + serdev =alloca (64 ); + sprintf(serdev,"virtserialport,chardev=bt%d,name=bt.%d", i, i);
argv[pos++] = "-chardev";
argv[pos++] = chrdev;tools/test-runner.c +0
-0@@ -360,65 +366,12 @@ static void start_qemu(void) execve(argv[0], argv, qemu_envp); }tools/test-runner.c +0
-45-static int open_serial (const char *path) -{ -struct termios ti; -int fd, saved_ldisc, ldisc =N_HCI ; - - fd =open (path,O_RDWR |O_NOCTTY ); -if (fd <0 ) { -perror ("Failed to open serial port" ); -return -1 ; - } - -if (tcflush (fd,TCIOFLUSH ) <0 ) { -perror ("Failed to flush serial port" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { -perror ("Failed get serial line discipline" ); -close (fd); -return -1 ; - } - -/* Switch TTY to raw mode */ -memset (&ti,0 ,sizeof (ti)); -cfmakeraw (&ti); - - ti.c_cflag |= (B115200 |CLOCAL |CREAD ); - -/* Set flow control */ - ti.c_cflag |=CRTSCTS ; - -if (tcsetattr (fd,TCSANOW , &ti) <0 ) { -perror ("Failed to set serial port settings" ); -close (fd); -return -1 ; - } - -if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { -perror ("Failed set serial line discipline" ); -close (fd); -return -1 ; - } - - printf("Switched line discipline from %d to %d\n",
saved_ldisc, ldisc);tools/test-runner.c +1
-5- -return fd; -} - -static int attach_proto(const char *path, unsigned int proto, +static int attach_proto(int fd, unsigned int proto,unsigned int
mandatory_flags,
unsigned intoptional_flags) {
unsigned int flags =mandatory_flags|optional_flags;tools/test-runner.c +1
-5-int fd, dev_id; - - fd =open_serial (path); -if (fd <0 ) -return -1 ; +int dev_id;
if (ioctl(fd, HCIUARTSETFLAGS, flags)< 0) { if (errno == EINVAL) {tools/test-runner.c +0
-0@@ -895,13 +848,222 @@ static int start_audio_server(pid_t pids[2]) return 0; }tools/test-runner.c +17
-0+static bool find_attach_dev (char path[PATH_MAX ]) +{ +const char *vport_path ="/sys/class/virtio-ports" ; +struct dirent *entry; +DIR *dir; + + dir =opendir (vport_path); +if (!dir) +return false; + +while ((entry =readdir (dir)) !=NULL ) { +FILE *f; +char buf[64 ]; +size_t size; + +snprintf (path,PATH_MAX ,"%s/%s/name" , vport_path, + entr
y->d_name);tools/test-runner.c +11
-0+ f =fopen (path,"r" ); +if (!f) +continue ; + + size =fread (buf,1 ,sizeof (buf) -1 , f); + buf[size] =0 ; + +fclose (f); + +if (strncmp (buf,"bt." ,3 ) ==0 ) { + snprintf(path, PATH_MAX, "/dev/%s", entry-Quoted 1 line
d_name);tools/test-runner.c +52
-0+closedir (dir); +return true; + } + } + +closedir (dir); +return false; +} + +static void copy_fd_bidi (int src,int dst) +{ +fd_set rfds, wfds; +int fd[2 ] = { src, dst }; +uint8_t buf[2 ][4096 ]; +size_t size[2 ] = {0 ,0 }; +size_t pos[2 ] = {0 ,0 }; +int i, ret; + +/* Simple copying of data src <-> dst to both directions */ + +for (i =0 ; i <2 ; ++i) { +int flags =fcntl (fd[i],F_GETFL ); + +if (fcntl (fd[i],F_SETFL , flags |O_NONBLOCK ) <0 ) { +perror ("fcntl" ); + goto error; + } + } + +while (1 ) { +FD_ZERO (&rfds); +FD_ZERO (&wfds); + +for (i =0 ; i <2 ; ++i) { +if (size[i]) +FD_SET (fd[i], &wfds); +else +FD_SET (fd[1 - i], &rfds); + } + + ret =select (FD_SETSIZE , &rfds, &wfds,NULL ,NULL ); +if (ret <0 ) { +if (errno ==EINTR ) +continue ; +perror ("select" ); + goto error; + } + +for (i =0 ; i <2 ; ++i) { +ssize_t s; + +if (!size[i] &&FD_ISSET (fd[1 - i], &rfds)){
tools/test-runner.c +1
-0+ s = read(fd[1 - i], buf[i],
sizeof(buf[i]));tools/test-runner.c +14
-0+if (s >=0 ) { + size[i] = s; + pos[i] =0 ; + }else if (errno ==EINTR ) { +/* ok */ + }else { +perror ("read" ); + goto error; + } + + } + +if (size[i]) { + s = write(fd[i], buf[i] + pos[i],size[i]);
tools/test-runner.c +4
-0+if (s >=0 ) { + size[i] -= s; + pos[i] += s; + } else if (errno == EINTR || errno== EAGAIN
tools/test-runner.c +1
-0+ || errno ==EWOULDBLOCK) {
tools/test-runner.c +12
-0+/* ok */ + }else { +perror ("write" ); + goto error; + } + } + } + } +return ; + +error: + fprintf(stderr, "Bluetooth controller forward terminatedwith error\n");
tools/test-runner.c +4
-0+exit (1 ); +} + +static int start_controller_forward(const char *path, pid_t
*controller_pid)tools/test-runner.c +7
-0+{ +struct termios ti; +pid_t pid; +int src =-1 , dst =-1 , fd =-1 ; +int ret, saved_ldisc, ldisc =N_HCI ; + +/* virtio-serial ports cannot be used for HCI line disciple, so
tools/test-runner.c +30
-0+* openpty() serial device and forward data to/from it. +*/ + + src =open (path,O_RDWR ); +if (src <0 ) + goto error; + +/* Raw mode TTY */ +memset (&ti,0 ,sizeof (ti)); +cfmakeraw (&ti); + ti.c_cflag |=B115200 |CLOCAL |CREAD ; + +/* With flow control */ + ti.c_cflag |=CRTSCTS ; + + ret =openpty (&dst, &fd,NULL , &ti,NULL ); +if (ret <0 ) + goto error; + +if (ioctl (fd,TIOCGETD , &saved_ldisc) <0 ) { +perror ("Failed get serial line discipline" ); + goto error; + } + +if (ioctl (fd,TIOCSETD , &ldisc) <0 ) { +perror ("Failed set serial line discipline" ); + goto error; + } + + printf("Switched line discipline from %d to %d\n",
saved_ldisc, ldisc);tools/test-runner.c +37
-0+ + pid =fork (); +if (pid <0 ) { +perror ("Failed to fork new process" ); + goto error; + }else if (pid ==0 ) { +close (fd); +copy_fd_bidi (src, dst); +exit (0 ); + } + + *controller_pid = pid; + +close (src); +close (dst); +return fd; + +error: +if (src >=0 ) +close (src); +if (dst >=0 ) +close (dst); +if (fd >=0 ) +close (fd); +return -1 ; +} + +static int attach_controller (pid_t *controller_pid) +{ +unsigned int basic_flags, extra_flags; +char path[PATH_MAX ]; +int fd; + + *controller_pid =-1 ; + +if (!find_attach_dev (path)) { + printf("Failed to find Bluetooth controllervirtio\n");
tools/test-runner.c +17
-0+return -1 ; + } + +printf ("Forwarding Bluetooth controller from %s\n" , path); + + fd =start_controller_forward (path, controller_pid); +if (fd <0 ) { +printf ("Failed to forward Bluetooth controller\n" ); +return -1 ; + } + + basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); + extra_flags = (1 <<HCI_UART_VND_DETECT ); + +printf ("Attaching Bluetooth controller\n" ); + + return attach_proto(fd, HCI_UART_H4, basic_flags,
extra_flags);tools/test-runner.c +2
-0+} +static void
run_command(char *cmdname, char *home)
{
char *argv[9], *envp[3];
int pos = 0, idx = 0;
intserial_fd;
pid_tpid,dbus_pid,daemon_pid,monitor_pid,emulator_pid,tools/test-runner.c +1
-1- dbus_session_pid, audio_pid[2]; + dbus_session_pid, audio_pid[2 ], controller_pid;int i;
if (!home) {tools/test-runner.c +0
-0@@ -910,18 +1072,11 @@ static void run_command(char *cmdname, char *home) }
if (num_devs) {tools/test-runner.c +0
-9-const char *node ="/dev/ttyS1" ; -unsigned int basic_flags, extra_flags; - -printf ("Attaching BR/EDR controller to %s\n" , node); - - basic_flags = (1 <<HCI_UART_RESET_ON_INIT ); - extra_flags = (1 <<HCI_UART_VND_DETECT ); - - serial_fd = attach_proto(node, HCI_UART_H4,
basic_flags,
-extra_flags);tools/test-runner.c +2
-1- }else + serial_fd =attach_controller (&controller_pid); + }else {
serial_fd= -1;tools/test-runner.c +2
-0+ controller_pid =-1 ; + }
if (start_dbus) {
create_dbus_system_conf();tools/test-runner.c +0
-0@@ -1063,6 +1218,11 @@ start_next: monitor_pid = -1; }tools/test-runner.c +5
-0+if (corpse == controller_pid) { +printf ("Controller terminated\n" ); + controller_pid =-1 ; + } +for (i = 0; i < 2; ++i) {
if (corpse == audio_pid[i]) {printf("Audio server %d terminated\n", i);
PATCH v3 10/20 doc: enable KVM paravirtualization & clock support in tester kernel config
In reply to Pauli VirtanenEnable KVM guest and PTP options in tester kernel config.
This allows synchronizing tester VM guest with host clock, needed for testers that want to compare timestamps outside the VM guest.
--- doc/ci.config | 8 ++++++++ doc/test-runner.rst | 16 ++++++++++++++++ doc/tester.config | 8 ++++++++ 3 files changed, 32 insertions(+)
doc/ci.config +8 -0
@@ -8,6 +8,14 @@ CONFIG_VIRTIO=y CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_CONSOLE=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT=y +CONFIG_KVM_GUEST=y + +CONFIG_PTP_1588_CLOCK=y +CONFIG_PTP_1588_CLOCK_KVM=y +CONFIG_PTP_1588_CLOCK_VMCLOCK=y + CONFIG_NET=y CONFIG_INET=y
doc/test-runner.rst +16 -0
@@ -122,6 +122,22 @@ options may be useful: CONFIG_DEBUG_MUTEXES=y CONFIG_KASAN=y +Other +----- + +For tests requiring accurate time inside the VM, possible with KVM: + +.. code-block:: + + CONFIG_HYPERVISOR_GUEST=y + CONFIG_PARAVIRT=y + CONFIG_KVM_GUEST=y + + CONFIG_PTP_1588_CLOCK=y + CONFIG_PTP_1588_CLOCK_KVM=y + CONFIG_PTP_1588_CLOCK_VMCLOCK=y + + EXAMPLES ========
doc/tester.config +8 -0
@@ -3,6 +3,14 @@ CONFIG_VIRTIO=y CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_CONSOLE=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT=y +CONFIG_KVM_GUEST=y + +CONFIG_PTP_1588_CLOCK=y +CONFIG_PTP_1588_CLOCK_KVM=y +CONFIG_PTP_1588_CLOCK_VMCLOCK=y + CONFIG_NET=y CONFIG_INET=y
Signature · 1 line
2.53.0
PATCH v3 11/20 doc: add functional/integration testing documentation
In reply to Pauli VirtanenAdd documentation for functional/integration test suite.
--- doc/test-functional.rst | 780 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 doc/test-functional.rst
doc/test-functional.rst new file +780 -0
@@ -0,0 +1,780 @@ +=============== +test-functional +=============== + +**test-functional** [*OPTIONS*] + +DESCRIPTION +=========== + +**test-functional(1)** is used for functional testing of BlueZ and +kernel using multiple virtual machine environments, connected by real +or virtual controllers. + +OPTIONS +======= + +The `test-functional` script simply runs `Pytest +<https://pytest.org>`__ which can take the following options: +https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags + +The following additional options apply: + +:--kernel=<image>: Kernel image (or built Linux source tree root) to + use. See **test-runner(1)** and `tester.config` for required + kernel config. + + If not provided, value from `FUNCTIONAL_TESTING_KERNEL` + environment variable is used. If none, no image is used. + +:--usb=hci0,hci1: USB controllers to use in tests that require use of + real controllers. + + If not provided, value from `FUNCTIONAL_TESTING_CONTROLLERS` + environment variable is used. If none, all USB controllers + with suitable permissions are considered. + +:--force-usb: Force tests to use USB controllers instead of `btvirt`. + +:--vm-timeout=<seconds>: Specify timeout for communication with VM hosts. + +:--log-filter=[+-]<pattern>,[+-]<pattern>,...: Allow/deny lists + for filtering logging output. The pattern is a shell glob matching + to the logger names. + +:--build-dir=<path>: Path to build directory where to search for BlueZ + executables. + +:--list: Output brief lists of existing tests. + +Tests that require kernel image or USB controllers are skipped if none +are available. Normally, tests use `btvirt`. + +VM instances share a directory ``/run/shared`` with host machine, +located on host usually in ``/tmp/bluez-func-test-*/shared-*``. Core +dumps etc. are copied out from it before test instance is shut down. + + +REQUIREMENTS +============ + +General +------- + +The following are needed: + +- QEmu (x86_64) +- ``dbus-daemon`` available + +Recommended: + +- KVM-enabled x86_64 host system +- Preferably built BlueZ source tree +- ``chronyd`` available +- ``util-linux`` tools available +- ``agetty`` available + +Python +------ + +The following Python packages are required: + +.. code-block:: + + pytest + pexpect + dbus-python + +To install them via pip:: + + python3 -m pip install -r test/functional/requirements.txt + +On Fedora / RHEL:: + + sudo dnf install python3-pytest python3-pexpect python3-dbus + + +Kernel +------ + +The **test-functional(1)** tool requires a kernel image with similar +config as **test-runner(1)**. Simplest setup is + +.. code-block:: + + cp ../bluez/doc/tester.config .config + make olddefconfig + make -j8 + +To get log timestamps right, the kernel should have the following +configuration enabled: + +.. code-block:: + + CONFIG_HYPERVISOR_GUEST=y + CONFIG_PARAVIRT=y + CONFIG_KVM_GUEST=y + + CONFIG_PTP_1588_CLOCK=y + CONFIG_PTP_1588_CLOCK_KVM=y + CONFIG_PTP_1588_CLOCK_VMCLOCK=y + +USB +--- + +Some tests may require a hardware controller instead of the virtual `btvirt` one. + + +EXAMPLES +======== + +Run all tests +------------- + +.. code-block:: + + $ test/test-functional --kernel=/pathto/bzImage + + $ export FUNCTIONAL_TESTING_KERNEL=/pathto/bzImage + $ test/test-functional + +Show output during run +---------------------- + +.. code-block:: + + $ test/test-functional --log-cli-level=0 + +Show only specific loggers: + +.. code-block:: + + $ test/test-functional --log-cli-level=0 --log-filter=rpc,host + + $ test/test-functional --log-cli-level=0 --log-filter=*.bluetoothctl + +Filter out loggers: + +.. code-block:: + + $ test/test-functional --log-cli-level=0 --log-filter=-host + + $ test/test-functional --log-cli-level=0 --log-filter=host,-host.*.1 + +Run selected tests +------------------ + +.. code-block:: + + $ test/test-functional test/functional/test_cli_simple.py::test_bluetoothctl_script_show + + $ test/test-functional -k test_bluetoothctl_script_show + + $ test/test-functional -k 'test_btmgmt or test_bluetoothctl' + +Don't run tests with a given marker: + +.. code-block:: + + $ test/test-functional -m "not pipewire" + +Don't run known-failing tests: + +.. code-block:: + + $ test/test-functional -m "not xfail" + +Note that otherwise known-failing tests would be run, but with +failures suppressed. + +Run previously failed and stop on failure +----------------------------------------- + +.. code-block:: + + $ test/test-functional -x --ff + +List all tests +-------------- + +.. code-block:: + + $ test/test-functional --list + +Show errors from know-failing test +---------------------------------- + +.. code-block:: + + $ test/test-functional --runxfail -k test_btmgmt_info + +Redirect USB devices +-------------------- + +.. code-block:: + + $ test/test-functional --usb=hci0,hci1 + + $ export FUNCTIONAL_TESTING_CONTROLLERS=hci0,hci1 + $ test/test-functional -vv + +This does not require running as root. Changing device permissions is +sufficient. In verbose mode (``-vv``) some instructions are printed. + +Run all tests using the USB controllers: + +.. code-block:: + + $ test/test-functional --usb=hci0,hci1 --force-usb + +Run tests in parallel +--------------------- + +pytest-xdist is required for parallel execution. To run: + +.. code-block:: + + $ test/test-functional -n auto + +To reduce VM setup/teardowns: + +.. code-block:: + + $ test/test-functional -n auto --dist loadgroup + +Logging in to a test VM instance +-------------------------------- + +While test is running: + +.. code-block:: + + $ test/test-functional-attach + +For this to be useful, usually, you need to pause the test +e.g. by running with ``--trace`` option. + +To do it manually, when starting the tester will log a line like:: + + TTY: socat /tmp/bluez-func-test-q658swgi/bluez-func-test-tty-0 STDIO,rawer + +with the location of the socket where the serial is connected to. + +WRITING TESTS +============= + +The functional tests are written in files (test modules) names +`test/functional/test_*.py`. They are written using standard Pytest +style. See https://docs.pytest.org/en/stable/getting-started.html + +Example: Virtual machines +------------------------- + +.. code-block:: python + + from pytest_bluez import host_config, Bluetoothd, Bluetoothctl + + @host_config( + [Bluetoothd(), Bluetoothctl()], + [Bluetoothd(), Bluetoothctl()], + ) + def test_bluetoothctl_pair(hosts): + host0, host1 = hosts + + host0.bluetoothctl.send("scan on\n") + host0.bluetoothctl.expect(f"Controller {host0.bdaddr.upper()} Discovering: yes") + + host1.bluetoothctl.send("pairable on\n") + host1.bluetoothctl.expect("Changing pairable on succeeded") + host1.bluetoothctl.send("discoverable on\n") + host1.bluetoothctl.expect(f"Controller {host1.bdaddr.upper()} Discoverable: yes") + + host0.bluetoothctl.expect(f"Device {host1.bdaddr.upper()}") + host0.bluetoothctl.send(f"pair {host1.bdaddr}\n") + + idx, m = host0.bluetoothctl.expect(r"Confirm passkey (\d+).*:") + key = m[0].decode("utf-8") + + host1.bluetoothctl.expect(f"Confirm passkey {key}") + + host0.bluetoothctl.send("yes\n") + host1.bluetoothctl.send("yes\n") + + host0.bluetoothctl.expect("Pairing successful") + +The test declares a VM setup with two Qemu instances, where both hosts +run bluetoothd and start a bluetoothctl process. The Qemu instances +have `btvirt` virtual BT controllers and can see each other. + +The test itself runs on the parent host. + +The `host0/1.bluetoothctl.*` commands invoke RPC calls to one of the +the two VM instances. In this case, they are controlling the +`bluetoothctl` process using `pexpect` library to deal with its +command line. + +When the test body finishes executing, the test passes. Or, it fails +if any ``assert`` statement fails or an error is raised. For example, +above ``RemoteError`` due to bluetoothctl not proceeding as expected +in pairing is possible. + +The host configuration (bluetoothd + bluetoothctl above) is torn down +between test (SIGTERM/SIGKILL sent etc.). + +By default the VM instance itself continues running, and may be used +for other tests that share the same VM setup. + +Generally, the framework automatically orders the tests so that the VM +setup does not need to be restarted unless needed. + + +Example host plugin +------------------- + +The `host.bluetoothctl` implementation used above is as follows: + +.. code-block:: python + + from pytest_bluez import HostPlugin, Bluetoothd + + class Bluetoothctl(Pexpect): + # Declare unique plugin name + name = "bluetoothctl" + + # Declare plugin dependencies to be loaded first + depends = [Bluetoothd()] + + # These run on parent host side: + + def __init__(self, subdir, name): + self.exe = utils.find_exe(subdir, name) + + def presetup(self): + pass + + # These run on VM side at setup/teardown: + + def setup(self, impl): + self.log = logging.getLogger(self.name) + self.log_stream = utils.LogStream(self.name) + self.ctl = pexpect.spawn(self.exe, logfile=self.log_stream.stream) + + def teardown(self): + self.ctl.terminate() + + # These define custom RPC methods that can be called + + def expect(self, *a, **kw): + ret = self.ctl.expect(*a, **kw) + self.log.debug("match found") + return ret, self.ctl.match.groups() + + def send(self, *a, **kw): + return self.ctl.send(*a, **kw) + + + +Host plugins are for injecting code to run on the VM side test hosts. +The host plugins have scope of one test. The VM side test framework +sends SIGTERM and SIGKILL to all processes in the test process group +to reset the state between each test. + +The plugins are declared by inheriting from `HostPlugin`. Their +`__init__()` is supposed to only store declarative configuration on +`self` and runs on parent side early in the test discovery phase. The +`presetup` runs on parent side in test setup phase, before VM +environment is started. The plugin can for example do +`pytest.skip(reason="something")` to skip the test. + +The `setup()` and `teardown()` methods run on VM-side at host +environment start and end. All other methods can be invoked via RPC +by the parent tester, and any values returned by them are passed via +RPC back to the parent. + +To load a plugin to a VM host, pass it to `host_config()` in the +declaration of a given test. + +Test fixtures +============= + +The following test fixtures are used to deal with spawning VM hosts: + +hosts +----- + +.. code-block:: + + Session-scope fixture that expands to a list of VM host proxies + (`HostProxy`), with configuration as specified in `host_config`. The + VM instances used may be reused by other tests. The userspace test + runner is torn down between tests. + + Example: + + def test_something(hosts): + host0 = hosts[0] + host1 = hosts[1] + +hosts_once +---------- + +.. code-block:: + + def test_something(hosts_once): + host0 = hosts_once[0] + host1 = hosts_once[1] + +Function-scope fixture. Same as `hosts`, but spawn separate VM +instances for this test only. + +Others +------ + +The following fixtures are defined, but mainly for use as dependencies +to `hosts`: `kernel` (selected kernel image), `usb_indices` (selected +USB controllers), `host_setup` (current host plugin configurations), +`vm_setup` (VM host configuration), `vm` (VM instances without +userspace setup), `vm_once` (same but with function scope). + +Utilities +========= + +In addition to standard Pytest features, the following items are +available in the `pytest_bluez` module. + +host_config +----------- + +.. code-block:: + + @host_config(*host_setup, hw=False, reuse=False) + + Declare host configuration. + + Args: + *host_setup: each argument is a list of plugins to be loaded on a host. + The number of arguments specifies the number of hosts. + hw (bool): whether to require hardware BT controller + reuse (bool): whether to define a setup where the test host processes + are not required to be torn down between tests. This is only useful + for tests that do not perturb e.g. bluetoothd state too much. + + Returns: + callable: decorator setting pytest attributes + + Example: + + @host_config([Bluetoothd()], [Bluetoothd()]) + def test_something(hosts): + host0, host1 = hosts + + Example: + + # Allow not restarting Bluetoothd between tests sharing this configuration + base_config = host_config([Bluetoothd()], reuse=True) + + @base_config + def test_one(hosts): + host0, = hosts + + @base_config + def test_two(hosts): + # Note: uses same Bluetoothd() instance as above + host0, = hosts + +parametrized_host_config +------------------------ + +.. code-block:: + + Declare parametrized host configurations. + + See https://docs.pytest.org/en/stable/how-to/parametrize.html for the + concept. + + Args: + param_host_setups (list): list of host setups + hw (bool): whether to require hardware BT controller + reuse (bool): whether to define a setup where the test host processes + are not required to be torn down between tests. This is only useful + for tests that do not perturb e.g. bluetoothd state too much. + + Returns: + callable: decorator setting pytest attributes + +HostProxy +--------- + +.. code-block:: + + class HostProxy: + """ + Parent-side proxy for VM host: load plugins, RPC calls to plugins + """ + + def load(self, plugin: HostPlugin): + """ + Load given plugin to the VM host synchronously. + """ + + def start_load(self, plugin: HostPlugin): + """ + Initiate loading the given plugin to the VM host. Use + `wait_load` to wait for completion and make loaded plugins + usable. + + """ + + def wait_load(self): + """ + Wait for plugin loads to complete, and make plugins available. + """ + + def close(self) + """ + Shutdown this VM host tester instance. + """ + + def __getattr__(self, name): + """ + Get a proxy attribute for one of the loaded plugins + """ + +Parent host-side representation of one VM host with loadable plugins. + +Plugins are usually loaded based on `host_setup`, but can also be +loaded during the test itself. + +Loaded plugins appear as attributes on the host proxy. + +find_exe +-------- + +.. code-block:: + + from pytest_bluez import find_exe + bluetoothctl = find_exe("client", "bluetoothctl") + +Find absolute path to the given executable, either within BlueZ build +directory or on host. + + +mainloop_invoke +------------- + +.. code-block:: + + Blocking invoke of `func` in GLib main loop. + + Note: + + GLib main loop is only available for VM host plugins, not in tester. + + Example: + + value = mainloop_invoke(lambda: 123) + assert value == 123 + + Warning: + dbus-python **MUST** be used only from the GLib main loop, + as the library has concurrency bugs. All functions using it + **MUST** either run from GLib main loop eg. via mainloop_wrap + +mainloop_wrap +------------- + +.. code-block:: + + Wrap function to run in GLib main loop thread + + Note: + + GLib main loop is only available for VM host plugins, not in tester. + + Example: + + @mainloop_wrap + def func(): + bus = dbus.SystemBus() + +mainloop_wrap +------------- + +.. code-block:: + + Wrap function to assert it runs from GLib main loop + + Note: + + GLib main loop is only available for VM host plugins, not in tester. + + Example: + + @mainloop_assert + def func(): + bus = dbus.SystemBus() + +LogStream +--------- + +.. code-block:: + + from pytest_bluez import LogStream + + log_stream = LogStream("bluetoothctl") + subprocess.run(["bluetoothctl", "show"], stdout=log_stream.stream) + +Utility to redirect a stream to logging with accurate kernel-provided +timestamps. + +RemoteError +----------- + +.. code-block:: + + from pytest_bluez import RemoteError + + try: + host.call(foo) + except RemoteError as exc: + print(exc.traceback) + original_exception = exc.exc + +Exception raised on the VM side, passed through RPC. Properties: +`traceback` is a traceback string and `exc` is the original exception +instance raised on the remote side. + +Host plugins +============ + +The following host plugins are available: + +HostPlugin +---------- + +Base class for host plugins. See also example above. + +.. code-block:: + + class HostPlugin: + """ + Plugin to insert code to VM host side. + + Attributes: + name (str): unique name for the plugin + depends (tuple[HostPlugin]): plugins to be loaded before this one + value (object): object to appear as HostProxy attribute on parent side. + If None, the plugin is represented by a proxy object that does RPC + calls. Otherwise, must be a serializable value. + + """ + + name = None + depends = () + value = None + + def __init__(self): + """ + Configure plugin (runs on parent host side). This is + called at test discovery time, so should mainly store static + data. + + """ + pass + + def presetup(self): + """ + Parent host-side setup, before VM environment is started. May + use pytest.skip() to skip tests in case plugin cannot be set up. + + """ + pass + + def setup(self, impl): + """ + VM-side setup + + Args: + impl (Implementation): plugin host object + """ + pass + + def teardown(self): + """VM-side teardown""" + pass + +Bdaddr +------ + +Host plugin providing ``host.bdaddr``. +Loaded by default. + +Bluetoothctl +------------ + +.. code-block:: + + class Bluetoothctl(HostPlugin) + def expect(self, *a, **kw) + def send(self, *a, **kw) + +Host plugin for starting and controlling `bluetoothctl` with pexpect. + +Bluetoothd +---------- + +Host plugin starting Bluetoothd. + +Call +---- + +.. code-block:: + + class Call(HostPlugin) + + Host plugin providing ``host.call(func, *args, **kw)`` and `call_async` + which invoke the given functions on VM host side. Loaded by default. + + Example: + + result = host0.call(my_func, 1, 2, 3) + + Example: + + result_async = host0.call(my_func, 1, 2, 3, sync=False) + ... + result = result_async.wait() + +DbusSession +----------- + +Host plugin providing session DBus, at address +`impl["dbus-session"].address`. + +DbusSystem +---------- + +Host plugin providing system DBus, at address +`impl["dbus-system"].address`. + +Pexpect +------- + +.. code-block:: + + class Pexpect(env.HostPlugin) + + Host plugin for starting and controlling processes with pexpect. + + Example: + + btmgmt = host0.pexpect.spawn(find_exe("tools", "btmgmt")) + btmgmt.send("info\n") + btmgmt.expect("hci0") + btmgmt.close() + +Rcvbuf +------ + +Host plugin setting pipe buffer size defaults. +Loaded by default.
Signature · 1 line
2.53.0
PATCH v3 12/20 test: add functional/integration testing framework
In reply to Pauli VirtanenAdd framework for writing tests simulating "real" environments where BlueZ and other parts of the stack run on different virtual machine hosts that communicate with each other.
Implementation is factored into test/pytest_bluez/ support plugin that's in principle reusable for other projects, and test/functional/ actual test files.
Implements:
- RPC communication with tester instances running each of the VM hosts, so that tests can be written on the parent host which coordinates the execution.
- Extensible way to add stateful test-specific code inside the VM instances
- Logging control: output from different processes running inside the VM are separated and can be filtered.
- Test runner framework with Pytest, factored into a pytest plugin
- Grouping tests to minimize VM reboots
- Redirecting USB controllers to use for testing
There is no requirement that the tests spawn VM instances. The test runner finds any tests in test/**/test_*.py
--- doc/test-functional.rst | 7 +- test/functional/__init__.py | 2 + test/functional/requirements.txt | 4 + test/functional/test_bluetoothctl_vm.py | 152 +++++ test/functional/test_btmgmt_vm.py | 30 + test/pytest.ini | 13 + test/pytest_bluez/__init__.py | 10 + test/pytest_bluez/env.py | 784 ++++++++++++++++++++++++ test/pytest_bluez/host_plugins.py | 566 +++++++++++++++++ test/pytest_bluez/plugin.py | 559 +++++++++++++++++ test/pytest_bluez/rpc.py | 385 ++++++++++++ test/pytest_bluez/runner.py | 20 + test/pytest_bluez/tests/__init__.py | 2 + test/pytest_bluez/tests/test_rpc.py | 62 ++ test/pytest_bluez/tests/test_utils.py | 16 + test/pytest_bluez/utils.py | 706 +++++++++++++++++++++ test/test-functional | 7 + test/test-functional-attach | 52 ++ 18 files changed, 3374 insertions(+), 3 deletions(-) create mode 100644 test/functional/__init__.py create mode 100644 test/functional/requirements.txt create mode 100644 test/functional/test_bluetoothctl_vm.py create mode 100644 test/functional/test_btmgmt_vm.py create mode 100644 test/pytest.ini create mode 100644 test/pytest_bluez/__init__.py create mode 100644 test/pytest_bluez/env.py create mode 100644 test/pytest_bluez/host_plugins.py create mode 100644 test/pytest_bluez/plugin.py create mode 100644 test/pytest_bluez/rpc.py create mode 100644 test/pytest_bluez/runner.py create mode 100644 test/pytest_bluez/tests/__init__.py create mode 100644 test/pytest_bluez/tests/test_rpc.py create mode 100644 test/pytest_bluez/tests/test_utils.py create mode 100644 test/pytest_bluez/utils.py create mode 100755 test/test-functional create mode 100755 test/test-functional-attach
doc/test-functional.rst +4 -3
@@ -81,9 +81,10 @@ The following Python packages are required: .. code-block:: - pytest - pexpect - dbus-python + pytest>=8 + pexpect + dbus-python + PyGObject>=3.40 To install them via pip::
test/functional/__init__.py new file +2 -0
@@ -0,0 +1,2 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later
test/functional/requirements.txt new file +4 -0
@@ -0,0 +1,4 @@ +pytest>=8 +pexpect +dbus-python +PyGObject>=3.40
test/functional/test_bluetoothctl_vm.py new file +152 -0
152 lines not on this page · raw message
test/functional/test_btmgmt_vm.py new file +30 -0
@@ -0,0 +1,30 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later +""" +Tests for btmgmt using VM instances +""" +import sys +import pytest +import subprocess +import tempfile + +from pytest_bluez import host_config, find_exe, run + +pytestmark = [pytest.mark.vm] + +btmgmt = find_exe("tools", "btmgmt") + + +@host_config([]) +def test_btmgmt_info(hosts): + (host,) = hosts + + result = host.call( + run, + [btmgmt, "--index", "0", "info"], + stdout=subprocess.PIPE, + stdin=subprocess.DEVNULL, + encoding="utf-8", + ) + assert result.returncode == 0 + assert f"addr {host.bdaddr.upper()}" in result.stdout
test/pytest.ini new file +13 -0
@@ -0,0 +1,13 @@ +[pytest] +log_format = %(asctime)s %(levelname)-6s %(name)-20s: %(message)s +log_date_format = %Y-%m-%d %H:%M:%S.%f +log_level = 0 +log_file = test-functional.log +markers = + vm: tests requiring VM image + +addopts = + -p pytest_bluez + +# Default timeout +vm_timeout = 30
test/pytest_bluez/__init__.py new file +10 -0
@@ -0,0 +1,10 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later + +from .rpc import RemoteError + +from .env import * +from .utils import * +from .host_plugins import * + +from .plugin import *
test/pytest_bluez/env.py new file +784 -0
784 lines not on this page · raw message
test/pytest_bluez/host_plugins.py new file +566 -0
566 lines not on this page · raw message
test/pytest_bluez/plugin.py new file +559 -0
559 lines not on this page · raw message
test/pytest_bluez/rpc.py new file +385 -0
385 lines not on this page · raw message
test/pytest_bluez/runner.py new file +20 -0
@@ -0,0 +1,20 @@ +#!/usr/bin/python3 -P +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later +import sys +from pathlib import Path +from importlib.machinery import PathFinder + + +class SelfImport(PathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "pytest_bluez": + path = [str(Path(__file__).parent / "..")] + return super().find_spec(fullname, path, target) + + +sys.meta_path.insert(0, SelfImport()) + +import pytest_bluez.env + +sys.exit(pytest_bluez.env._main_runner())
test/pytest_bluez/tests/__init__.py new file +2 -0
@@ -0,0 +1,2 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later
test/pytest_bluez/tests/test_rpc.py new file +62 -0
62 lines not on this page · raw message
test/pytest_bluez/tests/test_utils.py new file +16 -0
@@ -0,0 +1,16 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later +import os +import pytest +import subprocess +import threading + +from .. import utils + + +def test_log_stream(caplog): + with utils.LogStream(__name__) as log_stream: + log_stream.stream.write(b"hello") + + (record,) = (r for r in caplog.records if r.name == __name__) + assert "hello" in record.message
test/pytest_bluez/utils.py new file +706 -0
382 lines not on this page · raw message
test/test-functional-attach new file +52 -0
52 lines not on this page · raw message
The rest of this diff is not shown. View the raw message.
Signature · 1 line
2.53.0
Part of the diff is not shown here. View the raw message.
PATCH v3 13/20 test: functional: add Pipewire-using audio streaming tests
In reply to Pauli VirtanenAdd tests for Pipewire hosts connecting A2DP, BAP, and HFP to each other and stream audio:
test/functional/test_pipewire.py::test_pipewire_a2dp test/functional/test_pipewire.py::test_pipewire_bap_bcast test/functional/test_pipewire.py::test_pipewire_bap_ucast test/functional/test_pipewire.py::test_pipewire_hfp
The A2DP test would have catched the regression fixed by 066a164a524 and the BAP test that fixed by 6b0a08776a.
--- test/functional/test_pipewire.py | 539 +++++++++++++++++++++++++++++++ test/pytest.ini | 1 + 2 files changed, 540 insertions(+) create mode 100644 test/functional/test_pipewire.py
test/functional/test_pipewire.py new file +539 -0
@@ -0,0 +1,539 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later +""" +Tests for Pipewire audio + +To use uninstalled version of Pipewire, run the tests in Pipewire +devenv:: + + meson devenv -C ../pipewire/builddir -w . test/test-functional -m pipewire -v +""" +import sys +import os +import re +import pytest +import subprocess +import tempfile +import time +import logging +import json +import dbus +import threading +from pathlib import Path + +import pytest + +from pytest_bluez import ( + HostPlugin, + host_config, + find_exe, + Bluetoothd, + Bluetoothctl, + DbusSession, + LogStream, + wait_until, + mainloop_wrap, +) + +pytestmark = [pytest.mark.vm, pytest.mark.pipewire] + +log = logging.getLogger(__name__) + +# Use larger VM instances in case ASAN is enabled +VM_MEM = "512M" + + +class Pipewire(HostPlugin): + """ + Launch Pipewire in VM instance + """ + + name = "pipewire" + depends = [DbusSession(), Bluetoothd()] + + def __init__( + self, + uuids=( + "0000110a-0000-1000-8000-00805f9b34fb", + "0000110b-0000-1000-8000-00805f9b34fb", + ), + roles="a2dp_sink a2dp_source", + config=None, + ): + self.uuids = tuple(uuids) + self.roles = str(roles) + self.config = config + + # For running Pipewire from build directory + self.devenv = {} + if os.environ.get("PW_UNINSTALLED"): + devenv_keys = [ + "WIREPLUMBER_MODULE_DIR", + "WIREPLUMBER_CONFIG_DIR", + "WIREPLUMBER_DATA_DIR", + "PIPEWIRE_CONFIG_DIR", + "PIPEWIRE_MODULE_DIR", + "SPA_PLUGIN_DIR", + "SPA_DATA_DIR", + "ACP_PATHS_DIR", + "ACP_PROFILES_DIR", + "GST_PLUGIN_PATH", + "ALSA_PLUGIN_DIR", + "LD_LIBRARY_PATH", + "PW_UNINSTALLED", + "PW_BUILDDIR", + ] + for key in devenv_keys: + value = os.environ.get(key) + if value is not None: + self.devenv[key] = value + + def presetup(self, config): + try: + self.exe_pw = find_exe("", "pipewire") + self.exe_wp = find_exe("", "wireplumber") + self.exe_dump = find_exe("", "pw-dump") + + # check version + res = subprocess.run( + [self.exe_wp, "--version"], stdout=subprocess.PIPE, encoding="utf-8" + ) + m = re.search("libwireplumber ([0-9.]+)", res.stdout) + if m: + version = tuple(int(x) for x in m.group(1).split(".")) + if version < (0, 5, 8): + raise ValueError("wireplumber too old") + else: + raise ValueError("wireplumber version unknown") + except (FileNotFoundError, ValueError) as exc: + pytest.skip(reason=f"Pipewire: {exc}") + + @mainloop_wrap + def setup(self, impl): + self.play = None + self.record = None + self.log = logging.getLogger(self.name) + + self.tmpdir = tempfile.TemporaryDirectory(prefix="pipewire-", dir="/run") + conf_dir = Path(self.tmpdir.name) / "config" + runtime_dir = Path(self.tmpdir.name) / "runtime" + state_dir = Path(self.tmpdir.name) / "state" + + dropin_dir = conf_dir / "wireplumber" / "wireplumber.conf.d" + wp_conf = dropin_dir / "01-config.conf" + wp_extra_conf = dropin_dir / "02-extra-config.conf" + + conf_dir.mkdir() + runtime_dir.mkdir() + dropin_dir.mkdir(parents=True) + state_dir.mkdir() + + environ = dict(os.environ) + environ.update(self.devenv) + + environ["XDG_CONFIG_HOME"] = str(conf_dir) + environ["XDG_STATE_HOME"] = str(runtime_dir) + environ["XDG_RUNTIME_HOME"] = str(runtime_dir) + environ["PIPEWIRE_RUNTIME_DIR"] = str(runtime_dir) + environ["XDG_STATE_HOME"] = str(state_dir) + environ["PIPEWIRE_DEBUG"] = "2" + environ["WIREPLUMBER_DEBUG"] = ( + "spa.bluez5.iso:3,spa.bluez5*:4,s-monitors:4,m-lua-scripting:4,s-linking:4,s-device:4" + ) + + # Handle devenv + if "WIREPLUMBER_CONFIG_DIR" in environ: + environ["WIREPLUMBER_CONFIG_DIR"] = ( + environ["WIREPLUMBER_CONFIG_DIR"] + ":" + str(conf_dir / "wireplumber") + ) + + with open(wp_conf, "w") as f: + text = f""" + monitor.bluez.properties = {{ + bluez5.roles = [ {self.roles} ] + bluez5.decode-buffer.latency = 4096 + }} + """ + f.write(text) + + if self.config is not None: + with open(wp_extra_conf, "w") as f: + f.write(self.config) + + log.info(f"Starting pipewire: {self.exe_pw}") + + self.logger = LogStream("pipewire") + self.pw = subprocess.Popen( + self.exe_pw, + env=environ, + stdout=self.logger.stream, + stderr=subprocess.STDOUT, + ) + + log.info(f"Starting wireplumber: {self.exe_wp}") + + self.wp = subprocess.Popen( + self.exe_wp, + env=environ, + stdout=self.logger.stream, + stderr=subprocess.STDOUT, + ) + + # Wait for Pipewire's bluetooth services + log.info("Wait for Pipewire...") + bus = dbus.SystemBus() + bus.set_exit_on_disconnect(False) + adapter = dbus.Interface( + bus.get_object("org.bluez", "/org/bluez/hci0"), + "org.freedesktop.DBus.Properties", + ) + + def cond(): + uuids = [str(uuid) for uuid in adapter.Get("org.bluez.Adapter1", "UUIDs")] + return all(uuid in uuids for uuid in self.uuids) + + wait_until(cond) + + os.environ["PIPEWIRE_RUNTIME_DIR"] = str(runtime_dir) + + # Wait for wireplumber session services + def cond(): + try: + data = json.loads(self.pw_dump()) + except: + return False + for item in data: + if item.get("type", None) != "PipeWire:Interface:Client": + continue + if item["info"]["props"]["application.name"] != "WirePlumber": + continue + if "api.bluez" in item["info"]["props"].get("session.services", ""): + return True + return False + + wait_until(cond) + + log.info("Pipewire ready") + + def pw_dump(self): + ret = subprocess.run(["pw-dump"], stdout=subprocess.PIPE, encoding="utf-8") + return ret.stdout + + def pw_play(self): + self.play = subprocess.Popen( + [ + "pw-play", + "--raw", + "--rate", + "4000", + "--channels", + "1", + "--format", + "s8", + "-", + ], + stdin=subprocess.PIPE, + ) + self.play_thread = threading.Thread( + target=self._play_thread, args=(self.play.stdin,) + ) + self.play_thread.start() + + def _play_thread(self, stream): + block = bytes([j % 256 for j in range(4096)]) + while True: + try: + stream.write(block) + except: + self.log.info("pw_play ended") + break + + def pw_record(self): + self.record = subprocess.Popen( + [ + "pw-record", + "-P", + "media.class=Audio/Sink", + "--raw", + "--format", + "s8", + "--rate", + "4000", + "--channels", + "1", + "-", + ], + stdout=subprocess.PIPE, + ) + self.record_thread = threading.Thread( + target=self._record_thread, args=(self.record.stdout,) + ) + self.record_thread.start() + self.record_signal = threading.Event() + + def _record_thread(self, stream): + while True: + try: + block = stream.read(256) + if not block: + break + except: + self.log.info("pw_record failed") + break + + # If we get anything nonzero, some signal is getting + # through. Can't check exactness due to encoding and + # possibly heavy underruns in VM environment. + if any(list(block)): + self.log.info("pw_record signal found") + self.record_success = True + self.record_signal.set() + return + else: + self.log.debug("pw_record: waiting for signal") + + self.log.error("pw_record: no signal found") + self.record_success = False + self.record_signal.set() + + def pw_record_wait_signal(self, timeout=160): + res = self.record_signal.wait(timeout=timeout) + return res and self.record_success + + def teardown(self): + log.info("Stop pipewire") + self.pw.terminate() + self.wp.terminate() + if self.play is not None: + try: + self.play.stdin.close() + except BrokenPipeError: + pass + self.play.terminate() + self.play_thread.join() + if self.record is not None: + try: + self.record.stdout.close() + except BrokenPipeError: + pass + self.record.terminate() + self.record_thread.join() + self.tmpdir.cleanup() + + [email protected] +def paired_hosts(hosts, host_setup): + from .test_bluetoothctl_vm import ( + test_bluetoothctl_pair_bredr, + test_bluetoothctl_pair_le, + ) + + le = any( + "ControllerMode = le" in (p.conf or "") + for plugins in host_setup["setup"] + for p in plugins + if isinstance(p, Bluetoothd) + ) + + if le: + test_bluetoothctl_pair_le(hosts) + else: + test_bluetoothctl_pair_bredr(hosts) + + return hosts + + +a2dp_host = [Bluetoothctl(), Pipewire(roles="a2dp_sink a2dp_source")] + + +@host_config(a2dp_host, a2dp_host, mem=VM_MEM) +def test_pipewire_a2dp(paired_hosts): + host0, host1 = paired_hosts + + # Connect + host1.bluetoothctl.send(f"trust {host0.bdaddr}\n") + host0.bluetoothctl.send(f"connect {host1.bdaddr}\n") + + # Wait for pipewire devices to appear + check_pipewire_devices_exist(host0, "a2dp-sink") + + # Test streaming + host1.pipewire.pw_record() + host0.pipewire.pw_play() + + assert host1.pipewire.pw_record_wait_signal() + + +bap_ucast_host = [ + Bluetoothd(conf="[General]\nControllerMode = le\n", args=["-E", "-K"]), + Bluetoothctl(), + Pipewire( + roles="bap_sink bap_source", uuids=("00001850-0000-1000-8000-00805f9b34fb",) + ), +] + + +@host_config(bap_ucast_host, bap_ucast_host, mem=VM_MEM) +def test_pipewire_bap_ucast(paired_hosts): + host0, host1 = paired_hosts + + # Connect + host1.bluetoothctl.send(f"trust {host0.bdaddr}\n") + + host0.bluetoothctl.send(f"scan off\n") + host0.bluetoothctl.send(f"connect {host1.bdaddr}\n") + + # Wait for pipewire devices to appear + check_pipewire_devices_exist(host0, "bap-sink") + + # Test streaming + host1.pipewire.pw_record() + host0.pipewire.pw_play() + + assert host1.pipewire.pw_record_wait_signal() + + +bcast_src_config = """ +monitor.bluez.properties = { + bluez5.bcast_source.config = [ + { + "broadcast_code": "Test", + "encryption": false, + "bis": [ { "qos_preset": "16_2_1" } ] + } + ] +} +""" + +bap_bcast_src_host = [ + Bluetoothd(conf="[General]\nControllerMode = le\n", args=["-E", "-K"]), + Bluetoothctl(), + Pipewire( + roles="bap_bcast_source", + uuids=("00001850-0000-1000-8000-00805f9b34fb",), + config=bcast_src_config, + ), +] + +bap_bcast_snk_host = [ + Bluetoothd(conf="[General]\nControllerMode = le\n", args=["-E", "-K"]), + Bluetoothctl(), + Pipewire(roles="bap_bcast_sink", uuids=("00001850-0000-1000-8000-00805f9b34fb",)), +] + + +@host_config(bap_bcast_src_host, bap_bcast_snk_host, mem=VM_MEM) +def test_pipewire_bap_bcast(hosts): + host0, host1 = hosts + + # Start broadcasting + check_pipewire_devices_exist(host0, "bap-sink") + host0.pipewire.pw_play() + + # Connect + host1.bluetoothctl.send(f"scan on\n") + + host0.bluetoothctl.send(f"advertise on\n") + + host1.pipewire.pw_record() + + idx, m = host1.bluetoothctl.expect(f"Transport (/org/bluez/hci0/.+)") + transport = m[0].decode("utf-8") + + # BUG!: issuing transport select immediately causes failure + # BUG!: as it tries to enter broadcasting state while config(1) + # BUG!: is not finished and BROADCASTING state gets cancelled via + # BUG!: transport.c:bap_state_changed() + # BUG!: -> transport_update_playing(transport, FALSE) + # BUG!: -> transport_set_state(transport, TRANSPORT_STATE_IDLE) + + # TODO: fix the bug and go to transport.select without waiting here + check_pipewire_devices_exist(host1, "device") + + host1.bluetoothctl.send(f"transport.select {transport}\n") + + check_pipewire_devices_exist(host1, "bap-source") + + # Test streaming + assert host1.pipewire.pw_record_wait_signal() + + +hfp_hf_host = [ + Bluetoothctl(), + Pipewire( + roles="hfp_hf", + uuids=("0000111e-0000-1000-8000-00805f9b34fb",), + ), +] + +hfp_ag_host = [ + Bluetoothctl(), + Pipewire( + roles="hfp_ag", + uuids=("0000111f-0000-1000-8000-00805f9b34fb",), + ), +] + + +@host_config(hfp_ag_host, hfp_hf_host, mem=VM_MEM) +def test_pipewire_hfp(paired_hosts): + host0, host1 = paired_hosts + + # Connect + host1.bluetoothctl.send(f"trust {host0.bdaddr}\n") + + host0.bluetoothctl.send(f"scan off\n") + host0.bluetoothctl.send(f"connect {host1.bdaddr}\n") + + # Wait for pipewire devices to appear + check_pipewire_devices_exist(host0, "hfp") + + # Test streaming + host1.pipewire.pw_record() + host0.pipewire.pw_play() + + assert host1.pipewire.pw_record_wait_signal() + + +def check_pipewire_devices_exist(host, profile="a2dp-sink"): + factories = { + "a2dp-sink": ("api.bluez5.a2dp.sink",), + "a2dp-source": ("api.bluez5.a2dp.source",), + "hfp": ("api.bluez5.sco.sink", "api.bluez5.sco.source"), + "bap-sink": ("api.bluez5.media.sink",), + "bap-source": ("api.bluez5.media.source",), + "bap-duplex": ("api.bluez5.media.sink", "api.bluez5.media.source"), + "device": ("bluez5",), + }[profile] + + text = "" + + def cond(): + nonlocal text + + text = host.pipewire.pw_dump() + try: + data = json.loads(text) + except: + return False + + seen = set() + for item in data: + if item.get("type", None) == "PipeWire:Interface:Node": + props = item["info"]["props"] + seen.add(props.get("factory.name", None)) + continue + if item.get("type", None) == "PipeWire:Interface:Device": + props = item["info"]["props"] + seen.add(props.get("device.api", None)) + continue + + if not set(factories).difference(seen): + return True + + return False + + try: + wait_until(cond) + except TimeoutError: + assert False, f"pipewire devices not seen within timeout:\n{text}"
test/pytest.ini +1 -0
@@ -4,6 +4,7 @@ log_date_format = %Y-%m-%d %H:%M:%S.%f log_level = 0 log_file = test-functional.log markers = + pipewire: tests requiring Pipewire vm: tests requiring VM image addopts =
Signature · 1 line
2.53.0
PATCH v3 14/20 test: functional: add --btmon option to start btmon
In reply to Pauli VirtanenAfter test finishes, any btmon dumps created are copied to current working directory.
--- doc/test-functional.rst | 9 +++ test/pytest_bluez/__init__.py | 1 + test/pytest_bluez/btmon.py | 116 ++++++++++++++++++++++++++++++++++ test/pytest_bluez/plugin.py | 33 +++++++--- 4 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 test/pytest_bluez/btmon.py
doc/test-functional.rst +9 -0
@@ -34,6 +34,9 @@ The following additional options apply: environment variable is used. If none, all USB controllers with suitable permissions are considered. +:--btmon: Launch btmon on all hosts to log events, and dump traffic to + test-functional-\*.btsnoop + :--force-usb: Force tests to use USB controllers instead of `btvirt`. :--vm-timeout=<seconds>: Specify timeout for communication with VM hosts.
@@ -726,6 +729,12 @@ Bluetoothd Host plugin starting Bluetoothd. +Btmon +----- + +Host plugin providing btmon running in the background. +Usually should be loaded via `--btmon`. + Call ----
test/pytest_bluez/__init__.py +1 -0
@@ -6,5 +6,6 @@ from .rpc import RemoteError from .env import * from .utils import * from .host_plugins import * +from .btmon import * from .plugin import *
test/pytest_bluez/btmon.py new file +116 -0
116 lines not on this page · raw message
test/pytest_bluez/plugin.py +24 -9
@@ -11,6 +11,7 @@ from pathlib import Path import pytest from . import utils, env +from .btmon import Btmon __all__ = [
@@ -110,6 +111,11 @@ def pytest_addoption(parser): parser.addini( "vm_timeout", "Default timeout for communication with VM etc.", default="30" ) + group.addoption( + "--btmon", + action="store_true", + help="Launch btmon on all hosts to log events, and dump traffic to test-functional-*.btsnoop", + ) # host_plugins.Rcvbuf: parser.addini(
@@ -459,6 +465,9 @@ def _hosts_impl(request, vm, setup, name, reuse): h.set_instance_name(f"{name}.{idx}") + if request.session.config.option.btmon: + plugins = (Btmon(),) + plugins + for p in plugins: h.start_load(p)
@@ -477,18 +486,24 @@ def _close_hosts(request, vm, name): if name is None: return - success = True try: - vm.close_hosts() - except: - success = False - warnings.warn(traceback.format_exc()) + if request.session.config.option.btmon and name is not None: + for h in vm.hosts: + if hasattr(h, "btmon"): + h.btmon._call_noreply("teardown") finally: - _copy_host_files(vm) + success = True + try: + vm.close_hosts() + except: + success = False + warnings.warn(traceback.format_exc()) + finally: + _copy_host_files(vm) - # Stop VM if tester is not responding - if not success: - vm.stop() + # Stop VM if tester is not responding + if not success: + vm.stop() def _copy_host_files(vm):
Signature · 1 line
2.53.0
PATCH v3 15/20 build: add functional testing target
In reply to Pauli VirtanenThis adds check-functional: target that runs the functional test suite.
Also add a --enable-functional-testing=<kernel-image> argument for configure that can be used to include it in the check: make target, possibly with a predefined kernel image.
--- Makefile.am | 9 +++++++++ configure.ac | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+)
Makefile.am +9 -0
@@ -786,6 +786,15 @@ endif TESTS = $(unit_tests) AM_TESTS_ENVIRONMENT = MALLOC_CHECK_=3 MALLOC_PERTURB_=69 +check-functional: all + $(srcdir)/test/test-functional -v --kernel="$(FUNCTIONAL_TESTING_KERNEL)" \ + --bluez-build-dir="$(top_builddir)" --bluez-src-dir="$(srcdir)" \ + $(srcdir)/test + +if FUNCTIONAL_TESTING +check: check-functional +endif + if DBUS_RUN_SESSION AM_TESTS_ENVIRONMENT += dbus-run-session -- endif
configure.ac +22 -0
@@ -414,6 +414,28 @@ if (test "${enable_testing}" = "yes"); then #include <linux/net_tstamp.h>]]) fi +AC_ARG_ENABLE(functional-testing, AS_HELP_STRING([--enable-functional-testing], + [enable functional testing tools]), + [enable_functional_testing=yes; functional_testing_kernel=${enableval}], + [enable_functional_testing=no]) +AM_CONDITIONAL(FUNCTIONAL_TESTING, test "${enable_functional_testing}" = "yes") +AC_ARG_VAR(FUNCTIONAL_TESTING_KERNEL, [vmlinux image to use for functional testing]) +FUNCTIONAL_TESTING_KERNEL=${functional_testing_kernel} + +if (test "${enable_functional_testing}" = "yes"); then + if (test "${enable_client}" = "no" || \ + test "${enable_tools}" != "yes" || \ + test "${enable_testing}" != "yes"); then + AC_MSG_ERROR([--enable-functional-testing requires --enable-client --enable-tools --enable-testing]) + fi + AC_MSG_CHECKING([pytest and dependencies]) + python3 -m pip install --dry-run --no-index -r "${srcdir}/test/functional/requirements.txt" >/dev/null + if (test "$?" != "0"); then + AC_MSG_ERROR([pytest or dependencies missing]) + fi + AC_MSG_RESULT([ok]) +fi + AC_ARG_ENABLE(experimental, AS_HELP_STRING([--enable-experimental], [enable experimental tools]), [enable_experimental=${enableval}])
Signature · 1 line
2.53.0
PATCH v3 16/20 test: functional: impose Python code formatting
In reply to Pauli VirtanenCheck Python code formatting of the functional test suite.
--- doc/test-functional.rst | 3 +++ test/functional/test_tests.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 test/functional/test_tests.py
doc/test-functional.rst +3 -0
@@ -271,6 +271,9 @@ The functional tests are written in files (test modules) names `test/functional/test_*.py`. They are written using standard Pytest style. See https://docs.pytest.org/en/stable/getting-started.html +Use `Black <https://black.readthedocs.io/en/stable/>`__ to autoformat +Python test code. + Example: Virtual machines -------------------------
test/functional/test_tests.py new file +23 -0
@@ -0,0 +1,23 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: GPL-2.0-or-later +""" +Tests for the test suite itself +""" +import sys +import subprocess +import warnings +from pathlib import Path + +import pytest + + +def test_formatting(): + pytest.importorskip("black") + + result = subprocess.run( + [sys.executable, "-mblack", "--check", "--diff", "-q", Path(__file__).parent], + stdout=subprocess.PIPE, + encoding="utf-8", + ) + if result.returncode != 0: + warnings.warn(f"Formatting incorrect:\n{result.stdout}")
Signature · 1 line
2.53.0
PATCH v3 17/20 test: functional: add option for building kernel image first
In reply to Pauli VirtanenAdd convenience option test/test-functional --kernel-build to build a kernel image first.
--- doc/test-functional.rst | 21 +++++++++- test/pytest.ini | 4 ++ test/pytest_bluez/build_kernel.py | 43 ++++++++++++++++++++ test/pytest_bluez/plugin.py | 65 ++++++++++++++++++++++++++++++- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 test/pytest_bluez/build_kernel.py
doc/test-functional.rst +20 -1
@@ -11,6 +11,15 @@ DESCRIPTION kernel using multiple virtual machine environments, connected by real or virtual controllers. +EXAMPLE +======= + +.. code-block:: + + $ ./configure --enable-functional-testing --enable-testing --enable-tools + $ make -j8 + $ test/test-functional --kernel-build -v + OPTIONS =======
@@ -50,6 +59,12 @@ The following additional options apply: :--list: Output brief lists of existing tests. +:--kernel-build=no/use/auto/force: Build a suitable kernel image from source. + +:--kernel-upstream=<GIT_URL>: URL for Git clone of kernel sources. + +:--kernel-branch=<GIT_BRANCH>: Git branch to build from. + Tests that require kernel image or USB controllers are skipped if none are available. Normally, tests use `btvirt`.
@@ -102,7 +117,11 @@ Kernel ------ The **test-functional(1)** tool requires a kernel image with similar -config as **test-runner(1)**. Simplest setup is +config as **test-runner(1)**. If given `--kernel-build` option, a +suitable image is built from sources downloaded under +`test/.pytest_cache`. + +Simplest setup is .. code-block::
test/pytest.ini +4 -0
@@ -12,3 +12,7 @@ addopts = # Default timeout vm_timeout = 30 + +# Default sources for kernel-build when requested +kernel_upstream = https://git.kernel.org/pub/scm/linux/kernel/git/bluetooth/bluetooth-next.git/ +kernel_branch = master
test/pytest_bluez/build_kernel.py new file +43 -0
43 lines not on this page · raw message
test/pytest_bluez/plugin.py +64 -1
83 lines not on this page · raw message
Signature · 1 line
2.53.0
PATCH v3 18/20 test: functional: add custom Agent1 implementation for testing
In reply to Pauli VirtanenEnable more precise testing of the Agent1 interface, and allow doing operations like pairing without bluetoothctl.
--- doc/test-functional.rst | 137 +++++++++++++ test/functional/test_agent.py | 46 +++++ test/pytest_bluez/__init__.py | 1 + test/pytest_bluez/agent.py | 371 ++++++++++++++++++++++++++++++++++ 4 files changed, 555 insertions(+) create mode 100644 test/functional/test_agent.py create mode 100644 test/pytest_bluez/agent.py
doc/test-functional.rst +137 -0
@@ -729,6 +729,143 @@ Base class for host plugins. See also example above. """VM-side teardown""" pass +Agent +----- + +DBus org.bluez.Agent1 test implementation. + +.. code-block:: + + class Agent(env.HostPlugin): + """ + Host plugin providing org.bluez.Agent1 test implementation. + + Asynchronous events are handled via expect(). + + Example: + + host.agent.device_method(host1.bdaddr, "Pair") + event = host.agent.expect("org.bluez.Agent1.RequestConfirmation") + assert event.passkey == 1234 + host.agent.reply() + """ + + depends = [Bluetoothd()] + name = "agent" + + def __init__(self, capability="KeyboardDisplay", path="/agent"): + + def has_device(self, address): + """ + Return True if device with given address exists + """ + + def device_method(self, address, method, *a, **kw): + """ + Call given org.bluez.Device1 DBus method + + Args: + address (str): bdaddr of target device + method (str): name of DBus method, without interface prefix + *a, **kw: argument passed to the DBus method call + + Events: + AgentEvent(kind="org.bluez.Device1.{method}:reply") + """ + + def adapter_method(self, method, *a, **kw): + """ + Call given org.bluez.Adapter1 DBus method + + Args: + method (str): name of DBus method, without interface prefix + *a, **kw: argument passed to the DBus method call + + Events: + AgentEvent(kind="org.bluez.Adapter1.{method}") + """ + + def adapter_set(self, key, value): + """ + Set given org.bluez.Adapter1 property + """ + + def adapter_get(self, key): + """ + Get given org.bluez.Adapter1 property + """ + + def get_event(self, block=True): + """ + Get most recent pending AgentEvent, blocking optional + """ + + def expect(self, kinds): + """ + Get most recent pending AgentEvent and assert its kind + + Returns: + event (AgentEvent) + """ + + def reply(self, *value): + """ + Provide DBus reply to the most recent pending AgentEvent + + Arguments: + *value: DBus reply return values + """ + + def reply_error(self, err=None): + """ + Provide DBus error reply to the most recent pending AgentEvent + + Arguments: + err (dbus.DBusException): DBus error. Default: org.bluez.Error.Rejected + """ + +.. code-block:: + + class Event: + """ + Asynchronous event + + Properties: + kind (str): event kind + info (dict): event properties (also available as attributes) + """ + +.. code-block:: + + class EventPluginMixin: + """ + Simple expect() / reply() pattern for handing async events in + host plugins. + """ + +.. code-block:: + + def dbus_service_event_method( + interface, name, args=(), in_signature="", out_signature="", sync=True + ): + """ + dbus.service.method that pushes Event instances to self.events + + Example: + + class AgentObject(dbus.service.Object): + @utils.mainloop_assert + def __init__(self, bus, path, events): + self.events = events + super().__init__(bus, path) + + AuthorizeService = dbus_service_event_method( + "org.bluez.Agent1", + "AuthorizeService", ("device", "uuid"), "os", sync=False + ) + + """ + Bdaddr ------
test/functional/test_agent.py new file +46 -0
46 lines not on this page · raw message
test/pytest_bluez/__init__.py +1 -0
@@ -7,5 +7,6 @@ from .env import * from .utils import * from .host_plugins import * from .btmon import * +from .agent import * from .plugin import *
test/pytest_bluez/agent.py new file +371 -0
371 lines not on this page · raw message
Signature · 1 line
2.53.0
PATCH v3 19/20 test: functional: warn on kernel warning/bug messages
In reply to Pauli VirtanenEmit warnings if there is a WARNING/BUG message from kernel.
--- test/pytest_bluez/env.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-)
test/pytest_bluez/env.py +22 -3
@@ -581,8 +581,8 @@ class Environment: errors = "\n".join(errors) raise RuntimeError(f"Errors closing hosts:\n{errors}") - def _add_log(self, *a, **kw): - f = utils.LogStream(*a, **kw) + def _add_log(self, *a, cls=utils.LogStream, **kw): + f = cls(*a, **kw) self.log_streams.append(f) return f.stream
@@ -743,7 +743,7 @@ class Environment: host_names.append(f"host.{ENV_INDEX}.{idx}") - logger = self._add_log(host_names[-1]) + logger = self._add_log(host_names[-1], cls=_HostLogStream) self.jobs.append(Popen(cmd, stdout=logger, stderr=logger, stdin=DEVNULL)) # Start log reader
@@ -782,3 +782,22 @@ class Environment: def __exit__(self, type, value, tb): self.stop() + + +class _HostLogStream(utils.LogStream): + """ + Log streams that parses kernel BUG:/WARNING: + """ + + def __init__(self, name): + super().__init__(name) + self._warn_re = re.compile( + rb"^(BUG:|WARNING:|general protection fault|Kernel panic)" + ) + + def _do_log(self, line, anc): + super()._do_log(line, anc) + + if self._warn_re.match(line): + fmt_line = line.decode("utf-8", errors="surrogateescape") + warnings.warn(fmt_line)
Signature · 1 line
2.53.0
PATCH v3 20/20 test: functional: add basic obex file transfer tests
In reply to Pauli VirtanenAdd tests
test/functional/test_obex.py::test_obex_ftp_get
test/functional/test_obex.py::test_obex_ftp_list
test/functional/test_obex.py::test_obexctl_list
--- test/functional/test_obex.py | 285 ++++++++++++++++++++++++++++++ test/pytest_bluez/host_plugins.py | 61 +++++++ 2 files changed, 346 insertions(+) create mode 100644 test/functional/test_obex.py
test/functional/test_obex.py new file +285 -0
@@ -0,0 +1,285 @@ +# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +""" +Tests for Obex +""" +import sys +import os +import re +import pytest +import subprocess +import tempfile +import time +import logging +import json +import dbus +import threading +from pathlib import Path + +import pytest + +from pytest_bluez import ( + HostPlugin, + Agent, + host_config, + find_exe, + Bluetoothd, + Bluetoothctl, + Obexd, + LogStream, + wait_until, + mainloop_wrap, + mainloop_assert, + Event, + EventPluginMixin, + dbus_service_event_method, + Pexpect, + utils, +) + +pytestmark = [pytest.mark.vm] + +log = logging.getLogger(__name__) + + +BUS_NAME = "org.bluez.obex" +PATH = "/org/bluez/obex" +AGENT_MANAGER_INTERFACE = "org.bluez.obex.AgentManager1" +AGENT_INTERFACE = "org.bluez.obex.Agent1" +CLIENT_INTERFACE = "org.bluez.obex.Client1" +SESSION_INTERFACE = "org.bluez.obex.Session1" +FILE_TRANSFER_INTERFACE = "org.bluez.obex.FileTransfer1" +TRANSFER_INTERFACE = "org.bluez.obex.Transfer1" + +FTP_UUID = "00001106-0000-1000-8000-00805f9b34fb" + + +class ObexAgent(HostPlugin, EventPluginMixin): + depends = [Bluetoothd()] + name = "obex_agent" + + def __init__(self, path="/obexagent"): + self.path = path + + @mainloop_wrap + def setup(self, impl): + EventPluginMixin.setup(self, impl) + + self.bus = dbus.SessionBus() + self.bus.set_exit_on_disconnect(False) + + self.agent = ObexAgentObject(self.bus, self.path, self.events) + + bluez = self.bus.get_object(BUS_NAME, PATH) + self.manager = dbus.Interface(bluez, AGENT_MANAGER_INTERFACE) + self.manager.RegisterAgent(self.path) + + log.info("Obex agent registered") + + def cleanup(self): + path = Path("/run/obex") + for f in path.iterdir(): + f.unlink() + + +def agent_method(*a, **kw): + return dbus_service_event_method(AGENT_INTERFACE, *a, **kw) + + +class ObexAgentObject(dbus.service.Object): + @mainloop_assert + def __init__(self, bus, path, events): + self.events = events + super().__init__(bus, path) + + AuthorizePush = agent_method("AuthorizePush", ("path",), "o", "s", sync=False) + Cancel = agent_method("Cancel") + + +def write_obex_file(name, content): + with open(f"/run/obex/{name}", "w") as f: + f.write(content) + + +def read_file(name): + with open(name, "r") as f: + return f.read() + + +# +# Direct Obex Python client API tests +# + + +class ObexClient(HostPlugin, EventPluginMixin): + name = "obex" + + @mainloop_wrap + def setup(self, impl): + EventPluginMixin.setup(self, impl) + + self.transferred = 0 + self.transfer_path = None + self.transfer_size = 0 + + self.bus = dbus.SessionBus() + self.bus.set_exit_on_disconnect(False) + self.log = logging.getLogger(self.name) + self.client = dbus.Interface( + self.bus.get_object(BUS_NAME, PATH), CLIENT_INTERFACE + ) + + self.bus.add_signal_receiver( + self.properties_changed, + dbus_interface="org.freedesktop.DBus.Properties", + signal_name="PropertiesChanged", + path_keyword="path", + ) + + @mainloop_wrap + def connect(self, bdaddr): + def reply(path): + obj = self.bus.get_object(BUS_NAME, path) + self.session = dbus.Interface(obj, SESSION_INTERFACE) + self.ftp = dbus.Interface(obj, FILE_TRANSFER_INTERFACE) + + self._object_method( + self.client, "CreateSession", bdaddr, {"Target": "ftp"}, reply_handler=reply + ) + + @mainloop_assert + def properties_changed(self, interface, properties, invalidated, path): + if path != self.transfer_path: + return + + if "Status" in properties and ( + properties["Status"] == "complete" or properties["Status"] == "error" + ): + self.events.put( + Event( + f"{FILE_TRANSFER_INTERFACE}:{properties['Status']}", + properties=properties, + ) + ) + self.log.debug(f"Transfer {properties['Status']}") + + if "Transferred" not in properties: + return + + value = properties["Transferred"] + speed = (value - self.transferred) / 1000 + self.log.debug( + f"Transfer progress {value}/{self.transfer_size} at {speed} kBps" + ) + self.transferred = value + + @mainloop_wrap + def ftp_list_folder(self): + return self.ftp.ListFolder() + + @mainloop_wrap + def ftp_get_file(self, dst, src): + path, properties = self.ftp.GetFile(dst, src) + self.transfer_path = path + self.transfer_size = properties["Size"] + return properties["Filename"] + + [email protected] +def paired_hosts(hosts): + from .test_agent import test_agent_pair_bredr + + if hosts[0].agent.has_device(hosts[1].bdaddr): + return hosts + + test_agent_pair_bredr(hosts, True) + return hosts + + +obex_host_config = host_config( + [Agent(), Obexd(), ObexClient(), Pexpect()], + [Agent(), Obexd(), ObexAgent()], + reuse=True, +) + + [email protected] +def obex_hosts(paired_hosts): + host0, host1 = paired_hosts + + if hasattr(host0, "session"): + return paired_hosts + + host0.obex.connect(host1.bdaddr) + + service = host1.agent.expect("org.bluez.Agent1.AuthorizeService") + assert service.uuid == FTP_UUID + host1.agent.reply() + + host0.obex.expect("org.bluez.obex.Client1.CreateSession:reply") + + yield paired_hosts + + host1.obex_agent.cleanup() + + +@obex_host_config +def test_obex_ftp_list(obex_hosts): + host0, host1 = obex_hosts + + host1.call(write_obex_file, "test", "1234") + + (item,) = host0.obex.ftp_list_folder() + assert item["Type"] == "file" + assert item["Name"] == "test" + assert item["Size"] == 4 + + +@obex_host_config +def test_obex_ftp_get(obex_hosts): + host0, host1 = obex_hosts + + host1.call(write_obex_file, "test", "1234") + + filename = host0.obex.ftp_get_file("", "test") + host0.obex.expect("org.bluez.obex.FileTransfer1:complete") + assert host0.call(read_file, filename) == "1234" + + +# +# obexctl tests +# + + [email protected] +def obexctl(obex_hosts): + host0, host1 = obex_hosts + + exe = find_exe("tools", "obexctl") + obexctl = host0.pexpect.spawn([exe]) + + obexctl.expect("Client /org/bluez/obex") + obexctl.send(f"connect {host1.bdaddr} {FTP_UUID}\n") + + service = host1.agent.expect("org.bluez.Agent1.AuthorizeService") + assert service.uuid == FTP_UUID + host1.agent.reply() + + obexctl.expect("Connection successful") + obexctl.send(f"select /org/bluez/obex/client/session1\n") + + yield obexctl + + obexctl.close() + + +@obex_host_config +def test_obexctl_list(obex_hosts, obexctl): + host0, host1 = obex_hosts + + host1.call(write_obex_file, "test", "1234") + + obexctl.send(f"ls\n") + obexctl.expect(f"Type: file") + obexctl.expect(f"Name: test") + obexctl.expect(f"Size: 4")
test/pytest_bluez/host_plugins.py +61 -0
79 lines not on this page · raw message
Signature · 1 line
2.53.0
Quoted 3 lines · Pauli Virtanen
Add framework for writing tests simulating "real" environments where BlueZ and other parts of the stack run on different virtual machine hosts that communicate with each other.
Not having gone through the changes in a great level of detail, it looks fine to me.
Is having some documentation that we could integrate into the website something you're planning post merge of this branch?
Should probably have some explanations on how to deploy it on a bare metal machine, the dependencies for, say, Debian/Ubuntu/Fedora, and maybe considerations for implementing the test suite on CI runners (permissions, etc.).
Quoted 570 lines
*** v3 ***
https://github.com/pv/bluez/compare/func-test-v2-r..func-test-v3
* fix configure.ac
openpty()detection to match TOOLS conditional, to fix make distcheck* properly retry virtio RPC connection if it fails initially
* properly restart VM if previous test hangs
* allow custom parent host side proxy objects, use them for pexpect
* improve --list with out-of-tree test files
* fix missing
bus.set_exit_on_disconnect(False)for obex tests* have --vm-timeout etc. change values also on VM host side
* use larger-memory VM instances for Pipewire, in case ASAN enabled
* set reasonable inside-VM
ASAN_OPTIONdefault values* don't run btvirt under stdbuf, since not compatible with ASAN
*** v2 ***
https://github.com/pv/bluez/compare/func-test-v1-r..func-test-v2
* move unit/func_test -> test/functional & test/pytest_bluez
The
pytest_bluezplugin is in principle reusable for other projects,
so we can eg. have more complete Pipewire integration tests that can
live in Pipewire repository.*
openpty()is in -lutil on some platforms, detect this in autoconf* more emulator adjustments:
- fix SCO data packet support in btvirt
- more complete Reset command* improve logging: get timestamps from kernel, and reorder logs to timestamp order, so that lines from different hosts, btmon, and parent tester appear in right order regardless of whether VM console / btmon is lagging
- this requires accurate clock sync in the VM, so enable KVM PTP in config and run chronyd inside the VMs
- use virtio port instead of qemu console to export logs, since the console has fixed baud rate and is too slow* add --btmon & export btsnoop dumps from VM hosts
* fix compatibility with older Python versions
* add
parametrized_host_config()* split Pipewire test to A2DP/BAP/HFP and really stream audio. These catch the 5.86 regression fixed in
066a164a524e498and the 5.84 one in6b0a08776a* add support for tests that reuse tester environment, so they can run
faster without needing Bluetoothd teardown/setup in between* add HostPlugin.presetup (mainly for test skipping)
* deal with RPC virtio port buffer possibly containing unflushed commands from previous failed test
* add some Agent1 interface tests
* add basic Obex file transfer tests
* add support for logging in to a running test instance (for gdb etc)
* export any core dumps out from test environ
Some bells & whistles:
* add --kernel-build for kernel image build
* test suite Python code formatting checks
***
Implements:
- RPC communication with tester instances running each of the VM hosts.
Tests run on parent host, which instructs VM hosts what to do.- Extensible way to add stateful test-specific code inside the VM instances
- Logging control: output from different processes running inside the VM
are separated and can be filtered.- Test runner framework with Pytest (more convenient than Python/unittest)
- Automatic grouping of tests to minimize VM reboots
- Redirecting USB controllers to use for testing in addition to btvirt
- Fairly straightforward, ~1600 sloc for the framework
There is no requirement that the tests spawn VM instances, the test runner can be used for any tests written in Python.
See
doc/test-functional.rstfor various examples.Also
test/functional/test_bluetoothctl_vm.pyhas some simple cases, andtest/functional/test_pipewire.pyfor a more complicated setuphost0(qemu): Pipewire <-> BlueZ <-> kernel
<-> btvirt
host1(qemu): kernel <-> BlueZ <-> PipewireThe framework allows easily passing any data and code between the parent
and VM hosts, so writing tests is straightforward.***
Some examples:
$ test/test-functional --list -qtest/functional/lib/tests/test_rpc.py::test_basic test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_sho w[hosts1-vm1] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2]
$ test/test-functional -v --no-header ======================================= test session starts ======================================== collected 5 itemstest/functional/lib/tests/test_rpc.py::test_basic
PASSED [ 20%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_sho w[hosts1-vm1] SKIPPED [ 40%] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image) [ 60%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2] SKIPPED (No k...) [ 80%] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image) [100%]=================================== 1 passed, 4 skipped in 0.19s
===================================$ test/test-functional --kernel=../linux ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items
test/functional/lib/tests/test_rpc.py
. [ 20%]test/functional/test_bluetoothctl_vm.py
. [ 40%]test/functional/test_btmgmt_vm.py
. [ 60%]test/functional/test_bluetoothctl_vm.py
. [ 80%]test/functional/test_pipewire.py
. [100%]============================== 5 passed in 41.92s
==============================$ test/test-functional --kernel=../linux -k test_btmgmt ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py
. [100%]======================= 1 passed, 4 deselected in 9.15s
========================$ grep btmgmt test-functional.log 13:15:42 INFO rpc.host.0.0 : client: call_plugin ('call', '__call__', <function run at 0x7f27b81ce140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.rpc : server: call_plugin ('call', '__call__', <function run at 0x7fd5e35a1010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.run : $ /home/pauli/prj/external/bluez/build/tools/btmgmt --index 0 info$ test/test-functional --kernel=../linux -k test_btmgmt --log-cli- level=0 ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] -------------------------------- live log setup ---------------------
-----------
13:00:31 INFO func_test.lib.env : Starting btvirt:/usr/bin/stdbuf-o L -e L/home/pauli/prj/external/bluez/build/emulator/btvirt-- server=/tmp/bluez-func-test-8t6ychy8
13:00:31 OUT btvirt : Bluetooth emulator ver 5.86 13:00:31 INFO func_test.lib.env : Starting host:/home/pauli/prj/external/bluez/build/tools/test-runner-- kernel=../linux/arch/x86/boot/bzImage -u/tmp/bluez-func-test- 8t6ychy8/bt-server-bredrle -o -chardev -o socket,id=ser0,path=/tmp/bluez-func-test-8t6ychy8/bluez-func-test- rpc-0,server=on,wait=off -o -device -o virtio-serial -o -device -o virtserialport,chardev=ser0,name=bluez-func-test-rpc -H --/usr/bin/python3-P/home/pauli/prj/external/bluez/test/functional/lib/runner.py/dev/ttyS2
13:00:31 OUT btvirt : Request for/tmp/bluez-func-test-8t6ychy8/bt-server-bredrle
13:00:32 OUT host.0.0 : early console inextract_kernel
13:00:32 OUT host.0.0 :input_data:0x000000000425c2c4
...
13:00:39 INFO rpc.host.0.0 : client:call_plugin('call', '__call__', <function run at0x7f7547472140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 DEBUG host.0.0.rpc : server: done 13:00:39 INFO host.0.0.rpc : server:call_plugin('call', '__call__', <function run at0x7f77dcc81010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 INFO host.0.0.run : $/home/pauli/prj/external/bluez/build/tools/btmgmt--index 0 info
13:00:40 OUT host.0.0.run.out : hci0: Primary controller
13:00:40 OUT host.0.0.run.out : addr 00:AA:01:00:00:42
version 11 manufacturer 1521 class0x000000
13:00:40 OUT host.0.0.run.out : supported settings: powered
connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy- configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver
13:00:40 OUT host.0.0.run.out : current settings: br/edr
13:00:40 OUT host.0.0.run.out : name
13:00:40 OUT host.0.0.run.out : short name
13:00:40 INFO host.0.0.run : (return code 0) 13:00:40 DEBUG rpc.host.0.0 : client-reply
PASSED
[100%]
13:00:40 OUT host.0.0 : qemu-system-x86_64:
terminating on signal 15 from pid 149047 (python3) ======================= 1 passed, 4 deselected in 8.84s
========================$ test/test-functional --kernel=../linux -k test_bluetoothctl_pair -- log-cli-level=0 --log-filter=*.bluetoothctl,rpc.* --force-usb ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2]
-------------------------------- live log setup ---------------------
-----------
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f268712d160>,) {} 13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f268712d2b0>,) {} 13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f268712d010>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f26871542d0>,) {} 13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f2687154410>,) {} 13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f2687154190>,) {}
13:03:20 INFO rpc.host.0.0 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.1 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.1 : client-reply -------------------------------- live log call ----------------------
-----------
13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'show\n') {}
13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Powered: yes') {}
...
13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'pair 70:1a:b8:73:99:bb\n') {} 13:03:23 OUT host.0.0.bluetoothctl: pair 70:1a:b8:73:99:bb 13:03:23 DEBUG rpc.host.0.0 : client-reply 13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey (\\d+).*:') {} 13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> pair 70:1a:b8:73:99:bb
13:03:23 OUT host.0.0.bluetoothctl: Attempting to pair with 70:1A:B8:73:99:BB
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0device_flags_changed: 70:1A:B8:73:99:BB (BR/EDR)
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> supp:
0x00000007 curr:0x00000000
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 type 7 discovering off
13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 70:1A:B8:73:99:BB type BR/EDR connectedeir_len12 13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Connected: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> Request confirmation
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey 237345') {} 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> hci0 84:5C:F3:77:31:19 type BR/EDR connectedeir_len12 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [NEW] Device 84:5C:F3:77:31:19 BlueZ 5.86
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> Request confirmation
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Pairing successful') {} 13:03:25 OUT host.0.0.bluetoothctl: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> hci0new_link_key70:1A:B8:73:99:BB type0x08pin_len0store_hint1 13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Bonded: yes
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB AddressType: public
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110c-0000-1000-8000-00805f9b34fb 13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110e-0000-1000-8000-00805f9b34fb 13:03:26 DEBUG rpc.host.0.0 : client-reply
PASSED
[100%]
------------------------------ live log teardown --------------------
-----------
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:98:FF:qemu-system-x86_64: terminating on signal 15 from pid 149357 (python3)======================= 1 passed, 4 deselected in 13.22s
=======================$ test/test-functional -k test_btmgmt --kernel=../linux --trace ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.pyQuoted 5 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
PDB runcall (IO-capturing
turned off)/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(19 )test_btmgmt_info()
-> (host,) = hosts
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21 )test_btmgmt_info()
-> result = host.call(
(Pdb) p host.bdaddr
'00:aa:01:00:00:42'
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(22 )test_btmgmt_info()
-> run,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(23 )test_btmgmt_info()
-> [btmgmt, "--index", "0", "info"],
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(24 )test_btmgmt_info()
-> stdout=subprocess.PIPE,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(25 )test_btmgmt_info()
-> stdin=subprocess.DEVNULL,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(26 )test_btmgmt_info()
-> encoding="utf-8",
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21 )test_btmgmt_info()
-> result = host.call(
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(28 )test_btmgmt_info()
-> assert result.returncode == 0
(Pdb) p result CompletedProcess(args=['/home/pauli/prj/external/bluez/build/tools/bt mgmt', '--index', '0', 'info'], returncode=0, stdout='hci0:\tPrimary controller\n\taddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000\n\tsupported settings: powered connectable fast- connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy- configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver \n\tcurrent settings:
br/edr \n\tname \n\tshort name \n')
(Pdb) print(result.stdout)
hci0: Primary controlleraddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class
0x000000
supported settings: powered connectable fast-connectable
discoverable bondable link-security ssp br/edr le advertising secure- conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past-sender past-receiver
current settings: br/edr
name
short name
(Pdb) q!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!
======================= 4 deselected in 75.91s (0:01:15)
=======================***
Pauli Virtanen (20):
emulator: btvirt: check pkt lengths, don't get stuck on malformed
emulator: btvirt: allow specifying where server unix sockets are made
emulator: btvirt: support SCO data packets
emulator: btdev: clear more state on Reset
test-runner: enable path argument for --unix
test-runner: Add -o/--option option
test-runner: allow source tree root for -k
doc: enableCONFIG_VIRTIO_CONSOLEin tester config
test-runner: use virtio-serial for implementing -u device forwarding
doc: enable KVM paravirtualization & clock support in tester kernel config
doc: add functional/integration testing documentation
test: add functional/integration testing framework
test: functional: add Pipewire-using audio streaming tests
test: functional: add --btmon option to start btmon
build: add functional testing target
test: functional: impose Python code formatting
test: functional: add option for building kernel image first
test: functional: add custom Agent1 implementation for testing
test: functional: warn on kernel warning/bug messages
test: functional: add basic obex file transfer testsMakefile.am | 9 + Makefile.tools | 2 + configure.ac | 31 + doc/ci.config | 9 + doc/test-functional.rst | 949
++++++++++++++++++++++++
doc/test-runner.rst | 17 + doc/tester.config | 9 + emulator/btdev.c | 117 +-- emulator/main.c | 37 +- emulator/server.c | 21 + test/functional/__init__.py | 2 + test/functional/requirements.txt | 4 + test/functional/test_agent.py | 46 ++test/functional/test_bluetoothctl_vm.py| 152 ++++ test/functional/test_btmgmt_vm.py | 30 + test/functional/test_obex.py | 285 +++++++ test/functional/test_pipewire.py | 539 ++++++++++++++ test/functional/test_tests.py | 23 + test/pytest.ini | 18 + test/pytest_bluez/__init__.py | 12 + test/pytest_bluez/agent.py | 371 +++++++++ test/pytest_bluez/btmon.py | 116 +++ test/pytest_bluez/build_kernel.py | 43 ++ test/pytest_bluez/env.py | 803 ++++++++++++++++++++ test/pytest_bluez/host_plugins.py | 627 ++++++++++++++++ test/pytest_bluez/plugin.py | 637 ++++++++++++++++ test/pytest_bluez/rpc.py | 385 ++++++++++ test/pytest_bluez/runner.py | 20 + test/pytest_bluez/tests/__init__.py | 2 + test/pytest_bluez/tests/test_rpc.py | 62 ++ test/pytest_bluez/tests/test_utils.py | 16 + test/pytest_bluez/utils.py | 706 ++++++++++++++++++ test/test-functional | 7 + test/test-functional-attach | 52 ++ tools/test-runner.c | 376 +++++++---35 files changed, 6383 insertions(+), 152 deletions(-)create mode 100644
doc/test-functional.rst
create mode 100644test/functional/__init__.pycreate mode 100644test/functional/requirements.txtcreate mode 100644test/functional/test_agent.pycreate mode 100644test/functional/test_bluetoothctl_vm.pycreate mode 100644test/functional/test_btmgmt_vm.pycreate mode 100644test/functional/test_obex.pycreate mode 100644test/functional/test_pipewire.pycreate mode 100644test/functional/test_tests.pycreate mode 100644 test/pytest.ini
create mode 100644test/pytest_bluez/__init__.pycreate mode 100644test/pytest_bluez/agent.pycreate mode 100644test/pytest_bluez/btmon.pycreate mode 100644test/pytest_bluez/build_kernel.pycreate mode 100644test/pytest_bluez/env.pycreate mode 100644test/pytest_bluez/host_plugins.pycreate mode 100644test/pytest_bluez/plugin.pycreate mode 100644test/pytest_bluez/rpc.pycreate mode 100644test/pytest_bluez/runner.pycreate mode 100644test/pytest_bluez/tests/__init__.pycreate mode 100644test/pytest_bluez/tests/test_rpc.pycreate mode 100644test/pytest_bluez/tests/test_utils.pycreate mode 100644test/pytest_bluez/utils.pycreate mode 100755 test/test-functional
create mode 100755 test/test-functional-attach
Hi,
30. maaliskuuta 2026 12.30.55 UTC Bastien Nocera <[email protected]> kirjoitti:
Quoted 8 lines · Pauli Virtanen
Quoted 3 lines
Add framework for writing tests simulating "real" environments where BlueZ and other parts of the stack run on different virtual machine hosts that communicate with each other.
Not having gone through the changes in a great level of detail, it looks fine to me.
Quoted 7 lines
Is having some documentation that we could integrate into the website something you're planning post merge of this branch?
Should probably have some explanations on how to deploy it on a bare metal machine, the dependencies for, say, Debian/Ubuntu/Fedora, and maybe considerations for implementing the test suite on CI runners (permissions, etc.).
There is some documentation included in the series
With some setup instructions, but maybe not detailed enough.
Quoted 531 lines
Quoted 531 lines
*** v3 ***
https://github.com/pv/bluez/compare/func-test-v2-r..func-test-v3
* fix configure.ac
openpty()detection to match TOOLS conditional, to fix make distcheck* properly retry virtio RPC connection if it fails initially
* properly restart VM if previous test hangs
* allow custom parent host side proxy objects, use them for pexpect
* improve --list with out-of-tree test files
* fix missing
bus.set_exit_on_disconnect(False)for obex tests* have --vm-timeout etc. change values also on VM host side
* use larger-memory VM instances for Pipewire, in case ASAN enabled
* set reasonable inside-VM
ASAN_OPTIONdefault values* don't run btvirt under stdbuf, since not compatible with ASAN
*** v2 ***
https://github.com/pv/bluez/compare/func-test-v1-r..func-test-v2
* move unit/func_test -> test/functional & test/pytest_bluez
The
pytest_bluezplugin is in principle reusable for other projects,
so we can eg. have more complete Pipewire integration tests that can
live in Pipewire repository.*
openpty()is in -lutil on some platforms, detect this in autoconf* more emulator adjustments:
- fix SCO data packet support in btvirt
- more complete Reset command* improve logging: get timestamps from kernel, and reorder logs to timestamp order, so that lines from different hosts, btmon, and parent tester appear in right order regardless of whether VM console / btmon is lagging
- this requires accurate clock sync in the VM, so enable KVM PTP in config and run chronyd inside the VMs
- use virtio port instead of qemu console to export logs, since the console has fixed baud rate and is too slow* add --btmon & export btsnoop dumps from VM hosts
* fix compatibility with older Python versions
* add
parametrized_host_config()* split Pipewire test to A2DP/BAP/HFP and really stream audio. These catch the 5.86 regression fixed in
066a164a524e498and the 5.84 one in6b0a08776a* add support for tests that reuse tester environment, so they can run
faster without needing Bluetoothd teardown/setup in between* add HostPlugin.presetup (mainly for test skipping)
* deal with RPC virtio port buffer possibly containing unflushed commands from previous failed test
* add some Agent1 interface tests
* add basic Obex file transfer tests
* add support for logging in to a running test instance (for gdb etc)
* export any core dumps out from test environ
Some bells & whistles:
* add --kernel-build for kernel image build
* test suite Python code formatting checks
***
Implements:
- RPC communication with tester instances running each of the VM hosts.
Tests run on parent host, which instructs VM hosts what to do.- Extensible way to add stateful test-specific code inside the VM instances
- Logging control: output from different processes running inside the VM
are separated and can be filtered.- Test runner framework with Pytest (more convenient than Python/unittest)
- Automatic grouping of tests to minimize VM reboots
- Redirecting USB controllers to use for testing in addition to btvirt
- Fairly straightforward, ~1600 sloc for the framework
There is no requirement that the tests spawn VM instances, the test runner can be used for any tests written in Python.
See
doc/test-functional.rstfor various examples.Also
test/functional/test_bluetoothctl_vm.pyhas some simple cases, andtest/functional/test_pipewire.pyfor a more complicated setuphost0(qemu): Pipewire <-> BlueZ <-> kernel
<-> btvirt
host1(qemu): kernel <-> BlueZ <-> PipewireThe framework allows easily passing any data and code between the parent
and VM hosts, so writing tests is straightforward.***
Some examples:
$ test/test-functional --list -qtest/functional/lib/tests/test_rpc.py::test_basic test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_sho w[hosts1-vm1] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2]
$ test/test-functional -v --no-header ======================================= test session starts ======================================== collected 5 itemstest/functional/lib/tests/test_rpc.py::test_basic
PASSED [ 20%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_sho w[hosts1-vm1] SKIPPED [ 40%] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image) [ 60%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2] SKIPPED (No k...) [ 80%] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image) [100%]=================================== 1 passed, 4 skipped in 0.19s
===================================$ test/test-functional --kernel=../linux ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items
test/functional/lib/tests/test_rpc.py
. [ 20%]test/functional/test_bluetoothctl_vm.py
. [ 40%]test/functional/test_btmgmt_vm.py
. [ 60%]test/functional/test_bluetoothctl_vm.py
. [ 80%]test/functional/test_pipewire.py
. [100%]============================== 5 passed in 41.92s
==============================$ test/test-functional --kernel=../linux -k test_btmgmt ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py
. [100%]======================= 1 passed, 4 deselected in 9.15s
========================$ grep btmgmt test-functional.log 13:15:42 INFO rpc.host.0.0 : client: call_plugin ('call', '__call__', <function run at 0x7f27b81ce140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.rpc : server: call_plugin ('call', '__call__', <function run at 0x7fd5e35a1010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.run : $ /home/pauli/prj/external/bluez/build/tools/btmgmt --index 0 info$ test/test-functional --kernel=../linux -k test_btmgmt --log-cli- level=0 ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] -------------------------------- live log setup ---------------------
-----------
13:00:31 INFO func_test.lib.env : Starting btvirt:/usr/bin/stdbuf-o L -e L/home/pauli/prj/external/bluez/build/emulator/btvirt-- server=/tmp/bluez-func-test-8t6ychy8
13:00:31 OUT btvirt : Bluetooth emulator ver 5.86 13:00:31 INFO func_test.lib.env : Starting host:/home/pauli/prj/external/bluez/build/tools/test-runner-- kernel=../linux/arch/x86/boot/bzImage -u/tmp/bluez-func-test- 8t6ychy8/bt-server-bredrle -o -chardev -o socket,id=ser0,path=/tmp/bluez-func-test-8t6ychy8/bluez-func-test- rpc-0,server=on,wait=off -o -device -o virtio-serial -o -device -o virtserialport,chardev=ser0,name=bluez-func-test-rpc -H --/usr/bin/python3-P/home/pauli/prj/external/bluez/test/functional/lib/runner.py/dev/ttyS2
13:00:31 OUT btvirt : Request for/tmp/bluez-func-test-8t6ychy8/bt-server-bredrle
13:00:32 OUT host.0.0 : early console inextract_kernel
13:00:32 OUT host.0.0 :input_data:0x000000000425c2c4
...
13:00:39 INFO rpc.host.0.0 : client:call_plugin('call', '__call__', <function run at0x7f7547472140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 DEBUG host.0.0.rpc : server: done 13:00:39 INFO host.0.0.rpc : server:call_plugin('call', '__call__', <function run at0x7f77dcc81010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 INFO host.0.0.run : $/home/pauli/prj/external/bluez/build/tools/btmgmt--index 0 info
13:00:40 OUT host.0.0.run.out : hci0: Primary controller
13:00:40 OUT host.0.0.run.out : addr 00:AA:01:00:00:42
version 11 manufacturer 1521 class0x000000
13:00:40 OUT host.0.0.run.out : supported settings: powered
connectable fast-connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy- configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver
13:00:40 OUT host.0.0.run.out : current settings: br/edr
13:00:40 OUT host.0.0.run.out : name
13:00:40 OUT host.0.0.run.out : short name
13:00:40 INFO host.0.0.run : (return code 0) 13:00:40 DEBUG rpc.host.0.0 : client-reply
PASSED
[100%]
13:00:40 OUT host.0.0 : qemu-system-x86_64:
terminating on signal 15 from pid 149047 (python3) ======================= 1 passed, 4 deselected in 8.84s
========================$ test/test-functional --kernel=../linux -k test_bluetoothctl_pair -- log-cli-level=0 --log-filter=*.bluetoothctl,rpc.* --force-usb ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts 0-vm2]
-------------------------------- live log setup ---------------------
-----------
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f268712d160>,) {} 13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f268712d2b0>,) {} 13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f268712d010>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f26871542d0>,) {} 13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f2687154410>,) {} 13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f2687154190>,) {}
13:03:20 INFO rpc.host.0.0 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.1 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.1 : client-reply -------------------------------- live log call ----------------------
-----------
13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'show\n') {}
13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Powered: yes') {}
...
13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'pair 70:1a:b8:73:99:bb\n') {} 13:03:23 OUT host.0.0.bluetoothctl: pair 70:1a:b8:73:99:bb 13:03:23 DEBUG rpc.host.0.0 : client-reply 13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey (\\d+).*:') {} 13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> pair 70:1a:b8:73:99:bb
13:03:23 OUT host.0.0.bluetoothctl: Attempting to pair with 70:1A:B8:73:99:BB
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0device_flags_changed: 70:1A:B8:73:99:BB (BR/EDR)
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> supp:
0x00000007 curr:0x00000000
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 type 7 discovering off
13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 70:1A:B8:73:99:BB type BR/EDR connectedeir_len12 13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Connected: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> Request confirmation
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey 237345') {} 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> hci0 84:5C:F3:77:31:19 type BR/EDR connectedeir_len12 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [NEW] Device 84:5C:F3:77:31:19 BlueZ 5.86
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]> Request confirmation
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [BlueZ 5.86]> [agent] Confirm passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Pairing successful') {} 13:03:25 OUT host.0.0.bluetoothctl: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> hci0new_link_key70:1A:B8:73:99:BB type0x08pin_len0store_hint1 13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB Bonded: yes
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB AddressType: public
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110c-0000-1000-8000-00805f9b34fb 13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:73:99:BB UUIDs: 0000110e-0000-1000-8000-00805f9b34fb 13:03:26 DEBUG rpc.host.0.0 : client-reply
PASSED
[100%]
------------------------------ live log teardown --------------------
-----------
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device 70:1A:B8:98:FF:qemu-system-x86_64: terminating on signal 15 from pid 149357 (python3)======================= 1 passed, 4 deselected in 13.22s
=======================$ test/test-functional -k test_btmgmt --kernel=../linux --trace ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout-2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.pyQuoted 5 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
PDB runcall (IO-capturing
turned off)/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(19 )test_btmgmt_info()
-> (host,) = hosts
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21 )test_btmgmt_info()
-> result = host.call(
(Pdb) p host.bdaddr
'00:aa:01:00:00:42'
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(22 )test_btmgmt_info()
-> run,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(23 )test_btmgmt_info()
-> [btmgmt, "--index", "0", "info"],
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(24 )test_btmgmt_info()
-> stdout=subprocess.PIPE,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(25 )test_btmgmt_info()
-> stdin=subprocess.DEVNULL,
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(26 )test_btmgmt_info()
-> encoding="utf-8",
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21 )test_btmgmt_info()
-> result = host.call(
(Pdb) nQuoted 2 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(28 )test_btmgmt_info()
-> assert result.returncode == 0
(Pdb) p result CompletedProcess(args=['/home/pauli/prj/external/bluez/build/tools/bt mgmt', '--index', '0', 'info'], returncode=0, stdout='hci0:\tPrimary controller\n\taddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000\n\tsupported settings: powered connectable fast- connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy- configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver \n\tcurrent settings:
br/edr \n\tname \n\tshort name \n')
(Pdb) print(result.stdout)
hci0: Primary controlleraddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class
0x000000
supported settings: powered connectable fast-connectable
discoverable bondable link-security ssp br/edr le advertising secure- conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past-sender past-receiver
current settings: br/edr
name
short name
(Pdb) q!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!
======================= 4 deselected in 75.91s (0:01:15)
=======================***
Pauli Virtanen (20):
emulator: btvirt: check pkt lengths, don't get stuck on malformed
emulator: btvirt: allow specifying where server unix sockets are made
emulator: btvirt: support SCO data packets
emulator: btdev: clear more state on Reset
test-runner: enable path argument for --unix
test-runner: Add -o/--option option
test-runner: allow source tree root for -k
doc: enableCONFIG_VIRTIO_CONSOLEin tester config
test-runner: use virtio-serial for implementing -u device forwarding
doc: enable KVM paravirtualization & clock support in tester kernel config
doc: add functional/integration testing documentation
test: add functional/integration testing framework
test: functional: add Pipewire-using audio streaming tests
test: functional: add --btmon option to start btmon
build: add functional testing target
test: functional: impose Python code formatting
test: functional: add option for building kernel image first
test: functional: add custom Agent1 implementation for testing
test: functional: warn on kernel warning/bug messages
test: functional: add basic obex file transfer testsMakefile.am | 9 + Makefile.tools | 2 + configure.ac | 31 + doc/ci.config | 9 + doc/test-functional.rst | 949
++++++++++++++++++++++++
doc/test-runner.rst | 17 + doc/tester.config | 9 + emulator/btdev.c | 117 +-- emulator/main.c | 37 +- emulator/server.c | 21 + test/functional/__init__.py | 2 + test/functional/requirements.txt | 4 + test/functional/test_agent.py | 46 ++test/functional/test_bluetoothctl_vm.py| 152 ++++ test/functional/test_btmgmt_vm.py | 30 + test/functional/test_obex.py | 285 +++++++ test/functional/test_pipewire.py | 539 ++++++++++++++ test/functional/test_tests.py | 23 + test/pytest.ini | 18 + test/pytest_bluez/__init__.py | 12 + test/pytest_bluez/agent.py �
Quoted 24 lines · Pauli Virtanen
Hi,
30. maaliskuuta 2026 12.30.55 UTC Bastien Nocera <[email protected]> kirjoitti:
Quoted 14 lines · Pauli Virtanen
Quoted 5 lines
Add framework for writing tests simulating "real" environments where
BlueZ and other parts of the stack run on different virtual machine
hosts that communicate with each other.Not having gone through the changes in a great level of detail, it looks fine to me.
Is having some documentation that we could integrate into the website
something you're planning post merge of this branch?Quoted 5 lines
Should probably have some explanations on how to deploy it on a bare
metal machine, the dependencies for, say, Debian/Ubuntu/Fedora, and maybe considerations for implementing the test suite on CI runners (permissions, etc.).
Quoted 5 lines
There is some documentation included in the series
With some setup instructions, but maybe not detailed enough.
Thanks, patch 11/20 never actually got to my mailbox, and I did receive some warnings about bounces from the mailing-list, I wonder if that's related :/
This is already very useful documentation, and we can always rework it down the line as we use it, and folks run into questions.
Thanks
Quoted 620 lines
Quoted 620 lines
Quoted 620 lines
*** v3 ***
https://github.com/pv/bluez/compare/func-test-v2-r..func-test-v3
* fix configure.ac
openpty()detection to match TOOLS conditional,
to fix make distcheck* properly retry virtio RPC connection if it fails initially
* properly restart VM if previous test hangs
* allow custom parent host side proxy objects, use them for pexpect
* improve --list with out-of-tree test files
* fix missing
bus.set_exit_on_disconnect(False)for obex tests* have --vm-timeout etc. change values also on VM host side
* use larger-memory VM instances for Pipewire, in case ASAN enabled
* set reasonable inside-VM
ASAN_OPTIONdefault values* don't run btvirt under stdbuf, since not compatible with ASAN
*** v2 ***
https://github.com/pv/bluez/compare/func-test-v1-r..func-test-v2
* move unit/func_test -> test/functional & test/pytest_bluez
The
pytest_bluezplugin is in principle reusable for other projects,
so we can eg. have more complete Pipewire integration tests that
can
live in Pipewire repository.*
openpty()is in -lutil on some platforms, detect this in autoconf* more emulator adjustments:
- fix SCO data packet support in btvirt
- more complete Reset command* improve logging: get timestamps from kernel, and reorder logs to timestamp order, so that lines from different hosts, btmon, and parent tester appear in right order regardless of whether VM console / btmon is lagging
- this requires accurate clock sync in the VM, so enable KVM PTP in
config and run chronyd inside the VMs
- use virtio port instead of qemu console to export logs, since the
console has fixed baud rate and is too slow* add --btmon & export btsnoop dumps from VM hosts
* fix compatibility with older Python versions
* add
parametrized_host_config()* split Pipewire test to A2DP/BAP/HFP and really stream audio. These catch the 5.86 regression fixed in
066a164a524e498and the 5.84 one in6b0a08776a* add support for tests that reuse tester environment, so they can
run
faster without needing Bluetoothd teardown/setup in between* add HostPlugin.presetup (mainly for test skipping)
* deal with RPC virtio port buffer possibly containing unflushed commands from previous failed test
* add some Agent1 interface tests
* add basic Obex file transfer tests
* add support for logging in to a running test instance (for gdb etc)
* export any core dumps out from test environ
Some bells & whistles:
* add --kernel-build for kernel image build
* test suite Python code formatting checks
***
Implements:
- RPC communication with tester instances running each of the VM hosts.
Tests run on parent host, which instructs VM hosts what to do.- Extensible way to add stateful test-specific code inside the VM instances
- Logging control: output from different processes running inside the
VM
are separated and can be filtered.- Test runner framework with Pytest (more convenient than Python/unittest)
- Automatic grouping of tests to minimize VM reboots
- Redirecting USB controllers to use for testing in addition to btvirt
- Fairly straightforward, ~1600 sloc for the framework
There is no requirement that the tests spawn VM instances, the test
runner can be used for any tests written in Python.See
doc/test-functional.rstfor various examples.Also
test/functional/test_bluetoothctl_vm.pyhas some simple cases,
andtest/functional/test_pipewire.pyfor a more complicated setuphost0(qemu): Pipewire <-> BlueZ <-> kernel
<-> btvirt
host1(qemu): kernel <-> BlueZ <-> PipewireThe framework allows easily passing any data and code between the parent
and VM hosts, so writing tests is straightforward.***
Some examples:
$ test/test-functional --list -qtest/functional/lib/tests/test_rpc.py::test_basic test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[h osts
0-vm2] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script_sho
w[hosts1-vm1] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2]$ test/test-functional -v --no-header ======================================= test session starts ======================================== collected 5 itemstest/functional/lib/tests/test_rpc.py::test_basic PASSED [ 20%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_script
_sho
w[hosts1-vm1] SKIPPED [ 40%] test/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image) [ 60%] test/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[h osts
0-vm2] SKIPPED (No k...) [ 80%] test/functional/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED
(No kernel image) [100%]=================================== 1 passed, 4 skipped in 0.19s
===================================$ test/test-functional --kernel=../linux ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout- 2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items
test/functional/lib/tests/test_rpc.py
. [ 20%]test/functional/test_bluetoothctl_vm.py
. [ 40%]test/functional/test_btmgmt_vm.py
. [ 60%]test/functional/test_bluetoothctl_vm.py
. [ 80%]test/functional/test_pipewire.py
. [100%]============================== 5 passed in 41.92s
==============================$ test/test-functional --kernel=../linux -k test_btmgmt ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout- 2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.py
. [100%]======================= 1 passed, 4 deselected in 9.15s
========================$ grep btmgmt test-functional.log 13:15:42 INFO rpc.host.0.0 : client: call_plugin ('call', '__call__', <function run at 0x7f27b81ce140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.rpc : server: call_plugin ('call', '__call__', <function run at 0x7fd5e35a1010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0', 'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:15:42 INFO host.0.0.run : $ /home/pauli/prj/external/bluez/build/tools/btmgmt --index 0 info$ test/test-functional --kernel=../linux -k test_btmgmt --log- cli- level=0 ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout- 2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] -------------------------------- live log setup -----------------
----
-----------
13:00:31 INFO func_test.lib.env : Starting btvirt:/usr/bin/stdbuf-o L -e L/home/pauli/prj/external/bluez/build/emulator/btvirt-- server=/tmp/bluez-func-test-8t6ychy8
13:00:31 OUT btvirt : Bluetooth emulator ver 5.86
13:00:31 INFO func_test.lib.env : Starting host:/home/pauli/prj/external/bluez/build/tools/test-runner-- kernel=../linux/arch/x86/boot/bzImage -u/tmp/bluez-func-test- 8t6ychy8/bt-server-bredrle -o -chardev -o socket,id=ser0,path=/tmp/bluez-func-test-8t6ychy8/bluez-func- test-
rpc-0,server=on,wait=off -o -device -o virtio-serial -o -device - o
virtserialport,chardev=ser0,name=bluez-func-test-rpc -H --/usr/bin/python3-P/home/pauli/prj/external/bluez/test/functional/lib/runner.py/dev/ttyS2
13:00:31 OUT btvirt : Request for/tmp/bluez-func-
test-8t6ychy8/bt-server-bredrle
13:00:32 OUT host.0.0 : early console inextract_kernel
13:00:32 OUT host.0.0 :input_data:0x000000000425c2c4
...
13:00:39 INFO rpc.host.0.0 : client:call_plugin('call',
'__call__', <function run at0x7f7547472140>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0',
'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 DEBUG host.0.0.rpc : server: done 13:00:39 INFO host.0.0.rpc : server:call_plugin('call',
'__call__', <function run at0x7f77dcc81010>, ['/home/pauli/prj/external/bluez/build/tools/btmgmt', '--index', '0',
'info']) {'stdout': -1, 'stdin': -3, 'encoding': 'utf-8'} 13:00:39 INFO host.0.0.run : $/home/pauli/prj/external/bluez/build/tools/btmgmt--index 0 info
13:00:40 OUT host.0.0.run.out : hci0: Primary
controller
13:00:40 OUT host.0.0.run.out : addr 00:AA:01:00:00:42
version 11 manufacturer 1521 class0x000000
13:00:40 OUT host.0.0.run.out : supported settings:
powered
connectable fast-connectable discoverable bondable link-security ssp
br/edr le advertising secure-conn debug-keys privacy static-addr phy-
configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver
13:00:40 OUT host.0.0.run.out : current settings: br/edr
13:00:40 OUT host.0.0.run.out : name
13:00:40 OUT host.0.0.run.out : short name
13:00:40 INFO host.0.0.run : (return code 0) 13:00:40 DEBUG rpc.host.0.0 : client-reply PASSED
[100%]
13:00:40 OUT host.0.0 : qemu-system-x86_64:
terminating on signal 15 from pid 149047 (python3) ======================= 1 passed, 4 deselected in 8.84s
========================$ test/test-functional --kernel=../linux -k test_bluetoothctl_pair -- log-cli-level=0 --log-filter=*.bluetoothctl,rpc.* --force-usb ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout- 2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selectedtest/functional/test_bluetoothctl_vm.py::test_bluetoothctl_pair[h osts
0-vm2]
-------------------------------- live log setup -----------------
----
-----------
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f268712d160>,) {}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f268712d2b0>,) {} 13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,)
{}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,)
{}
13:03:20 INFO rpc.host.0.0 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f268712d010>,)
{}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bdaddr object at0x7f26871542d0>,) {}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Call object at0x7f2687154410>,) {} 13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.DbusSystem object at0x7f2687aa30e0>,)
{}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothd object at0x7f2687aa3230>,)
{}
13:03:20 INFO rpc.host.0.1 : client:start_load(<func_test.lib.host_plugins.Bluetoothctl object at0x7f2687154190>,)
{}
13:03:20 INFO rpc.host.0.0 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.1 : client:wait_load() {} 13:03:21 DEBUG rpc.host.0.1 : client-reply -------------------------------- live log call ------------------
----
-----------
13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'show\n') {}
13:03:21 DEBUG rpc.host.0.0 : client-reply 13:03:21 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Powered: yes') {}
...
13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'pair 70:1a:b8:73:99:bb\n') {} 13:03:23 OUT host.0.0.bluetoothctl: pair 70:1a:b8:73:99:bb 13:03:23 DEBUG rpc.host.0.0 : client-reply 13:03:23 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey (\\d+).*:') {} 13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> pair 70:1a:b8:73:99:bb
13:03:23 OUT host.0.0.bluetoothctl: Attempting to pair with 70:1A:B8:73:99:BB
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0device_flags_changed: 70:1A:B8:73:99:BB (BR/EDR) 13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> supp:
0x00000007 curr:0x00000000
13:03:23 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 type 7
discovering off
13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> hci0 70:1A:B8:73:99:BB type BR/EDR connectedeir_len12 13:03:25 OUT host.0.0.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]>
[CHG] Device 70:1A:B8:73:99:BB Connected: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> Request confirmation
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'expect', 'Confirm passkey 237345') {} 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> hci0 84:5C:F3:77:31:19 type BR/EDR connectedeir_len12 13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [NEW] Device
84:5C:F3:77:31:19 BlueZ 5.86
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [bluetoothctl]> [BlueZ 5.86]>
Request confirmation
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [agent] Confirm
passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.0 : client-reply 13:03:25 INFO rpc.host.0.1 : client:call_plugin('bluetoothctl', 'send', 'yes\n') {}
13:03:25 OUT host.0.1.bluetoothctl: [BlueZ 5.86]> [agent] Confirm
passkey 237345 (yes/no): yes
13:03:25 DEBUG rpc.host.0.1 : client-reply 13:03:25 INFO rpc.host.0.0 : client:call_plugin('bluetoothctl', 'expect', 'Pairing successful') {} 13:03:25 OUT host.0.0.bluetoothctl: yes
13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> hci0new_link_key70:1A:B8:73:99:BB type0x08pin_len0store_hint1 13:03:25 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device
70:1A:B8:73:99:BB Bonded: yes
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device
70:1A:B8:73:99:BB AddressType: public
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device
70:1A:B8:73:99:BB UUIDs: 0000110c-0000-1000-8000-00805f9b34fb 13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device
70:1A:B8:73:99:BB UUIDs: 0000110e-0000-1000-8000-00805f9b34fb 13:03:26 DEBUG rpc.host.0.0 : client-reply PASSED
[100%]
------------------------------ live log teardown ----------------
----
-----------
13:03:26 OUT host.0.0.bluetoothctl: [BlueZ 5.86]> [CHG] Device
70:1A:B8:98:FF:qemu-system-x86_64: terminating on signal 15 from pid
149357 (python3)======================= 1 passed, 4 deselected in 13.22s
=======================$ test/test-functional -k test_btmgmt --kernel=../linux --trace ============================= test session starts ============================== platform linux -- Python 3.14.3, pytest-8.3.5, pluggy-1.6.0 rootdir: /home/pauli/prj/external/bluez/unit configfile: pytest.ini plugins: cov-5.0.0, forked-1.6.0, rerunfailures-15.0, timeout- 2.4.0, xdist-3.7.0, hypothesis-6.123.0, flaky-3.8.1, anyio-4.12.1 collected 5 items / 4 deselected / 1 selected
test/functional/test_btmgmt_vm.pyQuoted 6 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
PDB runcall (IO-capturing
turned off)
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(19
)test_btmgmt_info()-> (host,) = hosts
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21
)test_btmgmt_info()-> result = host.call(
(Pdb) p host.bdaddr
'00:aa:01:00:00:42'
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(22
)test_btmgmt_info()-> run,
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(23
)test_btmgmt_info()-> [btmgmt, "--index", "0", "info"],
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(24
)test_btmgmt_info()-> stdout=subprocess.PIPE,
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(25
)test_btmgmt_info()-> stdin=subprocess.DEVNULL,
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(26
)test_btmgmt_info()-> encoding="utf-8",
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(21
)test_btmgmt_info()-> result = host.call(
(Pdb) nQuoted 3 lines
/home/pauli/prj/external/bluez/test/functional/test_btmgmt_vm.py(28
)test_btmgmt_info()-> assert result.returncode == 0
(Pdb) p result CompletedProcess(args=['/home/pauli/prj/external/bluez/build/tool s/bt
mgmt', '--index', '0', 'info'], returncode=0, stdout='hci0:\tPrimary
controller\n\taddr 00:AA:01:00:00:42 version 11 manufacturer 1521 class 0x000000\n\tsupported settings: powered connectable fast- connectable discoverable bondable link-security ssp br/edr le advertising secure-conn debug-keys privacy static-addr phy- configuration cis-central cis-peripheral iso-broadcaster sync- receiver ll-privacy past-sender past-receiver \n\tcurrent settings:
br/edr \n\tname \n\tshort name \n')
(Pdb) print(result.stdout)
hci0: Primary controlleraddr 00:AA:01:00:00:42 version 11 manufacturer 1521class
0x000000
supported settings: powered connectable fast-connectable
discoverable bondable link-security ssp br/edr le advertising secure-
conn debug-keys privacy static-addr phy-configuration cis-central cis-peripheral iso-broadcaster sync-receiver ll-privacy past- sender
past-receiver
current settings: br/edr
name
short name
(Pdb) q!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!
======================= 4 deselected in 75.91s (0:01:15)
=======================***
Pauli Virtanen (20):
emulator: btvirt: check pkt lengths, don't get stuck on malformed
emulator: btvirt: allow specifying where server unix sockets are
made
emulator: btvirt: support SCO data packets
emulator: btdev: clear more state on Reset
test-runner: enable path argument for --unix
test-runner: Add -o/--option option
test-runner: allow source tree root for -k
doc: enableCONFIG_VIRTIO_CONSOLEin tester config
test-runner: use virtio-serial for implementing -u device forwarding
doc: enable KVM paravirtualization & clock support in tester kernel
config
doc: add functional/integration testing documentation
test: add functional/integration testing framework
test: functional: add Pipewire-using audio streaming tests
test: functional: add --btmon option to start btmon
build: add functional testing target
test: functional: impose Python code formatting
test: functional: add option for building kernel image first
test: functional: add custom Agent1 implementation for testing
test: functional: warn on kernel warning/bug messages
test: functional: add basic obex file transfer testsMakefile.am | 9 + Makefile.tools | 2 + configure.ac | 31 + doc/ci.config | 9 + doc/test-functional.rst | 949
++++++++++++++++++++++++
doc/test-runner.rst | 17 + doc/tester.config | 9 + emulator/btdev.c | 117 +-- emulator/main.c | 37 +- emulator/server.c | 21 + test/functional/__init__.py | 2 + test/functional/requirements.txt | 4 + test/functional/test_agent.py | 46 ++test/functional/test_bluetoothctl_vm.py| 152 ++++ test/functional/test_btmgmt_vm.py | 30 + test/functional/test_obex.py | 285 +++++++ test/functional/test_pipewire.py | 539 ++++++++++++++ test/functional/test_tests.py | 23 + test/pytest.ini | 18 + test/pytest_bluez/__init__.py | 12 + test/pytest_bluez/agent.py �