Thread (22 messages) flat view 22 messages, 4 authors, 8d ago
COOLING8d

[PATCH BlueZ v5 3/3] test-functional: Cover BAP metadata reconfiguration

From: dt <hidden>
Date: 2026-09-17 19:19:16
Subsystem: the rest · Maintainer: Linus Torvalds

From: Derek Tern <redacted>

Reconfigure both unicast streams using custom presets with empty or
Media metadata at ATT MTU 64. The second Configured notification arrives
separately, exposing premature QoS setup from a local metadata callback.

Check that old transports disappear, both replacements retain their
codec configuration and metadata, and both can be acquired.

Remove the fixture's redundant configuration command: SelectProperties
already configures both streams after pairing.
---
 doc/functional-bap.rst      |  38 ++++++++--
 test/functional/test_bap.py | 139 +++++++++++++++++++++++++++++++++---
 2 files changed, 160 insertions(+), 17 deletions(-)
diff --git a/doc/functional-bap.rst b/doc/functional-bap.rst
index 3fc3439c1..60c38c5ed 100644
--- a/doc/functional-bap.rst
+++ b/doc/functional-bap.rst
@@ -67,17 +67,14 @@ test_bap_unicast_transport_created
 	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.
+	3. Wait for automatic stream configuration through the registered
+	   endpoint's ``SelectProperties`` method.
 	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.
+	3. One transport per location is created 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``.
@@ -108,6 +105,35 @@ test_bap_unicast_transport_acquire
 	be established, as `bluetoothd` sets up the ISO listener on its own
 	when the stream is enabled.
 
+test_bap_unicast_reconfigure_metadata[empty|media]
+------------------------------------------------
+
+:Setup: As above, with both transports already created and ATT MTU 64.
+
+:Steps:
+	1. Create two custom presets from the existing codec configurations
+	   and QoS, with empty or Media streaming-context metadata. Call
+	   ``MediaEndpoint1.ClearConfiguration`` on the remote endpoint.
+	2. Wait for completion and check that both old transports are gone.
+	3. Issue ``endpoint.config`` for both presets without waiting
+	   between requests.
+	4. Acquire both replacement transports.
+
+:Expected: Both configuration requests succeed. Two replacement transports
+	retain their codec configurations and the requested metadata. Both
+	acquisitions succeed and the transports become active.
+
+:Notes: MTU 64 meets BAP's minimum. It fits the two-ASE Codec Configuration
+	response and one Codec Configured notification together, but not the
+	second ASE notification. This exercises the interval in which the
+	initiator has received a successful response but is still waiting
+	for a fresh Configured state before starting QoS.
+
+	Release uses D-Bus directly: bluetoothctl has no command for
+	``ClearConfiguration``. This test covers the initiator's playback
+	streams; it does not exercise microphone streams or simultaneous
+	playback and capture.
+
 BROADCAST
 =========
 
diff --git a/test/functional/test_bap.py b/test/functional/test_bap.py
index ce11dc38d..06dd2720b 100644
--- a/test/functional/test_bap.py
+++ b/test/functional/test_bap.py
@@ -4,13 +4,21 @@
 Tests for BAP (LE Audio) using bluetoothctl in VM instances
 """
 
-import re
+import threading
 import warnings
 
+import dbus
 import pytest
 
-from pytest_bluezenv import Bluetoothd, Pexpect, find_exe, host_config
-from pytest_bluezenv.utils import bluez_src_dir
+from pytest_bluezenv import (
+    Bluetoothd,
+    Pexpect,
+    find_exe,
+    get_dbus,
+    host_config,
+    mainloop_wrap,
+)
+from pytest_bluezenv.utils import DEFAULT_TIMEOUT, bluez_src_dir
 
 pytestmark = [pytest.mark.vm]
 
@@ -24,8 +32,6 @@ KernelExperimental = true
 ControllerMode = le
 """
 
-PRESET = "16_2_1"
-
 TRANSPORT_RE = r"Transport (/org/bluez/\S+/fd\d+)"
 
 
@@ -214,12 +220,7 @@ def unicast_hosts(hosts):
 
     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")
-
+    # SelectProperties configures the streams automatically after pairing.
     yield host0, host1, initiator, acceptor
 
 
@@ -274,6 +275,122 @@ def test_bap_unicast_transport_acquire(unicast_hosts):
     )
 
 
