pebble/tests/fw/comm/test_gatt_client_discovery.c
2025-01-27 11:38:16 -08:00

520 lines
20 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_events.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
extern TimerID bt_driver_gatt_get_watchdog_timer_id(void);
#define TEST_GATT_CONNECTION_ID (1234)
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_assert_event(const BTDeviceInternal *device, BTErrno status) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.subtype,
PebbleBLEGATTClientEventTypeServiceChange);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.info->status,
status);
const BTDeviceInternal event_device = event.bluetooth.le.gatt_client_service.info->device;
const bool equal_devices = bt_device_equal(&device->opaque,
&event_device.opaque);
cl_assert_equal_b(equal_devices, true);
// clear the event
fake_event_clear_last();
fake_event_reset_count();
}
static void prv_simulate_and_assert_discovery_of_one_service(const BTDeviceInternal *device) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(device, BTErrnoOK);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_discovery__initialize(void) {
fake_gatt_init();
fake_event_init();
gap_le_connection_init();
}
void test_gatt_client_discovery__cleanup(void) {
gap_le_connection_deinit();
// make sure we haven't leaked any memory!
fake_pbl_malloc_check_net_allocs();
}
// -----------------------------------------------------------------------------
// Edge cases
void test_gatt_client_discovery__not_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidParameter);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__already_in_progress(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery for device:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Start again (expect to fail):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidState);
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__event_is_sent_when_already_discovered(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Simulate discovery of 1 service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
prv_assert_event(&device, BTErrnoOK);
fake_event_clear_last();
// Start discovery again, expect not to run (already discovered):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// Expect event:
prv_assert_event(&device, BTErrnoOK);
}
void test_gatt_client_discovery__disconnected_during_discovery(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate disconnection:
gap_le_connection_remove(&device);
// Process racing discovery indication:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// Bluetopia's GATT module does *NOT* emit a service discovery completion event for disconnections
// Test that our API *does* emit an service discovery event with "disconnected" reason:
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__complete_error(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate getting one service indication...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// ... then a failure:
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT,
TEST_GATT_CONNECTION_ID);
// Expect event with error status and 0 services:
prv_assert_event(&device,
BTErrnoWithBluetopiaError(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT));
// Expect service discovery to be stopped:
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Watchdog timeout tests
static void prv_fire_watchdog_timeouts(const BTDeviceInternal *device, int retries) {
for (int i = 0; i <= retries; ++i) {
const int start_count = fake_gatt_is_service_discovery_start_count();
const int stop_count = fake_gatt_is_service_discovery_stop_count();
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Check whether GATT_Stop_Service_Discovery has been called:
cl_assert_equal_i(stop_count + 1, fake_gatt_is_service_discovery_stop_count());
if (i < GATT_CLIENT_DISCOVERY_MAX_RETRY) {
// Check whether GATT_Start_Service_Discovery has been called, except for the last iteration:
cl_assert_equal_i(start_count + 1, fake_gatt_is_service_discovery_start_count());
// No client event:
prv_assert_no_event();
} else {
// Last iteration: expect event with error status and 0 services:
prv_assert_event(device, BTErrnoServiceDiscoveryTimeout);
}
}
}
void test_gatt_client_discovery__watchdog_error_out_after_max_retries(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
}
void test_gatt_client_discovery__watchdog_retry_counter_not_affecting_successive_process(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
// Make sure the previous retry counter doesn't affect any new discovery process:
fake_event_clear_last();
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Fire watchdog one less time than the maximum:
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY - 1);
// Finally make the Bluetopia discovery events come in:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
void test_gatt_client_discovery__watchdog_race_with_stopping(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Stop_Service_Discovery fail:
// (Service discovery has finished in the mean time, disconnected, ...)
fake_gatt_set_stop_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// No event should be generated, because the finishing / disconnecting / ... should cause
// Bluetopia to call back to prv_handle_bluetopia_discovery_event() and therefore the normal
// path will be taken.
prv_assert_no_event();
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__watchdog_race_with_restarting(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Start_Service_Discovery fail:
// (Disconnected in the mean time, ...)
fake_gatt_set_start_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Stopping did not fail, but restarting did. In this case we need to generate an event that
// the discovery process failed. The error from GATT_Start_Service_Discovery is expected to be
// passed in the event.
prv_assert_event(&device, BTErrnoWithBluetopiaError(BTGATT_ERROR_INVALID_PARAMETER));
}
// -------------------------------------------------------------------------------------------------
// Re-discovery
extern BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device);
void test_gatt_client_discovery__rediscover_not_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
fake_event_clear_last();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Expect all services nodes to be cleaned up:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 0);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
}
void test_gatt_client_discovery__rediscover_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Put one service, but do not finish...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
prv_assert_no_event();
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
const int stop_count_before_rediscovery = fake_gatt_is_service_discovery_stop_count();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(stop_count_before_rediscovery + 1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
void test_gatt_client_discovery__multiple_jobs_pending(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
ATTHandleRange range_alt = {
.start = 0x3001,
.end = 0x4000
};
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// start a discovery job, pretend nothing is found
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// pend up another service discovery job
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_i(1, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Nothing was found so we should just have a completion event
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// next job should be in progress
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// BP service was discovered
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// start another job, should be pended
gatt_client_discovery_discover_range(connection, &range);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Since we are restarting discovery over the same range and discovered one
// service two events should be generated, one about the discovery complete
// and one invalidating the just discovered service
cl_assert_equal_i(2, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(3, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
gatt_client_discovery_discover_range(connection, &range_alt);
// BP service should have been discovered
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Only one event should have been pended since we were not rediscovering the
// same handle range
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
// Nothing discovered for final query
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(4, fake_gatt_is_service_discovery_start_count());
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__partial_and_full_discovery_jobs_intermixed(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
// queue up a few jobs - note only one should be running at any time
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
for (int i = 0; i < 10; i++) {
gatt_client_discovery_discover_range(connection, &range);
}
// kick off a full discovery
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
prv_simulate_and_assert_discovery_of_one_service(&device);
// None of the batched up jobs should have run
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// nothing should still be running
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Test vectors
static void prv_assert_blood_pressure_service(const GATTService *service) {
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const uint16_t service_handle = bp_service->handle;
cl_assert_equal_i(service->att_handle, service_handle);
cl_assert_equal_b(uuid_equal(&service->uuid, &bp_service->uuid), true);
cl_assert_equal_i(service->num_att_handles_included_services, bp_service->num_included_services);
cl_assert_equal_i(service->num_characteristics, bp_service->num_characteristics);
const GATTCharacteristic *characteristic_one = service->characteristics;
const Characteristic *expected_characteristic1 = &bp_service->characteristics[0];
cl_assert_equal_i(characteristic_one->att_handle_offset, expected_characteristic1->handle - service_handle);
cl_assert_equal_i(characteristic_one->num_descriptors, expected_characteristic1->num_descriptors);
cl_assert_equal_i(characteristic_one->descriptors[0].att_handle_offset,
expected_characteristic1->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_one->properties, expected_characteristic1->properties);
cl_assert_equal_b(uuid_equal(&characteristic_one->uuid, &expected_characteristic1->uuid), true);
// Second characteristic is tacked right after the first one:
const GATTCharacteristic *characteristic_two =
(const GATTCharacteristic *) &characteristic_one->descriptors[1];
const Characteristic *expected_characteristic2 = &bp_service->characteristics[1];
cl_assert_equal_i(characteristic_two->att_handle_offset, expected_characteristic2->handle - service_handle);
cl_assert_equal_i(characteristic_two->num_descriptors, expected_characteristic2->num_descriptors);
cl_assert_equal_i(characteristic_two->descriptors[0].att_handle_offset,
expected_characteristic2->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_two->properties, expected_characteristic2->properties);
cl_assert_equal_b(uuid_equal(&characteristic_two->uuid, &expected_characteristic2->uuid), true);
}
void test_gatt_client_discovery__single_blood_pressure_service(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
const GATTService *service = connection->gatt_remote_services->service;
prv_assert_blood_pressure_service(service);
}