* [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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ messages in thread