Skip to content

Basic integrationtests for BleakClient#1888

Merged
dlech merged 4 commits into
hbldh:developfrom
timrid:basic-client-integration-tests
Dec 29, 2025
Merged

Basic integrationtests for BleakClient#1888
dlech merged 4 commits into
hbldh:developfrom
timrid:basic-client-integration-tests

Conversation

@timrid

@timrid timrid commented Dec 28, 2025

Copy link
Copy Markdown
Contributor

This PR adds basic integration tests for the BleakClient.

Unfortunately, there is a problem in the Alpine VM where terminating a BleakClient connection always takes 20 seconds. This greatly increases the testing time in CI... Locally on my Ubuntu, terminating the connection always takes a constant 4 seconds. I find that long too, but it's still reasonably tolerable. However, I can't explain why it takes 20 seconds in Alpine VM. @dlech, do you have any idea what could be causing this?

My last commit adds additional logging output, enables the log output in CI and only executes one test case because the log output is huge... Searching for Disconnected from device after helps :) I will revert that changes once the problem is found.

E.g this run outputs:

00:02:28 [INFO] Disconnected from device after 20.12 seconds.

@codecov

codecov Bot commented Dec 28, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 48.24%. Comparing base (8a40874) to head (00fbafe).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1888      +/-   ##
===========================================
+ Coverage    46.26%   48.24%   +1.97%     
===========================================
  Files           38       38              
  Lines         3839     3839              
  Branches       463      463              
===========================================
+ Hits          1776     1852      +76     
+ Misses        1953     1864      -89     
- Partials       110      123      +13     
Flag Coverage Δ
bluez-integration-py3.10 36.10% <ø> (+2.26%) ⬆️
bluez-integration-py3.11 36.10% <ø> (+1.97%) ⬆️
bluez-integration-py3.12 36.10% <ø> (+1.97%) ⬆️
bluez-integration-py3.13 36.10% <ø> (+1.97%) ⬆️
bluez-integration-py3.14 34.43% <ø> (+2.03%) ⬆️
macos-latest-py3.10 17.84% <ø> (ø)
macos-latest-py3.11 ?
macos-latest-py3.12 ?
macos-latest-py3.13 17.84% <ø> (ø)
macos-latest-py3.14 17.60% <ø> (ø)
ubuntu-latest-py3.10 ?
ubuntu-latest-py3.11 ?
ubuntu-latest-py3.12 22.55% <ø> (ø)
ubuntu-latest-py3.13 22.55% <ø> (ø)
ubuntu-latest-py3.14 20.51% <ø> (ø)
windows-latest-py3.10 17.42% <ø> (ø)
windows-latest-py3.11 17.42% <ø> (ø)
windows-latest-py3.12 17.42% <ø> (ø)
windows-latest-py3.13 17.42% <ø> (ø)
windows-latest-py3.14 17.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator

do you have any idea what could be causing this?

According to the BlueZ source code, it should only be 2 seconds.

#define DISCONNECT_TIMER	2

...

void device_request_disconnect(struct btd_device *device, DBusMessage *msg)
{
	...
	device->disconn_timer = timeout_add_seconds(DISCONNECT_TIMER,
							disconnect_all,
							device, NULL);
	...
}

There are 3 possible implementations for timeout_add_seconds() though depending on which run loop is being used (ell, glib or mainloop). So maybe whichever implementation is being used on alpine isn't working well? Or maybe the timer source in qemu isn't working well? I would expect a timer on the order of 2 seconds to be fairly accurate though. Certainly not 10x off.

@dlech dlech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good start.

