tests: Bluetooth: Add multi-central robustness test

This test's purpose is to verify we (as a peripheral) don't leak resources
when communicating with multiple centrals that connect and disconnect
intermittently.

Signed-off-by: Jonathan Rico <jonathan.rico@nordicsemi.no>
This commit is contained in:
Jonathan Rico 2024-06-27 09:58:33 +02:00 committed by Carles Cufí
commit 11b3aa1f1f
10 changed files with 697 additions and 0 deletions

View file

@ -11,6 +11,7 @@ set -ue
source ${ZEPHYR_BASE}/tests/bsim/compile.source
app=tests/bsim/bluetooth/host/l2cap/many_conns compile
app=tests/bsim/bluetooth/host/l2cap/multilink_peripheral compile
app=tests/bsim/bluetooth/host/l2cap/general compile
app=tests/bsim/bluetooth/host/l2cap/userdata compile
app=tests/bsim/bluetooth/host/l2cap/stress compile

View file

@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(multilink_peripheral)
add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib)
target_link_libraries(app PRIVATE testlib)
# This contains babblesim-specific helpers, e.g. device synchronization.
add_subdirectory(${ZEPHYR_BASE}/tests/bsim/babblekit babblekit)
target_link_libraries(app PRIVATE babblekit)
zephyr_include_directories(
${BSIM_COMPONENTS_PATH}/libUtilv1/src/
${BSIM_COMPONENTS_PATH}/libPhyComv1/src/
)
target_sources(app PRIVATE
src/main.c
src/dut.c
src/central.c
)

View file

@ -0,0 +1,18 @@
# Kconfig options for the test
#
# Only used as single point for log level configuration.
# Can be extended with any new kconfig, really.
#
# Copyright (c) 2024 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
menu "Test configuration"
module = APP
module-str = app
source "subsys/logging/Kconfig.template.log_config"
endmenu
source "Kconfig.zephyr"

View file

