From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 09/12] test: functional: add BAP unicast tests
Date: Wed, 9 Sep 2026 15:23:05 -0400 [thread overview]
Message-ID: <20260909192308.1306567-10-luiz.dentz@gmail.com> (raw)
In-Reply-To: <20260909192308.1306567-1-luiz.dentz@gmail.com>
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add tests covering BAP unicast via bluetoothctl: the initiator
registers a PAC Source endpoint, the acceptor a PAC Sink endpoint, and
after pairing the initiator configures the remote endpoint with a
preset.
test_bap_unicast_transport_created checks a MediaTransport1 object is
created on both sides with the expected endpoint, codec and state, and
test_bap_unicast_transport_acquire checks the initiator can acquire
them.
The endpoints are configured for stereo, so a stream is created per
location, each with its own CIS in the same CIG. All the transports
have to be acquired, as the CIS are only created once every CIS of the
CIG is ready.
Assisted-by: opencode:claude-opus-5
---
Makefile.am | 1 +
doc/functional-bap.rst | 109 +++++++++++++++++++++++
doc/functional-testing.rst | 1 +
test/functional/test_bap.py | 169 ++++++++++++++++++++++++++++++++++++
4 files changed, 280 insertions(+)
create mode 100644 doc/functional-bap.rst
create mode 100644 test/functional/test_bap.py
diff --git a/Makefile.am b/Makefile.am
index 2eef04edd760..efab756242e9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -500,6 +500,7 @@ EXTRA_DIST += doc/assigned-numbers.rst doc/supported-features.txt \
doc/functional-testing.rst \
doc/functional-a2dp.rst \
doc/functional-avrcp.rst \
+ doc/functional-bap.rst \
doc/functional-obex.rst \
doc/settings-storage.txt
diff --git a/doc/functional-bap.rst b/doc/functional-bap.rst
new file mode 100644
index 000000000000..6b67df541e97
--- /dev/null
+++ b/doc/functional-bap.rst
@@ -0,0 +1,109 @@
+==============
+functional-bap
+==============
+
+DESCRIPTION
+===========
+
+BAP (LE Audio) functional tests, `test/functional/test_bap.py`, driven
+through **bluetoothctl(1)**. See **functional-testing(7)** for the
+conventions used here, and **test-functional(1)** for how to run the
+suite.
+
+SETUP
+=====
+
+Two hosts, connected over LE:
+
+.. code-block::
+
+ +------------------------+ +------------------------+
+ | host0 | LE ACL | host1 |
+ | initiator (central) | --------------> | acceptor (peripheral) |
+ | bluetoothctl -a auto | | bluetoothctl -a auto |
+ | bap-source-lc3.bt | CIS 0x00 (LC3) | bap-sink-lc3.bt |
+ | PAC Source endpoint | ==============> | PAC Sink endpoint |
+ | | CIS 0x01 (LC3) | |
+ | | ==============> | |
+ +------------------------+ +------------------------+
+
+ one CIG holding one CIS per audio location
+
+ --> connection is initiated by ==> audio flows towards
+
+BAP requires the ISO socket support, so all hosts run `bluetoothd`
+with:
+
+.. code-block::
+
+ [General]
+ Experimental = true
+ KernelExperimental = true
+ ControllerMode = le
+
+`bluetoothctl` is started with ``-a auto``, so pairing and service
+authorization are accepted without prompting, and with an endpoint
+registration script:
+
+``client/scripts/bap-source-lc3.bt`` on host0
+ Registers a local PAC Source endpoint
+ (``00002bcb-0000-1000-8000-00805f9b34fb``) with LC3. host0 is the
+ initiator, i.e. the device sending audio.
+
+``client/scripts/bap-sink-lc3.bt`` on host1
+ Registers a local PAC Sink endpoint
+ (``00002bc9-0000-1000-8000-00805f9b34fb``) with LC3. host1 is the
+ acceptor, i.e. the device receiving audio.
+
+TEST CASES
+==========
+
+test_bap_unicast_transport_created
+----------------------------------
+
+:Setup: As above.
+
+:Steps:
+ 1. Start `bluetoothctl` with the scripts on both hosts.
+ 2. Pair over LE: ``scan on`` on the initiator, ``advertise on`` on
+ the acceptor, then ``pair``.
+ 3. Initiator: ``endpoint.config <remote endpoint>
+ /local/endpoint/ep0 16_2_1``, using the remote PAC Sink endpoint
+ exposed once the services are resolved.
+ 4. Initiator: ``transport.show <transport>`` for each transport.
+
+:Expected:
+ 1. ``Endpoint /local/endpoint/ep0 registered`` on both hosts.
+ 2. ``Pairing successful``.
+ 3. The remote endpoint appears as
+ ``Endpoint /org/bluez/hci0/dev_XX/pac_sinkN``, and configuring it
+ creates one transport per location on *both* hosts.
+ 4. Each transport of the initiator reports the local PAC Source
+ UUID (``00002bcb-...``), ``Codec: 0x06`` for LC3, ``Device:``
+ pointing at the acceptor device object, and ``State: idle``.
+
+:Notes: The endpoints are registered with the locations and contexts
+ `bluetoothctl` uses itself, ``0x0fff`` for the sink and ``0x000f``
+ for the source. With narrower Supported Contexts the acceptor
+ rejects the ``Enable`` with ``Invalid Metadata``, because the
+ metadata of the stream carries the ``Unspecified`` context.
+
+test_bap_unicast_transport_acquire
+----------------------------------
+
+:Setup: As above, with the transports already created.
+
+:Steps: Initiator: ``transport.acquire <transport> <transport>``, for
+ all the transports that were created.
+
+:Expected: ``Acquire successful: fd <fd> MTU <read>:<write>`` for each
+ transport, and each of them moves to ``State: active``.
+
+:Notes: *All* the transports have to be acquired: the controller only
+ creates the CIS once every CIS of the CIG is ready, so acquiring a
+ single transport leaves the stream waiting until it times out,
+ without ``LE Create CIS`` ever being sent.
+
+ The acceptor does not have to acquire its transports for the CIS to
+ be established, as `bluetoothd` sets up the ISO listener on its own
+ when the stream is enabled.
diff --git a/doc/functional-testing.rst b/doc/functional-testing.rst
index 03539dfea0a5..60c6762dee73 100644
--- a/doc/functional-testing.rst
+++ b/doc/functional-testing.rst
@@ -14,6 +14,7 @@ are documented separately:
- **functional-a2dp(7)**: `test/functional/test_a2dp.py`
- **functional-avrcp(7)**: `test/functional/test_avrcp.py`
+- **functional-bap(7)**: `test/functional/test_bap.py`
- **functional-obex(7)**: `test/functional/test_obex.py`
Each test case is described as:
diff --git a/test/functional/test_bap.py b/test/functional/test_bap.py
new file mode 100644
index 000000000000..69430d7a6699
--- /dev/null
+++ b/test/functional/test_bap.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+# SPDX-License-Identifier: GPL-2.0-or-later
+"""
+Tests for BAP (LE Audio) using bluetoothctl in VM instances
+"""
+
+import warnings
+
+import pytest
+
+from pytest_bluezenv import Bluetoothd, Pexpect, find_exe, host_config
+from pytest_bluezenv.utils import bluez_src_dir
+
+pytestmark = [pytest.mark.vm]
+
+PAC_SINK_UUID = "00002bc9-0000-1000-8000-00805f9b34fb"
+PAC_SOURCE_UUID = "00002bcb-0000-1000-8000-00805f9b34fb"
+
+# BAP requires the ISO socket support, which is kernel experimental
+BAP_CONF = """[General]
+Experimental = true
+KernelExperimental = true
+ControllerMode = le
+"""
+
+PRESET = "16_2_1"
+
+TRANSPORT_RE = r"Transport (/org/bluez/\S+/fd\d+)"
+
+
+def dev_addr(host):
+ return host.bdaddr.upper().replace(":", "_")
+
+
+def expect_all(ctl, patterns):
+ """
+ Expect all the given patterns, in any order.
+ """
+ patterns = list(patterns)
+
+ while patterns:
+ idx, _ = ctl.expect(patterns)
+ patterns.pop(idx)
+
+
+def script(name):
+ src = bluez_src_dir()
+ if src is None:
+ pytest.skip("BlueZ source directory not known")
+
+ path = src / "client" / "scripts" / name
+ if not path.exists():
+ pytest.skip(f"{path} not found")
+
+ return str(path)
+
+
+def start_bluetoothctl(host, init_script):
+ exe = find_exe("client", "bluetoothctl")
+ # -a auto: accept pairing and authorize services without prompting
+ ctl = host.pexpect.spawn([exe, "-a", "auto", "--init-script", script(init_script)])
+ ctl.expect("Endpoint /local/endpoint/ep0 registered")
+ return ctl
+
+
+def pair_le(host0, ctl0, host1, ctl1):
+ ctl0.send("scan on\n")
+ ctl0.expect(f"Controller {host0.bdaddr.upper()} Discovering: yes")
+
+ ctl1.send("advertise on\n")
+ ctl1.expect("Advertising object registered")
+
+ ctl0.expect(f"Device {host1.bdaddr.upper()}")
+ ctl0.send(f"pair {host1.bdaddr.upper()}\n")
+
+ # See test_bluetoothctl_pair_le: passkey confirmation is handled by
+ # the auto agent, but legacy passkey entry still needs an answer
+ idx, m = ctl0.expect([r"\[agent\].*Passkey:.*m(\d+)", "Pairing successful"])
+
+ if idx == 0:
+ warnings.warn(
+ "BUG: we got passkey authentication, bluetoothd/kernel should be fixed"
+ )
+ key = m[0].decode("utf-8")
+ ctl1.expect(r"\[agent\] Enter passkey \(number in 0-999999\):")
+ ctl1.send(f"{key}\n")
+ ctl0.expect("Pairing successful")
+
+ ctl0.send("scan off\n")
+
+
+unicast_host_config = host_config(
+ [Bluetoothd(conf=BAP_CONF), Pexpect()],
+ [Bluetoothd(conf=BAP_CONF), Pexpect()],
+)
+
+
+@pytest.fixture
+def unicast_hosts(hosts):
+ """
+ Initiator (host0) with a local PAC Source endpoint and acceptor
+ (host1) with a local PAC Sink endpoint, paired over LE, with the
+ remote PAC Sink endpoint configured.
+ """
+ host0, host1 = hosts
+
+ initiator = start_bluetoothctl(host0, "bap-source-lc3.bt")
+ acceptor = start_bluetoothctl(host1, "bap-sink-lc3.bt")
+
+ pair_le(host0, initiator, host1, acceptor)
+
+ # Remote PAC Sink endpoint is exposed once services are resolved
+ _, m = initiator.expect(r"Endpoint (/org/bluez/\S+/pac_sink\d+)")
+ remote = m[0].decode("utf-8")
+
+ initiator.send(f"endpoint.config {remote} /local/endpoint/ep0 {PRESET}\n")
+
+ yield host0, host1, initiator, acceptor
+
+
+def expect_transports(ctl):
+ """
+ The endpoint is configured for stereo, so one stream per location is
+ created, each with its own CIS in the same CIG.
+ """
+ _, m = ctl.expect(r"Transport (/org/bluez/\S+/fd0)")
+ left = m[0].decode("utf-8")
+ _, m = ctl.expect(r"Transport (/org/bluez/\S+/fd1)")
+ right = m[0].decode("utf-8")
+
+ return left, right
+
+
+@unicast_host_config
+def test_bap_unicast_transport_created(unicast_hosts):
+ host0, host1, initiator, acceptor = unicast_hosts
+
+ left, right = expect_transports(initiator)
+ expect_transports(acceptor)
+
+ for transport in (left, right):
+ initiator.send(f"transport.show {transport}\n")
+ initiator.expect(f"Transport {transport}")
+ initiator.expect(rf"UUID: .*\({PAC_SOURCE_UUID}\)")
+ initiator.expect(r"Codec: 0x06")
+ initiator.expect(f"Device: /org/bluez/hci0/dev_{dev_addr(host1)}")
+ initiator.expect("State: idle")
+
+
+@unicast_host_config
+def test_bap_unicast_transport_acquire(unicast_hosts):
+ host0, host1, initiator, acceptor = unicast_hosts
+
+ left, right = expect_transports(initiator)
+
+ # The CIS are only created once every CIS of the CIG is ready, so
+ # all the transports need to be acquired
+ initiator.send(f"transport.acquire {left} {right}\n")
+
+ acquired = r"Acquire successful: fd \d+ MTU \d+:\d+"
+ expect_all(
+ initiator,
+ [
+ acquired,
+ acquired,
+ f"Transport {left} State: active",
+ f"Transport {right} State: active",
+ ],
+ )
--
2.55.0
next prev parent reply other threads:[~2026-09-09 19:23 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-09 19:22 [PATCH BlueZ v1 00/12] Add functional tests for A2DP and BAP Luiz Augusto von Dentz
2026-09-09 19:22 ` [PATCH BlueZ v1 01/12] build: add doc/test-functional.rst to EXTRA_DIST Luiz Augusto von Dentz
2026-09-10 18:28 ` Add functional tests for A2DP and BAP bluez.test.bot
2026-09-09 19:22 ` [PATCH BlueZ v1 02/12] doc: describe the functional test cases Luiz Augusto von Dentz
2026-09-09 19:22 ` [PATCH BlueZ v1 03/12] client: do not prompt for LE Audio settings on A2DP endpoints Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 04/12] client: add A2DP endpoint registration scripts Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 05/12] test: functional: add A2DP tests Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 06/12] client: rename media endpoint scripts to include the codec Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 07/12] client: add BAP endpoint registration scripts Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 08/12] doc: bluetoothctl: document init script option and scripts Luiz Augusto von Dentz
2026-09-09 19:23 ` Luiz Augusto von Dentz [this message]
2026-09-09 19:23 ` [PATCH BlueZ v1 10/12] test: functional: add BAP broadcast tests Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 11/12] test: functional: add BAP broadcast assistant test Luiz Augusto von Dentz
2026-09-09 19:23 ` [PATCH BlueZ v1 12/12] bap: reuse the PA sync established to discover a Broadcast Source Luiz Augusto von Dentz
2026-09-10 20:50 ` [PATCH BlueZ v1 00/12] Add functional tests for A2DP and BAP patchwork-bot+bluetooth
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260909192308.1306567-10-luiz.dentz@gmail.com \
--to=luiz.dentz@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.