I wouldn't mind splitting this up into a few files already (connecting, characteristics, descriptors, pairing, properties) as I expect we will eventually add many more tests to cover error paths. Doing that in a single file would make the file too long.

Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
virtual_characteristic = Characteristic(
READ_CHARACTERISITC_UUID,
Characteristic.Properties.READ,
Characteristic.READABLE,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Characteristic.READABLE,
Attribute.Permission.READABLE,

Bumble source code says: Permission flags(legacy-use only) on Characteristic.READABLE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Characteristic.Permission.READABLE is more readable than Attribute.Permission.READABLE

Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
Comment thread tests/integration/test_client.py Outdated
assert isinstance(client.mtu_size, int)


@pytest.mark.skipif(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than two different tests where one gets skipped, I would write it as one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to combine these two cases. To get around the question, I've completely removed the unimplemented case for now. Then we can think about it again later when we add the pairing tests.

Comment thread tests/integration/test_client.py Outdated
@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator

do you have any idea what could be causing this?

I just tried running tests locally with --bleak-bluez-vhci for the first time and it was super-slow too. So I think most of the problem is with the vhci interface. I wonder if the problem is a performance issue with pyserial-asyncio? I will test it.

@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator

Profiling shows that it is spending all of it's time on epoll() in the asyncio run loop. I haven't been able to figure out how to show which tasks/coroutines are waiting at this time though.

@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator

The problem seems to be in the Linux kernel. Running btmon shows:

> ACL Data RX: Handle 0 flags 0x02 dlen 5                                       #178 [hci0] 8.012991
      ATT: Write Response (0x13) len 0
bluetoothd[3701]: @ MGMT Command: Disconnect (0x0014) plen 7                {0x0001} [hci0] 9.918092
        LE Address: DA:96:79:7A:C8:B2 (Static)
< HCI Command: LE Create Connection Cancel (0x08|0x000e) plen 0                 #179 [hci0] 28.448065

We can see the expected 2 second delay from bluetoothd between the last operation (Write Response) and and the MGMT Disconnect command.

However, there is an 18 second delay between the MGMT command to the kernel and actually sending the HCI command.

@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator

OK, the disconnect is actually coming from a 20 second timeout in hci_le_create_conn_sync() in the Linux kernel.

I think the kernel is expecting HCI_EV_LE_ENHANCED_CONN_COMPLETE instead of HCI_EV_LE_CONN_COMPLETE because either HCI_LE_LL_PRIVACY or HCI_LE_EXT_ADV is supported on the VHCI device. So we should be able to tweak bumble to give the expected response.

@dlech

dlech commented Dec 28, 2025

Copy link
Copy Markdown
Collaborator
diff --git a/tests/integration/bluez_controller.py b/tests/integration/bluez_controller.py
index f8f5142..07168c4 100644
--- a/tests/integration/bluez_controller.py
+++ b/tests/integration/bluez_controller.py
@@ -147,6 +147,7 @@ async def open_bluez_bluetooth_controller_link(
                 host_sink=hci_transport.sink,
                 link=link,
             )
+            bluez_controller.le_features = bytes.fromhex("0000000000000000")
             bluez_controller.manufacturer_name = BLEAK_TEST_MANUFACTURER_ID
 
             # Wait up to 5 seconds for the new adapter to appear via InterfacesAdded

Seems to do the trick. I just set it to all 0s and didn't check to see what else I was disabling yet though.

@timrid timrid force-pushed the basic-client-integration-tests branch from d1c4201 to e8d8dc8 Compare December 29, 2025 14:52
@timrid timrid force-pushed the basic-client-integration-tests branch from e8d8dc8 to 8b38834 Compare December 29, 2025 14:54
@timrid

timrid commented Dec 29, 2025

Copy link
Copy Markdown
Contributor Author

Thanks for your investigations in the timing problem and fixing it in #1890. I would never have found it so quickly.

I have addressed all your points, so this PR is ready for a new review.

Comment thread tests/integration/test_client_properties.py Outdated
Comment thread tests/integration/test_client_properties.py Outdated
Comment thread tests/integration/test_client_properties.py
Comment thread tests/integration/test_client_properties.py Outdated
Comment thread tests/integration/test_client_descriptors.py Outdated
Comment thread tests/integration/test_client_connecting.py Outdated
Comment thread tests/integration/test_client_connecting.py Outdated
@timrid

timrid commented Dec 29, 2025

Copy link
Copy Markdown
Contributor Author

I have addressed all your points, so this PR is ready for a new review.

@timrid timrid requested a review from dlech December 29, 2025 21:07
@dlech dlech merged commit 66f0b20 into hbldh:develop Dec 29, 2025
23 checks passed
@dlech

dlech commented Dec 29, 2025

Copy link
Copy Markdown
Collaborator

Thanks!

@timrid timrid deleted the basic-client-integration-tests branch December 30, 2025 09:29
JPHutchins added a commit to JPHutchins/bleak that referenced this pull request Jun 1, 2026
Lock in the current BlueZ and WinRT pair()/unpair() contract before the
pairing-agent series reworks it, so later refactors cannot silently
regress today's behavior.

- winrt: idempotent already-paired return, can_pair guard, Just Works
  success + handler detach, result-status failure, the deprecated
  protection_level keyword, and unpair status handling.
- bluez: already-paired return, Trusted-then-Pair + resolvable-private-
  address refresh, D-Bus failure propagation (no agent is registered on
  develop, so a real Pair fails, cf. hbldh#1380 / hbldh#1434), and unpair
  RemoveDevice / DoesNotExist mapping.
- integration: a connect/disconnect/unpair smoke test for the existing
  unpair path.

Expands the integration harness from hbldh#1888. Addresses dlech's request on
hbldh#1990 to split the work and add tests for existing pairing functions
first. Part of the pairing series continuing hbldh#1864.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants