diff --git a/CODEOWNERS b/CODEOWNERS index 8f4482182e..06f6736e1b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,6 +92,7 @@ /tests/lib/ble_adv/ @nrfconnect/ncs-bm-test /tests/lib/bm_storage/ @nrfconnect/ncs-bm /tests/lib/bm_timer/ @nrfconnect/ncs-bm @nrfconnect/ncs-bm-test +/tests/subsys/bluetooth/services/ble_hrs/ @nrfconnect/ncs-bm-test # Zephyr module /zephyr/ @nrfconnect/ncs-co-build-system diff --git a/tests/subsys/bluetooth/services/ble_hrs/CMakeLists.txt b/tests/subsys/bluetooth/services/ble_hrs/CMakeLists.txt new file mode 100644 index 0000000000..e6a965ad54 --- /dev/null +++ b/tests/subsys/bluetooth/services/ble_hrs/CMakeLists.txt @@ -0,0 +1,39 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(unit_test_ble_hrs) + +set(SOFTDEVICE_VARIANT "s115") +set(SOFTDEVICE_INCLUDE_DIR "${ZEPHYR_NRF_BM_MODULE_DIR}/components/softdevice/\ +${SOFTDEVICE_VARIANT}/${SOFTDEVICE_VARIANT}_API/include") + +cmock_handle(${SOFTDEVICE_INCLUDE_DIR}/ble_gatts.h) + +target_compile_definitions(app PRIVATE + NRF54L15_XXAA + SVCALL_AS_NORMAL_FUNCTION + SUPPRESS_INLINE_IMPLEMENTATION + CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT=1 + CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS=20 + CONFIG_NRF_SDH_BLE_GATT_MAX_MTU_SIZE=23 +) + +target_include_directories(app PRIVATE + ${SOFTDEVICE_INCLUDE_DIR} + ${ZEPHYR_NRF_BM_MODULE_DIR}/include + ${ZEPHYR_HAL_NORDIC_MODULE_DIR}/nrfx/mdk +) + +# Generate and add test file +test_runner_generate(src/unity_test.c) +target_sources(app PRIVATE src/unity_test.c) + +# Unit under test +target_sources(app PRIVATE ${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/ble_hrs/hrs.c) diff --git a/tests/subsys/bluetooth/services/ble_hrs/prj.conf b/tests/subsys/bluetooth/services/ble_hrs/prj.conf new file mode 100644 index 0000000000..a8788847da --- /dev/null +++ b/tests/subsys/bluetooth/services/ble_hrs/prj.conf @@ -0,0 +1,6 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +CONFIG_UNITY=y diff --git a/tests/subsys/bluetooth/services/ble_hrs/src/unity_test.c b/tests/subsys/bluetooth/services/ble_hrs/src/unity_test.c new file mode 100644 index 0000000000..76a22d9c5a --- /dev/null +++ b/tests/subsys/bluetooth/services/ble_hrs/src/unity_test.c @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ +#include + +#include +#include +#include +#include + +#include +#include + +#include "cmock_ble_gatts.h" + +void ble_hrs_evt_handler(struct ble_hrs *hrs, const struct ble_hrs_evt *evt) +{ +} + +void ble_hrs_on_ble_evt(const ble_evt_t *evt, struct ble_hrs *hrs); + +uint32_t stub_sd_ble_gatts_hvx( + uint16_t conn_handle, const ble_gatts_hvx_params_t *p_hvx_params, int cmock_num_calls) +{ + if (p_hvx_params && p_hvx_params->p_len) { + *(p_hvx_params->p_len) = 0; + } + return NRF_SUCCESS; +} + +uint32_t stub_sd_ble_gatts_characteristic_add( + uint16_t service_handle, + const ble_gatts_char_md_t *p_char_md, + const ble_gatts_attr_t *p_attr_char_value, + ble_gatts_char_handles_t *p_handles, + int cmock_num_calls) +{ + TEST_ASSERT_NOT_NULL(p_char_md); + TEST_ASSERT_NOT_NULL(p_attr_char_value); + TEST_ASSERT_NOT_NULL(p_handles); + if (p_attr_char_value->p_uuid->uuid == BLE_UUID_HEART_RATE_MEASUREMENT_CHAR) { + TEST_ASSERT_EQUAL(BLE_GATTS_VLOC_STACK, p_char_md->p_cccd_md->vloc); + TEST_ASSERT_TRUE(p_char_md->char_props.notify); + TEST_ASSERT_TRUE(p_attr_char_value->p_attr_md->vlen); + TEST_ASSERT_GREATER_OR_EQUAL(0, p_attr_char_value->init_len); + TEST_ASSERT_TRUE(p_attr_char_value->max_len); + } else if (p_attr_char_value->p_uuid->uuid == BLE_UUID_BODY_SENSOR_LOCATION_CHAR) { + TEST_ASSERT_TRUE(p_char_md->char_props.read); + TEST_ASSERT_EQUAL(sizeof(uint8_t), p_attr_char_value->init_len); + TEST_ASSERT_EQUAL(sizeof(uint8_t), p_attr_char_value->max_len); + TEST_ASSERT_EQUAL(BLE_GATTS_VLOC_STACK, p_attr_char_value->p_attr_md->vloc); + } else { + TEST_FAIL_MESSAGE("Unexpected UUID in characteristic add stub"); + } + TEST_ASSERT_EQUAL(BLE_UUID_TYPE_BLE, p_attr_char_value->p_uuid->type); + TEST_ASSERT_NOT_NULL(p_attr_char_value->p_value); + + return NRF_SUCCESS; +} + +void test_ble_hrs_rr_interval_add(void) +{ + struct ble_hrs hrs = { + .evt_handler = ble_hrs_evt_handler, + .service_handle = 0, + .conn_handle = BLE_CONN_HANDLE_INVALID, + .rr_interval_count = 0, + .max_hrm_len = 0, + .is_sensor_contact_supported = false, + .is_sensor_contact_detected = false + }; + int err; + + /* Initialize the heart rate service. */ + memset(&hrs, 0, sizeof(hrs)); + + /* Add RR interval measurement. */ + err = ble_hrs_rr_interval_add(&hrs, 100); + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_EQUAL(1, hrs.rr_interval_count); + TEST_ASSERT_EQUAL(100, hrs.rr_interval[0]); + + /* Add another RR interval measurement. */ + err = ble_hrs_rr_interval_add(&hrs, 200); + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_EQUAL(2, hrs.rr_interval_count); + TEST_ASSERT_EQUAL(200, hrs.rr_interval[1]); + + /* Add a third RR interval measurement. */ + err = ble_hrs_rr_interval_add(&hrs, 300); + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_EQUAL(3, hrs.rr_interval_count); + TEST_ASSERT_EQUAL(300, hrs.rr_interval[2]); +} + +void test_ble_hrs_rr_interval_add_efault(void) +{ + /* Try to use null for hrs struct */ + int err = ble_hrs_rr_interval_add(NULL, 0); + + TEST_ASSERT_EQUAL(-EFAULT, err); +} + +void test_ble_hrs_rr_interval_add_overflow(void) +{ + struct ble_hrs hrs = { + .max_hrm_len = CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS * sizeof(uint16_t) + 1 + }; + uint16_t rr_interval_second = 0; + int err; + + /* Fill the buffer to max */ + for (int i = 0; i < CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS; i++) { + err = ble_hrs_rr_interval_add(&hrs, i + 1); + TEST_ASSERT_EQUAL(0, err); + } + rr_interval_second = hrs.rr_interval[1]; + + /* Now adding one more should remove the first one and add new to last*/ + err = ble_hrs_rr_interval_add(&hrs, 999); + + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_EQUAL(CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS, hrs.rr_interval_count); + TEST_ASSERT_EQUAL(999, hrs.rr_interval[CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS - 1]); + TEST_ASSERT_EQUAL(rr_interval_second, hrs.rr_interval[0]); +} + +void test_ble_hrs_rr_interval_buffer_is_full(void) +{ + struct ble_hrs hrs = {0}; + int err; + + /* Check if buffer is empty */ + TEST_ASSERT_FALSE(ble_hrs_rr_interval_buffer_is_full(&hrs)); + + /* Fill the buffer to max */ + for (int i = 0; i < CONFIG_BLE_HRS_MAX_BUFFERED_RR_INTERVALS; i++) { + err = ble_hrs_rr_interval_add(&hrs, i + 1); + TEST_ASSERT_EQUAL(0, err); + } + + /* Check if buffer is full */ + TEST_ASSERT_TRUE(ble_hrs_rr_interval_buffer_is_full(&hrs)); +} + +void test_ble_hrs_sensor_contact_supported_set(void) +{ + struct ble_hrs hrs = {0}; + int err; + + hrs.conn_handle = BLE_CONN_HANDLE_INVALID; + /* Set sensor contact supported to true */ + err = ble_hrs_sensor_contact_supported_set(&hrs, true); + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_TRUE(hrs.is_sensor_contact_supported); +} + +void test_ble_hrs_sensor_contact_supported_set_eisconn(void) +{ + struct ble_hrs hrs = {0}; + int err; + + /* Try to set sensor contact supported while in connection + * Simulate being in a connection + */ + const ble_evt_t evt = { + .header.evt_id = BLE_GAP_EVT_CONNECTED + }; + ble_hrs_on_ble_evt(&evt, &hrs); + err = ble_hrs_sensor_contact_supported_set(&hrs, true); + TEST_ASSERT_EQUAL(-EISCONN, err); +} + +void test_ble_hrs_sensor_contact_supported_set_efault(void) +{ + int err; + + /* Try to use null for hrs struct */ + err = ble_hrs_sensor_contact_supported_set(NULL, false); + TEST_ASSERT_EQUAL(-EFAULT, err); +} + +void test_ble_hrs_sensor_contact_detected_update(void) +{ + struct ble_hrs hrs = {0}; + int err; + + /* Update sensor contact detected state */ + err = ble_hrs_sensor_contact_detected_update(&hrs, true); + TEST_ASSERT_EQUAL(0, err); + TEST_ASSERT_TRUE(hrs.is_sensor_contact_detected); +} + +void test_ble_hrs_body_sensor_location_set(void) +{ + struct ble_hrs hrs = {0}; + uint8_t body_sensor_location = BLE_HRS_BODY_SENSOR_LOCATION_FINGER; + int err; + + __cmock_sd_ble_gatts_value_set_ExpectAndReturn( + hrs.conn_handle, hrs.bsl_handles.value_handle, NULL, NRF_SUCCESS); + __cmock_sd_ble_gatts_value_set_IgnoreArg_p_value(); + err = ble_hrs_body_sensor_location_set(&hrs, body_sensor_location); + TEST_ASSERT_EQUAL(0, err); + + __cmock_sd_ble_gatts_value_set_ExpectAndReturn( + hrs.conn_handle, hrs.bsl_handles.value_handle, NULL, NRF_ERROR_INVALID_ADDR); + __cmock_sd_ble_gatts_value_set_IgnoreArg_p_value(); + err = ble_hrs_body_sensor_location_set(&hrs, body_sensor_location); + TEST_ASSERT_EQUAL(-EINVAL, err); + + /* Try to use null for hrs struct */ + err = ble_hrs_body_sensor_location_set(NULL, body_sensor_location); + TEST_ASSERT_EQUAL(-EFAULT, err); +} + +void test_ble_hrs_heart_rate_measurement_send_enotconn(void) +{ + struct ble_hrs hrs = { + .evt_handler = ble_hrs_evt_handler, + .service_handle = 0, + .conn_handle = BLE_CONN_HANDLE_INVALID, + .rr_interval_count = 2, + .max_hrm_len = 0, + .is_sensor_contact_supported = true, + .is_sensor_contact_detected = false + }; + uint16_t heart_rate_measurement = 72; + int err; + + __cmock_sd_ble_gatts_hvx_IgnoreAndReturn(BLE_ERROR_INVALID_CONN_HANDLE); + err = ble_hrs_heart_rate_measurement_send(&hrs, heart_rate_measurement); + TEST_ASSERT_EQUAL(-ENOTCONN, err); +} + +void test_ble_hrs_heart_rate_measurement_send_epipe(void) +{ + struct ble_hrs hrs = { + .evt_handler = ble_hrs_evt_handler, + .service_handle = 0, + .conn_handle = BLE_CONN_HANDLE_INVALID, + .rr_interval_count = 2, + .max_hrm_len = 0, + .is_sensor_contact_supported = true, + .is_sensor_contact_detected = false + }; + uint16_t heart_rate_measurement = 72; + int err; + + __cmock_sd_ble_gatts_hvx_ExpectAndReturn(hrs.conn_handle, NULL, NRF_ERROR_INVALID_STATE); + __cmock_sd_ble_gatts_hvx_IgnoreArg_p_hvx_params(); + err = ble_hrs_heart_rate_measurement_send(&hrs, heart_rate_measurement); + TEST_ASSERT_EQUAL(-EPIPE, err); +} + +void test_ble_hrs_heart_rate_measurement_send_einval(void) +{ + struct ble_hrs hrs = { + .evt_handler = ble_hrs_evt_handler, + .service_handle = 0, + .conn_handle = BLE_CONN_HANDLE_INVALID, + .rr_interval_count = 2, + .max_hrm_len = 0, + .is_sensor_contact_supported = true, + .is_sensor_contact_detected = false + }; + uint16_t heart_rate_measurement = 72; + int err; + + __cmock_sd_ble_gatts_hvx_ExpectAndReturn(hrs.conn_handle, NULL, NRF_ERROR_BUSY); + __cmock_sd_ble_gatts_hvx_IgnoreArg_p_hvx_params(); + err = ble_hrs_heart_rate_measurement_send(&hrs, heart_rate_measurement); + TEST_ASSERT_EQUAL(-EINVAL, err); + + /* Set up the stub to manipulate p_len */ + __cmock_sd_ble_gatts_hvx_Stub(stub_sd_ble_gatts_hvx); + err = ble_hrs_heart_rate_measurement_send(&hrs, heart_rate_measurement); + TEST_ASSERT_EQUAL(-EINVAL, err); +} + +void test_ble_hrs_heart_rate_measurement_send_efault(void) +{ + uint16_t heart_rate_measurement = 72; + int err; + + err = ble_hrs_heart_rate_measurement_send(NULL, heart_rate_measurement); + TEST_ASSERT_EQUAL(-EFAULT, err); + +} + +void test_ble_hrs_init_efault(void) +{ + int err; + + err = ble_hrs_init(NULL, NULL); + TEST_ASSERT_EQUAL(-EFAULT, err); +} + +void test_ble_hrs_init_einval(void) +{ + struct ble_hrs hrs = {0}; + struct ble_hrs_config hrs_config = { + .evt_handler = ble_hrs_evt_handler, + .is_sensor_contact_supported = true, + .body_sensor_location = (uint8_t[]){ BLE_HRS_BODY_SENSOR_LOCATION_FINGER } + }; + int err; + + __cmock_sd_ble_gatts_service_add_IgnoreAndReturn(NRF_ERROR_INVALID_ADDR); + err = ble_hrs_init(&hrs, &hrs_config); + TEST_ASSERT_EQUAL(-EINVAL, err); +} + +void test_ble_hrs_init(void) +{ + struct ble_hrs hrs = {0}; + struct ble_hrs_config hrs_config = { + .evt_handler = ble_hrs_evt_handler, + .is_sensor_contact_supported = true, + .body_sensor_location = (uint8_t[]){ BLE_HRS_BODY_SENSOR_LOCATION_FINGER } + }; + int err; + + __cmock_sd_ble_gatts_service_add_IgnoreAndReturn(NRF_SUCCESS); + __cmock_sd_ble_gatts_characteristic_add_Stub(stub_sd_ble_gatts_characteristic_add); + err = ble_hrs_init(&hrs, &hrs_config); + TEST_ASSERT_EQUAL(0, err); +} + +extern int unity_main(void); + +int main(void) +{ + return unity_main(); +} diff --git a/tests/subsys/bluetooth/services/ble_hrs/testcase.yaml b/tests/subsys/bluetooth/services/ble_hrs/testcase.yaml new file mode 100644 index 0000000000..e8b5db8421 --- /dev/null +++ b/tests/subsys/bluetooth/services/ble_hrs/testcase.yaml @@ -0,0 +1,4 @@ +tests: + lib.ble_hrs: + platform_allow: native_sim + tags: unittest