+def clear_remote_transports(remote):
+    """Release the peer ASEs while retaining the connected GATT session."""
+    done = threading.Event()
+    errors = []
+
+    def failed(error):
+        errors.append(error)
+        done.set()
+
+    @mainloop_wrap
+    def clear():
+        dbus.Interface(
+            get_dbus().get_object("org.bluez", remote), "org.bluez.MediaEndpoint1"
+        ).ClearConfiguration(
+            dbus.ObjectPath(remote), reply_handler=done.set, error_handler=failed
+        )
+
+    clear()
+    assert done.wait(DEFAULT_TIMEOUT), "release did not complete"
+    if errors:
+        raise errors[0]
+
+
+@mainloop_wrap
+def remote_transport_properties(remote):
+    objects = dbus.Interface(
+        get_dbus().get_object("org.bluez", "/"),
+        "org.freedesktop.DBus.ObjectManager",
+    ).GetManagedObjects()
+    return {
+        str(path): props["org.bluez.MediaTransport1"]
+        for path, props in objects.items()
+        if path.startswith(remote + "/") and "org.bluez.MediaTransport1" in props
+    }
+
+
+def add_preset(ctl, name, props, metadata):
+    """Make a custom preset from a transport's codec configuration and QoS."""
+
+    def expect_reply(pattern):
+        failures = FAILURES + [r"(Invalid [^\r\n]*)", r"(No preset found)"]
+        idx, matches = ctl.expect(failures + [pattern], timeout=REPLY_TIMEOUT)
+        if idx < len(failures):
+            raise AssertionError(matches[0].decode("utf-8") if matches else "failed")
+
+    caps = " ".join(f"0x{byte:02x}" for byte in props["Configuration"])
+    meta = " ".join(f"0x{byte:02x}" for byte in metadata) or "no"
+    # dbus.Byte formats as a character unless converted to an ordinary int.
+    qos = {str(key): int(value) for key, value in props["QoS"].items()}
+    ctl.send(f'endpoint.presets /local/endpoint/ep0 {name} "{caps}"\n')
+    for prompt, value in [
+        ("Enter Target Latency", "Balance"),
+        ("Enter SDU Interval", qos["Interval"]),
+        ("Enter Framing", qos["Framing"]),
+        ("Enter PHY", qos["PHY"]),
+        ("Enter Max SDU", qos["SDU"]),
+        ("Enter RTN", qos["Retransmissions"]),
+        ("Enter Max Transport Latency", qos["Latency"]),
+        ("Enter Presentation Delay", qos["PresentationDelay"]),
+        ("Enter Metadata", meta),
+    ]:
+        expect_reply(prompt)
+        ctl.send(f"{value}\n")
+    ctl.send("version\n")
+    expect_reply(r"Version \d")
+
+
+# MTU 64 meets BAP's minimum and fits a two-ASE Codec Configuration response
+# with one Codec Configured notification, but not both ASE notifications.
+@host_config(
+    [Bluetoothd(conf=BAP_CONF + "\n[GATT]\nExchangeMTU = 64\n"), Pexpect()],
+    [Bluetoothd(conf=BAP_CONF), Pexpect()],
+)
+@pytest.mark.parametrize("metadata", [b"", b"\x03\x02\x04\x00"], ids=["empty", "media"])
+def test_bap_unicast_reconfigure_metadata(unicast_hosts, metadata):
+    host0, host1, initiator, acceptor = unicast_hosts
+    transports = expect_transports(initiator)
+    expect_transports(acceptor)
+    remote = transports[0].rsplit("/", 1)[0]
+    original = host0.call(remote_transport_properties, remote)
+    assert set(original) == set(transports)
+    presets = [f"metadata-{i}" for i in range(len(transports))]
+    for name, transport in zip(presets, transports):
+        add_preset(initiator, name, original[transport], metadata)
+
+    host0.call(clear_remote_transports, remote)
+    # Transport paths can be reused: check removal before recreating them.
+    assert not host0.call(remote_transport_properties, remote)
+
+    # PTY input permits both asynchronous requests without waiting for replies.
+    initiator.send(
+        "".join(
+            f"endpoint.config {remote} /local/endpoint/ep0 {name}\n" for name in presets
+        )
+    )
+    expect_all(initiator, [r"Endpoint /local/endpoint/ep0 configured"] * 2)
+    current = host0.call(remote_transport_properties, remote)
+    assert len(current) == len(original)
+    assert sorted(bytes(p["Configuration"]) for p in current.values()) == sorted(
+        bytes(p["Configuration"]) for p in original.values()
+    )
+    assert all(bytes(p["Metadata"]) == metadata for p in current.values())
+    left, right = sorted(current)
+    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",
+        ],
+    )
+
+
 # Broadcast code used by the broadcast scripts, see BCAST_CODE in
 # client/player.c
 BCAST_CODE = (
-- 
2.55.0

Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help