public inbox for linux-bluetooth@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH BlueZ 00/11] Functional/integration testing
@ 2026-02-28 12:51 Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
                   ` (11 more replies)
  0 siblings, 12 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: 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.

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 unit/func_test/test_bluetoothctl_vm.py has some simple cases, and
unit/func_test/test_pipewire.py for a more complicated setup

    host0(qemu): Pipewire <-> BlueZ <-> kernel
    <-> btvirt
    host1(qemu): kernel <-> BlueZ <-> Pipewire

The framework allows easily passing any data and code between the parent
and VM hosts, so writing tests is straightforward.

***

Some examples:

$ unit/test-functional --list -q

unit/func_test/lib/tests/test_rpc.py::test_basic
unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2]
unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1]
unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1]
unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2]

$ unit/test-functional -v --no-header
======================================= test session starts ========================================
collected 5 items

unit/func_test/lib/tests/test_rpc.py::test_basic PASSED                                      [ 20%]
unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1] SKIPPED    [ 40%]
unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image)     [ 60%]
unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] SKIPPED (No k...) [ 80%]
unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image)         [100%]

=================================== 1 passed, 4 skipped in 0.19s ===================================

$ unit/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

unit/func_test/lib/tests/test_rpc.py .                                   [ 20%]
unit/func_test/test_bluetoothctl_vm.py .                                 [ 40%]
unit/func_test/test_btmgmt_vm.py .                                       [ 60%]
unit/func_test/test_bluetoothctl_vm.py .                                 [ 80%]
unit/func_test/test_pipewire.py .                                        [100%]

============================== 5 passed in 41.92s ==============================

$ unit/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

unit/func_test/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

$ unit/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

unit/func_test/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/unit/func_test/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-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 ========================

$ unit/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

unit/func_test/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-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 =======================

$ COLUMNS=80 unit/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

unit/func_test/test_btmgmt_vm.py
>>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(19)test_btmgmt_info()
-> (host,) = hosts
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
-> result = host.call(
(Pdb) p host.bdaddr
'00:aa:01:00:00:42'
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(22)test_btmgmt_info()
-> run,
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(23)test_btmgmt_info()
-> [btmgmt, "--index", "0", "info"],
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(24)test_btmgmt_info()
-> stdout=subprocess.PIPE,
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(25)test_btmgmt_info()
-> stdin=subprocess.DEVNULL,
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(26)test_btmgmt_info()
-> encoding="utf-8",
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
-> result = host.call(
(Pdb) n
> /home/pauli/prj/external/bluez/unit/func_test/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 (11):
  emulator: btvirt: check pkt lengths, don't get stuck on malformed
  emulator: btvirt: allow specifying where server unix sockets are made
  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: add functional/integration testing documentation
  unit: add functional/integration testing framework
  unit: func_test: add Pipewire-using smoke tests
  build: add functional testing target

 Makefile.am                            |   7 +
 configure.ac                           |  17 +
 doc/ci.config                          |   1 +
 doc/test-functional.rst                | 374 +++++++++++++++++++
 doc/test-runner.rst                    |   1 +
 doc/tester.config                      |   1 +
 emulator/main.c                        |  37 +-
 emulator/server.c                      |   9 +
 tools/test-runner.c                    | 376 ++++++++++++++-----
 unit/func_test/__init__.py             |   0
 unit/func_test/conftest.py             | 277 ++++++++++++++
 unit/func_test/lib/__init__.py         |   5 +
 unit/func_test/lib/env.py              | 484 +++++++++++++++++++++++++
 unit/func_test/lib/host_plugins.py     | 269 ++++++++++++++
 unit/func_test/lib/rpc.py              | 293 +++++++++++++++
 unit/func_test/lib/runner.py           |  10 +
 unit/func_test/lib/tests/__init__.py   |   0
 unit/func_test/lib/tests/test_rpc.py   |  49 +++
 unit/func_test/lib/utils.py            | 266 ++++++++++++++
 unit/func_test/requirements.txt        |   3 +
 unit/func_test/test_bluetoothctl_vm.py |  76 ++++
 unit/func_test/test_btmgmt_vm.py       |  29 ++
 unit/func_test/test_pipewire.py        | 137 +++++++
 unit/pytest.ini                        |   7 +
 unit/test-functional                   |   8 +
 25 files changed, 2631 insertions(+), 105 deletions(-)
 create mode 100644 doc/test-functional.rst
 create mode 100644 unit/func_test/__init__.py
 create mode 100644 unit/func_test/conftest.py
 create mode 100644 unit/func_test/lib/__init__.py
 create mode 100644 unit/func_test/lib/env.py
 create mode 100644 unit/func_test/lib/host_plugins.py
 create mode 100644 unit/func_test/lib/rpc.py
 create mode 100644 unit/func_test/lib/runner.py
 create mode 100644 unit/func_test/lib/tests/__init__.py
 create mode 100644 unit/func_test/lib/tests/test_rpc.py
 create mode 100644 unit/func_test/lib/utils.py
 create mode 100644 unit/func_test/requirements.txt
 create mode 100644 unit/func_test/test_bluetoothctl_vm.py
 create mode 100644 unit/func_test/test_btmgmt_vm.py
 create mode 100644 unit/func_test/test_pipewire.py
 create mode 100644 unit/pytest.ini
 create mode 100755 unit/test-functional

-- 
2.53.0


^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 13:45   ` Functional/integration testing bluez.test.bot
  2026-02-28 12:51 ` [PATCH BlueZ 02/11] emulator: btvirt: allow specifying where server unix sockets are made Pauli Virtanen
                   ` (10 subsequent siblings)
  11 siblings, 1 reply; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

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(+)

diff --git a/emulator/server.c b/emulator/server.c
index fa2bc07be..f14e14cd2 100644
--- a/emulator/server.c
+++ b/emulator/server.c
@@ -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;
 			}
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 02/11] emulator: btvirt: allow specifying where server unix sockets are made
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 03/11] test-runner: enable path argument for --unix Pauli Virtanen
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: 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(-)

diff --git a/emulator/main.c b/emulator/main.c
index 456fcd98e..09d6e9adb 100644
--- a/emulator/main.c
+++ b/emulator/main.c
@@ -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");
 	}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 03/11] test-runner: enable path argument for --unix
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 02/11] emulator: btvirt: allow specifying where server unix sockets are made Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 04/11] test-runner: Add -o/--option option Pauli Virtanen
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Allow specifying the path for the controller socket to be used.
---
 tools/test-runner.c | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/tools/test-runner.c b/tools/test-runner.c
index 48b7c1589..331cb6eb1 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -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;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 04/11] test-runner: Add -o/--option option
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (2 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 03/11] test-runner: enable path argument for --unix Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 05/11] test-runner: allow source tree root for -k Pauli Virtanen
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Allow passing arbitrary arguments to QEMU.
---
 tools/test-runner.c | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/tools/test-runner.c b/tools/test-runner.c
index 331cb6eb1..3de3a9d74 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -41,6 +41,7 @@
 #endif
 
 #define CMDLINE_MAX (2048 * 10)
+#define EXTRA_OPT_MAX 64
 
 static 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;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 05/11] test-runner: allow source tree root for -k
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (3 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 04/11] test-runner: Add -o/--option option Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 06/11] doc: enable CONFIG_VIRTIO_CONSOLE in tester config Pauli Virtanen
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Allow passing source tree root for -k option, look up kernel below it.
---
 tools/test-runner.c | 42 ++++++++++++++++++++++++++++--------------
 1 file changed, 28 insertions(+), 14 deletions(-)

diff --git a/tools/test-runner.c b/tools/test-runner.c
index 3de3a9d74..b3e0b0cfe 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -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);
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 06/11] doc: enable CONFIG_VIRTIO_CONSOLE in tester config
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (4 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 05/11] test-runner: allow source tree root for -k Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding Pauli Virtanen
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Enable 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(+)

diff --git a/doc/ci.config b/doc/ci.config
index 31e49ba96..a48c1af9d 100644
--- a/doc/ci.config
+++ b/doc/ci.config
@@ -6,6 +6,7 @@
 
 CONFIG_VIRTIO=y
 CONFIG_VIRTIO_PCI=y
+CONFIG_VIRTIO_CONSOLE=y
 
 CONFIG_NET=y
 CONFIG_INET=y
diff --git a/doc/test-runner.rst b/doc/test-runner.rst
index 64715e2e7..d030787a4 100644
--- a/doc/test-runner.rst
+++ b/doc/test-runner.rst
@@ -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
diff --git a/doc/tester.config b/doc/tester.config
index 4ee306405..015e7cc1a 100644
--- a/doc/tester.config
+++ b/doc/tester.config
@@ -1,6 +1,7 @@
 CONFIG_PCI=y
 CONFIG_VIRTIO=y
 CONFIG_VIRTIO_PCI=y
+CONFIG_VIRTIO_CONSOLE=y
 
 CONFIG_NET=y
 CONFIG_INET=y
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (5 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 06/11] doc: enable CONFIG_VIRTIO_CONSOLE in tester config Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-03-02 21:03   ` Luiz Augusto von Dentz
  2026-02-28 12:51 ` [PATCH BlueZ 08/11] doc: add functional/integration testing documentation Pauli Virtanen
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: 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: 0

This 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.
---
 tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++-----------
 1 file changed, 230 insertions(+), 70 deletions(-)

diff --git a/tools/test-runner.c b/tools/test-runner.c
index b3e0b0cfe..576313b79 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -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;
+				} else if (errno == EINTR || errno == EAGAIN
+						|| errno == EWOULDBLOCK) {
+					/* ok */
+				} else {
+					perror("write");
+					goto error;
+				}
+			}
+		}
+	}
+	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


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 08/11] doc: add functional/integration testing documentation
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (6 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 09/11] unit: add functional/integration testing framework Pauli Virtanen
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Add documentation for functional/integration test suite.
---
 doc/test-functional.rst | 374 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 374 insertions(+)
 create mode 100644 doc/test-functional.rst

