/* * 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/ams/ams.h" #include "comm/bt_conn_mgr.h" #include "services/normal/music.h" #include "services/normal/music_internal.h" #include "clar.h" // Stubs & Fakes /////////////////////////////////////////////////////////// #include "fake_events.h" #include "fake_gatt_client_operations.h" #include "fake_gatt_client_subscriptions.h" #include "fake_pebble_tasks.h" #include "fake_rtc.h" #include "stubs_app_install_manager.h" #include "stubs_app_manager.h" #include "stubs_bt_lock.h" #include "stubs_hexdump.h" #include "stubs_logging.h" #include "stubs_mutex.h" #include "stubs_passert.h" #include "stubs_pbl_malloc.h" #include "stubs_serial.h" #include "stubs_tick.h" void analytics_event_ams(uint8_t type, int32_t aux_info) { } struct { ResponseTimeState state; uint16_t max_period_secs; } s_conn_mgr_states[NumBtConsumer]; void conn_mgr_set_ble_conn_response_time(GAPLEConnection *hdl, BtConsumer consumer, ResponseTimeState state, uint16_t max_period_secs) { s_conn_mgr_states[consumer].state = state; s_conn_mgr_states[consumer].max_period_secs = max_period_secs; } GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) { return NULL; } BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) { return (BTDeviceInternal) { .address.octets = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, }, }; } static void (*s_launcher_task_callback)(void *data); static void *s_launcher_task_callback_data; void launcher_task_add_callback(void (*callback)(void *data), void *data) { // Simple fake, can only handle one call cl_assert_equal_p(s_launcher_task_callback, NULL); s_launcher_task_callback = callback; s_launcher_task_callback_data = data; } // Tests: Disover AMS /////////////////////////////////////////////////////////// #define NUM_AMS_INSTANCES 2 static BLECharacteristic s_characteristics[NUM_AMS_INSTANCES][NumAMSCharacteristic] = { // AMS instance one: [0] = { [AMSCharacteristicRemoteCommand] = 1, [AMSCharacteristicEntityUpdate] = 2, [AMSCharacteristicEntityAttribute] = 3, }, // AMS instance two: [1] = { [AMSCharacteristicRemoteCommand] = 4, [AMSCharacteristicEntityUpdate] = 5, [AMSCharacteristicEntityAttribute] = 6, }, }; static const BLECharacteristic s_unknown_characteristic = 999; static void prv_assert_can_handle_characteristics(uint32_t instance_idx, bool expect_can_handle) { for (AMSCharacteristic c = 0; c < NumAMSCharacteristic; ++c) { cl_assert_equal_b(ams_can_handle_characteristic(s_characteristics[instance_idx][c]), expect_can_handle); } } static void prv_discover_ams(uint8_t num_instances) { cl_assert(num_instances <= 2); if (num_instances) { for (int i = 0; i < num_instances && i < NUM_AMS_INSTANCES; i++) { ams_handle_service_discovered(&s_characteristics[i][0]); } } else { ams_invalidate_all_references(); } } void test_ams__cannot_handle_any_characteristic_after_destroy(void) { prv_discover_ams(1); ams_destroy(); prv_assert_can_handle_characteristics(0, false /* expect_can_handle */); } void test_ams__cannot_handle_unknown_characteristic(void) { prv_discover_ams(1); cl_assert_equal_b(ams_can_handle_characteristic(s_unknown_characteristic), false); } void test_ams__discover_of_ams_should_subscribe_to_entity_update_characteristic(void) { // Pass in 2 instances, it should be able to cope with this prv_discover_ams(2 /* num_instances */); // Assert ams.c can now handle the characteristic reference for the first instance: prv_assert_can_handle_characteristics(0, true /* expect_can_handle */); // Assert ams.c can not handle the characteristic reference for the second instance: prv_assert_can_handle_characteristics(1, false /* expect_can_handle */); // The first instance is expected to be used. BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; fake_gatt_client_subscriptions_assert_subscribe(entity_update, BLESubscriptionNotifications, GAPLEClientKernel); } void test_ams__connect_to_music_service_upon_subscribing_entity_update_characteristic(void) { prv_discover_ams(1 /* num_instances */); // Not connected yet (still need to subscribe): cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); // Music service should be connected now: cl_assert_equal_s(music_get_connected_server_debug_name(), ams_music_server_debug_name()); // Rediscovery will disconnect music service (until resubscribed): ams_invalidate_all_references(); prv_discover_ams(1 /* num_instances */); cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); } void test_ams__dont_connect_music_service_if_subscribe_entity_update_characteristic_fails(void) { prv_discover_ams(1 /* num_instances */); // Simulate failed subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorUnlikelyError); // Not connected because subscription failed: cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); } void test_ams__update_characteristics_ams_not_found(void) { // AMS Found: prv_discover_ams(1 /* num_instances */); // AMS Disappeared: prv_discover_ams(0 /* num_instances */); // Verify ams.c cannot handle the previous characteristics any more: prv_assert_can_handle_characteristics(0, false /* expect_can_handle */); } // Tests: Register for Entity Updates /////////////////////////////////////////////////////////// static const uint8_t s_register_player_entity[] = {0x00, // Apple bug #21283910 // See ams.c, prv_get_registration_cmd_for_entity // 0x00, 0x01, 0x02}; static const uint8_t s_register_queue_entity[] = {0x01, 0x00, 0x01, 0x02, 0x03}; static const uint8_t s_register_track_entity[] = {0x02, 0x00, 0x01, 0x02, 0x03}; void test_ams__register_for_entity_updates(void) { prv_discover_ams(1 /* num_instances */); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); // Expect to have written the command to register for the Player entity: fake_gatt_client_op_assert_write(entity_update, s_register_player_entity, sizeof(s_register_player_entity), GAPLEClientKernel, true /* is_response_required */); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false); // Expect to have written the command to register for the Queue entity: fake_gatt_client_op_assert_write(entity_update, s_register_queue_entity, sizeof(s_register_queue_entity), GAPLEClientKernel, true /* is_response_required */); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false); // Expect to have written the command to register for the Track entity: fake_gatt_client_op_assert_write(entity_update, s_register_track_entity, sizeof(s_register_track_entity), GAPLEClientKernel, true /* is_response_required */); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true); // After GATT re-discovery, the registration needs to happen again ams_invalidate_all_references(); prv_discover_ams(1 /* num_instances */); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false); } void test_ams__register_for_entity_updates_retry_if_out_of_resources(void) { prv_discover_ams(1 /* num_instances */); // Simulate not having enough resources to process the request: fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); // Nothing should have been written because out of resources: fake_gatt_client_op_assert_no_write(); // Resources come available again: fake_gatt_client_op_set_write_return_value(BTErrnoOK); // Simulate processing the callback to retry: cl_assert(s_launcher_task_callback != NULL); s_launcher_task_callback(s_launcher_task_callback_data); // Expect to have written the command to register for the Player entity: fake_gatt_client_op_assert_write(entity_update, s_register_player_entity, sizeof(s_register_player_entity), GAPLEClientKernel, true /* is_response_required */); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); } static const MusicServerImplementation s_dummy_server_implementation = {}; static void prv_set_dummy_server_connected(bool connected) { music_set_connected_server(&s_dummy_server_implementation, connected /* connected */); } void test_ams__dont_register_if_another_music_server_is_already_connected(void) { prv_discover_ams(1 /* num_instances */); // Not connected yet (still need to subscribe): cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); prv_set_dummy_server_connected(true /* connected */); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); // Nothing should have been written, because there's already a music server connected. fake_gatt_client_op_assert_no_write(); // Clean up after ourselves: prv_set_dummy_server_connected(false /* connected */); } // Tests: Sending Remote Commands /////////////////////////////////////////////////////////// static void prv_connect_ams(void) { prv_discover_ams(1 /* num_instances */); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); // Simulate successful write responses (for the Entity Update registration write requests): ams_handle_write_response(entity_update, BLEGATTErrorSuccess); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); ams_handle_write_response(entity_update, BLEGATTErrorSuccess); cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true); fake_gatt_client_op_clear_write_list(); } static uint8_t prv_ams_command_for_music_command(MusicCommand command) { switch (command) { case MusicCommandPlay: return 0x00; case MusicCommandPause: return 0x01; case MusicCommandTogglePlayPause: return 0x02; case MusicCommandNextTrack: return 0x03; case MusicCommandPreviousTrack: return 0x04; case MusicCommandVolumeUp: return 0x05; case MusicCommandVolumeDown: return 0x06; case MusicCommandAdvanceRepeatMode: return 0x07; case MusicCommandAdvanceShuffleMode: return 0x08; case MusicCommandSkipForward: return 0x09; case MusicCommandSkipBackward: return 0x0A; case MusicCommandLike: return 0x0B; case MusicCommandDislike: return 0x0C; case MusicCommandBookmark: return 0x0D; default: cl_assert(false); return 0xFF; } } void test_ams__send_remote_command(void) { prv_connect_ams(); // Exercise all MusicCommand types (currently they are all supported): for (MusicCommand music_cmd = 0; music_cmd < NumMusicCommand; ++music_cmd) { music_command_send(music_cmd); BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand]; const uint8_t ams_cmd = prv_ams_command_for_music_command(music_cmd); fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd), GAPLEClientKernel, true /* is_response_required */); // Simulate receiving the response: ams_handle_write_response(remote_command, BLEGATTErrorSuccess); } // Invalid/Unsupported command: music_command_send(NumMusicCommand); fake_gatt_client_op_assert_no_write(); } void test_ams__send_remote_command_non_kernel_main_task(void) { prv_connect_ams(); // Simulate Music app calling music_command_send(): stub_pebble_tasks_set_current(PebbleTask_App); music_command_send(MusicCommandPlay); // Process the KernelMain callback: cl_assert(s_launcher_task_callback != NULL); stub_pebble_tasks_set_current(PebbleTask_KernelMain); s_launcher_task_callback(s_launcher_task_callback_data); BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand]; const uint8_t ams_cmd = prv_ams_command_for_music_command(MusicCommandPlay); fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd), GAPLEClientKernel, true /* is_response_required */); } void test_ams__send_remote_command_non_kernel_main_task_then_disconnect(void) { prv_connect_ams(); // Simulate Music app calling music_command_send(): stub_pebble_tasks_set_current(PebbleTask_App); music_command_send(MusicCommandPlay); // Simulate disconnecting: ams_destroy(); // Process the KernelMain callback: cl_assert(s_launcher_task_callback != NULL); stub_pebble_tasks_set_current(PebbleTask_KernelMain); s_launcher_task_callback(s_launcher_task_callback_data); // No crashes, no writes fake_gatt_client_op_assert_no_write(); } // Tests: music_needs_user_to_start_playback_on_phone /////////////////////////////////////////////////////////// void test_ams__music_needs_user_to_start_playback_on_phone(void) { prv_connect_ams(); // No metadata received yet, user needs to start playback on phone cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true); // Received metadata // cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false); // Metadata cleared cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true); } // Tests: music_request_..._latency /////////////////////////////////////////////////////////// void test_ams__music_request_reduced_latency(void) { prv_connect_ams(); music_request_reduced_latency(true /* reduced_latency */); cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state, ResponseTimeMiddle); cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].max_period_secs, MAX_PERIOD_RUN_FOREVER); music_request_reduced_latency(false /* reduced_latency */); cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state, ResponseTimeMax); } void test_ams__music_request_low_latency_for_period(void) { prv_connect_ams(); const uint32_t period_s = 1234; music_request_low_latency_for_period(period_s * 1000); cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].state, ResponseTimeMin); cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].max_period_secs, period_s); } // Tests: Receiving Player updates (the happy paths) /////////////////////////////////////////////////////////// static void prv_receive_entity_update(const uint8_t *update, uint16_t update_length) { BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_read_or_notification(entity_update, update, update_length, BLEGATTErrorSuccess); } void test_ams__receive_player_name_update(void) { prv_connect_ams(); // 0000 00 00 00 4d 75 73 69 63 ...Music uint8_t player_name_update[] = { 0x00, 0x00, 0x00, 0x4d, 0x75, 0x73, 0x69, 0x63, }; prv_receive_entity_update(player_name_update, sizeof(player_name_update)); char player_name[MUSIC_BUFFER_LENGTH]; cl_assert_equal_b(music_get_player_name(player_name), true); cl_assert_equal_s(player_name, "Music"); } void test_ams__receive_player_playback_info_update(void) { prv_connect_ams(); // Receive: playing, 200% playback rate, elapsed time 184.755s // 0000 00 01 00 31 2c 32 2e 30 2c 31 38 34 2e 37 35 35 ...1,2.0 ,184.755 uint8_t playback_info_update[] = { 0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30, 0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35, }; prv_receive_entity_update(playback_info_update, sizeof(playback_info_update)); cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePlaying); // music_get_pos relies on having a sensible track duration, so simulate receiving this too: // 02 03 00 33 31 39 2e 35 30 37 ...319.5 07 uint8_t track_duration_update[] = { 0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35, 0x30, 0x37, }; prv_receive_entity_update(track_duration_update, sizeof(track_duration_update)); uint32_t track_pos_ms; uint32_t track_duration_ms; music_get_pos(&track_pos_ms, &track_duration_ms); cl_assert_equal_i(track_pos_ms, 184755); cl_assert_equal_i(track_duration_ms, 319507); cl_assert_equal_i(music_get_playback_rate_percent(), 200); } void test_ams__receive_player_playback_info_update_nulls(void) { prv_connect_ams(); // Receive: paused, empty string, empty string // 0000 00 01 00 30 2c 2c ...0,, uint8_t playback_info_update[] = { 0x00, 0x01, 0x00, 0x30, 0x2c, 0x2c, }; prv_receive_entity_update(playback_info_update, sizeof(playback_info_update)); cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePaused); uint32_t track_pos_ms; uint32_t track_duration_ms; music_get_pos(&track_pos_ms, &track_duration_ms); cl_assert_equal_i(track_pos_ms, 0); cl_assert_equal_i(track_duration_ms, 0); cl_assert_equal_i(music_get_playback_rate_percent(), 0); } void test_ams__receive_player_volume_update(void) { prv_connect_ams(); // Receive volume of 0.604925 // 00 02 00 30 2e 36 30 34 39 32 35 ...0.604 925 uint8_t volume_update[] = { 0x00, 0x02, 0x00, 0x30, 0x2e, 0x36, 0x30, 0x34, 0x39, 0x32, 0x35, }; prv_receive_entity_update(volume_update, sizeof(volume_update)); cl_assert_equal_i(music_get_volume_percent(), 60); } // Tests: Receiving Player updates (the unhappy paths) /////////////////////////////////////////////////////////// static void prv_assert_initial_playback_state(void) { cl_assert_equal_i(music_get_playback_state(), MusicPlayStateUnknown); uint32_t track_pos_ms; uint32_t track_duration_ms; music_get_pos(&track_pos_ms, &track_duration_ms); cl_assert_equal_i(track_pos_ms, 0); cl_assert_equal_i(track_duration_ms, 0); cl_assert_equal_i(music_get_playback_rate_percent(), 0); } void test_ams__receive_non_numeric_player_playback_info_update(void) { prv_connect_ams(); // Receive: 'A', 'B.0' playback rate, elapsed time 184.755s // 0000 00 01 00 41 2c 42 2e 30 2c 31 38 34 2e 37 35 35 ...A,B.0 ,184.755 uint8_t nan_playback_info_update[] = { 0x00, 0x01, 0x00, 0x41, 0x2c, 0x42, 0x2e, 0x30, 0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35, }; prv_receive_entity_update(nan_playback_info_update, sizeof(nan_playback_info_update)); prv_assert_initial_playback_state(); } void test_ams__receive_incomplete_csv_list_player_playback_info_update(void) { prv_connect_ams(); // Receive: playing, 200% playback rate // 0000 00 01 00 31 2c 32 2e 30 ...1,2.0 uint8_t incomplete_playback_info_update[] = { 0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30, }; prv_receive_entity_update(incomplete_playback_info_update, sizeof(incomplete_playback_info_update)); prv_assert_initial_playback_state(); } void test_ams__receive_malformed_player_volume_update(void) { prv_connect_ams(); cl_assert_equal_i(music_get_volume_percent(), 0); // Receive volume of 0.604925 // 00 02 00 30 2e 41 30 34 39 32 35 ...0.A04 925 uint8_t volume_update[] = { 0x00, 0x02, 0x00, 0x30, 0x2e, 0x41, 0x30, 0x34, 0x39, 0x32, 0x35, }; prv_receive_entity_update(volume_update, sizeof(volume_update)); cl_assert_equal_i(music_get_volume_percent(), 0); } // Tests: Receiving Track updates /////////////////////////////////////////////////////////// void test_ams__receive_track_artist_update(void) { prv_connect_ams(); // 0000 02 00 00 4d 69 6c 65 73 20 44 61 76 69 73 ...Miles Davis uint8_t track_artist_update[] = { 0x02, 0x00, 0x00, 0x4d, 0x69, 0x6c, 0x65, 0x73, 0x20, 0x44, 0x61, 0x76, 0x69, 0x73, }; prv_receive_entity_update(track_artist_update, sizeof(track_artist_update)); char artist[MUSIC_BUFFER_LENGTH]; music_get_now_playing(NULL, artist, NULL); cl_assert_equal_s(artist, "Miles Davis"); } void test_ams__receive_track_title_update(void) { prv_connect_ams(); // 0000 02 02 00 53 6f 20 57 68 61 74 ...So Wh at uint8_t track_title_update[] = { 0x02, 0x02, 0x00, 0x53, 0x6f, 0x20, 0x57, 0x68, 0x61, 0x74, }; prv_receive_entity_update(track_title_update, sizeof(track_title_update)); char title[MUSIC_BUFFER_LENGTH]; music_get_now_playing(title, NULL, NULL); cl_assert_equal_s(title, "So What"); } void test_ams__receive_track_album_update(void) { prv_connect_ams(); // 0000 02 01 00 4b 69 6e 64 20 4f 66 20 42 6c 75 65 20 ...Kind Of Blue // 0010 28 4c 65 67 61 63 79 20 45 64 69 74 69 6f 6e 29 (Legacy Edition) uint8_t track_album_update[] = { 0x02, 0x01, 0x00, 0x4b, 0x69, 0x6e, 0x64, 0x20, 0x4f, 0x66, 0x20, 0x42, 0x6c, 0x75, 0x65, 0x20, 0x28, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, }; prv_receive_entity_update(track_album_update, sizeof(track_album_update)); char album[MUSIC_BUFFER_LENGTH]; music_get_now_playing(NULL, NULL, album); cl_assert_equal_s(album, "Kind Of Blue (Legacy Edition)"); } // Tests: Music service capabilities /////////////////////////////////////////////////////////// void test_ams__supported_capabilities(void) { prv_connect_ams(); // music_is_progress_reporting_supported() relies on a valid track duration uint8_t track_duration_update[] = { 0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35, 0x30, 0x37, }; prv_receive_entity_update(track_duration_update, sizeof(track_duration_update)); cl_assert_equal_b(music_is_playback_state_reporting_supported(), true); cl_assert_equal_b(music_is_progress_reporting_supported(), true); cl_assert_equal_b(music_is_volume_reporting_supported(), true); cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true); for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) { cl_assert_equal_b(music_is_command_supported(cmd), true); } cl_assert_equal_b(music_is_command_supported(NumMusicCommand), false); } // Tests: Create & Destroy /////////////////////////////////////////////////////////// void test_ams__create_again_trips_assert(void) { cl_assert_passert(ams_create()); } void test_ams__create_works_again_after_destroy(void) { ams_destroy(); ams_create(); // No assert hit. } void test_ams__destroy_after_destroy_is_fine(void) { ams_destroy(); ams_destroy(); // No assert hit. } void test_ams__destroy_disconnects_from_music_service(void) { prv_discover_ams(1 /* num_instances */); // Simulate successful subscription: BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate]; ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess); ams_destroy(); cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); } // Test setup /////////////////////////////////////////////////////////// void test_ams__initialize(void) { s_launcher_task_callback = NULL; memset(s_conn_mgr_states, 0, sizeof(s_conn_mgr_states)); fake_rtc_init(1234, 5678); fake_event_init(); fake_gatt_client_subscriptions_init(); fake_gatt_client_op_init(); stub_pebble_tasks_set_current(PebbleTask_KernelMain); ams_create(); cl_assert_equal_s(music_get_connected_server_debug_name(), NULL); } void test_ams__cleanup(void) { ams_destroy(); fake_gatt_client_op_deinit(); fake_gatt_client_subscriptions_deinit(); }