pebble/tests/fw/comm/test_ppogatt.c

1107 lines
46 KiB
C
Raw Permalink Normal View History

/*
* 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/kernel_le_client/ppogatt/ppogatt.h"
#include "comm/ble/kernel_le_client/ppogatt/ppogatt_internal.h"
#include "services/common/comm_session/session_transport.h"
#include "services/common/regular_timer.h"
#include <util/size.h>
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_bt_conn_mgr.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mfg_info.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_serial.h"
// Fakes
///////////////////////////////////////////////////////////
#include "fake_gatt_client_operations.h"
#include "fake_gatt_client_subscriptions.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_session.h"
#include "fake_system_task.h"
#define MTU_SIZE (158)
#define MAX_PAYLOAD_SIZE (MTU_SIZE - 3 /* ATT Header size */ - 1 /* PPoGATT Packet Header */)
uint16_t s_mtu_size;
int bt_driver_gap_le_disconnect(const BTDeviceInternal *peer_address) {
return 0;
}
uint16_t gap_le_connection_get_gatt_mtu(const BTDeviceInternal *device) {
return s_mtu_size;
}
GAPLEConnection *gap_le_connection_get_gateway(void) {
return NULL;
}
GAPLEConnection *gatt_client_characteristic_get_connection(BLECharacteristic characteristic_ref) {
return NULL;
}
static BTDeviceInternal s_device = {};
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
return s_device;
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// Helpers
///////////////////////////////////////////////////////////
extern Transport *ppogatt_client_for_uuid(const Uuid *uuid);
extern bool ppogatt_has_client_for_uuid(const Uuid *uuid);
extern uint32_t ppogatt_client_count(void);
extern void ppogatt_trigger_rx_ack_send_timeout(void);
extern TransportDestination ppogatt_get_destination(Transport *transport);
static const uint8_t s_num_service_instances = 2;
static BLECharacteristic s_characteristics[s_num_service_instances][PPoGATTCharacteristicNum] = {
[0] = {
[PPoGATTCharacteristicData] = 01,
[PPoGATTCharacteristicMeta] = 02,
},
[1] = {
[PPoGATTCharacteristicData] = 11,
[PPoGATTCharacteristicMeta] = 12,
},
};
static const BLECharacteristic s_unknown_characteristics = 0x55;
static const PPoGATTMetaV0 s_meta_v0_app = {
.ppogatt_min_version = PPOGATT_MIN_VERSION,
.ppogatt_max_version = USE_PPOGATT_VERSION,
.app_uuid = UuidMake(0xA4, 0x83, 0x2A, 0x0E, 0x74, 0x54, 0x45, 0x32,
0xB2, 0xA2, 0x4E, 0x6F, 0x8F, 0x7B, 0x68, 0x6F)
};
static const PPoGATTMetaV0 s_meta_v0_system = {
.ppogatt_min_version = PPOGATT_MIN_VERSION,
.ppogatt_max_version = USE_PPOGATT_VERSION,
.app_uuid = (const Uuid) UUID_SYSTEM,
};
static const PPoGATTMetaV1 s_meta_v1_hybrid = {
.ppogatt_min_version = 0,
.ppogatt_max_version = 0,
.app_uuid = (const Uuid) UUID_SYSTEM,
.pp_session_type = PPoGATTSessionType_Hybrid,
};
static const PPoGATTMetaV1 s_meta_v1_system_inferred = {
.ppogatt_min_version = 0,
.ppogatt_max_version = 0,
.app_uuid = (const Uuid) UUID_SYSTEM,
.pp_session_type = PPoGATTSessionType_InferredFromUuid,
};
static const PPoGATTMetaV1 s_meta_v1_app_inferred = {
.ppogatt_min_version = 0,
.ppogatt_max_version = 0,
.app_uuid = UuidMake(0xA4, 0x83, 0x2A, 0x0E, 0x74, 0x54, 0x45, 0x32,
0xB2, 0xA2, 0x4E, 0x6F, 0x8F, 0x7B, 0x68, 0x6F),
.pp_session_type = PPoGATTSessionType_InferredFromUuid,
};
static PPoGATTPacket s_reset_complete = (const PPoGATTPacket) {
.sn = 0,
.type = PPoGATTPacketTypeResetComplete,
};
static PPoGATTPacket s_server_reset_request = (const PPoGATTPacket) {
.sn = 0,
.type = PPoGATTPacketTypeResetRequest,
};
static PPoGATTPacket * s_client_reset_request;
static uint16_t s_client_reset_request_size;
static PPoGATTPacket * s_client_reset_complete;
static uint16_t s_client_reset_complete_size;
static int s_ppogatt_version;
static int s_tx_window_size;
static int s_rx_window_size;
static void prv_create_expected_reset_request(void) {
s_client_reset_request_size = sizeof(PPoGATTPacket) + sizeof(PPoGATTResetRequestClientIDPayload);
s_client_reset_request = (PPoGATTPacket *) malloc(s_client_reset_request_size);
*s_client_reset_request = (const PPoGATTPacket) {
.sn = 0,
.type = PPoGATTPacketTypeResetRequest,
};
PPoGATTResetRequestClientIDPayload *client_id_payload =
(PPoGATTResetRequestClientIDPayload *)s_client_reset_request->payload;
*client_id_payload = (const PPoGATTResetRequestClientIDPayload) {
.ppogatt_version = s_ppogatt_version,
};
memcpy(client_id_payload->serial_number, mfg_get_serial_number(), MFG_SERIAL_NUMBER_SIZE);
}
static void prv_create_expected_reset_complete(void) {
s_client_reset_complete_size = sizeof(PPoGATTPacket);
if (s_ppogatt_version > 0) {
s_client_reset_complete_size += sizeof(PPoGATTResetCompleteClientIDPayloadV1);
}
s_client_reset_complete = (PPoGATTPacket *) malloc(s_client_reset_complete_size);
*s_client_reset_complete = (const PPoGATTPacket) {
.sn = 0,
.type = PPoGATTPacketTypeResetComplete,
};
if (s_ppogatt_version > 0) {
*((PPoGATTResetCompleteClientIDPayloadV1 *)s_client_reset_complete->payload) =
(const PPoGATTResetCompleteClientIDPayloadV1) {
.ppogatt_max_rx_window = s_rx_window_size,
.ppogatt_max_tx_window = s_tx_window_size,
};
}
}
static void prv_receive_reset_request(BLECharacteristic characteristic) {
ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) &s_server_reset_request,
sizeof(s_server_reset_request), BLEGATTErrorSuccess);
}
static void prv_receive_reset_complete(BLECharacteristic characteristic) {
ppogatt_handle_read_or_notification(characteristic, (const uint8_t *)s_client_reset_complete,
s_client_reset_complete_size, BLEGATTErrorSuccess);
}
static const uint8_t s_short_data_fragment[] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
};
static void prv_receive_short_data_fragment(BLECharacteristic characteristic, uint8_t sn) {
PPoGATTPacket *packet = malloc(sizeof(PPoGATTPacket) + sizeof(s_short_data_fragment));
packet->sn = sn;
packet->type = PPoGATTPacketTypeData;
memcpy(packet->payload, s_short_data_fragment, sizeof(s_short_data_fragment));
ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) packet,
sizeof(s_short_data_fragment), BLEGATTErrorSuccess);
free(packet);
}
static void prv_receive_ack(BLECharacteristic characteristic, uint8_t sn) {
const PPoGATTPacket ack = (const PPoGATTPacket) {
.sn = sn,
.type = PPoGATTPacketTypeAck,
};
ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) &ack,
sizeof(ack), BLEGATTErrorSuccess);
}
static void prv_assert_sent_reset_request(BLECharacteristic characteristic) {
fake_gatt_client_op_assert_write(characteristic,
(const uint8_t *) s_client_reset_request,
s_client_reset_request_size,
GAPLEClientKernel, false /* is_response_required */);
}
static void prv_assert_sent_reset_complete(BLECharacteristic characteristic) {
struct PACKED {
PPoGATTPacketType type:3;
uint8_t sn:PPOGATT_SN_BITS;
PPoGATTResetCompleteClientIDPayloadV1 payload;
} expected_response = {
.sn = 0,
.type = PPoGATTPacketTypeResetComplete
};
if (s_ppogatt_version > 0) {
expected_response.payload = (const PPoGATTResetCompleteClientIDPayloadV1) {
.ppogatt_max_rx_window = PPOGATT_V1_DESIRED_RX_WINDOW_SIZE,
.ppogatt_max_tx_window = PPOGATT_V0_WINDOW_SIZE,
};
}
fake_gatt_client_op_assert_write(characteristic,
(const uint8_t *) &expected_response, s_client_reset_complete_size,
GAPLEClientKernel, false /* is_response_required */);
if (s_ppogatt_version > 0) {
s_tx_window_size = MIN(s_tx_window_size, expected_response.payload.ppogatt_max_tx_window);
s_rx_window_size = MIN(s_rx_window_size, expected_response.payload.ppogatt_max_rx_window);
} else {
s_tx_window_size = s_rx_window_size = PPOGATT_V0_WINDOW_SIZE;
}
}
static void prv_assert_sent_ack(BLECharacteristic characteristic, uint8_t sn) {
const PPoGATTPacket ack = (const PPoGATTPacket) {
.sn = sn,
.type = PPoGATTPacketTypeAck,
};
fake_gatt_client_op_assert_write(characteristic, (const uint8_t *) &ack, sizeof(ack),
GAPLEClientKernel, false /* is_response_required */);
}
static void prv_assert_sent_data(BLECharacteristic characteristic, uint8_t sn,
const uint8_t *data, size_t length) {
cl_assert(length <= MAX_PAYLOAD_SIZE);
PPoGATTPacket *packet = malloc(sizeof(PPoGATTPacket) + length);
packet->sn = sn;
packet->type = PPoGATTPacketTypeData;
memcpy(packet->payload, data, length);
fake_gatt_client_op_assert_write(characteristic, (const uint8_t *) packet,
sizeof(PPoGATTPacket) + length,
GAPLEClientKernel, false /* is_response_required */);
free(packet);
}
// Tests
///////////////////////////////////////////////////////////
void test_ppogatt__initialize(void) {
s_ppogatt_version = USE_PPOGATT_VERSION;
s_tx_window_size = 25;
s_rx_window_size = 25;
prv_create_expected_reset_request();
prv_create_expected_reset_complete();
s_mtu_size = MTU_SIZE;
fake_pbl_malloc_clear_tracking();
fake_gatt_client_op_init();
fake_gatt_client_subscriptions_init();
regular_timer_init();
fake_comm_session_init();
ppogatt_create();
}
void test_ppogatt__cleanup(void) {
ppogatt_destroy();
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_i(regular_timer_seconds_count(), 0);
regular_timer_deinit();
fake_gatt_client_op_deinit();
fake_gatt_client_subscriptions_deinit();
// Check for leaks:
fake_pbl_malloc_check_net_allocs();
fake_pbl_malloc_clear_tracking();
fake_comm_session_cleanup();
free(s_client_reset_request);
free(s_client_reset_complete);
}
void prv_notify_services_discovered(int num_services_to_register) {
for (int i = 0; i < s_num_service_instances && i < num_services_to_register; i++) {
ppogatt_handle_service_discovered(s_characteristics[i]);
}
}
void test_ppogatt__find_pebble_app_and_3rd_party_app(void) {
prv_notify_services_discovered(s_num_service_instances);
// Assert GATT reads requests to Meta characteristics happened:
fake_gatt_client_op_assert_read(s_characteristics[0][PPoGATTCharacteristicMeta],
GAPLEClientKernel);
fake_gatt_client_op_assert_read(s_characteristics[1][PPoGATTCharacteristicMeta],
GAPLEClientKernel);
// Simulate read responses:
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system, sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
ppogatt_handle_read_or_notification(s_characteristics[1][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_app, sizeof(s_meta_v0_app),
BLEGATTErrorSuccess);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_app.app_uuid), true);
}
void test_ppogatt__handles_unknown_read_response(void) {
uint8_t data;
ppogatt_handle_read_or_notification(s_unknown_characteristics,
&data, sizeof(data), BLEGATTErrorSuccess);
// No crashes / asserts etc.
}
void test_ppogatt__handles_too_short_meta_length(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system) - 1 /* missing last byte */,
BLEGATTErrorSuccess);
// No client created:
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}
void test_ppogatt__handles_meta_v1(void) {
struct {
const PPoGATTMetaV1 *meta;
TransportDestination expected_destination;
} metas[] = {
{
.meta = &s_meta_v1_hybrid,
.expected_destination = TransportDestinationHybrid,
},
{
.meta = &s_meta_v1_system_inferred,
.expected_destination = TransportDestinationSystem,
},
{
.meta = &s_meta_v1_app_inferred,
.expected_destination = TransportDestinationApp,
},
};
for (int i = 0; i < ARRAY_LENGTH(metas); ++i) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) metas[i].meta,
sizeof(PPoGATTMetaV1),
BLEGATTErrorSuccess);
// Client created:
cl_assert_equal_i(ppogatt_client_count(), 1);
Transport *client = ppogatt_client_for_uuid(&metas[i].meta->app_uuid);
cl_assert_equal_i(ppogatt_get_destination(client), metas[i].expected_destination);
ppogatt_close(client);
}
}
void test_ppogatt__handles_unsupported_meta_ppogatt_version(void) {
PPoGATTMetaV0 future_meta_non_compatible = s_meta_v0_system;
future_meta_non_compatible.ppogatt_min_version = 0xaa;
future_meta_non_compatible.ppogatt_max_version = 0xff;
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &future_meta_non_compatible,
sizeof(future_meta_non_compatible),
BLEGATTErrorSuccess);
// No client created:
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&future_meta_non_compatible.app_uuid), false);
}
void test_ppogatt__handles_invalid_uuid_meta(void) {
PPoGATTMetaV0 meta_invalid_uuid = s_meta_v0_system;
meta_invalid_uuid.app_uuid = UUID_INVALID;
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &meta_invalid_uuid,
sizeof(meta_invalid_uuid),
BLEGATTErrorSuccess);
// No client created:
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&meta_invalid_uuid.app_uuid), false);
}
void test_ppogatt__deletes_existing_client_after_rediscovery(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
// Client created:
cl_assert_equal_i(ppogatt_client_count(), 1);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
Transport *client = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Rediscovery:
ppogatt_invalidate_all_references();
prv_notify_services_discovered(1);
// Still one client:
cl_assert_equal_i(ppogatt_client_count(), 1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
// Still one client:
cl_assert_equal_i(ppogatt_client_count(), 1);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
Transport *client2 = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
}
void test_ppogatt__invalidate_characteristic_refs_immediately_after_update(void) {
prv_notify_services_discovered(1);
ppogatt_handle_service_removed(&s_characteristics[0][0], PPoGATTCharacteristicNum);
const bool can_handle =
ppogatt_can_handle_characteristic(s_characteristics[0][PPoGATTCharacteristicData]);
cl_assert_equal_b(can_handle, false);
}
void test_ppogatt__handle_subscribe_to_unknown_characteristic(void) {
ppogatt_handle_subscribe(s_unknown_characteristics, BLESubscriptionNotifications,
BLEGATTErrorSuccess);
// Expect to unsubscribe from the unknown characteristic:
fake_gatt_client_subscriptions_assert_subscribe(s_unknown_characteristics, BLESubscriptionNone,
GAPLEClientKernel);
}
void test_ppogatt__cleanup_client_when_meta_read_fails(void) {
fake_gatt_client_op_set_read_return_value(BTErrnoInvalidParameter);
prv_notify_services_discovered(1);
cl_assert_equal_i(ppogatt_client_count(), 0);
}
void test_ppogatt__cleanup_client_when_meta_read_gets_error_response(void) {
fake_gatt_client_op_set_read_return_value(BTErrnoOK);
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
NULL, 0,
BLEGATTErrorInvalidHandle);
cl_assert_equal_i(ppogatt_client_count(), 0);
}
void test_ppogatt__cleanup_client_when_data_subscription_cccd_write_failed(void) {
fake_gatt_client_subscriptions_set_subscribe_return_value(BTErrnoInvalidParameter);
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}
void test_ppogatt__cleanup_client_when_data_subscription_error_response(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
// Expect subscribe request was made:
fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications,
GAPLEClientKernel);
// Simulate getting the subscription failure:
ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications, BLEGATTErrorReadNotPermitted);
cl_assert_equal_i(ppogatt_client_count(), 0);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}
static void prv_discover_and_read_meta_and_reset(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
// Expect subscribe request was made:
fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications,
GAPLEClientKernel);
// Simulate getting the subscription confirmation:
ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect Reset to be initiated ("Reset Request" sent by FW):
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
// Session should still no have opened yet:
cl_assert_equal_i(fake_comm_session_open_call_count(), 0);
}
void test_ppogatt__open_session_when_found_pebble_app(void) {
prv_discover_and_read_meta_and_reset();
// Simulate getting "Reset Complete" from remote:
prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// Expect "Reset Complete" to be sent by FW:
prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// Expect Session to be opened now:
cl_assert_equal_i(fake_comm_session_open_call_count(), 1);
}
void test_ppogatt__start_reset_upon_out_of_range_ack(void) {
test_ppogatt__open_session_when_found_pebble_app();
// Simulate getting an Ack that's outside of the window of outstanding SNs:
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], PPOGATT_SN_MOD_DIV / 2);
// Expect Reset to be initiated ("Reset Request" sent by FW):
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}
void test_ppogatt__ignore_retransmitted_ack(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
for (uint8_t sn = 0; sn < 3; ++sn) {
const bool success =
fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment));
cl_assert_equal_b(success, true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
// Receive ACK for first data packet with sn=0:
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0);
// Pretend data packets with sn=1 got lost in the ether, but data sn=2 was received...
// Receive a retransmit for the ACK sn=0, to indicate data was missing.
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0);
// The retransmitted ACK should be ignored.
fake_gatt_client_op_assert_no_write();
// Session shouldn't get closed:
cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}
void test_ppogatt__ignore_server_reset_request_while_resetting_due_to_server_reset_request(void) {
test_ppogatt__open_session_when_found_pebble_app();
prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
fake_gatt_client_op_assert_no_write();
}
void test_ppogatt__ignore_server_reset_request_while_resetting_due_to_own_reset_request(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect Reset to be initiated ("Reset Request" sent by FW):
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
fake_gatt_client_op_assert_no_write();
}
void test_ppogatt__timeout_waiting_for_reset_complete_remote_initiated(void) {
test_ppogatt__open_session_when_found_pebble_app();
prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// Timeout waiting for "Reset Complete":
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
// Expect "Reset Request" sent by FW:
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}
void test_ppogatt__timeout_waiting_for_reset_complete_self_initiated(void) {
prv_discover_and_read_meta_and_reset();
// Timeout waiting for "Reset Complete":
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
// Expect "Reset Request" sent by FW:
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}
void test_ppogatt__server_reset_request_while_pending_ack(void) {
test_ppogatt__open_session_when_found_pebble_app();
// Simulate outbound queue full, so ack will have to wait until there's buffer space:
fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
// Receive data (that needs to be ack'd):
uint8_t sn = 0;
prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], sn);
fake_gatt_client_op_assert_no_write();
// Receive Reset Request:
prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
fake_gatt_client_op_assert_no_write();
// Simulate outbound queue having space again:
fake_gatt_client_op_set_write_return_value(BTErrnoOK);
ppogatt_handle_buffer_empty();
// Expect Reset Complete to be sent out, but nothing more than that:
prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
fake_gatt_client_op_assert_no_write();
// In the past we had a bug here where the pending ACK would get sent out.
// See https://pebbletechnology.atlassian.net/browse/PBL-24651
}
void test_ppogatt__ignore_invalid_packet_type(void) {
test_ppogatt__open_session_when_found_pebble_app();
PPoGATTPacket packet = {
.sn = 0,
.type = PPoGATTPacketTypeInvalidRangeStart,
};
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicData],
(const uint8_t *) &packet, sizeof(packet),
BLEGATTErrorSuccess);
// No crash etc, client still alive:
cl_assert_equal_i(ppogatt_client_count(), 1);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}
void test_ppogatt__ignore_reset_complete_while_open(void) {
test_ppogatt__open_session_when_found_pebble_app();
// Simulate getting "Reset Complete" from remote:
prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// No crash etc, client still alive:
cl_assert_equal_i(ppogatt_client_count(), 1);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}
void test_ppogatt__ignore_data_during_reset(void) {
prv_notify_services_discovered(1);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect Reset to be initiated ("Reset Request" sent by FW):
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
// Receive data:
prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], 3 /* sn */);
// Simulate getting "Reset Complete" from remote:
prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// Expect "Reset Complete" to be sent by FW:
prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
// Expect Session to be opened now:
cl_assert_equal_i(fake_comm_session_open_call_count(), 1);
}
void test_ppogatt__ignore_zero_length_notification(void) {
test_ppogatt__open_session_when_found_pebble_app();
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicData], NULL, 0,
BLEGATTErrorSuccess);
// No crash etc, client still alive:
cl_assert_equal_i(ppogatt_client_count(), 1);
cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}
void test_ppogatt__ack_received_data(void) {
test_ppogatt__open_session_when_found_pebble_app();
// Receive data:
for (uint8_t i = 0; i < PPOGATT_SN_MOD_DIV + 1; ++i) {
const uint8_t sn = i % PPOGATT_SN_MOD_DIV;
ppogatt_trigger_rx_ack_send_timeout();
prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], sn);
prv_assert_sent_ack(s_characteristics[0][PPoGATTCharacteristicData], sn);
}
}
void test_ppogatt__close(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *client = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
cl_assert(client);
ppogatt_close(client);
cl_assert_equal_p(NULL, ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid));
}
void test_ppogatt__missing_inbound_packet(void) {
test_ppogatt__open_session_when_found_pebble_app();
// Receive data:
prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData],
1 /* sn (expecting sn=0) */);
// Expect nothing to be sent, rely on other end to hit time-out and retransmit
fake_gatt_client_op_assert_no_write();
}
void test_ppogatt__send_data_max_payload_size(void) {
test_ppogatt__open_session_when_found_pebble_app();
uint8_t *data = malloc(MAX_PAYLOAD_SIZE);
memset(data, MAX_PAYLOAD_SIZE, 0x55);
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
for (uint8_t sn = 0; sn < s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport, data,
MAX_PAYLOAD_SIZE), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
data, MAX_PAYLOAD_SIZE);
}
free(data);
}
void test_ppogatt__cap_number_of_data_packets_in_flight(void) {
test_ppogatt__open_session_when_found_pebble_app();
uint8_t sn = 0;
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get s_rx_window_size packets in flight:
for (sn = 0; sn < s_tx_window_size; ++sn) {
const bool success = fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment));
printf("SEND %d %d\n", sn, success);
cl_assert_equal_b(success, true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
printf("done\n");
// Enqueue another:
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment)), true);
ppogatt_send_next(transport);
fake_gatt_client_op_assert_no_write();
// Ack the first one (sn=0):
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0 /* sn */);
// The last enqueued one should now be sent out:
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
void test_ppogatt__receive_ack_for_all_packets_in_flight(void) {
test_ppogatt__open_session_when_found_pebble_app();
uint8_t sn = 0;
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get s_tx_window_size packets in flight:
for (sn = 0; sn < s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment)), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
// Ack the last one (sn == s_tx_window_size - 1), which will be interpreted as Ack'ing all
// the packets before it too:
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData],
s_tx_window_size - 1 /* sn */);
// We should now be able to submit s_tx_window_size packets again:
for (sn = s_tx_window_size; sn < 2 * s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment)), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
}
void test_ppogatt__handle_client_disappearing_for_send_callback(void) {
// ppogatt_send_next() is called from the KernelBG task sometimes.
// It's possible that the pointer is dangling by the time the callback executes.
// Therefore ppogatt_send_next() should be able to handle this dangling pointer gracefully.
uint8_t fake_client = 0;
ppogatt_send_next((struct Transport *) &fake_client);
// No crashes, no writes, etc.
fake_gatt_client_op_assert_no_write();
}
void test_ppogatt__handle_bluetooth_stack_queue_full_and_empty_events(void) {
test_ppogatt__open_session_when_found_pebble_app();
fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment)), true);
ppogatt_send_next(transport);
fake_gatt_client_op_assert_no_write();
fake_gatt_client_op_set_write_return_value(BTErrnoOK);
ppogatt_handle_buffer_empty();
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], 0 /* sn */,
s_short_data_fragment, sizeof(s_short_data_fragment));
}
void test_ppogatt__retransmit_timed_out_data_packets_all_at_once(void) {
test_ppogatt__open_session_when_found_pebble_app();
uint8_t sn = 0;
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get s_tx_window_size packets in flight:
for (sn = 0; sn < s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
// Simulate the regular timer firing a bunch of times to expire the timeout for all the packets:
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
fake_comm_session_process_send_next();
// The data should *NOT* get concatenated in a single packet, even though it might fit. The
// fragmentation should be the same as the previous transmission pass, because there is a race
// condition where there are Ack(s) in flight for the "original" data packets. Because we're
// using the same SNs, we cannot change the fragmentation, because we cannot know whether they
// would refer to the old or new fragmentation.
for (sn = 0; sn < s_tx_window_size; ++sn) {
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
}
void test_ppogatt__retransmit_timed_out_data_packets_first_but_not_later_ones(void) {
test_ppogatt__open_session_when_found_pebble_app();
uint8_t sn = 0;
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get s_tx_window_size packets in flight:
uint8_t secs_passed = 0;
for (sn = 0; sn < s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
if (sn == 0 || sn == 1) {
// Make the first and second packet time out each, one second earlier
// than the 3rd and 4rd packets:
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
++secs_passed;
}
cl_assert(secs_passed < PPOGATT_TIMEOUT_TICKS);
}
// Simulate the regular timer firing a bunch of times to expire the timeout for the in-flight packets
// This will trigger a retransmit of the un-acked packets
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS - 1; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
fake_comm_session_process_send_next();
for (sn = 0; sn < s_tx_window_size; ++sn) {
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
}
void test_ppogatt__retransmit_timed_out_data_packets_race_everything_acked_at_once(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get s_tx_window_size packets in flight:
uint8_t sn = 0;
for (; sn < s_tx_window_size; ++sn) {
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
// Time-out all packets in flight, rolling back for retransmission:
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
// Simulate receiving an ack for the last, after the roll-back, but before the packets are
// retransmitted (the last part shouldn't matter much, but simplifies the test a bit)
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData],
(sn - 1) % PPOGATT_SN_MOD_DIV);
// Some new data has been queued up in the mean time:
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
// Only now the system task callback is fired (prv_send_next_packets_async):
fake_comm_session_process_send_next();
// Expect the new data to come through, no retransmissions at all.
// (They all got considered Ack'd by the one Ack)
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
void test_ppogatt__retransmit_max_number_of_times(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get a packet in flight:
uint8_t sn = 0;
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
ppogatt_send_next(transport);
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
for (int j = 0; j < PPOGATT_TIMEOUT_COUNT_MAX - 1; ++j) {
// Time-out the packet over and over until (max - 1) is reached:
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
fake_comm_session_process_send_next();
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
// The last straw:
for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}
void test_ppogatt__make_sure_timeout_reset_after_data_ack(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
uint8_t num_packets = s_tx_window_size;
// Get a packet in flight:
for (int sn = 0; sn < num_packets; sn++) {
cl_assert_equal_b(
fake_comm_session_send_buffer_write_raw_by_transport(
transport, s_short_data_fragment, sizeof(s_short_data_fragment) - sn), true);
ppogatt_send_next(transport);
}
for (int sn = 0; sn < num_packets; sn++) {
for (int i = 0; i < (PPOGATT_TIMEOUT_TICKS - 1); ++i) {
regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
}
prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */);
}
fake_comm_session_process_send_next();
for (int sn = 0; sn < num_packets; sn++) {
prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}
// There should be no writes we haven't already checked for. That would only happen if we timed
// out!
fake_gatt_client_op_assert_no_write();
}
void test_ppogatt__mtu_zero_due_to_disconnection(void) {
test_ppogatt__open_session_when_found_pebble_app();
Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
// Get a packet in flight:
uint8_t sn = 0;
cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
s_short_data_fragment,
sizeof(s_short_data_fragment) - sn), true);
fake_malloc_set_largest_free_block(1000);
s_mtu_size = 0;
ppogatt_send_next(transport);
// No crash
}
//! When client ID info got added to the Reset Packet (PBL-14099), a potential buffer overrun
//! situation got introduced accidentally. This test is a white-box test to catch this issue.
//! For the Reset Packet, a buffer needs to be allocated. The size of this buffer is based upon
//! the MTU of the connection. It's possible the lookup fails and returns 0. In this case, the
//! packet shouldn't be attempted to be written at all, because it will not fit and overrun the
//! buffer.
void test_ppogatt__mtu_zero_due_to_service_rediscovery_while_resetting(void) {
ppogatt_handle_service_discovered(s_characteristics[0]);
ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
(const uint8_t *) &s_meta_v0_system,
sizeof(s_meta_v0_system),
BLEGATTErrorSuccess);
// Expect subscribe request was made:
fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications,
GAPLEClientKernel);
// During service re-discovery the cached characteristic handles will be stale for a brief period.
// This will cause the gatt_client_characteristic_get_device to return BT_DEVICE_INTERNAL_INVALID
// and eventually gap_le_connection_get_gatt_mtu call to return 0. See PBL-22038.
s_mtu_size = 0;
// Simulate getting the subscription confirmation, this will normally trigger PPoGATT to try to
// write out the Reset packet, but because the MTU is couldn't be looked up, no packet should get
// sent out:
ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect nothing to be sent out by FW:
fake_gatt_client_op_assert_no_write();
// No crash nor DUMA failures
}
void test_ppogatt__unsubcribe_when_no_memory_for_comm_session(void) {
// TODO
}