diff --git a/doc/test-functional.rst b/doc/test-functional.rst
new file mode 100644
index 000000000..72325ec25
--- /dev/null
+++ b/doc/test-functional.rst
@@ -0,0 +1,374 @@
+===============
+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`.
+
+
+REQUIREMENTS
+============
+
+Python
+------
+
+The following Python packages are required:
+
+.. code-block::
+
+   pytest
+   pexpect
+   dbus-python
+
+To install them via pip::
+
+	python3 -m pip install -r unit/func_test/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
+
+USB
+---
+
+Some tests may require a hardware controller instead of the virtual `btvirt` one.
+
+
+EXAMPLES
+========
+
+Run all tests
+-------------
+
+.. code-block::
+
+	$ unit/test-functional --kernel=/pathto/bzImage
+
+	$ export FUNCTIONAL_TESTING_KERNEL=/pathto/bzImage
+	$ unit/test-functional
+
+Show output during run
+----------------------
+
+.. code-block::
+
+	$ unit/test-functional --log-cli-level=0
+
+Show only specific loggers:
+
+.. code-block::
+
+	$ unit/test-functional --log-cli-level=0 --log-filter=rpc,host
+
+	$ unit/test-functional --log-cli-level=0 --log-filter=*.bluetoothctl
+
+Filter out loggers:
+
+.. code-block::
+
+	$ unit/test-functional --log-cli-level=0 --log-filter=-host
+
+	$ unit/test-functional --log-cli-level=0 --log-filter=host,-host.*.1
+
+Run selected tests
+------------------
+
+.. code-block::
+
+	$ unit/test-functional unit/func_test/test_cli_simple.py::test_bluetoothctl_script_show
+
+	$ unit/test-functional -k test_bluetoothctl_script_show
+
+	$ unit/test-functional -k 'test_btmgmt or test_bluetoothctl'
+
+Don't run tests with a given marker:
+
+.. code-block::
+
+	$ unit/test-functional -m "not pipewire"
+
+Don't run known-failing tests:
+
+.. code-block::
+
+	$ unit/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::
+
+	$ unit/test-functional -x --ff
+
+List all tests
+--------------
+
+.. code-block::
+
+	$ unit/test-functional --list
+
+Show errors from know-failing test
+----------------------------------
+
+.. code-block::
+
+	$ unit/test-functional --runxfail -k test_btmgmt_info
+
+Redirect USB devices
+--------------------
+
+.. code-block::
+
+	$ unit/test-functional --usb=hci0,hci1
+
+	$ export FUNCTIONAL_TESTING_CONTROLLERS=hci0,hci1
+	$ unit/test-functional
+
+
+WRITING TESTS
+=============
+
+The functional tests are written in files (test modules) names
+`unit/func_test/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 .lib import host_config, Bluetoothd, Bluetoothctl
+
+   @host_config(
+       [Bluetoothd(), Bluetoothctl()],
+       [Bluetoothd(), Bluetoothctl()],
+   )
+   def test_bluetoothctl_pair(hosts):
+       host0, host1 = hosts
+
+       host0.bluetoothctl.send("show\n")
+       host0.bluetoothctl.expect("Powered: yes")
+
+       host1.bluetoothctl.send("show\n")
+       host1.bluetoothctl.expect("Powered: yes")
+
+       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 .lib import HostPlugin
+
+   class Bluetoothctl(HostPlugin):
+       # Declare a unique name:
+
+       name = "bluetoothctl"
+
+       # Declare dependencies on other plugins
+
+       depends = [Bluetoothd()]
+
+       # The following is to be run on parent host outside VMs:
+
+       def __init__(self):
+           self.exe = utils.find_exe("client", "bluetoothctl")
+
+       # These run inside VM on plugin setup / teardown:
+
+       def setup(self, impl):
+           self.logger = utils.LogStream("bluetoothctl")
+           self.ctl = pexpect.spawn(self.exe, logfile=self.logger.stream)
+
+       def teardown(self):
+           self.ctl.terminate()
+           self.ctl.wait()
+
+       # These declare the custom RPC-callable methods of the plugin:
+
+       def expect(self, *a, **kw):
+           ret = self.ctl.expect(*a, **kw)
+           log.debug("pexpect: found")
+           return ret, self.ctl.match.groups()
+
+       def expect_prompt(self):
+           prompt = "\\[[a-zA-Z0-9. -]+\\]>"
+           return self.expect(prompt)
+
+       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
+`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.
+
+Reference
+---------
+
+In addition to standard Pytest features, the following items are
+available in the `.lib` submodule.
+
+TODO: not complete
+
+host_config
+~~~~~~~~~~~
+
+.. code-block::
+
+   def host_config(*host_setup, hw=False)
+
+Declare host configuration.
+
+- \*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
+
+find_exe
+~~~~~~~~
+
+.. code-block::
+
+   from .lib import find_exe
+   bluetoothctl = find_exe("client", "bluetoothctl")
+
+Find absolute path to the given executable, either within BlueZ build
+directory or on host.
+
+RemoteError
+~~~~~~~~~~~
+
+.. code-block::
+
+   from .lib import RemoteError
+
+   try:
+       host.call(foo)
+   except RemoteError as exc:
+       print("    ".join(exc.traceback))
+       original_exception = exc.exc
+
+Exception raised on the VM side, passed through RPC. Properties:
+`traceback` is a list of traceback lines and `exc` is the original
+exception instance raised on the remote side.
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 09/11] unit: add functional/integration testing framework
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (7 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 08/11] doc: add functional/integration testing documentation Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 10/11] unit: func_test: add Pipewire-using smoke tests Pauli Virtanen
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: 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.

This uses tools/test-runner to launch VM instances, but does not use the
intra-VM setup code to allow reusing the same VM instances for multiple
tests.

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 (more convenient than Python/unittest)

- 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 can be used for any tests written in Python.
---
 unit/func_test/__init__.py             |   0
 unit/func_test/conftest.py             | 277 ++++++++++++++
 unit/func_test/lib/__init__.py         |   5 +
 unit/func_test/lib/env.py              | 484 +++++++++++++++++++++++++
 unit/func_test/lib/host_plugins.py     | 269 ++++++++++++++
 unit/func_test/lib/rpc.py              | 293 +++++++++++++++
 unit/func_test/lib/runner.py           |  10 +
 unit/func_test/lib/tests/__init__.py   |   0
 unit/func_test/lib/tests/test_rpc.py   |  49 +++
 unit/func_test/lib/utils.py            | 266 ++++++++++++++
 unit/func_test/requirements.txt        |   3 +
 unit/func_test/test_bluetoothctl_vm.py |  76 ++++
 unit/func_test/test_btmgmt_vm.py       |  29 ++
 unit/pytest.ini                        |   6 +
 unit/test-functional                   |   8 +
 15 files changed, 1775 insertions(+)
 create mode 100644 unit/func_test/__init__.py
 create mode 100644 unit/func_test/conftest.py
 create mode 100644 unit/func_test/lib/__init__.py
 create mode 100644 unit/func_test/lib/env.py
 create mode 100644 unit/func_test/lib/host_plugins.py
 create mode 100644 unit/func_test/lib/rpc.py
 create mode 100644 unit/func_test/lib/runner.py
 create mode 100644 unit/func_test/lib/tests/__init__.py
 create mode 100644 unit/func_test/lib/tests/test_rpc.py
 create mode 100644 unit/func_test/lib/utils.py
 create mode 100644 unit/func_test/requirements.txt
 create mode 100644 unit/func_test/test_bluetoothctl_vm.py
 create mode 100644 unit/func_test/test_btmgmt_vm.py
 create mode 100644 unit/pytest.ini
 create mode 100755 unit/test-functional

diff --git a/unit/func_test/__init__.py b/unit/func_test/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/unit/func_test/conftest.py b/unit/func_test/conftest.py
new file mode 100644
index 000000000..c9db1a457
--- /dev/null
+++ b/unit/func_test/conftest.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+import os
+import re
+import logging
+import fnmatch
+import pytest
+from pathlib import Path
+
+
+def pytest_addoption(parser):
+    parser.addoption(
+        "--kernel",
+        action="store",
+        default=None,
+        help=("Kernel image to use"),
+    )
+    parser.addoption(
+        "--usb",
+        action="store",
+        default=None,
+        help=("USB HCI devices to use, e.g. 'hci0,hci1'"),
+    )
+    parser.addoption(
+        "--force-usb",
+        action="store_true",
+        default=None,
+        help=("Force tests to run with USB controllers instead of btvirt"),
+    )
+    parser.addoption(
+        "--build-dir",
+        action="store",
+        default=None,
+        type=Path,
+        help=("Build directory to find development binaries"),
+    )
+    parser.addoption(
+        "--list",
+        action="store_true",
+        default=None,
+        help=("List tests"),
+    )
+    parser.addoption(
+        "--log-filter",
+        action="append",
+        default=None,
+        help=(
+            "Enable/disable loggers by name. Can be passed multiple times. Example: +host.0,-rpc"
+        ),
+    )
+    parser.addoption(
+        "--vm-timeout",
+        action="store",
+        default=20,
+        type=float,
+        help="Timeout in seconds for waiting for RPC reply with VM (default: 20 s)",
+    )
+
+
+def pytest_configure(config):
+    from .lib import utils
+
+    if config.option.build_dir is not None:
+        utils.BUILD_DIR = config.option.build_dir
+
+
+def pytest_report_collectionfinish(config, start_path, items):
+    if config.option.list:
+        print()
+        for item in items:
+            print(f"unit/{item.nodeid}")
+        print()
+        os._exit(0)
+
+
+def pytest_collection_modifyitems(session, config, items):
+    # Sort VM-using tests to minimize VM setup/teardown
+
+    def sort_key(item):
+        for m in item.own_markers:
+            setup = item.callspec.params.get("vm_setup", None)
+            if setup is not None:
+                return tuple(sorted(setup.items()))
+        return ()
+
+    if not config.option.list:
+        items.sort(key=sort_key)
+
+
+def pytest_sessionstart(session):
+    from .lib import utils
+
+    config = session.config
+
+    if config.option.log_filter is not None:
+        allow = set()
+        deny = set()
+        for item in config.option.log_filter:
+            for name in item.split(","):
+                if name.startswith("+"):
+                    allow.add(name[1:])
+                elif name.startswith("-"):
+                    deny.add(name[1:])
+                else:
+                    allow.add(name)
+
+        filter = _LogFilter(allow, deny)
+
+        for handler in logging.root.handlers:
+            if any(type(f) == _LogFilter for f in handler.filters):
+                continue
+
+            handler.addFilter(filter)
+
+    for handler in logging.root.handlers:
+        fmt = getattr(handler, "formatter", None)
+        if hasattr(fmt, "add_color_level"):
+            fmt.add_color_level(utils.OUT, "yellow")
+
+
+@pytest.fixture(autouse=True)
+def setup_logging(pytestconfig, caplog):
+    caplog.set_level(0)
+
+
+class _LogFilter(logging.Filter):
+    def __init__(self, allow=(), deny=()):
+        if allow:
+            allow_re = "|".join(self._re(x) for x in allow)
+            self.allow = re.compile(allow_re)
+        else:
+            self.allow = None
+        if deny:
+            deny_re = "|".join(self._re(x) for x in deny)
+            self.deny = re.compile(deny_re)
+        else:
+            self.deny = None
+
+    def _re(self, name):
+        pat = fnmatch.translate(name)
+        return f"{pat}$|{pat}\\."
+
+    def filter(self, record):
+        if self.deny is not None and self.deny.match(record.name):
+            return False
+        if self.allow is not None and self.allow.match(record.name):
+            return True
+        return self.allow is None
+
+
+@pytest.fixture(scope="session")
+def kernel(pytestconfig):
+    """
+    Fixture for kernel image
+    """
+    kernel = pytestconfig.getoption("kernel")
+
+    if kernel is None:
+        kernel = os.environ.get("FUNCTIONAL_TESTING_KERNEL")
+
+    if not kernel:
+        pytest.skip("No kernel image")
+
+    if Path(kernel).is_dir():
+        kernel = str(Path(kernel) / "arch" / "x86" / "boot" / "bzImage")
+
+    if not Path(kernel).is_file():
+        pytest.skip("no kernel image")
+
+    return kernel
+
+
+@pytest.fixture(scope="session")
+def usb_indices(pytestconfig):
+    """
+    Fixture for available USB controllers
+    """
+    from .lib import env
+
+    usb_indices = pytestconfig.getoption("usb")
+
+    if usb_indices is None:
+        usb_indices = os.environ.get("FUNCTIONAL_TESTING_CONTROLLERS")
+
+    if usb_indices is None:
+        usb_indices = [item.name for item in Path("/sys/class/bluetooth").iterdir()]
+    else:
+        usb_indices = usb_indices.split(", ")
+
+    messages = []
+    for name in list(usb_indices):
+        subsys = Path("/sys/class/bluetooth") / name / "device" / "subsystem"
+        if subsys.resolve() != Path("/sys/bus/usb"):
+            usb_indices.remove(name)
+            continue
+
+        try:
+            env.Environment.check_controller(name)
+            messages.append("")
+        except ValueError as exc:
+            usb_indices.remove(name)
+            messages.append(str(exc))
+
+    return usb_indices, messages
+
+
+@pytest.fixture(scope="session")
+def host_setup(request):
+    if getattr(request, "param", None) is None:
+        raise pytest.fail("host setup not specified")
+
+    return request.param
+
+
+@pytest.fixture(scope="session")
+def vm_setup(request):
+    if getattr(request, "param", None) is None:
+        raise pytest.fail("env setup not specified")
+
+    return request.param
+
+
+def _vm_impl(request, kernel, num_hosts, hw):
+    from .lib import Environment
+
+    config = request.session.config
+
+    if hw or config.option.force_usb:
+        usb_indices, messages = request.getfixturevalue("usb_indices")
+        if len(usb_indices) < num_hosts:
+            message = "\n".join(m for m in messages[:num_hosts] if m)
+            pytest.skip(reason=f"Not enough USB controllers: {message}")
+    else:
+        usb_indices = None
+
+    with Environment(
+        kernel, num_hosts, usb_indices=usb_indices, timeout=config.option.vm_timeout
+    ) as vm:
+        yield vm
+
+
+def _hosts_impl(request, vm, setup):
+    from .lib import Bdaddr, Call
+
+    for h, plugins in zip(vm.hosts, setup):
+        for p in plugins:
+            h.start_load(p)
+
+    for h in vm.hosts:
+        h.wait_load()
+
+    yield vm.hosts
+
+    for h in vm.hosts:
+        h.close()
+
+
+@pytest.fixture(scope="package")
+def vm(request, kernel, vm_setup):
+    yield from _vm_impl(request, kernel, **vm_setup)
+
+
+@pytest.fixture
+def hosts(request, vm, host_setup):
+    yield from _hosts_impl(request, vm, **host_setup)
+
+
+# Same with single-test scope:
+
+
+@pytest.fixture
+def vm_once(request, kernel, vm_setup):
+    yield from _vm_impl(request, kernel, **vm_setup)
+
+
+@pytest.fixture
+def hosts_once(request, vm_module, host_setup):
+    yield from _hosts_impl(request, vm_module, **host_setup)
diff --git a/unit/func_test/lib/__init__.py b/unit/func_test/lib/__init__.py
new file mode 100644
index 000000000..b2767105e
--- /dev/null
+++ b/unit/func_test/lib/__init__.py
@@ -0,0 +1,5 @@
+from .rpc import RemoteError
+
+from .env import *
+from .utils import *
+from .host_plugins import *
diff --git a/unit/func_test/lib/env.py b/unit/func_test/lib/env.py
new file mode 100644
index 000000000..70c0c6ffb
--- /dev/null
+++ b/unit/func_test/lib/env.py
@@ -0,0 +1,484 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Test environment:
+
+- one or more qemu instances running Linux kernel + BlueZ + other stack
+- connected by btvirt, or real USB Bluetooth controllers
+- Python RPC connection to each via unix socket <-> qemu chardev
+
+"""
+import sys
+import os
+import signal
+import re
+import pwd
+import time
+import shlex
+import argparse
+import shutil
+import threading
+import tempfile
+import operator
+import logging
+import socket
+import pickle
+from pathlib import Path
+from subprocess import Popen, DEVNULL, PIPE, run
+
+from . import rpc, utils
+
+__all__ = ["HostPlugin", "Environment"]
+
+log = logging.getLogger(__name__)
+
+
+class HostPlugin:
+    value = None
+    depends = None
+
+    def __init__(self):
+        """Configure plugin (runs on host-side)"""
+        pass
+
+    def setup(self, impl: Implementation):
+        """VM-side setup"""
+        pass
+
+    def teardown(self):
+        """VM-side teardown"""
+        pass
+
+
+class HostProxy:
+    """
+    Parent-side proxy for VM host: load plugins, RPC calls to plugins
+    """
+
+    def __init__(self, path, timeout, name):
+        self._path = path
+        self._active_conn = None
+        self._timeout = timeout
+        self._plugins = {}
+        self._name = name
+
+    def load(self, plugin: HostPlugin):
+        self.start_load(plugin)
+        self.wait_load()
+
+    def start_load(self, plugin: HostPlugin):
+        if plugin.name in self._plugins:
+            # Already loaded
+            return
+        self._conn.call_noreply("start_load", plugin)
+        self._plugins[plugin.name] = None
+
+    def wait_load(self):
+        for name, value in self._conn.call("wait_load").items():
+            if value is None:
+                value = _PluginProxy(name, self._active_conn)
+            self._plugins[name] = value
+
+    @property
+    def _conn(self):
+        if self._active_conn is None:
+            self._active_conn = rpc.client_unix_socket(
+                self._path, timeout=self._timeout, name=self._name
+            )
+        return self._active_conn
+
+    def __getattr__(self, name):
+        if name not in self._plugins:
+            raise AttributeError(name)
+        return self._plugins[name]
+
+    def close(self):
+        self._plugins = {}
+        if self._active_conn is not None:
+            self._active_conn.close()
+            self._active_conn = None
+
+
+class _PluginProxy:
+    """
+    Host-side proxy for a plugin: RPC calls
+    """
+
+    def __init__(self, name, conn):
+        self._name = name
+        self._conn = conn
+
+    def __call__(self, *a, **kw):
+        return self._conn.call("call_plugin", self._name, "__call__", *a, **kw)
+
+    def __getattr__(self, name):
+        if name.startswith("_"):
+            raise AttributeError(name)
+        return lambda *a, **kw: self._conn.call(
+            "call_plugin", self._name, name, *a, **kw
+        )
+
+
+class Implementation:
+    """
+    VM-side main instance: setup/teardown plugins, plugin RPC server side
+    """
+
+    def __init__(self):
+        self.plugins = {}
+        self.plugin_order = []
+        self.load_error = False
+
+    def start_load(self, plugin):
+        try:
+            log.info(f"Plugin {plugin.name} load")
+            plugin.setup(self)
+        except:
+            self.load_error = True
+            raise
+        self.plugins[plugin.name] = plugin
+        self.plugin_order.append(plugin.name)
+        log.info(f"Plugin {plugin.name} ready")
+
+    def wait_load(self):
+        if self.load_error:
+            raise RuntimeError("load failed")
+        log.debug(f"Plugins ready")
+        return {p.name: getattr(p, "value", None) for p in self.plugins.values()}
+
+    def unload(self, name):
+        self.plugin_order.remove(name)
+        p = self.plugins.pop(name)
+        method = getattr(p, "teardown", None)
+        if method is not None:
+            try:
+                method()
+            except BaseException as exc:
+                log.error(f"plugin teardown error: {exc}")
+
+    def call_plugin(self, name, method, *a, **kw):
+        return getattr(self.plugins[name], method)(*a, **kw)
+
+    def teardown(self):
+        while self.plugin_order:
+            self.unload(self.plugin_order[-1])
+
+
+def _find_rpc_vport():
+    """
+    Find RPC control virtio port
+    """
+    for port in Path("/sys/class/virtio-ports").iterdir():
+        with open(port / "name", "rb") as f:
+            name = f.read(64)
+            if name == b"bluez-func-test-rpc\n":
+                return f"/dev/{port.name}"
+
+    return None
+
+
+def _main_runner_instance():
+    """
+    VM-side tester main instance
+    """
+    dev = _find_rpc_vport()
+    if dev is not None:
+        print(f"Test RPC server on {dev}", file=sys.stderr)
+        rpc.server_file(dev, Implementation())
+        return
+
+    import termios
+    import tty
+
+    with open(sys.argv[1], "r+b", buffering=0) as f:
+        mode = termios.tcgetattr(f.fileno())
+        tty.cfmakeraw(mode)
+        mode = termios.tcsetattr(f.fileno(), termios.TCSANOW, mode)
+        rpc.server_stream(f, Implementation())
+
+
+class _RunnerLogHandler(logging.Handler):
+    def flush(self):
+        sys.stderr.flush()
+
+    def emit(self, record):
+        try:
+            msg = record.getMessage()
+            if record.exc_info:
+                msg += "\n"
+                msg += traceback.format_exception(*record.exc_info)
+            name = record.name
+            levelno = record.levelno
+            for line in msg.splitlines():
+                sys.stderr.write(f"\x00{name}\x01{levelno}\x02{line}\n")
+            self.flush()
+        except RecursionError:
+            raise
+        except Exception:
+            self.handleError(record)
+
+
+def _main_runner():
+    """
+    VM-side tester supervisor
+    """
+    logging.basicConfig(level=0, handlers=[_RunnerLogHandler()])
+
+    # Preload libraries
+    import dbus
+    import pexpect
+
+    # Keep one instance running
+    while True:
+        log.info("Starting test instance")
+
+        pid = os.fork()
+        if pid == 0:
+            os.setpgid(0, 0)
+            _main_runner_instance()
+            os._exit(0)
+        else:
+            status = 1
+
+            try:
+                _, status = os.waitpid(pid, 0)
+            except ChildProcessError:
+                pass
+
+            log.info("Terminating test instance")
+
+            for sig in [signal.SIGTERM, signal.SIGCONT, signal.SIGKILL]:
+                try:
+                    os.kill(-pid, sig)
+                    time.sleep(0.5 if sig == signal.SIGCONT else 0.1)
+                except ProcessLookupError:
+                    break
+
+            if status != 0:
+                time.sleep(0.1)
+
+
+ENV_INDEX = -1
+
+
+class Environment:
+    def __init__(self, kernel, num_hosts, usb_indices=None, virtio=True, timeout=20):
+        if Path(kernel).is_dir():
+            self.kernel = str(Path(kernel) / "arch" / "x86" / "boot" / "bzImage")
+        else:
+            self.kernel = str(kernel)
+
+        self.num_hosts = operator.index(num_hosts)
+        self.jobs = []
+        self.log_streams = []
+        self.hosts = []
+        self.virtio = bool(virtio)
+        self.timeout = float(timeout)
+        self.path = None
+
+        if usb_indices is None:
+            self.usb_indices = None
+        elif usb_indices is not None and self.num_hosts <= len(usb_indices):
+            self.usb_indices = tuple(usb_indices)
+        else:
+            raise ValueError(
+                "USB redirection enabled, but not enough controllers for each host"
+            )
+
+        if sys.version_info >= (3, 12):
+            self.runner = [sys.executable, "-P"]
+        else:
+            self.runner = [sys.executable]
+        self.runner += [str((Path(__file__).parent / "runner.py").absolute())]
+
+        try:
+            self.stdbuf = [utils.find_exe("", "stdbuf"), "-o", "L", "-e", "L"]
+        except FileNotFoundError:
+            self.stdbuf = []
+
+    def start(self):
+        self.path = Path(tempfile.mkdtemp(prefix="bluez-func-test-"))
+
+        if self.usb_indices is None:
+            args = self._start_btvirt()
+        else:
+            args = self._start_usb()
+
+        paths, names = self._start_runners(args)
+        self._start_hosts(paths, names)
+
+    def stop(self):
+        for job in self.jobs:
+            if job.poll() is not None:
+                continue
+            job.terminate()
+
+        while self.jobs:
+            job = self.jobs.pop()
+            if job.poll() is None:
+                job.wait()
+
+        while self.log_streams:
+            self.log_streams.pop().close()
+
+        while self.hosts:
+            self.hosts.pop().close()
+
+        # Clean up tmpdir (btvirt, own sockets, rmdir)
+        if self.path is not None:
+            for f in list(self.path.iterdir()):
+                if f.name.startswith("bt-server-"):
+                    f.unlink()
+                if f.name.startswith("bluez-func-test-rpc-"):
+                    f.unlink()
+
+            self.path.rmdir()
+            self.path = None
+
+    def _add_log(self, *a, **kw):
+        f = utils.LogStream(*a, **kw)
+        self.log_streams.append(f)
+        return f.stream
+
+    def _start_btvirt(self):
+        exe = utils.find_exe("emulator", "btvirt")
+        logger = self._add_log("btvirt")
+
+        cmd = self.stdbuf + [exe, f"--server={self.path}"]
+        log.info("Starting btvirt: {}".format(utils.quoted(cmd)))
+
+        job = Popen(
+            cmd,
+            stdout=logger,
+            stderr=logger,
+            stdin=DEVNULL,
+        )
+        self.jobs.append(job)
+
+        socket = self.path / "bt-server-bredrle"
+        utils.wait_files([job], [socket])
+        return [[f"-u{socket}"]] * self.num_hosts
+
+    @classmethod
+    def check_controller(cls, name):
+        subsys = Path("/sys/class/bluetooth") / name / "device" / "subsystem"
+        if subsys.resolve() != Path("/sys/bus/usb"):
+            raise ValueError(f"{devname} is not an USB device")
+
+        devpath = Path(f"/sys/class/bluetooth/{name}/device/../")
+        with open(devpath / "busnum", "r") as f:
+            busnum = "{:03}".format(int(f.read().strip()))
+        with open(devpath / "devnum", "r") as f:
+            devnum = "{:03}".format(int(f.read().strip()))
+
+        devname = f"/dev/bus/usb/{busnum}/{devnum}"
+        if not Path(devname).exists():
+            raise ValueError(f"{devname} does not exist")
+
+        try:
+            with open(devname, "wb") as f:
+                pass
+        except IOError:
+            user = pwd.getpwuid(os.getuid()).pw_name.strip()
+            message = (
+                f"error: cannot open {devname} for {name} USB redirection. "
+                f"Run: 'sudo setfacl -m user:{user}:rw- {devname}' "
+                f"to grant the permission"
+            )
+            raise ValueError(message)
+
+        return busnum, devnum
+
+    def _start_usb(self):
+        args = []
+
+        for index in self.usb_indices[: self.num_hosts]:
+            busnum, devnum = self.check_controller(index)
+            args.append(["-U", f"usb-host,hostbus={busnum},hostaddr={devnum}"])
+
+        return args
+
+    def _start_runners(self, args):
+        global ENV_INDEX
+
+        test_runner = utils.find_exe("tools", "test-runner")
+
+        socket_paths = []
+        host_names = []
+
+        ENV_INDEX += 1
+
+        for idx, arg in enumerate(args):
+            socket_path = str(self.path / f"bluez-func-test-rpc-{idx}")
+            socket_paths.append(socket_path)
+
+            qemu_args = [
+                "-chardev",
+                f"socket,id=ser0,path={socket_path},server=on,wait=off",
+            ]
+            if self.virtio:
+                qemu_args += [
+                    "-device",
+                    "virtio-serial",
+                    "-device",
+                    "virtserialport,chardev=ser0,name=bluez-func-test-rpc",
+                ]
+            else:
+                qemu_args += [
+                    "-device",
+                    "pci-serial,chardev=ser0",
+                ]
+
+            extra_args = []
+            for q in qemu_args:
+                extra_args += ["-o", q]
+
+            extra_args += ["-H"]
+
+            tty = 1
+            if self.usb_indices is None:
+                tty += 1
+
+            cmd = (
+                [test_runner, f"--kernel={self.kernel}"]
+                + arg
+                + extra_args
+                + ["--"]
+                + self.runner
+                + [f"/dev/ttyS{tty}"]
+            )
+
+            log.info("Starting host: {}".format(utils.quoted(cmd)))
+
+            host_names.append(f"host.{ENV_INDEX}.{idx}")
+
+            logger = self._add_log(
+                host_names[-1],
+                pattern=".*\x00([^\x00-\x03]+)\x01([^\x00-\x03]+)\x02",
+            )
+            self.jobs.append(Popen(cmd, stdout=logger, stderr=logger, stdin=DEVNULL))
+
+        utils.wait_files(self.jobs, socket_paths)
+        return socket_paths, host_names
+
+    def _start_hosts(self, socket_paths, host_names):
+        if len(socket_paths) != self.num_hosts:
+            raise RuntimeError("Wrong number of sockets")
+
+        for path, name in zip(socket_paths, host_names):
+            host = HostProxy(path, timeout=self.timeout, name=name)
+            host._conn
+            self.hosts.append(host)
+
+    def __del__(self):
+        self.stop()
+
+    def __enter__(self):
+        try:
+            self.start()
+        except:
+            self.stop()
+            raise
+        return self
+
+    def __exit__(self, type, value, tb):
+        self.stop()
diff --git a/unit/func_test/lib/host_plugins.py b/unit/func_test/lib/host_plugins.py
new file mode 100644
index 000000000..bb6a451ab
--- /dev/null
+++ b/unit/func_test/lib/host_plugins.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Fixtures for testing
+"""
+import os
+import sys
+import subprocess
+import collections
+import logging
+import tempfile
+import time
+import shutil
+from pathlib import Path
+
+import pytest
+import pexpect
+
+from . import env, utils
+
+__all__ = ["host_config", "Bdaddr", "Call", "Bluetoothd", "Bluetoothctl", "DbusSession"]
+
+
+log = logging.getLogger(__name__)
+
+
+class Bdaddr(env.HostPlugin):
+    name = "bdaddr"
+
+    def setup(self, impl):
+        self.value = utils.get_bdaddr()
+
+
+class Call(env.HostPlugin):
+    name = "call"
+
+    def __call__(self, func, *a, **kw):
+        return func(*a, **kw)
+
+
+class _Dbus(env.HostPlugin):
+    def __init__(self):
+        self.exe = utils.find_exe("", "dbus-daemon")
+
+    def setup(self, impl):
+        self.logger = utils.LogStream(self.name)
+
+        self.tmpdir = tempfile.TemporaryDirectory(prefix=f"{self.name}-", dir="/run")
+        self.config = Path(self.tmpdir.name) / "config.xml"
+
+        socket = (Path(self.tmpdir.name) / "socket").resolve()
+        self.address = "unix:path={}".format(socket)
+
+        with open(self.config, "w") as f:
+            text = f"""
+            <!DOCTYPE busconfig PUBLIC
+                    "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+                    "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+            <busconfig>
+            <type>{self.dbus_type}</type>
+            <listen>{self.address}</listen>
+            <policy context="default">
+            <allow user="*"/>
+            <allow own="*"/>
+            <allow send_type="method_call"/>
+            <allow send_type="signal"/>
+            <allow send_type="method_return"/>
+            <allow send_type="error"/>
+            <allow receive_type="method_call"/>
+            <allow receive_type="signal"/>
+            <allow receive_type="method_return"/>
+            <allow receive_type="error"/>
+            </policy>
+            </busconfig>
+            """
+            f.write(text)
+
+        cmd = [
+            self.exe,
+            "--nofork",
+            "--nopidfile",
+            "--nosyslog",
+            f"--config-file={self.config}",
+        ]
+
+        self.logger.log.debug(
+            "Starting dbus-session @ {}: {}".format(self.address, utils.quoted(cmd))
+        )
+
+        self.job = subprocess.Popen(
+            cmd,
+            stdout=self.logger.stream,
+            stderr=subprocess.STDOUT,
+        )
+        utils.wait_files([self.job], [socket])
+        self.logger.log.debug("dbus-session ready")
+
+        if self.dbus_type == "system":
+            os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = self.address
+        elif self.dbus_type == "session":
+            os.environ["DBUS_SESSION_BUS_ADDRESS"] = self.address
+
+    def teardown(self):
+        self.job.terminate()
+        self.job.wait()
+        self.tmpdir.cleanup()
+
+
+class DbusSystem(_Dbus):
+    name = "dbus-system"
+    dbus_type = "system"
+
+
+class DbusSession(_Dbus):
+    name = "dbus-session"
+    dbus_type = "session"
+
+
+class Bluetoothd(env.HostPlugin):
+    name = "bluetoothd"
+    depends = [DbusSystem()]
+
+    def __init__(self, debug=True, conf=None, args=()):
+        self.conf = conf
+        self.args = tuple(args)
+        if debug and "-d" not in self.args:
+            self.args += ("-d",)
+
+    def setup(self, impl):
+        import dbus
+
+        exe = utils.find_exe("src", "bluetoothd")
+
+        self.tmpdir = tempfile.TemporaryDirectory(prefix="bluetoothd-state-")
+        state_dir = Path(self.tmpdir.name) / "state"
+        conf = Path(self.tmpdir.name) / "main.conf"
+
+        state_dir.mkdir()
+
+        if self.conf is None:
+            shutil.copyfile(utils.SRC_DIR / "src" / "main.conf", conf)
+        else:
+            with open(str(conf), "w") as f:
+                f.write(self.conf)
+
+        envvars = dict(os.environ)
+        envvars["STATE_DIRECTORY"] = str(state_dir)
+
+        cmd = [exe, "--nodetach", "-f", str(conf)] + list(self.args)
+
+        log.info("Start bluetoothd: {}".format(utils.quoted(cmd)))
+
+        self.logger = utils.LogStream("bluetoothd")
+        self.job = subprocess.Popen(
+            cmd,
+            env=envvars,
+            stdin=subprocess.DEVNULL,
+            stdout=self.logger.stream,
+            stderr=subprocess.STDOUT,
+        )
+
+        # Wait for the adapter to appear powered
+        bus = dbus.SystemBus()
+        while True:
+            try:
+                adapter = dbus.Interface(
+                    bus.get_object("org.bluez", "/org/bluez/hci0"),
+                    "org.freedesktop.DBus.Properties",
+                )
+                if adapter.Get("org.bluez.Adapter1", "Powered"):
+                    break
+            except dbus.DBusException:
+                pass
+            time.sleep(0.5)
+
+        log.info("Bluetoothd ready")
+
+    def teardown(self):
+        log.info("Stop bluetoothd")
+        self.job.terminate()
+        self.job.wait()
+        self.tmpdir.cleanup()
+
+
+class Bluetoothctl(env.HostPlugin):
+    name = "bluetoothctl"
+    depends = [Bluetoothd()]
+
+    def __init__(self):
+        self.exe = utils.find_exe("client", "bluetoothctl")
+
+    def setup(self, impl):
+        self.logger = utils.LogStream("bluetoothctl")
+        self.ctl = pexpect.spawn(self.exe, logfile=self.logger.stream)
+
+    def teardown(self):
+        self.ctl.terminate()
+        self.ctl.wait()
+
+    def expect(self, *a, **kw):
+        ret = self.ctl.expect(*a, **kw)
+        log.debug("pexpect: found")
+        return ret, self.ctl.match.groups()
+
+    def expect_prompt(self):
+        prompt = "\\[[a-zA-Z0-9. -]+\\]>"
+        return self.expect(prompt)
+
+    def send(self, *a, **kw):
+        return self.ctl.send(*a, **kw)
+
+
+HOST_SETUPS = {}
+
+
+def _expand_plugins(plugins):
+    """
+    Resolve plugin dependencies to linear load order
+    """
+    plugins = [Bdaddr(), Call()] + list(plugins)
+    to_load = []
+    seen = set()
+
+    while plugins:
+        deps = []
+        for dep in plugins[0].depends or ():
+            if type(dep) not in seen:
+                deps.append(dep)
+                seen.add(type(dep))
+                continue
+
+        if deps:
+            plugins = deps + plugins
+            continue
+
+        to_load.append(plugins.pop(0))
+
+    return tuple(to_load)
+
+
+def host_config(*host_setup, hw=False):
+    """
+    Declare host configuration.
+
+    - *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
+
+    """
+    setup = tuple(_expand_plugins(plugins) for plugins in host_setup)
+
+    host_setup = dict(setup=setup)
+    vm_setup = dict(num_hosts=len(setup), hw=hw)
+
+    vm_setup_name = "vm{}{}".format(len(setup), "hw" if hw else "")
+
+    idx = HOST_SETUPS.setdefault(tuple(sorted(host_setup.items())), len(HOST_SETUPS))
+    host_setup_name = f"hosts{idx}"
+
+    def decorator(func):
+        func = pytest.mark.parametrize(
+            "host_setup", [host_setup], indirect=True, ids=[host_setup_name]
+        )(func)
+        func = pytest.mark.parametrize(
+            "vm_setup", [vm_setup], indirect=True, ids=[vm_setup_name]
+        )(func)
+        return func
+
+    return decorator
diff --git a/unit/func_test/lib/rpc.py b/unit/func_test/lib/rpc.py
new file mode 100644
index 000000000..159c42ef5
--- /dev/null
+++ b/unit/func_test/lib/rpc.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Simple RPC over sockets / character devices
+
+"""
+import sys
+import os
+import struct
+import socket
+import fcntl
+import select
+import time
+import pickle
+import logging
+import traceback
+from pathlib import Path
+
+log = logging.getLogger("rpc")
+
+__all__ = [
+    "Connection",
+    "RemoteError",
+    "server_stream",
+    "server_file",
+    "server_unix_socket",
+    "client_unix_socket",
+]
+
+
+class RemoteError(Exception):
+    def __init__(self, exc, traceback):
+        super().__init__(str(exc))
+        self.exc = exc
+        self.traceback = traceback
+
+    def __str__(self):
+        tb = "    ".join(self.traceback)
+        return f"{self.exc}\nRemote traceback:\n    {tb}"
+
+
+def server_stream(stream, implementation):
+    """
+    Run client side on the given stream.
+
+    Parameters
+    ----------
+    stream : file
+        Stream to use for I/O
+    implementation : object
+        Object on which remote methods are called
+
+    """
+    conn = _Connection(stream, None)
+
+    conn._flush()
+    conn._send("hello")
+
+    while True:
+        sys.stdout.flush()
+        msg = conn._recv()
+        message = msg["message"]
+
+        if message in ("call", "call-noreply"):
+            log.info(f"server: {msg['method']} {msg['a']} {msg['kw']}")
+            try:
+                method = getattr(implementation, msg["method"])
+                result = method(*msg["a"], **msg["kw"])
+                if message == "call":
+                    conn._send("call:reply", result=result)
+            except BaseException as exc:
+                if message == "call":
+                    conn._send(
+                        "call:reply",
+                        error=exc,
+                        traceback=traceback.format_exception(exc),
+                    )
+                else:
+                    log.error(traceback.format_exc())
+            log.debug("server: done")
+        elif message == "quit":
+            method = getattr(implementation, "teardown", None)
+            if method is not None:
+                try:
+                    method()
+                except BaseException as exc:
+                    log.error(f"implementation quit() failed: {exc}")
+
+            log.info(f"server: quit")
+            return
+        else:
+            raise RuntimeError(f"unknown {message=}")
+
+
+def server_file(filename, implementation):
+    """Open given file and run server on it"""
+    with open(filename, "r+b", buffering=0) as stream:
+        server_stream(stream, implementation)
+
+
+def server_unix_socket(socket_path, implementation):
+    """Open given file and run server on it"""
+    with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
+        sock.bind(str(socket_path))
+        sock.listen(1)
+
+        s, addr = sock.accept()
+        try:
+            server_stream(s, implementation)
+        finally:
+            s.close()
+
+
+def client_unix_socket(socket_path, timeout=10, name=None):
+    """
+    Connect client to Unix socket
+
+    Parameters
+    ----------
+    socket_path : str
+        Path to Unix socket to bind to and listen
+    proxy_cls : type
+        Proxy class to make instance of
+
+    Returns
+    -------
+    conn : Connection
+        Client connection object
+
+    """
+    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+
+    end = time.time() + timeout
+    while time.time() < end:
+        try:
+            sock.connect(str(socket_path))
+            break
+        except (FileNotFoundError, ConnectionRefusedError, OSError):
+            time.sleep(max(0, min(0.5, end - time.time())))
+    else:
+        sock.connect(str(socket_path))
+
+    conn = _Connection(sock, timeout, name=name)
+
+    reply = conn._recv()
+    if reply["message"] != "hello":
+        raise RuntimeError("Bad hello message")
+
+    return conn
+
+
+class _Connection:
+    """
+    Bidirectional message queue on a stream, pickle-based
+    """
+
+    def __init__(self, stream, timeout, name=None):
+        fd = stream.fileno()
+        flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+        self.stream = stream
+        self.timeout = timeout
+
+        if name is None:
+            self.log = log
+        else:
+            self.log = logging.getLogger(f"rpc.{name}")
+
+    def _do_recv(self, size):
+        recv = getattr(self.stream, "recv", None) or self.stream.read
+        try:
+            return recv(size)
+        except BlockingIOError:
+            return None
+
+    def _do_send(self, data):
+        send = getattr(self.stream, "send", None) or self.stream.write
+        try:
+            return send(data)
+        except BlockingIOError:
+            return 0
+
+    def _flush(self):
+        while self._do_recv(8192):
+            pass
+
+    def _recvall(self, size, timeout=None):
+        if timeout is None:
+            timeout = self.timeout
+        if timeout is not None:
+            end = time.time() + timeout
+
+        data = b""
+        while len(data) < size:
+            if timeout is not None:
+                dt = end - time.time()
+                if dt <= 0:
+                    raise TimeoutError("Connection recv timed out")
+            else:
+                dt = None
+
+            r, w, x = select.select([self.stream], [], [self.stream], dt)
+
+            if x:
+                raise IOError("Connection failed")
+            elif not r:
+                continue
+
+            s = self._do_recv(size - len(data))
+            if not s:
+                raise IOError("Connection has no data")
+
+            data += s
+
+        return data
+
+    def _sendall(self, data, timeout=None):
+        if timeout is None:
+            timeout = self.timeout
+        if timeout is not None:
+            end = time.time() + timeout
+
+        while data:
+            if timeout is not None:
+                dt = end - time.time()
+                if dt <= 0:
+                    raise TimeoutError("Connection send timed out")
+            else:
+                dt = None
+
+            r, w, x = select.select([], [self.stream], [self.stream], dt)
+
+            if x:
+                raise IOError("Connection failed")
+            elif not w:
+                continue
+
+            size = self._do_send(data)
+            if not size:
+                continue
+
+            data = data[size:]
+
+    def _recv(self, timeout=None):
+        (size,) = struct.unpack("<Q", self._recvall(8, timeout=timeout))
+        if size > 2**24:
+            raise ValueError("Invalid size")
+        data = self._recvall(size, timeout=timeout)
+        return pickle.loads(data)
+
+    def _send(self, message, timeout=None, **kw):
+        data = pickle.dumps(
+            dict(message=message, **kw), protocol=pickle.HIGHEST_PROTOCOL
+        )
+        size = struct.pack("<Q", len(data))
+        self._sendall(size + data, timeout=timeout)
+
+    def call_noreply(self, method, *a, **kw):
+        self.log.info(f"client: {method} {a} {kw}")
+
+        timeout = kw.pop("timeout", None)
+
+        self._send("call-noreply", method=str(method), a=a, kw=kw, timeout=timeout)
+
+    def call(self, method, *a, **kw):
+        self.log.info(f"client: {method} {a} {kw}")
+
+        timeout = kw.pop("timeout", None)
+
+        self._send("call", method=str(method), a=a, kw=kw, timeout=timeout)
+        reply = self._recv(timeout=timeout)
+        if reply["message"] != "call:reply":
+            raise RuntimeError("Invalid reply")
+
+        if reply.get("error"):
+            raise RemoteError(reply["error"], reply["traceback"])
+
+        self.log.debug(f"client-reply")
+        return reply["result"]
+
+    def close(self):
+        try:
+            self._send("quit")
+        except BrokenPipeError:
+            pass
+
+        self.stream.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, tb):
+        self.close()
diff --git a/unit/func_test/lib/runner.py b/unit/func_test/lib/runner.py
new file mode 100644
index 000000000..bbfb00c12
--- /dev/null
+++ b/unit/func_test/lib/runner.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python3 -P
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent / ".." / ".."))
+
+import func_test.lib.env
+
+sys.exit(func_test.lib.env._main_runner())
diff --git a/unit/func_test/lib/tests/__init__.py b/unit/func_test/lib/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/unit/func_test/lib/tests/test_rpc.py b/unit/func_test/lib/tests/test_rpc.py
new file mode 100644
index 000000000..3a7786bd7
--- /dev/null
+++ b/unit/func_test/lib/tests/test_rpc.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+import os
+import pytest
+import subprocess
+import threading
+
+from func_test.lib import rpc
+
+
+def test_basic(tmp_path):
+
+    def impl_1(text):
+        print("pid", os.getpid())
+        return f"1: got {text}"
+
+    class Impl2:
+        def method(self, text):
+            print("pid", os.getpid())
+            return f"2: got {text}"
+
+        def error(self):
+            raise FloatingPointError("test")
+
+    socket_1 = tmp_path / "socket.1"
+    socket_2 = tmp_path / "socket.2"
+
+    def server_1():
+        rpc.server_unix_socket(socket_1, impl_1)
+
+    def server_2():
+        rpc.server_unix_socket(socket_2, Impl2())
+
+    s_1 = threading.Thread(target=server_1)
+    s_2 = threading.Thread(target=server_2)
+
+    s_1.start()
+    s_2.start()
+
+    try:
+        with rpc.client_unix_socket(socket_1) as c_1, rpc.client_unix_socket(
+            socket_2
+        ) as c_2:
+            assert c_1.call("__call__", "hello 1") == "1: got hello 1"
+            assert c_2.call("method", "hello 2") == "2: got hello 2"
+            with pytest.raises(rpc.RemoteError, match="Remote traceback"):
+                c_2.call("error")
+    finally:
+        s_1.join()
+        s_2.join()
diff --git a/unit/func_test/lib/utils.py b/unit/func_test/lib/utils.py
new file mode 100644
index 000000000..3d50b1fce
--- /dev/null
+++ b/unit/func_test/lib/utils.py
@@ -0,0 +1,266 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Utilities for end-to-end testing.
+
+"""
+import os
+import io
+import re
+import logging
+import subprocess
+import shlex
+import shutil
+import threading
+import time
+from pathlib import Path
+
+__all__ = ["run", "find_exe", "get_bdaddr", "quoted", "LogStream"]
+
+
+SRC_DIR = (Path(__file__).parent / ".." / ".." / "..").absolute()
+BUILD_DIR = None
+
+_LOG_LOCK = threading.Lock()
+
+log = logging.getLogger(f"run")
+
+OUT = 5
+logging.addLevelName(OUT, "OUT")
+
+
+def find_exe(subdir, name):
+    """
+    Find executable, either in BlueZ build tree or system
+    """
+    paths = [
+        SRC_DIR / "builddir" / subdir / name,
+        SRC_DIR / "build" / subdir / name,
+        SRC_DIR / subdir / name,
+        shutil.which(name),
+    ]
+    if BUILD_DIR is not None:
+        paths.insert(0, BUILD_DIR / subdir / name)
+    for exe in paths:
+        exe = str(exe)
+        if exe and os.path.isfile(exe):
+            return os.path.normpath(exe)
+
+    raise FileNotFoundError(name)
+
+
+def run(*args, input=None, capture_output=False, timeout=None, check=False, **kwargs):
+    """
+    Same as subprocess.run() but log output while running.
+    """
+    if input is not None:
+        if kwargs.get("stdin") is not None:
+            raise ValueError("stdin and input arguments may not both be used.")
+        kwargs["stdin"] = subprocess.PIPE
+
+    if capture_output:
+        if kwargs.get("stdout") is not None or kwargs.get("stderr") is not None:
+            raise ValueError(
+                "stdout and stderr arguments may not be used " "with capture_output."
+            )
+        kwargs["stdout"] = subprocess.PIPE
+        kwargs["stderr"] = subprocess.PIPE
+
+    stdout = kwargs.get("stdout", None)
+    stderr = kwargs.get("stderr", None)
+    encoding = kwargs.pop("encoding", None)
+    errors = kwargs.pop("errors", "strict")
+
+    stdout_buf = None
+    stderr_buf = None
+
+    if stdout == subprocess.PIPE:
+        stdout = stdout_buf = io.BytesIO()
+    elif isinstance(stdout, int):
+        stdout = None
+
+    stdout_log = LogStream("run.out", tee=stdout)
+    kwargs["stdout"] = stdout_log.stream
+
+    if stderr == subprocess.STDOUT:
+        stderr_log = None
+    else:
+        if stderr == subprocess.PIPE:
+            stderr = stderr_buf = io.BytesIO()
+        elif isinstance(stderr, int):
+            stderr = None
+
+        stderr_log = LogStream("run.err", tee=stderr)
+        kwargs["stderr"] = stderr_log.stream
+
+    log.info("    $ {}".format(quoted(args[0])))
+
+    with subprocess.Popen(*args, **kwargs) as process:
+        try:
+            stdout, stderr = process.communicate(input, timeout=timeout)
+        except subprocess.TimeoutExpired:
+            process.kill()
+            process.wait()
+        except:
+            process.kill()
+            raise
+        finally:
+            stdout_log.close()
+            if stderr_log is not None:
+                stderr_log.close()
+
+        if stdout_buf is not None:
+            stdout = stdout_buf.getvalue()
+            if encoding not in ("bytes", None):
+                stdout = stdout.decode(encoding=encoding, errors=errors)
+
+        if stderr_buf is not None:
+            stderr = stderr_buf.getvalue()
+            if encoding not in ("bytes", None):
+                stderr = stderr.decode(encoding=encoding, errors=errors)
+
+        retcode = process.poll()
+        if check and retcode:
+            raise subprocess.CalledProcessError(
+                retcode, process.args, output=stdout, stderr=stderr
+            )
+
+    log.info(f"(return code {retcode})")
+
+    return subprocess.CompletedProcess(process.args, retcode, stdout, stderr)
+
+
+def wait_files(jobs, paths, timeout=2):
+    """
+    Wait for subprocess.Popen instances until `paths` have been created.
+    """
+    start = time.time()
+
+    for path in paths:
+        while True:
+            if time.time() > start + timeout:
+                raise TimeoutError(f"Jobs {jobs} timed out")
+            for job in jobs:
+                if job.poll() is not None:
+                    raise RuntimeError("Process exited unexpectedly")
+            try:
+                if os.stat(path):
+                    break
+            except OSError:
+                time.sleep(0.25)
+
+
+def get_bdaddr(index=0):
+    """
+    Get bdaddr of controller with given index
+    """
+    btmgmt = find_exe("tools", "btmgmt")
+    res = subprocess.run(
+        [btmgmt, "--index", str(index), "info"],
+        stdout=subprocess.PIPE,
+        check=True,
+        encoding="utf-8",
+    )
+    m = re.search("addr ([A-Z0-9:]+) ", res.stdout)
+    if not m:
+        hciconfig = find_exe("tools", "hciconfig")
+        res = subprocess.run(
+            [hciconfig, f"hci{index}"],
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+        )
+        m = re.search("BD Address: ([A-Z0-9:]+)", res.stdout)
+        if not m:
+            raise ValueError("Can't find bdaddr")
+
+    return m.group(1).lower()
+
+
+def quoted(args):
+    """
+    Quote shell command
+    """
+    return " ".join(shlex.quote(arg) for arg in args)
+
+
+class LogStream:
+    """
+    Logger that forwards input from a stream to logging, and
+    optionally tees to another stream.  The input pipe is in
+    `LogStream.stream`.
+
+    """
+
+    def __init__(self, name, pattern=None, tee=None):
+        if pattern is not None:
+            self._logger_pattern = (pattern, name)
+            self.log = None
+        else:
+            self._logger_pattern = None
+            self.log = logging.getLogger(name)
+        self._ifd, self._ofd = os.pipe()
+        self.stream = os.fdopen(self._ofd, "wb", buffering=0)
+        self._pipeout = os.fdopen(self._ifd, "rb")
+        self._tee = tee
+        self._thread = threading.Thread(target=self._run)
+        self._thread.start()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
+    def _run(self):
+        while True:
+            line = self._pipeout.readline()
+            if not line:
+                break
+
+            fmt_line = line.decode(errors="surrogateescape")
+            fmt_line = self._filter(fmt_line)
+
+            with _LOG_LOCK:
+                log = self.log
+                level = OUT
+                if log is None:
+                    m = re.match(self._logger_pattern[0], fmt_line)
+                    if m:
+                        name = "{}.{}".format(self._logger_pattern[1], m.group(1))
+                        fmt_line = fmt_line[: m.start()] + fmt_line[m.end() :]
+                        try:
+                            level = int(m.group(2))
+                        except ValueError:
+                            pass
+                    else:
+                        name = self._logger_pattern[1]
+                    log = logging.getLogger(name)
+
+                log.log(level, fmt_line)
+
+                if self._tee is not None:
+                    self._tee.write(line)
+
+        self._pipeout.close()
+
+    def _filter(self, text):
+        # Filter out problematic ANSI codes etc
+        text = re.sub(r"\u001b\[=[0-9]+[hl]", r"", text)
+        text = re.sub(r"\u001b\[\?7l", r"", text)
+        text = re.sub(r"\u001b\[2J", r"", text)
+        text = re.sub(r"\u001bc", r"", text)
+        text = text.replace("\r", "")
+        text = text.rstrip("\n")
+        return text
+
+    def error(self, *a, **kw):
+        pass
+
+    def close(self):
+        if self._thread is not None:
+            self.stream.close()
+            self._thread.join()
+            self._thread = None
+
+    def __del__(self):
+        self.close()
diff --git a/unit/func_test/requirements.txt b/unit/func_test/requirements.txt
new file mode 100644
index 000000000..2ebe77723
--- /dev/null
+++ b/unit/func_test/requirements.txt
@@ -0,0 +1,3 @@
+pytest
+pexpect
+dbus-python
diff --git a/unit/func_test/test_bluetoothctl_vm.py b/unit/func_test/test_bluetoothctl_vm.py
new file mode 100644
index 000000000..e5bf3807a
--- /dev/null
+++ b/unit/func_test/test_bluetoothctl_vm.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Tests for bluetoothctl using VM instances
+"""
+import sys
+import pytest
+import subprocess
+import tempfile
+
+from .lib import host_config, find_exe, run, Bluetoothd, Bluetoothctl
+
+pytestmark = [pytest.mark.vm]
+
+bluetoothctl = find_exe("client", "bluetoothctl")
+
+
+@host_config(
+    [Bluetoothctl()],
+    [Bluetoothctl()],
+)
+def test_bluetoothctl_pair(hosts):
+    host0, host1 = hosts
+
+    host0.bluetoothctl.send("show\n")
+    host0.bluetoothctl.expect("Powered: yes")
+
+    host1.bluetoothctl.send("show\n")
+    host1.bluetoothctl.expect("Powered: yes")
+
+    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")
+
+
+def bluetoothctl_script(script):
+    with tempfile.NamedTemporaryFile(
+        mode="w", encoding="utf-8", delete_on_close=False
+    ) as f:
+        f.write(script)
+        f.write("\nquit")
+        f.close()
+        return run(
+            [bluetoothctl, "--init-script", f.name],
+            stdout=subprocess.PIPE,
+            stdin=subprocess.DEVNULL,
+            encoding="utf-8",
+        )
+
+
+@host_config([Bluetoothd()])
+def test_bluetoothctl_script_show(hosts):
+    (host,) = hosts
+
+    result = host.call(bluetoothctl_script, f"show")
+    assert result.returncode == 0
+
+    assert f"Controller {host.bdaddr.upper()}" in result.stdout
+    assert "Powered: " in result.stdout
+    assert "Discoverable: no" in result.stdout
diff --git a/unit/func_test/test_btmgmt_vm.py b/unit/func_test/test_btmgmt_vm.py
new file mode 100644
index 000000000..0a8d3ace6
--- /dev/null
+++ b/unit/func_test/test_btmgmt_vm.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Tests for btmgmt using VM instances
+"""
+import sys
+import pytest
+import subprocess
+import tempfile
+
+from .lib 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
diff --git a/unit/pytest.ini b/unit/pytest.ini
new file mode 100644
index 000000000..17b695861
--- /dev/null
+++ b/unit/pytest.ini
@@ -0,0 +1,6 @@
+[pytest]
+log_format = %(asctime)s %(levelname)-6s %(name)-20s:  %(message)s
+log_level = 0
+log_file = test-functional.log
+markers =
+    vm: tests requiring VM image
diff --git a/unit/test-functional b/unit/test-functional
new file mode 100755
index 000000000..9b919f117
--- /dev/null
+++ b/unit/test-functional
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# Examples:
+#
+#	./test-functional
+#	./test-functional --log-cli-level=0 -s
+#
+exec python3 -m pytest "$(dirname "$0")"/func_test "$@"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 10/11] unit: func_test: add Pipewire-using smoke tests
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (8 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 09/11] unit: add functional/integration testing framework Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-02-28 12:51 ` [PATCH BlueZ 11/11] build: add functional testing target Pauli Virtanen
  2026-03-02 21:18 ` [PATCH BlueZ 00/11] Functional/integration testing Luiz Augusto von Dentz
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

Add test for Pipewire hosts connecting A2DP/HFP BREDR services to each
other
---
 unit/func_test/test_pipewire.py | 137 ++++++++++++++++++++++++++++++++
 unit/pytest.ini                 |   1 +
 2 files changed, 138 insertions(+)
 create mode 100644 unit/func_test/test_pipewire.py

diff --git a/unit/func_test/test_pipewire.py b/unit/func_test/test_pipewire.py
new file mode 100644
index 000000000..5cec35b2b
--- /dev/null
+++ b/unit/func_test/test_pipewire.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+"""
+Tests for Pipewire audio
+"""
+import sys
+import os
+import pytest
+import subprocess
+import tempfile
+import time
+import logging
+import json
+import dbus
+from pathlib import Path
+
+from .lib import (
+    HostPlugin,
+    host_config,
+    find_exe,
+    Bluetoothd,
+    Bluetoothctl,
+    DbusSession,
+    LogStream,
+)
+
+pytestmark = [pytest.mark.vm, pytest.mark.pipewire]
+
+log = logging.getLogger(__name__)
+
+
+class Pipewire(HostPlugin):
+    name = "pipewire"
+    depends = [DbusSession(), Bluetoothd()]
+
+    def __init__(
+        self,
+        uuids=(
+            "0000110a-0000-1000-8000-00805f9b34fb",
+            "0000110b-0000-1000-8000-00805f9b34fb",
+        ),
+    ):
+        self.uuids = tuple(uuids)
+        try:
+            self.exe_pw = find_exe("", "pipewire")
+            self.exe_wp = find_exe("", "wireplumber")
+            self.exe_dump = find_exe("", "pw-dump")
+        except FileNotFoundError:
+            pytest.skip("skip", allow_module_level=True, reason="Pipewire not found")
+
+    def setup(self, impl):
+        self.tmpdir = tempfile.TemporaryDirectory(prefix="pipewire-", dir="/run")
+        conf_dir = Path(self.tmpdir.name) / "config"
+        runtime_dir = Path(self.tmpdir.name) / "runtime"
+
+        conf_dir.mkdir()
+        runtime_dir.mkdir()
+
+        environ = dict(os.environ)
+
+        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["PIPEWIRE_DEBUG"] = "2"
+        environ["WIREPLUMBER_DEBUG"] = "3"
+
+        os.environ["PIPEWIRE_RUNTIME_DIR"] = str(runtime_dir)
+
+        log.info("Start pipewire")
+
+        self.logger = LogStream("pipewire")
+        self.pw = subprocess.Popen(
+            self.exe_pw,
+            env=environ,
+            stdout=self.logger.stream,
+            stderr=subprocess.STDOUT,
+        )
+        self.wp = subprocess.Popen(
+            self.exe_wp,
+            env=environ,
+            stdout=self.logger.stream,
+            stderr=subprocess.STDOUT,
+        )
+
+        # Wait for Pipewire's bluetooth services
+        bus = dbus.SystemBus()
+        adapter = dbus.Interface(
+            bus.get_object("org.bluez", "/org/bluez/hci0"),
+            "org.freedesktop.DBus.Properties",
+        )
+        while True:
+            uuids = [str(uuid) for uuid in adapter.Get("org.bluez.Adapter1", "UUIDs")]
+            if all(uuid in uuids for uuid in self.uuids):
+                break
+            time.sleep(0.1)
+
+        log.info("Pipewire ready")
+
+    def pw_dump(self):
+        ret = subprocess.run(["pw-dump"], stdout=subprocess.PIPE, encoding="utf-8")
+        return ret.stdout
+
+    def teardown(self):
+        log.info("Stop pipewire")
+        self.pw.terminate()
+        self.wp.terminate()
+        self.pw.wait()
+        self.wp.wait()
+        self.tmpdir.cleanup()
+
+
+@host_config(
+    [Bluetoothctl(), Pipewire()],
+    [Bluetoothctl(), Pipewire()],
+)
+def test_pipewire(hosts):
+    from .test_bluetoothctl_vm import test_bluetoothctl_pair
+
+    host0, host1 = hosts
+
+    # Pair first
+    test_bluetoothctl_pair(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
+    for j in range(20):
+        text = host0.pipewire.pw_dump()
+        if "bluez_output." in text:
+            break
+        time.sleep(1)
+    else:
+        assert False, "no pipewire devices seen within timeout"
diff --git a/unit/pytest.ini b/unit/pytest.ini
index 17b695861..af780f897 100644
--- a/unit/pytest.ini
+++ b/unit/pytest.ini
@@ -3,4 +3,5 @@ log_format = %(asctime)s %(levelname)-6s %(name)-20s:  %(message)s
 log_level = 0
 log_file = test-functional.log
 markers =
+    pipewire: tests requiring Pipewire
     vm: tests requiring VM image
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH BlueZ 11/11] build: add functional testing target
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (9 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 10/11] unit: func_test: add Pipewire-using smoke tests Pauli Virtanen
@ 2026-02-28 12:51 ` Pauli Virtanen
  2026-03-02 21:18 ` [PATCH BlueZ 00/11] Functional/integration testing Luiz Augusto von Dentz
  11 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-02-28 12:51 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Pauli Virtanen

This 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  |  7 +++++++
 configure.ac | 17 +++++++++++++++++
 2 files changed, 24 insertions(+)

diff --git a/Makefile.am b/Makefile.am
index dee6aa6d0..906b3ef6b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -786,6 +786,13 @@ endif
 TESTS = $(unit_tests)
 AM_TESTS_ENVIRONMENT = MALLOC_CHECK_=3 MALLOC_PERTURB_=69
 
+check-functional: all
+	$(srcdir)/unit/test-functional -v --kernel="$(FUNCTIONAL_TESTING_KERNEL)" --build-dir="$(top_builddir)"
+
+if FUNCTIONAL_TESTING
+check: check-functional
+endif
+
 if DBUS_RUN_SESSION
 AM_TESTS_ENVIRONMENT += dbus-run-session --
 endif
diff --git a/configure.ac b/configure.ac
index 52de7d665..f4b65f7b5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -405,6 +405,23 @@ 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
+  AC_MSG_CHECKING([pytest and dependencies])
+  python3 -m pip install --dry-run --no-index -r "${srcdir}/unit/func_test/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}])
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* RE: Functional/integration testing
  2026-02-28 12:51 ` [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
@ 2026-02-28 13:45   ` bluez.test.bot
  0 siblings, 0 replies; 19+ messages in thread
From: bluez.test.bot @ 2026-02-28 13:45 UTC (permalink / raw)
  To: linux-bluetooth, pav

[-- Attachment #1: Type: text/plain, Size: 58355 bytes --]

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=1059428

---Test result---

Test Summary:
CheckPatch                    PENDING   0.31 seconds
GitLint                       PENDING   0.27 seconds
BuildEll                      PASS      21.22 seconds
BluezMake                     FAIL      631.87 seconds
MakeCheck                     FAIL      13.18 seconds
MakeDistcheck                 FAIL      82.97 seconds
CheckValgrind                 FAIL      215.70 seconds
CheckSmatch                   FAIL      350.74 seconds
bluezmakeextell               FAIL      177.68 seconds
IncrementalBuild              PENDING   0.28 seconds
ScanBuild                     FAIL      489.22 seconds

Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:

##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:

##############################
Test: BluezMake - FAIL
Desc: Build BlueZ
Output:

tools/mgmt-tester.c: In function ‘main’:
tools/mgmt-tester.c:12984:5: note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying without
12984 | int main(int argc, char *argv[])
      |     ^~~~
unit/test-avdtp.c: In function ‘main’:
unit/test-avdtp.c:766:5: note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying without
  766 | int main(int argc, char *argv[])
      |     ^~~~
unit/test-avrcp.c: In function ‘main’:
unit/test-avrcp.c:989:5: note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying without
  989 | int main(int argc, char *argv[])
      |     ^~~~
/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:4224: all] Error 2
##############################
Test: MakeCheck - FAIL
Desc: Run Bluez Make Check
Output:

/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make: *** [Makefile:10903: check] Error 2
##############################
Test: MakeDistcheck - FAIL
Desc: Run Bluez Make Distcheck
Output:

/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/bluez-5.86/_build/sub/../../tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[2]: *** [Makefile:6194: tools/test-runner] Error 1
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [Makefile:4224: all] Error 2
make: *** [Makefile:10824: distcheck] Error 1
##############################
Test: CheckValgrind - FAIL
Desc: Run Bluez Make Check with Valgrind
Output:

tools/mgmt-tester.c: In function ‘main’:
tools/mgmt-tester.c:12984:5: note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying without
12984 | int main(int argc, char *argv[])
      |     ^~~~
/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:10903: check] Error 2
##############################
Test: CheckSmatch - FAIL
Desc: Run smatch tool with source
Output:

src/shared/crypto.c:271:21: warning: Variable length array is used.
src/shared/crypto.c:272:23: warning: Variable length array is used.
src/shared/gatt-helpers.c:768:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:846:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1339:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1370:23: warning: Variable length array is used.
src/shared/gatt-server.c:278:25: warning: Variable length array is used.
src/shared/gatt-server.c:618:25: warning: Variable length array is used.
src/shared/gatt-server.c:716:25: warning: Variable length array is used.
src/shared/bap.c:312:25: warning: array of flexible structures
src/shared/bap.c: note: in included file:
./src/shared/ascs.h:88:25: warning: array of flexible structures
src/shared/shell.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
src/shared/crypto.c:271:21: warning: Variable length array is used.
src/shared/crypto.c:272:23: warning: Variable length array is used.
src/shared/gatt-helpers.c:768:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:846:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1339:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1370:23: warning: Variable length array is used.
src/shared/gatt-server.c:278:25: warning: Variable length array is used.
src/shared/gatt-server.c:618:25: warning: Variable length array is used.
src/shared/gatt-server.c:716:25: warning: Variable length array is used.
src/shared/bap.c:312:25: warning: array of flexible structures
src/shared/bap.c: note: in included file:
./src/shared/ascs.h:88:25: warning: array of flexible structures
src/shared/shell.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
tools/mesh-cfgtest.c:1453:17: warning: unknown escape sequence: '\%'
tools/sco-tester.c: note: in included file:
./lib/bluetooth/bluetooth.h:232:15: warning: array of flexible structures
./lib/bluetooth/bluetooth.h:237:31: warning: array of flexible structures
tools/bneptest.c:634:39: warning: unknown escape sequence: '\%'
tools/seq2bseq.c:57:26: warning: Variable length array is used.
tools/obex-client-tool.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
src/advertising.c: note: in included file:
./src/shared/mgmt.h:95:25: error: redefinition of unsigned int enum mgmt_io_capability
src/adv_monitor.c: note: in included file:
./src/shared/mgmt.h:95:25: error: redefinition of unsigned int enum mgmt_io_capability
unit/avctp.c:505:34: warning: Variable length array is used.
unit/avctp.c:556:34: warning: Variable length array is used.
unit/test-avrcp.c:373:26: warning: Variable length array is used.
unit/test-avrcp.c:398:26: warning: Variable length array is used.
unit/test-avrcp.c:414:24: warning: Variable length array is used.
unit/avrcp-lib.c:1085:34: warning: Variable length array is used.
unit/avrcp-lib.c:1583:34: warning: Variable length array is used.
unit/avrcp-lib.c:1612:34: warning: Variable length array is used.
unit/avrcp-lib.c:1638:34: warning: Variable length array is used.
src/advertising.c: note: in included file:
./src/shared/mgmt.h:95:25: error: redefinition of unsigned int enum mgmt_io_capability
src/adv_monitor.c: note: in included file:
./src/shared/mgmt.h:95:25: error: redefinition of unsigned int enum mgmt_io_capability
mesh/mesh-io-mgmt.c:525:67: warning: Variable length array is used.
client/display.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
src/shared/crypto.c:271:21: warning: Variable length array is used.
src/shared/crypto.c:272:23: warning: Variable length array is used.
src/shared/gatt-helpers.c:768:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:846:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1339:31: warning: Variable length array is used.
src/shared/gatt-helpers.c:1370:23: warning: Variable length array is used.
src/shared/gatt-server.c:278:25: warning: Variable length array is used.
src/shared/gatt-server.c:618:25: warning: Variable length array is used.
src/shared/gatt-server.c:716:25: warning: Variable length array is used.
src/shared/bap.c:312:25: warning: array of flexible structures
src/shared/bap.c: note: in included file:
./src/shared/ascs.h:88:25: warning: array of flexible structures
src/shared/shell.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
monitor/packet.c:1993:26: warning: Variable length array is used.
monitor/packet.c: note: in included file:
monitor/bt.h:3866:52: warning: array of flexible structures
monitor/bt.h:3854:40: warning: array of flexible structures
monitor/msft.c: note: in included file:
monitor/msft.h:88:44: warning: array of flexible structures
tools/rctest.c:631:33: warning: non-ANSI function declaration of function 'automated_send_recv'
tools/hex2hcd.c:136:26: warning: Variable length array is used.
tools/meshctl.c:324:33: warning: non-ANSI function declaration of function 'forget_mesh_devices'
tools/mesh-gatt/node.c:456:39: warning: non-ANSI function declaration of function 'node_get_local_node'
tools/mesh-gatt/net.c:1239:30: warning: non-ANSI function declaration of function 'get_next_seq'
tools/mesh-gatt/net.c:2193:29: warning: non-ANSI function declaration of function 'net_get_default_ttl'
tools/mesh-gatt/net.c:2207:26: warning: non-ANSI function declaration of function 'net_get_seq_num'
tools/mesh-gatt/prov.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
tools/mesh-gatt/onoff-model.c: note: in included file (through /usr/include/readline/readline.h):
/usr/include/readline/rltypedefs.h:35:23: warning: non-ANSI function declaration of function 'Function'
/usr/include/readline/rltypedefs.h:36:25: warning: non-ANSI function declaration of function 'VFunction'
/usr/include/readline/rltypedefs.h:37:27: warning: non-ANSI function declaration of function 'CPFunction'
/usr/include/readline/rltypedefs.h:38:29: warning: non-ANSI function declaration of function 'CPPFunction'
ell/log.c:431:65: warning: non-ANSI function declaration of function 'register_debug_section'
ell/log.c:439:68: warning: non-ANSI function declaration of function 'free_debug_sections'
ell/random.c:60:42: warning: non-ANSI function declaration of function 'l_getrandom_is_supported'
ell/cipher.c:660:28: warning: non-ANSI function declaration of function 'init_supported'
ell/checksum.c:382:28: warning: non-ANSI function declaration of function 'init_supported'
ell/checksum.c:444:47: warning: non-ANSI function declaration of function 'l_checksum_cmac_aes_supported'
ell/cipher.c:519:24: warning: Variable length array is used.
ell/cert-crypto.c:36:33: warning: Variable length array is used.
ell/cert-crypto.c:142:36: warning: Variable length array is used.
ell/cert-crypto.c:198:36: warning: Variable length array is used.
ell/cert-crypto.c:251:31: warning: Variable length array is used.
ell/key.c:550:25: warning: Variable length array is used.
ell/dbus-service.c:548:49: warning: non-ANSI function declaration of function '_dbus_object_tree_new'
ell/dbus-filter.c:233:46: warning: Variable length array is used.
ell/tls.c:45:25: warning: Variable length array is used.
ell/tls.c:86:22: warning: Variable length array is used.
ell/tls.c:86:46: warning: Variable length array is used.
ell/tls.c:1819:26: warning: Variable length array is used.
ell/tls-suites.c:1079:25: warning: Variable length array is used.
ell/tls-suites.c:1081:34: warning: Variable length array is used.
ell/tls-suites.c:1084:41: warning: Variable length array is used.
ell/tls-suites.c:1133:41: warning: Variable length array is used.
emulator/btdev.c:470:29: warning: Variable length array is used.
emulator/bthost.c:700:28: warning: Variable length array is used.
emulator/bthost.c:701:32: warning: Variable length array is used.
emulator/bthost.c:918:28: warning: Variable length array is used.
emulator/bthost.c:952:28: warning: Variable length array is used.
emulator/bthost.c:953:32: warning: Variable length array is used.
/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:4224: all] Error 2
##############################
Test: bluezmakeextell - FAIL
Desc: Build Bluez with External ELL
Output:

/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:4224: all] Error 2
##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series
Output:

##############################
Test: ScanBuild - FAIL
Desc: Run Scan Build
Output:

src/shared/gatt-client.c:455:21: warning: Use of memory after it is freed
        gatt_db_unregister(op->client->db, op->db_id);
                           ^~~~~~~~~~
src/shared/gatt-client.c:700:2: warning: Use of memory after it is freed
        discovery_op_complete(op, false, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1000:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1106:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1300:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1365:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1640:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1645:2: warning: Use of memory after it is freed
        discover_all(op);
        ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1701:56: warning: Use of memory after it is freed
        notify_data->chrc->ccc_write_id = notify_data->att_id = att_id;
                                          ~~~~~~~~~~~~~~~~~~~ ^
src/shared/gatt-client.c:2154:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:2162:8: warning: Use of memory after it is freed
                                                        discovery_op_ref(op),
                                                        ^~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3340:2: warning: Use of memory after it is freed
        complete_write_long_op(req, success, 0, false);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3362:2: warning: Use of memory after it is freed
        request_unref(req);
        ^~~~~~~~~~~~~~~~~~
13 warnings generated.
src/shared/bap.c:1529:8: warning: Use of memory after it is freed
        bap = bt_bap_ref_safe(bap);
              ^~~~~~~~~~~~~~~~~~~~
src/shared/bap.c:2340:20: warning: Use of memory after it is freed
        return queue_find(stream->bap->streams, NULL, stream);
                          ^~~~~~~~~~~~~~~~~~~~
2 warnings generated.
src/shared/gatt-client.c:455:21: warning: Use of memory after it is freed
        gatt_db_unregister(op->client->db, op->db_id);
                           ^~~~~~~~~~
src/shared/gatt-client.c:700:2: warning: Use of memory after it is freed
        discovery_op_complete(op, false, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1000:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1106:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1300:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1365:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1640:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1645:2: warning: Use of memory after it is freed
        discover_all(op);
        ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1701:56: warning: Use of memory after it is freed
        notify_data->chrc->ccc_write_id = notify_data->att_id = att_id;
                                          ~~~~~~~~~~~~~~~~~~~ ^
src/shared/gatt-client.c:2154:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:2162:8: warning: Use of memory after it is freed
                                                        discovery_op_ref(op),
                                                        ^~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3340:2: warning: Use of memory after it is freed
        complete_write_long_op(req, success, 0, false);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3362:2: warning: Use of memory after it is freed
        request_unref(req);
        ^~~~~~~~~~~~~~~~~~
13 warnings generated.
tools/hciattach.c:817:7: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
        if ((n = read_hci_event(fd, resp, 10)) < 0) {
             ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/hciattach.c:865:7: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
        if ((n = read_hci_event(fd, resp, 4)) < 0) {
             ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/hciattach.c:887:8: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
                if ((n = read_hci_event(fd, resp, 10)) < 0) {
                     ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/hciattach.c:909:7: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
        if ((n = read_hci_event(fd, resp, 4)) < 0) {
             ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/hciattach.c:930:7: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
        if ((n = read_hci_event(fd, resp, 4)) < 0) {
             ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/hciattach.c:974:7: warning: Although the value stored to 'n' is used in the enclosing expression, the value is never actually read from 'n'
        if ((n = read_hci_event(fd, resp, 6)) < 0) {
             ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 warnings generated.
src/shared/bap.c:1529:8: warning: Use of memory after it is freed
        bap = bt_bap_ref_safe(bap);
              ^~~~~~~~~~~~~~~~~~~~
src/shared/bap.c:2340:20: warning: Use of memory after it is freed
        return queue_find(stream->bap->streams, NULL, stream);
                          ^~~~~~~~~~~~~~~~~~~~
2 warnings generated.
src/oui.c:50:2: warning: Value stored to 'hwdb' is never read
        hwdb = udev_hwdb_unref(hwdb);
        ^      ~~~~~~~~~~~~~~~~~~~~~
src/oui.c:53:2: warning: Value stored to 'udev' is never read
        udev = udev_unref(udev);
        ^      ~~~~~~~~~~~~~~~~
2 warnings generated.
tools/rfcomm.c:234:3: warning: Value stored to 'i' is never read
                i = execvp(cmdargv[0], cmdargv);
                ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/rfcomm.c:234:7: warning: Null pointer passed to 1st parameter expecting 'nonnull'
                i = execvp(cmdargv[0], cmdargv);
                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/rfcomm.c:354:8: warning: Although the value stored to 'fd' is used in the enclosing expression, the value is never actually read from 'fd'
                if ((fd = open(devname, O_RDONLY | O_NOCTTY)) < 0) {
                     ^    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/rfcomm.c:497:14: warning: Assigned value is garbage or undefined
        req.channel = raddr.rc_channel;
                    ^ ~~~~~~~~~~~~~~~~
tools/rfcomm.c:515:8: warning: Although the value stored to 'fd' is used in the enclosing expression, the value is never actually read from 'fd'
                if ((fd = open(devname, O_RDONLY | O_NOCTTY)) < 0) {
                     ^    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 warnings generated.
tools/hcidump.c:180:9: warning: Potential leak of memory pointed to by 'dp'
                                if (fds[i].fd == sock)
                                    ^~~
tools/hcidump.c:248:17: warning: Assigned value is garbage or undefined
                                dh->ts_sec  = htobl(frm.ts.tv_sec);
                                            ^ ~~~~~~~~~~~~~~~~~~~~
tools/hcidump.c:326:9: warning: 1st function call argument is an uninitialized value
                                if (be32toh(dp.flags) & 0x02) {
                                    ^~~~~~~~~~~~~~~~~
/usr/include/endian.h:46:22: note: expanded from macro 'be32toh'
#  define be32toh(x) __bswap_32 (x)
                     ^~~~~~~~~~~~~~
tools/hcidump.c:341:20: warning: 1st function call argument is an uninitialized value
                                frm.data_len = be32toh(dp.len);
                                               ^~~~~~~~~~~~~~~
/usr/include/endian.h:46:22: note: expanded from macro 'be32toh'
#  define be32toh(x) __bswap_32 (x)
                     ^~~~~~~~~~~~~~
tools/hcidump.c:346:14: warning: 1st function call argument is an uninitialized value
                                opcode = be32toh(dp.flags) & 0xffff;
                                         ^~~~~~~~~~~~~~~~~
/usr/include/endian.h:46:22: note: expanded from macro 'be32toh'
#  define be32toh(x) __bswap_32 (x)
                     ^~~~~~~~~~~~~~
tools/hcidump.c:384:17: warning: Assigned value is garbage or undefined
                        frm.data_len = btohs(dh.len);
                                     ^ ~~~~~~~~~~~~~
tools/hcidump.c:394:11: warning: Assigned value is garbage or undefined
                frm.len = frm.data_len;
                        ^ ~~~~~~~~~~~~
tools/hcidump.c:398:9: warning: 1st function call argument is an uninitialized value
                        ts = be64toh(ph.ts);
                             ^~~~~~~~~~~~~~
/usr/include/endian.h:51:22: note: expanded from macro 'be64toh'
#  define be64toh(x) __bswap_64 (x)
                     ^~~~~~~~~~~~~~
tools/hcidump.c:403:13: warning: 1st function call argument is an uninitialized value
                        frm.in = be32toh(dp.flags) & 0x01;
                                 ^~~~~~~~~~~~~~~~~
/usr/include/endian.h:46:22: note: expanded from macro 'be32toh'
#  define be32toh(x) __bswap_32 (x)
                     ^~~~~~~~~~~~~~
tools/hcidump.c:408:11: warning: Assigned value is garbage or undefined
                        frm.in = dh.in;
                               ^ ~~~~~
tools/hcidump.c:437:7: warning: Null pointer passed to 1st parameter expecting 'nonnull'
        fd = open(file, open_flags, 0644);
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
11 warnings generated.
tools/ciptool.c:351:7: warning: 5th function call argument is an uninitialized value
        sk = do_connect(ctl, dev_id, &src, &dst, psm, (1 << CMTP_LOOPBACK));
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
src/sdp-xml.c:126:10: warning: Assigned value is garbage or undefined
                buf[1] = data[i + 1];
                       ^ ~~~~~~~~~~~
src/sdp-xml.c:306:11: warning: Assigned value is garbage or undefined
                        buf[1] = data[i + 1];
                               ^ ~~~~~~~~~~~
src/sdp-xml.c:344:11: warning: Assigned value is garbage or undefined
                        buf[1] = data[i + 1];
                               ^ ~~~~~~~~~~~
3 warnings generated.
tools/sdptool.c:941:26: warning: Result of 'malloc' is converted to a pointer of type 'uint32_t', which is incompatible with sizeof operand type 'int'
                        uint32_t *value_int = malloc(sizeof(int));
                        ~~~~~~~~~~            ^~~~~~ ~~~~~~~~~~~
tools/sdptool.c:980:4: warning: 1st function call argument is an uninitialized value
                        free(allocArray[i]);
                        ^~~~~~~~~~~~~~~~~~~
tools/sdptool.c:3777:2: warning: Potential leak of memory pointed to by 'si.name'
        return add_service(0, &si);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~
tools/sdptool.c:4112:4: warning: Potential leak of memory pointed to by 'context.svc'
                        return -1;
                        ^~~~~~~~~
4 warnings generated.
tools/avtest.c:243:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 3);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:253:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 4);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:262:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 3);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:276:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf,
                                ^     ~~~~~~~~~~~~~~
tools/avtest.c:283:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf,
                                ^     ~~~~~~~~~~~~~~
tools/avtest.c:290:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf,
                                ^     ~~~~~~~~~~~~~~
tools/avtest.c:297:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf,
                                ^     ~~~~~~~~~~~~~~
tools/avtest.c:309:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 4);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:313:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:322:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 3);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:326:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:335:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 3);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:342:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:364:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 4);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:368:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:377:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 3);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:381:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:394:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 4);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:398:5: warning: Value stored to 'len' is never read
                                len = write(sk, buf, 2);
                                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:405:4: warning: Value stored to 'len' is never read
                        len = write(sk, buf, 2);
                        ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:415:4: warning: Value stored to 'len' is never read
                        len = write(sk, buf, 2);
                        ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:580:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 2);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:588:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, invalid ? 2 : 3);
                ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/avtest.c:602:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 4 + media_transport_size);
                ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/avtest.c:615:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:625:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:637:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:652:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:664:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:673:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 3);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:680:3: warning: Value stored to 'len' is never read
                len = write(sk, buf, 2);
                ^     ~~~~~~~~~~~~~~~~~
tools/avtest.c:716:2: warning: Value stored to 'len' is never read
        len = write(sk, buf, AVCTP_HEADER_LENGTH + sizeof(play_pressed));
        ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
32 warnings generated.
tools/btproxy.c:836:15: warning: Null pointer passed to 1st parameter expecting 'nonnull'
                        tcp_port = atoi(optarg);
                                   ^~~~~~~~~~~~
tools/btproxy.c:839:8: warning: Null pointer passed to 1st parameter expecting 'nonnull'
                        if (strlen(optarg) > 3 && !strncmp(optarg, "hci", 3))
                            ^~~~~~~~~~~~~~
2 warnings generated.
tools/create-image.c:76:3: warning: Value stored to 'fd' is never read
                fd = -1;
                ^    ~~
tools/create-image.c:84:3: warning: Value stored to 'fd' is never read
                fd = -1;
                ^    ~~
tools/create-image.c:92:3: warning: Value stored to 'fd' is never read
                fd = -1;
                ^    ~~
tools/create-image.c:105:2: warning: Value stored to 'fd' is never read
        fd = -1;
        ^    ~~
4 warnings generated.
tools/btgatt-client.c:1822:2: warning: Value stored to 'argv' is never read
        argv += optind;
        ^       ~~~~~~
1 warning generated.
tools/check-selftest.c:42:3: warning: Value stored to 'ptr' is never read
                ptr = fgets(result, sizeof(result), fp);
                ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
tools/btgatt-server.c:1208:2: warning: Value stored to 'argv' is never read
        argv -= optind;
        ^       ~~~~~~
1 warning generated.
tools/gatt-service.c:294:2: warning: 2nd function call argument is an uninitialized value
        chr_write(chr, value, len);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
tools/obex-server-tool.c:133:13: warning: Null pointer passed to 1st parameter expecting 'nonnull'
        data->fd = open(name, O_WRONLY | O_CREAT | O_NOCTTY, 0600);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/obex-server-tool.c:192:13: warning: Null pointer passed to 1st parameter expecting 'nonnull'
        data->fd = open(name, O_RDONLY | O_NOCTTY, 0);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
client/btpclient/btpclientctl.c:402:3: warning: Value stored to 'bit' is never read
                bit = 0;
                ^     ~
client/btpclient/btpclientctl.c:1655:2: warning: Null pointer passed to 2nd parameter expecting 'nonnull'
        memcpy(cp->data, ad_data, ad_len);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
src/sdp-client.c:353:14: warning: Access to field 'cb' results in a dereference of a null pointer
        (*ctxt)->cb = cb;
        ~~~~~~~~~~~~^~~~
1 warning generated.
src/sdpd-request.c:211:13: warning: Result of 'malloc' is converted to a pointer of type 'char', which is incompatible with sizeof operand type 'uint16_t'
                                pElem = malloc(sizeof(uint16_t));
                                        ^~~~~~ ~~~~~~~~~~~~~~~~
src/sdpd-request.c:239:13: warning: Result of 'malloc' is converted to a pointer of type 'char', which is incompatible with sizeof operand type 'uint32_t'
                                pElem = malloc(sizeof(uint32_t));
                                        ^~~~~~ ~~~~~~~~~~~~~~~~
2 warnings generated.
src/gatt-database.c:1175:10: warning: Value stored to 'bits' during its initialization is never read
        uint8_t bits[] = { BT_GATT_CHRC_CLI_FEAT_ROBUST_CACHING,
                ^~~~     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
src/gatt-client.c:1569:2: warning: Use of memory after it is freed
        notify_client_unref(client);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
unit/avrcp-lib.c:1968:3: warning: 1st function call argument is an uninitialized value
                g_free(text[i]);
                ^~~~~~~~~~~~~~~
1 warning generated.
unit/avdtp.c:756:25: warning: Use of memory after it is freed
                session->prio_queue = g_slist_remove(session->prio_queue, req);
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
unit/avdtp.c:763:24: warning: Use of memory after it is freed
                session->req_queue = g_slist_remove(session->req_queue, req);
                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
profiles/audio/avdtp.c:895:25: warning: Use of memory after it is freed
                session->prio_queue = g_slist_remove(session->prio_queue, req);
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
profiles/audio/avdtp.c:902:24: warning: Use of memory after it is freed
                session->req_queue = g_slist_remove(session->req_queue, req);
                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
profiles/audio/a2dp.c:442:8: warning: Use of memory after it is freed
                if (!cb->resume_cb)
                     ^~~~~~~~~~~~~
profiles/audio/a2dp.c:3354:20: warning: Access to field 'starting' results in a dereference of a null pointer (loaded from variable 'stream')
                stream->starting = TRUE;
                ~~~~~~           ^
profiles/audio/a2dp.c:3357:8: warning: Access to field 'suspending' results in a dereference of a null pointer (loaded from variable 'stream')
                if (!stream->suspending && stream->suspend_timer) {
                     ^~~~~~~~~~~~~~~~~~
profiles/audio/a2dp.c:3417:22: warning: Access to field 'suspending' results in a dereference of a null pointer (loaded from variable 'stream')
                stream->suspending = TRUE;
                ~~~~~~             ^
4 warnings generated.
profiles/audio/avrcp.c:1961:2: warning: Value stored to 'operands' is never read
        operands += sizeof(*pdu);
        ^           ~~~~~~~~~~~~
1 warning generated.
attrib/gatt.c:970:2: warning: Potential leak of memory pointed to by 'long_write'
        return prepare_write(long_write);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
src/sdpd-request.c:211:13: warning: Result of 'malloc' is converted to a pointer of type 'char', which is incompatible with sizeof operand type 'uint16_t'
                                pElem = malloc(sizeof(uint16_t));
                                        ^~~~~~ ~~~~~~~~~~~~~~~~
src/sdpd-request.c:239:13: warning: Result of 'malloc' is converted to a pointer of type 'char', which is incompatible with sizeof operand type 'uint32_t'
                                pElem = malloc(sizeof(uint32_t));
                                        ^~~~~~ ~~~~~~~~~~~~~~~~
2 warnings generated.
src/sdp-client.c:353:14: warning: Access to field 'cb' results in a dereference of a null pointer
        (*ctxt)->cb = cb;
        ~~~~~~~~~~~~^~~~
1 warning generated.
src/gatt-database.c:1175:10: warning: Value stored to 'bits' during its initialization is never read
        uint8_t bits[] = { BT_GATT_CHRC_CLI_FEAT_ROBUST_CACHING,
                ^~~~     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
src/sdp-xml.c:126:10: warning: Assigned value is garbage or undefined
                buf[1] = data[i + 1];
                       ^ ~~~~~~~~~~~
src/sdp-xml.c:306:11: warning: Assigned value is garbage or undefined
                        buf[1] = data[i + 1];
                               ^ ~~~~~~~~~~~
src/sdp-xml.c:344:11: warning: Assigned value is garbage or undefined
                        buf[1] = data[i + 1];
                               ^ ~~~~~~~~~~~
3 warnings generated.
src/gatt-client.c:1569:2: warning: Use of memory after it is freed
        notify_client_unref(client);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
gobex/gobex-header.c:95:2: warning: Null pointer passed to 2nd parameter expecting 'nonnull'
        memcpy(to, from, count);
        ^~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
gobex/gobex-transfer.c:423:7: warning: Use of memory after it is freed
        if (!g_slist_find(transfers, transfer))
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
mesh/main.c:162:3: warning: Value stored to 'optarg' is never read
                optarg += strlen("auto");
                ^         ~~~~~~~~~~~~~~
1 warning generated.
lib/bluetooth/hci.c:97:4: warning: Value stored to 'ptr' is never read
                        ptr += sprintf(ptr, "%s", m->str);
                        ^      ~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
client/player.c:2363:8: warning: Null pointer passed to 2nd parameter expecting 'nonnull'
                if (!strcmp(ep->path, pattern))
                     ^~~~~~~~~~~~~~~~~~~~~~~~~
client/player.c:3640:16: warning: Null pointer passed to 1st parameter expecting 'nonnull'
        codec->name = strdup(name);
                      ^~~~~~~~~~~~
2 warnings generated.
gdbus/watch.c:226:3: warning: Attempt to free released memory
                g_free(l->data);
                ^~~~~~~~~~~~~~~
1 warning generated.
lib/bluetooth/sdp.c:509:17: warning: Dereference of undefined pointer value
                uint8_t dtd = *(uint8_t *) dtds[i];
                              ^~~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:539:17: warning: Dereference of undefined pointer value
                uint8_t dtd = *(uint8_t *) dtds[i];
                              ^~~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:1885:26: warning: Potential leak of memory pointed to by 'ap'
        for (; pdlist; pdlist = pdlist->next) {
                                ^~~~~~
lib/bluetooth/sdp.c:1899:6: warning: Potential leak of memory pointed to by 'pds'
                ap = sdp_list_append(ap, pds);
                ~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:1944:10: warning: Potential leak of memory pointed to by 'u'
                        *seqp = sdp_list_append(*seqp, u);
                        ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:2049:4: warning: Potential leak of memory pointed to by 'lang'
                        sdp_list_free(*langSeq, free);
                        ^~~~~~~~~~~~~
lib/bluetooth/sdp.c:2138:9: warning: Potential leak of memory pointed to by 'profDesc'
        return 0;
               ^
lib/bluetooth/sdp.c:3270:8: warning: Potential leak of memory pointed to by 'pSvcRec'
                pSeq = sdp_list_append(pSeq, pSvcRec);
                ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:3271:9: warning: Potential leak of memory pointed to by 'pSeq'
                pdata += sizeof(uint32_t);
                ~~~~~~^~~~~~~~~~~~~~~~~~~
lib/bluetooth/sdp.c:4607:13: warning: Potential leak of memory pointed to by 'rec_list'
                        } while (scanned < attr_list_len && pdata_len > 0);
                                 ^~~~~~~
lib/bluetooth/sdp.c:4903:40: warning: Potential leak of memory pointed to by 'tseq'
        for (d = sdpdata->val.dataseq; d; d = d->next) {
                                              ^
lib/bluetooth/sdp.c:4939:8: warning: Potential leak of memory pointed to by 'subseq'
                tseq = sdp_list_append(tseq, subseq);
                ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12 warnings generated.
src/shared/gatt-client.c:455:21: warning: Use of memory after it is freed
        gatt_db_unregister(op->client->db, op->db_id);
                           ^~~~~~~~~~
src/shared/gatt-client.c:700:2: warning: Use of memory after it is freed
        discovery_op_complete(op, false, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1000:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1106:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1300:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1365:2: warning: Use of memory after it is freed
        discovery_op_complete(op, success, att_ecode);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1640:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1645:2: warning: Use of memory after it is freed
        discover_all(op);
        ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:1701:56: warning: Use of memory after it is freed
        notify_data->chrc->ccc_write_id = notify_data->att_id = att_id;
                                          ~~~~~~~~~~~~~~~~~~~ ^
src/shared/gatt-client.c:2154:6: warning: Use of memory after it is freed
        if (read_db_hash(op)) {
            ^~~~~~~~~~~~~~~~
src/shared/gatt-client.c:2162:8: warning: Use of memory after it is freed
                                                        discovery_op_ref(op),
                                                        ^~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3340:2: warning: Use of memory after it is freed
        complete_write_long_op(req, success, 0, false);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/shared/gatt-client.c:3362:2: warning: Use of memory after it is freed
        request_unref(req);
        ^~~~~~~~~~~~~~~~~~
13 warnings generated.
src/shared/bap.c:1529:8: warning: Use of memory after it is freed
        bap = bt_bap_ref_safe(bap);
              ^~~~~~~~~~~~~~~~~~~~
src/shared/bap.c:2340:20: warning: Use of memory after it is freed
        return queue_find(stream->bap->streams, NULL, stream);
                          ^~~~~~~~~~~~~~~~~~~~
2 warnings generated.
monitor/l2cap.c:1676:4: warning: Value stored to 'data' is never read
                        data += len;
                        ^       ~~~
monitor/l2cap.c:1677:4: warning: Value stored to 'size' is never read
                        size -= len;
                        ^       ~~~
2 warnings generated.
monitor/hwdb.c:59:2: warning: Value stored to 'hwdb' is never read
        hwdb = udev_hwdb_unref(hwdb);
        ^      ~~~~~~~~~~~~~~~~~~~~~
monitor/hwdb.c:64:2: warning: Value stored to 'udev' is never read
        udev = udev_unref(udev);
        ^      ~~~~~~~~~~~~~~~~
monitor/hwdb.c:106:2: warning: Value stored to 'hwdb' is never read
        hwdb = udev_hwdb_unref(hwdb);
        ^      ~~~~~~~~~~~~~~~~~~~~~
monitor/hwdb.c:111:2: warning: Value stored to 'udev' is never read
        udev = udev_unref(udev);
        ^      ~~~~~~~~~~~~~~~~
4 warnings generated.
tools/bluemoon.c:1102:8: warning: Null pointer passed to 1st parameter expecting 'nonnull'
                        if (strlen(optarg) > 3 && !strncmp(optarg, "hci", 3))
                            ^~~~~~~~~~~~~~
1 warning generated.
tools/meshctl.c:326:19: warning: Access to field 'mesh_devices' results in a dereference of a null pointer (loaded from variable 'default_ctrl')
        g_list_free_full(default_ctrl->mesh_devices, g_free);
                         ^~~~~~~~~~~~~~~~~~~~~~~~~~
tools/meshctl.c:762:2: warning: 2nd function call argument is an uninitialized value
        bt_shell_printf("Attempting to disconnect from %s\n", addr);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tools/meshctl.c:1957:2: warning: Value stored to 'len' is never read
        len = len + extra + strlen("local_node.json");
        ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 warnings generated.
In file included from tools/mesh-gatt/crypto.c:32:
./src/shared/util.h:244:9: warning: 1st function call argument is an uninitialized value
        return be32_to_cpu(get_unaligned((const uint32_t *) ptr));
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./src/shared/util.h:33:26: note: expanded from macro 'be32_to_cpu'
#define be32_to_cpu(val) bswap_32(val)
                         ^~~~~~~~~~~~~
/usr/include/byteswap.h:34:21: note: expanded from macro 'bswap_32'
#define bswap_32(x) __bswap_32 (x)
                    ^~~~~~~~~~~~~~
In file included from tools/mesh-gatt/crypto.c:32:
./src/shared/util.h:254:9: warning: 1st function call argument is an uninitialized value
        return be64_to_cpu(get_unaligned((const uint64_t *) ptr));
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./src/shared/util.h:34:26: note: expanded from macro 'be64_to_cpu'
#define be64_to_cpu(val) bswap_64(val)
                         ^~~~~~~~~~~~~
/usr/include/byteswap.h:37:21: note: expanded from macro 'bswap_64'
#define bswap_64(x) __bswap_64 (x)
                    ^~~~~~~~~~~~~~
2 warnings generated.
ell/util.c:853:8: warning: The left operand of '>' is a garbage value
        if (x > UINT8_MAX)
            ~ ^
ell/util.c:871:8: warning: The left operand of '>' is a garbage value
        if (x > UINT16_MAX)
            ~ ^
2 warnings generated.
ell/pem.c:131:8: warning: Dereference of null pointer (loaded from variable 'eol')
                        if (*eol == '\r' || *eol == '\n')
                            ^~~~
ell/pem.c:166:18: warning: Dereference of null pointer (loaded from variable 'eol')
                if (buf_len && *eol == '\r' && *buf_ptr == '\n') {
                               ^~~~
ell/pem.c:166:34: warning: Dereference of null pointer (loaded from variable 'buf_ptr')
                if (buf_len && *eol == '\r' && *buf_ptr == '\n') {
                                               ^~~~~~~~
ell/pem.c:304:11: warning: 1st function call argument is an uninitialized value
        result = pem_load_buffer(file.data, file.st.st_size,
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ell/pem.c:469:9: warning: 1st function call argument is an uninitialized value
        list = l_pem_load_certificate_list_from_data(file.data,
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 warnings generated.
ell/cert.c:645:41: warning: Access to field 'asn1_len' results in a dereference of a null pointer (loaded from variable 'cert')
        key = l_key_new(L_KEY_RSA, cert->asn1, cert->asn1_len);
                                               ^~~~~~~~~~~~~~
1 warning generated.
ell/gvariant-util.c:143:18: warning: The left operand of '>' is a garbage value
                        if (alignment > max_alignment)
                            ~~~~~~~~~ ^
ell/gvariant-util.c:456:5: warning: Dereference of null pointer
                        !children[0].fixed_size) {
                         ^~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
ell/ecc-external.c:77:11: warning: Assigned value is garbage or undefined
                dest[i] = src[i];
                        ^ ~~~~~~
ell/ecc-external.c:160:18: warning: The right operand of '-' is a garbage value
                diff = left[i] - right[i] - borrow;
                               ^ ~~~~~~~~
ell/ecc-external.c:227:14: warning: 2nd function call argument is an uninitialized value
                        product = mul_64_64(left[i], right[k - i]);
                                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ell/ecc-external.c:408:9: warning: Assigned value is garbage or undefined
        tmp[1] = product[3];
               ^ ~~~~~~~~~~
ell/ecc-external.c:435:22: warning: The left operand of '&' is a garbage value
        tmp[1] = product[3] & 0xffffffff00000000ull;
                 ~~~~~~~~~~ ^
ell/ecc-external.c:483:22: warning: The left operand of '&' is a garbage value
        tmp[1] = product[5] & 0xffffffff00000000ull;
                 ~~~~~~~~~~ ^
ell/ecc-external.c:688:28: warning: The left operand of '>>' is a garbage value
                tmp[i] = (product[8 + i] >> 9) | (product[9 + i] << 55);
                          ~~~~~~~~~~~~~~ ^
7 warnings generated.
In file included from tools/parser/l2cap.c:24:
tools/parser/parser.h:121:16: warning: Dereference of null pointer
                                time_t t = f->ts.tv_sec;
                                           ^~~~~~~~~~~~
tools/parser/parser.h:129:18: warning: Dereference of null pointer
                                                (long long)f->ts.tv_sec,
                                                           ^~~~~~~~~~~~
/usr/include/x86_64-linux-gnu/bits/stdio2.h:111:42: note: expanded from macro 'printf'
  __printf_chk (__USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
                                         ^~~~~~~~~~~
In file included from tools/parser/l2cap.c:24:
tools/parser/parser.h:132:18: warning: Access to field 'in' results in a dereference of a null pointer (loaded from variable 'f')
                printf("%c ", (f->in ? '>' : '<'));
                               ^~~~~
/usr/include/x86_64-linux-gnu/bits/stdio2.h:111:42: note: expanded from macro 'printf'
  __printf_chk (__USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
                                         ^~~~~~~~~~~
3 warnings generated.
In file included from tools/parser/sdp.c:24:
tools/parser/parser.h:121:16: warning: Dereference of null pointer
                                time_t t = f->ts.tv_sec;
                                           ^~~~~~~~~~~~
tools/parser/parser.h:129:18: warning: Dereference of null pointer
                                                (long long)f->ts.tv_sec,
                                                           ^~~~~~~~~~~~
/usr/include/x86_64-linux-gnu/bits/stdio2.h:111:42: note: expanded from macro 'printf'
  __printf_chk (__USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
                                         ^~~~~~~~~~~
In file included from tools/parser/sdp.c:24:
tools/parser/parser.h:132:18: warning: Access to field 'in' results in a dereference of a null pointer (loaded from variable 'f')
                printf("%c ", (f->in ? '>' : '<'));
                               ^~~~~
/usr/include/x86_64-linux-gnu/bits/stdio2.h:111:42: note: expanded from macro 'printf'
  __printf_chk (__USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
                                         ^~~~~~~~~~~
3 warnings generated.
In file included from tools/parser/ppp.c:22:
tools/parser/parser.h:159:2: warning: Undefined or garbage value returned to caller
        return *u8_ptr;
        ^~~~~~~~~~~~~~
tools/parser/ppp.c:108:30: warning: The left operand of '&' is a garbage value
        if (*((uint8_t *) frm->ptr) & 0x80)
            ~~~~~~~~~~~~~~~~~~~~~~~ ^
2 warnings generated.
emulator/serial.c:150:2: warning: Assigned value is garbage or undefined
        enum btdev_type uninitialized_var(type);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
emulator/serial.c:150:36: warning: Value stored to 'type' during its initialization is never read
        enum btdev_type uninitialized_var(type);
                                          ^~~~
emulator/serial.c:36:30: note: expanded from macro 'uninitialized_var'
#define uninitialized_var(x) x = x
                             ^   ~
emulator/serial.c:213:2: warning: Assigned value is garbage or undefined
        enum btdev_type uninitialized_var(dev_type);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
emulator/serial.c:213:36: warning: Value stored to 'dev_type' during its initialization is never read
        enum btdev_type uninitialized_var(dev_type);
                                          ^~~~~~~~
emulator/serial.c:36:30: note: expanded from macro 'uninitialized_var'
#define uninitialized_var(x) x = x
                             ^   ~
4 warnings generated.
emulator/server.c:218:2: warning: Assigned value is garbage or undefined
        enum btdev_type uninitialized_var(type);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
emulator/server.c:218:36: warning: Value stored to 'type' during its initialization is never read
        enum btdev_type uninitialized_var(type);
                                          ^~~~
emulator/server.c:36:30: note: expanded from macro 'uninitialized_var'
#define uninitialized_var(x) x = x
                             ^   ~
2 warnings generated.
emulator/b1ee.c:258:3: warning: Potential leak of memory pointed to by 'server_port'
                int opt;
                ^~~~~~~
emulator/b1ee.c:258:3: warning: Potential leak of memory pointed to by 'sniffer_port'
                int opt;
                ^~~~~~~
emulator/b1ee.c:289:2: warning: Value stored to 'argc' is never read
        argc = argc - optind;
        ^      ~~~~~~~~~~~~~
3 warnings generated.
/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:6194: tools/test-runner] Error 1
make[1]: *** Waiting for unfinished jobs....
emulator/btdev.c:6620:20: warning: Access to field 'link' results in a dereference of a null pointer (loaded from variable 'acl')
                le_past_received(acl->link, pa);
                                 ^~~~~~~~~
emulator/btdev.c:6720:25: warning: Access to field 'link' results in a dereference of a null pointer (loaded from variable 'acl')
                le_past_info_received(acl->link, ea);
                                      ^~~~~~~~~
2 warnings generated.
make: *** [Makefile:4224: all] Error 2


---
Regards,
Linux Bluetooth


^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding
  2026-02-28 12:51 ` [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding Pauli Virtanen
@ 2026-03-02 21:03   ` Luiz Augusto von Dentz
  2026-03-02 22:14     ` Pauli Virtanen
  0 siblings, 1 reply; 19+ messages in thread
From: Luiz Augusto von Dentz @ 2026-03-02 21:03 UTC (permalink / raw)
  To: Pauli Virtanen, Marcel Holtmann; +Cc: linux-bluetooth

Hi Pauli,

On Sat, Feb 28, 2026 at 7:52 AM Pauli Virtanen <pav@iki.fi> wrote:
>
> 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: 0

Interesting, so this comes down the pci-serial being unreliable? There
have been occurrences of this even without attaching any controller
with the VM, LL Privacy tests in mgmt-tester do occasionally timeout
like above, but perhaps that is a different issue.

> This 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.

I wonder if we could just use virtio_bt
(https://github.com/torvalds/linux/blob/master/drivers/bluetooth/virtio_bt.c)
here, instead of using a serial, so we don't have to copy the data
both ways? Or perhaps the problem is that this was never completed
upstream @Marcel Holtmann?

> ---
>  tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++-----------
>  1 file changed, 230 insertions(+), 70 deletions(-)
>
> diff --git a/tools/test-runner.c b/tools/test-runner.c
> index b3e0b0cfe..576313b79 100644
> --- a/tools/test-runner.c
> +++ b/tools/test-runner.c
> @@ -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;
> +                               } else if (errno == EINTR || errno == EAGAIN
> +                                               || errno == EWOULDBLOCK) {
> +                                       /* ok */
> +                               } else {
> +                                       perror("write");
> +                                       goto error;
> +                               }
> +                       }
> +               }
> +       }
> +       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
>
>


