Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions tests/subsys/bluetooth/services/ble_hrs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions tests/subsys/bluetooth/services/ble_hrs/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#
# Copyright (c) 2025 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
CONFIG_UNITY=y
337 changes: 337 additions & 0 deletions tests/subsys/bluetooth/services/ble_hrs/src/unity_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
/*
* Copyright (c) 2025 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#include <unity.h>

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include <bluetooth/services/ble_hrs.h>
#include <bluetooth/services/uuid.h>

#include "cmock_ble_gatts.h"

void ble_hrs_evt_handler(struct ble_hrs *hrs, const struct ble_hrs_evt *evt)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good with some tests on what is received here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some tests for ble_hrs_conn_params_evt should also improve the coverage.

Image

}

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));
Comment on lines +65 to +77
Copy link
Contributor

Choose a reason for hiding this comment

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

First you initialize hrs to something and then you zero it, which one do you want to do?


/* 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);
Comment on lines +233 to +235
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too

}

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);
Comment on lines +252 to +255
Copy link
Contributor

Choose a reason for hiding this comment

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

All these are groups in this function are separate tests

}

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);
Comment on lines +311 to +313
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a dedicated test

}

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();
}
4 changes: 4 additions & 0 deletions tests/subsys/bluetooth/services/ble_hrs/testcase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tests:
lib.ble_hrs:
platform_allow: native_sim
tags: unittest