@ -0,0 +1,42 @@
#### Logs, debug, etc options
CONFIG_ASSERT=y
# More debug
CONFIG_LOG=y
CONFIG_THREAD_NAME=y
CONFIG_LOG_THREAD_ID_PREFIX=y
CONFIG_LOG_FUNC_NAME_PREFIX_DBG=y
CONFIG_LOG_FUNC_NAME_PREFIX_INF=y
CONFIG_LOG_FUNC_NAME_PREFIX_WRN=y
CONFIG_LOG_FUNC_NAME_PREFIX_ERR=y
# CONFIG_APP_LOG_LEVEL_DBG=y
# CONFIG_BT_L2CAP_LOG_LEVEL_DBG=y
# CONFIG_BT_CONN_LOG_LEVEL_DBG=y
# CONFIG_BT_HCI_DRIVER_LOG_LEVEL_DBG=y
CONFIG_ARCH_POSIX_TRAP_ON_FATAL=y
#### Test-specific options
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CENTRAL=y
# Dependency of testlib/adv and testlib/scan.
CONFIG_BT_EXT_ADV=y
CONFIG_BT_DEVICE_NAME="dee-yu-tee"
# Dynamic channel depends on SMP
CONFIG_BT_SMP=y
CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y
# Disable auto-initiated procedures so they don't
# mess with the test's execution.
CONFIG_BT_AUTO_PHY_UPDATE=n
CONFIG_BT_AUTO_DATA_LEN_UPDATE=n
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n
CONFIG_BT_MAX_CONN=2

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/att.h>
#include <zephyr/bluetooth/l2cap.h>
#include <zephyr/logging/log.h>
#include "testlib/scan.h"
#include "testlib/conn.h"
#include "babblekit/flags.h"
#include "babblekit/sync.h"
#include "babblekit/testcase.h"
/* local includes */
#include "data.h"
LOG_MODULE_REGISTER(central, CONFIG_APP_LOG_LEVEL);
static struct bt_l2cap_le_chan le_chan;
static void sent_cb(struct bt_l2cap_chan *chan)
{
TEST_FAIL("Tester should not send data");
}
static int recv_cb(struct bt_l2cap_chan *chan, struct net_buf *buf)
{
LOG_DBG("received %d bytes", buf->len);
return 0;
}
static void l2cap_chan_connected_cb(struct bt_l2cap_chan *chan)
{
LOG_DBG("%p", chan);
}
static void l2cap_chan_disconnected_cb(struct bt_l2cap_chan *chan)
{
LOG_DBG("%p", chan);
}
static int server_accept_cb(struct bt_conn *conn, struct bt_l2cap_server *server,
struct bt_l2cap_chan **chan)
{
static struct bt_l2cap_chan_ops ops = {
.connected = l2cap_chan_connected_cb,
.disconnected = l2cap_chan_disconnected_cb,
.recv = recv_cb,
.sent = sent_cb,
};
memset(&le_chan, 0, sizeof(le_chan));
le_chan.chan.ops = &ops;
*chan = &le_chan.chan;
return 0;
}
static int l2cap_server_register(bt_security_t sec_level)
{
static struct bt_l2cap_server test_l2cap_server = {.accept = server_accept_cb};
test_l2cap_server.psm = 0;
test_l2cap_server.sec_level = sec_level;
int err = bt_l2cap_server_register(&test_l2cap_server);
TEST_ASSERT(err == 0, "Failed to register l2cap server (err %d)", err);
return test_l2cap_server.psm;
}
static void acl_connected(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (err) {
LOG_ERR("Failed to connect to %s (0x%02x)", addr, err);
return;
}
LOG_DBG("Connected to %s", addr);
}
static void acl_disconnected(struct bt_conn *conn, uint8_t reason)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("Disconnected from %s (reason 0x%02x)", addr, reason);
}
/* Read the comments on `entrypoint_dut()` first. */
void entrypoint_central(void)
{
int err;
struct bt_conn *conn = NULL;
bt_addr_le_t dut;
static struct bt_conn_cb central_cb = {
.connected = acl_connected,
.disconnected = acl_disconnected,
};
/* Mark test as in progress. */
TEST_START("central");
/* Initialize Bluetooth */
err = bt_conn_cb_register(&central_cb);
TEST_ASSERT(err == 0, "Can't register callbacks (err %d)", err);
err = bt_enable(NULL);
TEST_ASSERT(err == 0, "Can't enable Bluetooth (err %d)", err);
LOG_DBG("Bluetooth initialized");
int psm = l2cap_server_register(BT_SECURITY_L1);
LOG_DBG("Registered server PSM %x", psm);
/* The device address will not change. Scan only once in order to reduce
* test time.
*/
err = bt_testlib_scan_find_name(&dut, DUT_NAME);
TEST_ASSERT(!err, "Failed to start scan (err %d)", err);
/* DUT will terminate all devices when it's done. Mark the device as
* "passed" so bsim doesn't return a nonzero err code when the
* termination happens.
*/
TEST_PASS("central");
while (true) {
/* Create a connection using that address */
err = bt_testlib_connect(&dut, &conn);
TEST_ASSERT(!err, "Failed to initiate connection (err %d)", err);
LOG_DBG("Connected");
/* Receive in the background */
k_sleep(K_MSEC(1000));
/* Disconnect and destroy connection object */
err = bt_testlib_disconnect(&conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
TEST_ASSERT(!err, "Failed to disconnect (err %d)", err);
LOG_DBG("Disconnected");
/* Simulate the central going in and out of range. In the real world, it is unlikely
* to drop a connection and re-establish it after only a few milliseconds.
*/
k_sleep(K_MSEC(200));
}
}

View file

@ -0,0 +1,14 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_MULTILINK_PERIPHERAL_SRC_DATA_H_
#define ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_MULTILINK_PERIPHERAL_SRC_DATA_H_
#define SDU_NUM 200 /* Number of SDU (L2CAP buffers) to send */
#define SDU_LEN 20 /* Length in bytes of said SDUs */
#define DUT_NAME CONFIG_BT_DEVICE_NAME /* name to advertise with */
#endif /* ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_MULTILINK_PERIPHERAL_SRC_DATA_H_ */

View file

@ -0,0 +1,338 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/l2cap.h>
#include <zephyr/logging/log.h>
#include "babblekit/flags.h"
#include "babblekit/testcase.h"
/* local includes */
#include "data.h"
LOG_MODULE_REGISTER(dut, CONFIG_APP_LOG_LEVEL);
static DEFINE_FLAG(ADVERTISING);
static void sdu_destroy(struct net_buf *buf)
{
LOG_DBG("%p", buf);
net_buf_destroy(buf);
}
/* Only one SDU per link will be transmitted at a time */
NET_BUF_POOL_DEFINE(sdu_tx_pool, CONFIG_BT_MAX_CONN, BT_L2CAP_SDU_BUF_SIZE(SDU_LEN),
CONFIG_BT_CONN_TX_USER_DATA_SIZE, sdu_destroy);
static uint8_t tx_data[SDU_LEN];
struct test_ctx {
bt_addr_le_t peer;
struct bt_l2cap_le_chan le_chan;
size_t sdu_count; /* the number of SDUs that have been transferred until now */
};
static struct test_ctx contexts[CONFIG_BT_MAX_CONN];
static int send_data_over_l2cap(struct bt_l2cap_chan *chan, uint8_t *data, size_t len)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(chan->conn), addr, sizeof(addr));
LOG_DBG("[%s] chan %p data %p len %d", addr, chan, data, len);
struct net_buf *buf = net_buf_alloc(&sdu_tx_pool, K_NO_WAIT);
if (buf == NULL) {
TEST_FAIL("No more memory");
return -ENOMEM;
}
net_buf_reserve(buf, BT_L2CAP_SDU_CHAN_SEND_RESERVE);
net_buf_add_mem(buf, data, len);
int ret = bt_l2cap_chan_send(chan, buf);
TEST_ASSERT(ret == 0, "Failed sending: err %d", ret);
LOG_DBG("queued SDU", len);
return ret;
}
static void resume_sending_until_done(struct test_ctx *ctx)
{
struct bt_l2cap_chan *chan = &ctx->le_chan.chan;
TEST_ASSERT(ctx->le_chan.state == BT_L2CAP_CONNECTED,
"attempting to send on disconnected channel (%p)", chan);
LOG_DBG("%p, transmitted %d SDUs", chan, ctx->sdu_count);
if (ctx->sdu_count < SDU_NUM) {
send_data_over_l2cap(chan, tx_data, sizeof(tx_data));
} else {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(chan->conn), addr, sizeof(addr));
LOG_DBG("[%s] Done sending", addr);
}
}
static struct test_ctx *get_ctx_from_chan(struct bt_l2cap_chan *chan)
{
struct bt_l2cap_le_chan *le_chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan);
struct test_ctx *ctx = CONTAINER_OF(le_chan, struct test_ctx, le_chan);
TEST_ASSERT(PART_OF_ARRAY(contexts, ctx), "memory corruption");
return ctx;
}
static void sent_cb(struct bt_l2cap_chan *chan)
{
struct test_ctx *ctx = get_ctx_from_chan(chan);
LOG_DBG("%p", chan);
ctx->sdu_count++;
resume_sending_until_done(ctx);
}
static void l2cap_chan_connected_cb(struct bt_l2cap_chan *chan)
{
struct bt_l2cap_le_chan *le_chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan);
LOG_DBG("%p (tx mtu %d mps %d) (tx mtu %d mps %d)", chan, le_chan->tx.mtu, le_chan->tx.mps,
le_chan->rx.mtu, le_chan->rx.mps);
LOG_DBG("initiating SDU transfer");
resume_sending_until_done(get_ctx_from_chan(chan));
}
static void l2cap_chan_disconnected_cb(struct bt_l2cap_chan *chan)
{
LOG_DBG("%p", chan);
}
static int recv_cb(struct bt_l2cap_chan *chan, struct net_buf *buf)
{
TEST_FAIL("DUT should not receive data");
return 0;
}
static int connect_l2cap_channel(struct bt_conn *conn, struct bt_l2cap_le_chan *le_chan)
{
static struct bt_l2cap_chan_ops ops = {
.connected = l2cap_chan_connected_cb,
.disconnected = l2cap_chan_disconnected_cb,
.recv = recv_cb,
.sent = sent_cb,
};
memset(le_chan, 0, sizeof(*le_chan));
le_chan->chan.ops = &ops;
return bt_l2cap_chan_connect(conn, &le_chan->chan, 0x0080);
}
static bool addr_in_use(bt_addr_le_t *address)
{
return !bt_addr_le_eq(address, BT_ADDR_LE_ANY);
}
static struct test_ctx *alloc_ctx(void)
{
for (size_t i = 0; i < ARRAY_SIZE(contexts); i++) {
struct test_ctx *context = &contexts[i];
struct bt_l2cap_le_chan *le_chan = &context->le_chan;
if (le_chan->state != BT_L2CAP_DISCONNECTED) {
continue;
}
if (addr_in_use(&context->peer)) {
continue;
}
memset(context, 0, sizeof(struct test_ctx));
return context;
}
return NULL;
}
static struct test_ctx *get_ctx_from_address(const bt_addr_le_t *address)
{
for (size_t i = 0; i < ARRAY_SIZE(contexts); i++) {
struct test_ctx *context = &contexts[i];
if (bt_addr_le_eq(address, &context->peer)) {
return context;
}
}
return NULL;
}
static void acl_connected(struct bt_conn *conn, uint8_t err)
{
const bt_addr_le_t *central = bt_conn_get_dst(conn);
char addr[BT_ADDR_LE_STR_LEN];
struct test_ctx *ctx;
int ret;
bt_addr_le_to_str(central, addr, sizeof(addr));
TEST_ASSERT(err == 0, "Failed to connect to %s (0x%02x)", addr, err);
UNSET_FLAG(ADVERTISING);
LOG_DBG("[%s] Connected (conn %p)", addr, conn);
ctx = get_ctx_from_address(central);
if (ctx == NULL) {
LOG_DBG("no initialized context for %s, allocating..", addr);
ctx = alloc_ctx();
TEST_ASSERT(ctx, "Couldn't allocate ctx for conn %p", conn);
LOG_DBG("allocated context %p for %s", ctx, central);
bt_addr_le_copy(&ctx->peer, central);
}
ret = connect_l2cap_channel(conn, &ctx->le_chan);
TEST_ASSERT(!ret, "Error connecting l2cap channel (err %d)", ret);
}
static void acl_disconnected(struct bt_conn *conn, uint8_t reason)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("Disconnected from %s (reason 0x%02x)", addr, reason);
}
static void increment(struct bt_conn *conn, void *user_data)
{
size_t *conn_count = user_data;
(*conn_count)++;
}
static bool have_free_conn(void)
{
size_t conn_count = 0;
bt_conn_foreach(BT_CONN_TYPE_LE, increment, &conn_count);
return conn_count < CONFIG_BT_MAX_CONN;
}
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DUT_NAME, sizeof(DUT_NAME) - 1),
};
static void start_advertising(void)
{
int err;
LOG_DBG("starting advertiser");
err = bt_le_adv_start(BT_LE_ADV_CONN_ONE_TIME, ad, ARRAY_SIZE(ad), NULL, 0);
TEST_ASSERT(!err, "Advertising failed to start (err %d)", err);
}
static bool all_data_transferred(void)
{
size_t total_sdu_count = 0;
for (size_t i = 0; i < ARRAY_SIZE(contexts); i++) {
total_sdu_count += contexts[i].sdu_count;
}
TEST_ASSERT(total_sdu_count <= (SDU_NUM * CONFIG_BT_MAX_CONN),
"Received more SDUs than expected");
return total_sdu_count == (SDU_NUM * CONFIG_BT_MAX_CONN);
}
void entrypoint_dut(void)
{
/* Test purpose:
*
* For a peripheral device (DUT) that has multiple ACL connections to
* central devices: Verify that the data streams on one connection are
* not affected by one of the centrals going out of range or not
* responding.
*
* Three devices:
* - `dut`: sends L2CAP packets to p0 and p1
*
* DUT (in a loop):
* - advertise as connectable
* - [acl connected]
* - establish L2CAP channel
* - [l2 connected]
* - send L2CAP data until ACL disconnected or SDU_NUM SDUs reached
*
* p0/1/2 (in a loop):
* - scan & connect ACL
* - [acl connected]
* - [l2cap dynamic channel connected]
* - receive data from DUT
* - disconnect
*
* Verdict:
* - DUT is able to transfer SDU_NUM SDUs to all peers. Data can be
* dropped but resources should not leak, and the transfer should not
* stall.
*/
int err;
static struct bt_conn_cb peripheral_cb = {
.connected = acl_connected,
.disconnected = acl_disconnected,
};
/* Mark test as in progress. */
TEST_START("dut");
/* Initialize Bluetooth */
err = bt_conn_cb_register(&peripheral_cb);
TEST_ASSERT(err == 0, "Can't register callbacks (err %d)", err);
err = bt_enable(NULL);
TEST_ASSERT(err == 0, "Can't enable Bluetooth (err %d)", err);
LOG_DBG("Bluetooth initialized");
while (!all_data_transferred()) {
if (!have_free_conn() || IS_FLAG_SET(ADVERTISING)) {
/* Sleep to not hammer the CPU checking the `if` */
k_sleep(K_MSEC(10));
continue;
}
start_advertising();
SET_FLAG(ADVERTISING);
/* L2 channel is opened from conn->connected() */
/* L2 data transfer is initiated from l2->connected() */
/* L2 data transfer is initiated for next SDU from l2->sent() */
}
TEST_PASS_AND_EXIT("dut");
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include "bs_tracing.h"
#include "bstests.h"
#include "babblekit/testcase.h"
extern void entrypoint_dut(void);
extern void entrypoint_central(void);
extern enum bst_result_t bst_result;
static void test_end_cb(void)
{
/* This callback will fire right before the executable returns */
if (bst_result != Passed) {
TEST_PRINT("Test has not passed.");
}
}
static const struct bst_test_instance entrypoints[] = {
{
.test_id = "dut",
.test_delete_f = test_end_cb,
.test_main_f = entrypoint_dut,
},
{
.test_id = "central",
.test_delete_f = test_end_cb,
.test_main_f = entrypoint_central,
},
BSTEST_END_MARKER,
};
static struct bst_test_list *install(struct bst_test_list *tests)
{
return bst_add_tests(tests, entrypoints);
};
bst_test_install_t test_installers[] = {install, NULL};
int main(void)
{
bst_main();
return 0;
}

View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Copyright 2024 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
set -eu
: "${ZEPHYR_BASE:?ZEPHYR_BASE must be defined}"
INCR_BUILD=1
source ${ZEPHYR_BASE}/tests/bsim/compile.source
app="$(guess_test_relpath)" compile
wait_for_background_jobs

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Copyright (c) 2024 Nordic Semiconductor
# SPDX-License-Identifier: Apache-2.0
set -eu
source ${ZEPHYR_BASE}/tests/bsim/sh_common.source
test_name="$(guess_test_long_name)"
simulation_id=${test_name}
SIM_LEN_US=$((40 * 1000 * 1000))
test_exe="${BSIM_OUT_PATH}/bin/bs_${BOARD_TS}_${test_name}_prj_conf"
cd ${BSIM_OUT_PATH}/bin
Execute ./bs_2G4_phy_v1 -dump_imm -s=${simulation_id} -D=3 -sim_length=${SIM_LEN_US} $@
Execute "${test_exe}" -s=${simulation_id} -d=0 -rs=420 -RealEncryption=1 -testid=dut
# Start centrals with an offset, so the CONN_IND packets don't clash on-air.
# Since code executes in zero-time, this will always happen if we don't stagger
# the connection creation somehow.
Execute "${test_exe}" -s=${simulation_id} \
-d=1 -rs=169 -RealEncryption=1 -testid=central -delay_init -start_offset=1e3
Execute "${test_exe}" -s=${simulation_id} \
-d=2 -rs=690 -RealEncryption=1 -testid=central -delay_init -start_offset=10e3
wait_for_background_jobs