-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH BlueZ 00/11] Functional/integration testing
  2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
                   ` (10 preceding siblings ...)
  2026-02-28 12:51 ` [PATCH BlueZ 11/11] build: add functional testing target Pauli Virtanen
@ 2026-03-02 21:18 ` Luiz Augusto von Dentz
  2026-03-02 23:37   ` Pauli Virtanen
  11 siblings, 1 reply; 19+ messages in thread
From: Luiz Augusto von Dentz @ 2026-03-02 21:18 UTC (permalink / raw)
  To: Pauli Virtanen; +Cc: linux-bluetooth

Hi Pauli,

On Sat, Feb 28, 2026 at 7:52 AM Pauli Virtanen <pav@iki.fi> wrote:
>
> 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.
>
> 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 unit/func_test/test_bluetoothctl_vm.py has some simple cases, and
> unit/func_test/test_pipewire.py for a more complicated setup
>
>     host0(qemu): Pipewire <-> BlueZ <-> kernel
>     <-> btvirt
>     host1(qemu): kernel <-> BlueZ <-> Pipewire
>
> The framework allows easily passing any data and code between the parent
> and VM hosts, so writing tests is straightforward.
>
> ***
>
> Some examples:
>
> $ unit/test-functional --list -q
>
> unit/func_test/lib/tests/test_rpc.py::test_basic
> unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2]
> unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1]
> unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1]
> unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2]
>
> $ unit/test-functional -v --no-header
> ======================================= test session starts ========================================
> collected 5 items
>
> unit/func_test/lib/tests/test_rpc.py::test_basic PASSED                                      [ 20%]
> unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1] SKIPPED    [ 40%]
> unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image)     [ 60%]
> unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] SKIPPED (No k...) [ 80%]
> unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image)         [100%]
>
> =================================== 1 passed, 4 skipped in 0.19s ===================================
>
> $ unit/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
>
> unit/func_test/lib/tests/test_rpc.py .                                   [ 20%]
> unit/func_test/test_bluetoothctl_vm.py .                                 [ 40%]
> unit/func_test/test_btmgmt_vm.py .                                       [ 60%]
> unit/func_test/test_bluetoothctl_vm.py .                                 [ 80%]
> unit/func_test/test_pipewire.py .                                        [100%]
>
> ============================== 5 passed in 41.92s ==============================
>
> $ unit/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
>
> unit/func_test/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
>
> $ unit/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
>
> unit/func_test/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/unit/func_test/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-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 ========================
>
> $ unit/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
>
> unit/func_test/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-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 =======================
>
> $ COLUMNS=80 unit/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
>
> unit/func_test/test_btmgmt_vm.py
> >>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(19)test_btmgmt_info()
> -> (host,) = hosts
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
> -> result = host.call(
> (Pdb) p host.bdaddr
> '00:aa:01:00:00:42'
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(22)test_btmgmt_info()
> -> run,
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(23)test_btmgmt_info()
> -> [btmgmt, "--index", "0", "info"],
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(24)test_btmgmt_info()
> -> stdout=subprocess.PIPE,
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(25)test_btmgmt_info()
> -> stdin=subprocess.DEVNULL,
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(26)test_btmgmt_info()
> -> encoding="utf-8",
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
> -> result = host.call(
> (Pdb) n
> > /home/pauli/prj/external/bluez/unit/func_test/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 (11):
>   emulator: btvirt: check pkt lengths, don't get stuck on malformed
>   emulator: btvirt: allow specifying where server unix sockets are made
>   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: add functional/integration testing documentation
>   unit: add functional/integration testing framework
>   unit: func_test: add Pipewire-using smoke tests
>   build: add functional testing target
>
>  Makefile.am                            |   7 +
>  configure.ac                           |  17 +
>  doc/ci.config                          |   1 +
>  doc/test-functional.rst                | 374 +++++++++++++++++++
>  doc/test-runner.rst                    |   1 +
>  doc/tester.config                      |   1 +
>  emulator/main.c                        |  37 +-
>  emulator/server.c                      |   9 +
>  tools/test-runner.c                    | 376 ++++++++++++++-----
>  unit/func_test/__init__.py             |   0
>  unit/func_test/conftest.py             | 277 ++++++++++++++
>  unit/func_test/lib/__init__.py         |   5 +
>  unit/func_test/lib/env.py              | 484 +++++++++++++++++++++++++
>  unit/func_test/lib/host_plugins.py     | 269 ++++++++++++++
>  unit/func_test/lib/rpc.py              | 293 +++++++++++++++
>  unit/func_test/lib/runner.py           |  10 +
>  unit/func_test/lib/tests/__init__.py   |   0
>  unit/func_test/lib/tests/test_rpc.py   |  49 +++
>  unit/func_test/lib/utils.py            | 266 ++++++++++++++
>  unit/func_test/requirements.txt        |   3 +
>  unit/func_test/test_bluetoothctl_vm.py |  76 ++++
>  unit/func_test/test_btmgmt_vm.py       |  29 ++
>  unit/func_test/test_pipewire.py        | 137 +++++++
>  unit/pytest.ini                        |   7 +
>  unit/test-functional                   |   8 +
>  25 files changed, 2631 insertions(+), 105 deletions(-)
>  create mode 100644 doc/test-functional.rst
>  create mode 100644 unit/func_test/__init__.py
>  create mode 100644 unit/func_test/conftest.py
>  create mode 100644 unit/func_test/lib/__init__.py
>  create mode 100644 unit/func_test/lib/env.py
>  create mode 100644 unit/func_test/lib/host_plugins.py
>  create mode 100644 unit/func_test/lib/rpc.py
>  create mode 100644 unit/func_test/lib/runner.py
>  create mode 100644 unit/func_test/lib/tests/__init__.py
>  create mode 100644 unit/func_test/lib/tests/test_rpc.py
>  create mode 100644 unit/func_test/lib/utils.py
>  create mode 100644 unit/func_test/requirements.txt
>  create mode 100644 unit/func_test/test_bluetoothctl_vm.py
>  create mode 100644 unit/func_test/test_btmgmt_vm.py
>  create mode 100644 unit/func_test/test_pipewire.py
>  create mode 100644 unit/pytest.ini
>  create mode 100755 unit/test-functional
>
> --
> 2.53.0

Looks like a fairly solid start, that said I wouldn't mix these with
unit test, that is more of a unit/whitebox style testing not really
meant for end-to-end testing, Id probably have this under
test/functional or test/pytest, anyway we could possibly remove the
existing python tests and just convert them, or perhaps move them to
examples since they are just demostrating how to use our D-Bus APIs
using python rather than testing a specific use-case, etc.

Regarding pytest, Im not really an expert in the python testing
frameworks so I will probably need to do some digging to see what are
the options and there might be companies that already emply similar
testing environment so I guess it is a good chance to make an attempt
to convince more people to contribute upstream so we can consolidate
in a single framework for end-to-end testing.

-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding
  2026-03-02 21:03   ` Luiz Augusto von Dentz
@ 2026-03-02 22:14     ` Pauli Virtanen
  0 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-03-02 22:14 UTC (permalink / raw)
  To: Luiz Augusto von Dentz, Marcel Holtmann; +Cc: linux-bluetooth

Hi,

ma, 2026-03-02 kello 16:03 -0500, Luiz Augusto von Dentz kirjoitti:
> On Sat, Feb 28, 2026 at 7:52 AM Pauli Virtanen <pav@iki.fi> wrote:
> > 
> > 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: 0
> 
> Interesting, so this comes down the pci-serial being unreliable? 

The sporadic corruptions are not specific to btvirt (occur also with
the test rpc channel here if setup via serial chardev), are not present
with virtio-serial, and appear only when the parent machine is under
load. I did not find the root cause for this.

It sounds strange to me, I'd expect qemu serial be heavily used.

It could be test-runner.c is setting up the serial channel wrong for
the -u option, but I did not find if it could be fixed.

> There
> have been occurrences of this even without attaching any controller
> with the VM, LL Privacy tests in mgmt-tester do occasionally timeout
> like above, but perhaps that is a different issue.

mgmt-tester btdev connects via vhci and runs inside the vm, so this
sounds like different issue.

> > This 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.
> 
> I wonder if we could just use virtio_bt
> (https://github.com/torvalds/linux/blob/master/drivers/bluetooth/virtio_bt.c)
> here, instead of using a serial, so we don't have to copy the data
> both ways? Or perhaps the problem is that this was never completed
> upstream @Marcel Holtmann?

At first sight it looks like Qemu does not support virtio-bluetooth.

> > ---
> >  tools/test-runner.c | 300 +++++++++++++++++++++++++++++++++-----------
> >  1 file changed, 230 insertions(+), 70 deletions(-)
> > 
> > diff --git a/tools/test-runner.c b/tools/test-runner.c
> > index b3e0b0cfe..576313b79 100644
> > --- a/tools/test-runner.c
> > +++ b/tools/test-runner.c
> > @@ -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;
> > +                               } else if (errno == EINTR || errno == EAGAIN
> > +                                               || errno == EWOULDBLOCK) {
> > +                                       /* ok */
> > +                               } else {
> > +                                       perror("write");
> > +                                       goto error;
> > +                               }
> > +                       }
> > +               }
> > +       }
> > +       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
> > 
> > 
> 

-- 
Pauli Virtanen

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH BlueZ 00/11] Functional/integration testing
  2026-03-02 21:18 ` [PATCH BlueZ 00/11] Functional/integration testing Luiz Augusto von Dentz
@ 2026-03-02 23:37   ` Pauli Virtanen
  0 siblings, 0 replies; 19+ messages in thread
From: Pauli Virtanen @ 2026-03-02 23:37 UTC (permalink / raw)
  To: Luiz Augusto von Dentz; +Cc: linux-bluetooth

Hi,

ma, 2026-03-02 kello 16:18 -0500, Luiz Augusto von Dentz kirjoitti:
> Hi Pauli,
> 
> On Sat, Feb 28, 2026 at 7:52 AM Pauli Virtanen <pav@iki.fi> wrote:
> > 
> > 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.
> > 
> > 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 unit/func_test/test_bluetoothctl_vm.py has some simple cases, and
> > unit/func_test/test_pipewire.py for a more complicated setup
> > 
> >     host0(qemu): Pipewire <-> BlueZ <-> kernel
> >     <-> btvirt
> >     host1(qemu): kernel <-> BlueZ <-> Pipewire
> > 
> > The framework allows easily passing any data and code between the parent
> > and VM hosts, so writing tests is straightforward.
> > 
> > ***
> > 
> > Some examples:
> > 
> > $ unit/test-functional --list -q
> > 
> > unit/func_test/lib/tests/test_rpc.py::test_basic
> > unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2]
> > unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1]
> > unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1]
> > unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2]
> > 
> > $ unit/test-functional -v --no-header
> > ======================================= test session starts ========================================
> > collected 5 items
> > 
> > unit/func_test/lib/tests/test_rpc.py::test_basic PASSED                                      [ 20%]
> > unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_script_show[hosts1-vm1] SKIPPED    [ 40%]
> > unit/func_test/test_btmgmt_vm.py::test_btmgmt_info[hosts2-vm1] SKIPPED (No kernel image)     [ 60%]
> > unit/func_test/test_bluetoothctl_vm.py::test_bluetoothctl_pair[hosts0-vm2] SKIPPED (No k...) [ 80%]
> > unit/func_test/test_pipewire.py::test_pipewire[hosts3-vm2] SKIPPED (No kernel image)         [100%]
> > 
> > =================================== 1 passed, 4 skipped in 0.19s ===================================
> > 
> > $ unit/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
> > 
> > unit/func_test/lib/tests/test_rpc.py .                                   [ 20%]
> > unit/func_test/test_bluetoothctl_vm.py .                                 [ 40%]
> > unit/func_test/test_btmgmt_vm.py .                                       [ 60%]
> > unit/func_test/test_bluetoothctl_vm.py .                                 [ 80%]
> > unit/func_test/test_pipewire.py .                                        [100%]
> > 
> > ============================== 5 passed in 41.92s ==============================
> > 
> > $ unit/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
> > 
> > unit/func_test/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
> > 
> > $ unit/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
> > 
> > unit/func_test/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/unit/func_test/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-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 ========================
> > 
> > $ unit/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
> > 
> > unit/func_test/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-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 =======================
> > 
> > $ COLUMNS=80 unit/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
> > 
> > unit/func_test/test_btmgmt_vm.py
> > > > > > > > > > > > > > > > > > > > > > PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(19)test_btmgmt_info()
> > -> (host,) = hosts
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
> > -> result = host.call(
> > (Pdb) p host.bdaddr
> > '00:aa:01:00:00:42'
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(22)test_btmgmt_info()
> > -> run,
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(23)test_btmgmt_info()
> > -> [btmgmt, "--index", "0", "info"],
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(24)test_btmgmt_info()
> > -> stdout=subprocess.PIPE,
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(25)test_btmgmt_info()
> > -> stdin=subprocess.DEVNULL,
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(26)test_btmgmt_info()
> > -> encoding="utf-8",
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/test_btmgmt_vm.py(21)test_btmgmt_info()
> > -> result = host.call(
> > (Pdb) n
> > > /home/pauli/prj/external/bluez/unit/func_test/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 (11):
> >   emulator: btvirt: check pkt lengths, don't get stuck on malformed
> >   emulator: btvirt: allow specifying where server unix sockets are made
> >   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: add functional/integration testing documentation
> >   unit: add functional/integration testing framework
> >   unit: func_test: add Pipewire-using smoke tests
> >   build: add functional testing target
> > 
> >  Makefile.am                            |   7 +
> >  configure.ac                           |  17 +
> >  doc/ci.config                          |   1 +
> >  doc/test-functional.rst                | 374 +++++++++++++++++++
> >  doc/test-runner.rst                    |   1 +
> >  doc/tester.config                      |   1 +
> >  emulator/main.c                        |  37 +-
> >  emulator/server.c                      |   9 +
> >  tools/test-runner.c                    | 376 ++++++++++++++-----
> >  unit/func_test/__init__.py             |   0
> >  unit/func_test/conftest.py             | 277 ++++++++++++++
> >  unit/func_test/lib/__init__.py         |   5 +
> >  unit/func_test/lib/env.py              | 484 +++++++++++++++++++++++++
> >  unit/func_test/lib/host_plugins.py     | 269 ++++++++++++++
> >  unit/func_test/lib/rpc.py              | 293 +++++++++++++++
> >  unit/func_test/lib/runner.py           |  10 +
> >  unit/func_test/lib/tests/__init__.py   |   0
> >  unit/func_test/lib/tests/test_rpc.py   |  49 +++
> >  unit/func_test/lib/utils.py            | 266 ++++++++++++++
> >  unit/func_test/requirements.txt        |   3 +
> >  unit/func_test/test_bluetoothctl_vm.py |  76 ++++
> >  unit/func_test/test_btmgmt_vm.py       |  29 ++
> >  unit/func_test/test_pipewire.py        | 137 +++++++
> >  unit/pytest.ini                        |   7 +
> >  unit/test-functional                   |   8 +
> >  25 files changed, 2631 insertions(+), 105 deletions(-)
> >  create mode 100644 doc/test-functional.rst
> >  create mode 100644 unit/func_test/__init__.py
> >  create mode 100644 unit/func_test/conftest.py
> >  create mode 100644 unit/func_test/lib/__init__.py
> >  create mode 100644 unit/func_test/lib/env.py
> >  create mode 100644 unit/func_test/lib/host_plugins.py
> >  create mode 100644 unit/func_test/lib/rpc.py
> >  create mode 100644 unit/func_test/lib/runner.py
> >  create mode 100644 unit/func_test/lib/tests/__init__.py
> >  create mode 100644 unit/func_test/lib/tests/test_rpc.py
> >  create mode 100644 unit/func_test/lib/utils.py
> >  create mode 100644 unit/func_test/requirements.txt
> >  create mode 100644 unit/func_test/test_bluetoothctl_vm.py
> >  create mode 100644 unit/func_test/test_btmgmt_vm.py
> >  create mode 100644 unit/func_test/test_pipewire.py
> >  create mode 100644 unit/pytest.ini
> >  create mode 100755 unit/test-functional
> > 
> > --
> > 2.53.0
> 
> Looks like a fairly solid start, that said I wouldn't mix these with
> unit test, that is more of a unit/whitebox style testing not really
> meant for end-to-end testing, Id probably have this under
> test/functional or test/pytest, 

Ok.

> anyway we could possibly remove the
> existing python tests and just convert them, or perhaps move them to
> examples since they are just demostrating how to use our D-Bus APIs
> using python rather than testing a specific use-case, etc.

Most of these appear example test clients, and don't have the server
side. I guess there's some value in keeping them, and just put the
automated test suite to subfolder there.

> Regarding pytest, Im not really an expert in the python testing
> frameworks so I will probably need to do some digging to see what are
> the options and there might be companies that already emply similar
> testing environment so I guess it is a good chance to make an attempt
> to convince more people to contribute upstream so we can consolidate
> in a single framework for end-to-end testing.

Pytest is a generic test framework; I'd say there are only two relevant
choices for these in Python. It is widely used eg. by big parts of the
Python machine learning / scientific stack (Pytorch, numpy, etc) and
other projects, should be safe choice.

For specifically this kind of end-to-end or integration testing with
multiple VM images + controller redirection, which are the custom parts
here, it seemed not so easy to find something open source that's
immediately relevant. 

There are things for embedded / hardware testing (openhtf, pytest-
embedded) but not clear those are immediately helpful here. There are
of course applications for controlling fleets of VM/Docker images (eg.
Ansible), but these are typically via network and probably need less
spartan VM environment.

-- 
Pauli Virtanen

^ permalink raw reply	[flat|nested] 19+ messages in thread

* RE: Functional/integration testing
  2026-03-20 21:10 [PATCH BlueZ v2 01/20] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
@ 2026-03-20 22:14 ` bluez.test.bot
  0 siblings, 0 replies; 19+ messages in thread
From: bluez.test.bot @ 2026-03-20 22:14 UTC (permalink / raw)
  To: linux-bluetooth, pav

[-- Attachment #1: Type: text/plain, Size: 1991 bytes --]

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=1070106

---Test result---

Test Summary:
CheckPatch                    PENDING   0.50 seconds
GitLint                       PENDING   0.40 seconds
BuildEll                      PASS      20.67 seconds
BluezMake                     PASS      637.64 seconds
MakeCheck                     PASS      18.96 seconds
MakeDistcheck                 FAIL      79.18 seconds
CheckValgrind                 PASS      291.31 seconds
CheckSmatch                   WARNING   351.20 seconds
bluezmakeextell               PASS      180.73 seconds
IncrementalBuild              PENDING   0.44 seconds
ScanBuild                     PASS      988.84 seconds

Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:

##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:

##############################
Test: MakeDistcheck - FAIL
Desc: Run Bluez Make Distcheck
Output:

/usr/bin/ld: tools/test-runner.o: in function `start_controller_forward':
/github/workspace/src/src/bluez-5.86/_build/sub/../../tools/test-runner.c:989: undefined reference to `openpty'
collect2: error: ld returned 1 exit status
make[2]: *** [Makefile:6062: tools/test-runner] Error 1
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [Makefile:4148: all] Error 2
make: *** [Makefile:10668: distcheck] Error 1
##############################
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/1979/checks

---
Regards,
Linux Bluetooth


^ permalink raw reply	[flat|nested] 19+ messages in thread

* RE: Functional/integration testing
  2026-03-22 21:29 [PATCH BlueZ v3 01/20] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
@ 2026-03-22 22:38 ` bluez.test.bot
  0 siblings, 0 replies; 19+ messages in thread
From: bluez.test.bot @ 2026-03-22 22:38 UTC (permalink / raw)
  To: linux-bluetooth, pav

[-- Attachment #1: Type: text/plain, Size: 1477 bytes --]

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


^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2026-03-22 22:38 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-28 12:51 [PATCH BlueZ 00/11] Functional/integration testing Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 01/11] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
2026-02-28 13:45   ` Functional/integration testing bluez.test.bot
2026-02-28 12:51 ` [PATCH BlueZ 02/11] emulator: btvirt: allow specifying where server unix sockets are made Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 03/11] test-runner: enable path argument for --unix Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 04/11] test-runner: Add -o/--option option Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 05/11] test-runner: allow source tree root for -k Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 06/11] doc: enable CONFIG_VIRTIO_CONSOLE in tester config Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 07/11] test-runner: use virtio-serial for implementing -u device forwarding Pauli Virtanen
2026-03-02 21:03   ` Luiz Augusto von Dentz
2026-03-02 22:14     ` Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 08/11] doc: add functional/integration testing documentation Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 09/11] unit: add functional/integration testing framework Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 10/11] unit: func_test: add Pipewire-using smoke tests Pauli Virtanen
2026-02-28 12:51 ` [PATCH BlueZ 11/11] build: add functional testing target Pauli Virtanen
2026-03-02 21:18 ` [PATCH BlueZ 00/11] Functional/integration testing Luiz Augusto von Dentz
2026-03-02 23:37   ` Pauli Virtanen
  -- strict thread matches above, loose matches on Subject: below --
2026-03-20 21:10 [PATCH BlueZ v2 01/20] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
2026-03-20 22:14 ` Functional/integration testing bluez.test.bot
2026-03-22 21:29 [PATCH BlueZ v3 01/20] emulator: btvirt: check pkt lengths, don't get stuck on malformed Pauli Virtanen
2026-03-22 22:38 ` Functional/integration testing bluez.test.bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox