diff --git a/tests/bsim/bluetooth/host/adv/compile.sh b/tests/bsim/bluetooth/host/adv/compile.sh index 026b436f0cd..7639c0853b5 100755 --- a/tests/bsim/bluetooth/host/adv/compile.sh +++ b/tests/bsim/bluetooth/host/adv/compile.sh @@ -21,6 +21,9 @@ source ${ZEPHYR_BASE}/tests/bsim/compile.source app=tests/bsim/bluetooth/host/adv/resume compile app=tests/bsim/bluetooth/host/adv/resume conf_file=prj_2.conf compile +app=tests/bsim/bluetooth/host/adv/resume2/connectable compile +app=tests/bsim/bluetooth/host/adv/resume2/connecter compile +app=tests/bsim/bluetooth/host/adv/resume2/dut compile app=tests/bsim/bluetooth/host/adv/chain compile app=tests/bsim/bluetooth/host/adv/extended conf_file=prj_advertiser.conf compile app=tests/bsim/bluetooth/host/adv/extended conf_file=prj_scanner.conf compile diff --git a/tests/bsim/bluetooth/host/adv/resume2/_build.sh b/tests/bsim/bluetooth/host/adv/resume2/_build.sh new file mode 100755 index 00000000000..dfd28820f7e --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/_build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +set -eu +dotslash="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" + +cd "${dotslash}" + +( + cd connecter/ + ./_build.sh +) + +( + cd connectable/ + ./_build.sh +) + +( + cd dut/ + ./_build.sh +) diff --git a/tests/bsim/bluetooth/host/adv/resume2/connectable/CMakeLists.txt b/tests/bsim/bluetooth/host/adv/resume2/connectable/CMakeLists.txt new file mode 100644 index 00000000000..3770a46a75a --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connectable/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr HINTS $ENV{ZEPHYR_BASE}) +project(app) + +target_sources(app PRIVATE + main.c +) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ +) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib) +target_link_libraries(app PRIVATE + testlib +) diff --git a/tests/bsim/bluetooth/host/adv/resume2/connectable/_build.sh b/tests/bsim/bluetooth/host/adv/resume2/connectable/_build.sh new file mode 100755 index 00000000000..8c86c14a1ad --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connectable/_build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +set -eu +dotslash="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +bin_dir="${BSIM_OUT_PATH}/bin" +BOARD="${BOARD:-nrf52_bsim}" + +cd "${dotslash}" + +compile_path="${bin_dir}/bs_${BOARD}_" +compile_path+="$(realpath --relative-to "$(west topdir)"/zephyr prj.conf | tr /. _)" + +west build -b nrf52_bsim +cp -v build/zephyr/zephyr.exe "${compile_path}" diff --git a/tests/bsim/bluetooth/host/adv/resume2/connectable/main.c b/tests/bsim/bluetooth/host/adv/resume2/connectable/main.c new file mode 100644 index 00000000000..9347ec5ada6 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connectable/main.c @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +/* @file + * + * This app advertises connectable with the name "connectable". + * It only receives one connection at a time. When the remote + * disconnects, it starts advertising again. + * + * This app is added to the simulation simply to be a target for + * a connection from the DUT. + */ + +int main(void) +{ + int err; + + err = bt_enable(NULL); + __ASSERT_NO_MSG(!err); + + err = bt_set_name("connectable"); + __ASSERT_NO_MSG(!err); + + while (true) { + struct bt_conn *conn = NULL; + + bt_testlib_conn_wait_free(); + + err = bt_testlib_adv_conn( + &conn, BT_ID_DEFAULT, + (BT_LE_ADV_OPT_USE_NAME | BT_LE_ADV_OPT_FORCE_NAME_IN_AD)); + __ASSERT_NO_MSG(!err); + + bt_testlib_wait_disconnected(conn); + bt_testlib_conn_unref(&conn); + } + + return 0; +} diff --git a/tests/bsim/bluetooth/host/adv/resume2/connectable/prj.conf b/tests/bsim/bluetooth/host/adv/resume2/connectable/prj.conf new file mode 100644 index 00000000000..5b1934336a4 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connectable/prj.conf @@ -0,0 +1,21 @@ +CONFIG_TEST=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y + +CONFIG_ASSERT=y +CONFIG_BT_TESTING=y +CONFIG_LOG=y + +CONFIG_BT_DEVICE_NAME_DYNAMIC=y + +CONFIG_BT_EXT_ADV=y +CONFIG_BT_PRIVACY=n +CONFIG_BT_SCAN_WITH_IDENTITY=n + +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=1 +CONFIG_BT_ID_MAX=1 diff --git a/tests/bsim/bluetooth/host/adv/resume2/connecter/CMakeLists.txt b/tests/bsim/bluetooth/host/adv/resume2/connecter/CMakeLists.txt new file mode 100644 index 00000000000..3770a46a75a --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connecter/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr HINTS $ENV{ZEPHYR_BASE}) +project(app) + +target_sources(app PRIVATE + main.c +) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ +) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib) +target_link_libraries(app PRIVATE + testlib +) diff --git a/tests/bsim/bluetooth/host/adv/resume2/connecter/_build.sh b/tests/bsim/bluetooth/host/adv/resume2/connecter/_build.sh new file mode 100755 index 00000000000..8c86c14a1ad --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connecter/_build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +set -eu +dotslash="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +bin_dir="${BSIM_OUT_PATH}/bin" +BOARD="${BOARD:-nrf52_bsim}" + +cd "${dotslash}" + +compile_path="${bin_dir}/bs_${BOARD}_" +compile_path+="$(realpath --relative-to "$(west topdir)"/zephyr prj.conf | tr /. _)" + +west build -b nrf52_bsim +cp -v build/zephyr/zephyr.exe "${compile_path}" diff --git a/tests/bsim/bluetooth/host/adv/resume2/connecter/main.c b/tests/bsim/bluetooth/host/adv/resume2/connecter/main.c new file mode 100644 index 00000000000..ffdcb3183c5 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connecter/main.c @@ -0,0 +1,66 @@ +/* Copyright (c) 2024 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include + +LOG_MODULE_REGISTER(connecter, LOG_LEVEL_INF); + +/* @file + * + * This app scans for a device with the name "dut" and connects + * to it. It then waits for the connection to be disconnected, + * before starting over. + * + * This app is added to the simulation simply to exercise the + * the DUT's connectable advertiser. + * + * Multiple instances of this app are added to the simulation, + * to exercise BT_MAX_CONN of the DUT. + */ + +int main(void) +{ + int err; + + err = bt_enable(NULL); + __ASSERT_NO_MSG(!err); + + while (true) { + bt_addr_le_t result; + struct bt_conn *conn = NULL; + + bt_testlib_conn_wait_free(); + + err = bt_testlib_scan_find_name(&result, "dut"); + __ASSERT_NO_MSG(!err); + + /* The above scan will never timeout, but the below connect has + * a built-in timeout in the stack. + * + * The timeout causes `BT_HCI_ERR_UNKNOWN_CONN_ID`. + * + * The timeout is a good thing in this app. Maybe the DUT is + * going to change its address, so we should scan for the name + * again. + */ + + err = bt_testlib_connect(&result, &conn); + if (err) { + __ASSERT_NO_MSG(err == BT_HCI_ERR_UNKNOWN_CONN_ID); + } + + if (conn) { + bt_testlib_wait_disconnected(conn); + bt_testlib_conn_unref(&conn); + } + } + + return 0; +} diff --git a/tests/bsim/bluetooth/host/adv/resume2/connecter/prj.conf b/tests/bsim/bluetooth/host/adv/resume2/connecter/prj.conf new file mode 100644 index 00000000000..3aa128d9855 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/connecter/prj.conf @@ -0,0 +1,19 @@ +CONFIG_TEST=y + +CONFIG_BT=y +CONFIG_BT_CENTRAL=y + +CONFIG_ASSERT=y +CONFIG_BT_TESTING=y +CONFIG_LOG=y + +CONFIG_BT_EXT_ADV=n +CONFIG_BT_PRIVACY=n +CONFIG_BT_SCAN_WITH_IDENTITY=n + +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=1 +CONFIG_BT_ID_MAX=1 diff --git a/tests/bsim/bluetooth/host/adv/resume2/dut/CMakeLists.txt b/tests/bsim/bluetooth/host/adv/resume2/dut/CMakeLists.txt new file mode 100644 index 00000000000..3770a46a75a --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/dut/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr HINTS $ENV{ZEPHYR_BASE}) +project(app) + +target_sources(app PRIVATE + main.c +) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ +) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib) +target_link_libraries(app PRIVATE + testlib +) diff --git a/tests/bsim/bluetooth/host/adv/resume2/dut/_build.sh b/tests/bsim/bluetooth/host/adv/resume2/dut/_build.sh new file mode 100755 index 00000000000..8c86c14a1ad --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/dut/_build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +set -eu +dotslash="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +bin_dir="${BSIM_OUT_PATH}/bin" +BOARD="${BOARD:-nrf52_bsim}" + +cd "${dotslash}" + +compile_path="${bin_dir}/bs_${BOARD}_" +compile_path+="$(realpath --relative-to "$(west topdir)"/zephyr prj.conf | tr /. _)" + +west build -b nrf52_bsim +cp -v build/zephyr/zephyr.exe "${compile_path}" diff --git a/tests/bsim/bluetooth/host/adv/resume2/dut/main.c b/tests/bsim/bluetooth/host/adv/resume2/dut/main.c new file mode 100644 index 00000000000..9bf7f88e355 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/dut/main.c @@ -0,0 +1,212 @@ +/* Copyright (c) 2024 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +extern enum bst_result_t bst_result; + +LOG_MODULE_REGISTER(dut, LOG_LEVEL_INF); + +atomic_t connected_count; + +static void on_connected(struct bt_conn *conn, uint8_t conn_err) +{ + atomic_t count = atomic_inc(&connected_count) + 1; + + LOG_INF("Connected. Current count %d", count); +} + +static void on_disconnected(struct bt_conn *conn, uint8_t reason) +{ + atomic_t count = atomic_dec(&connected_count) - 1; + + LOG_INF("Disconnected. Current count %d", count); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = on_connected, + .disconnected = on_disconnected, +}; + +static void disconnect_all(void) +{ + for (size_t i = 0; i < CONFIG_BT_MAX_CONN; i++) { + int err; + struct bt_conn *conn = bt_testlib_conn_unindex(BT_CONN_TYPE_LE, i); + + if (conn) { + err = bt_testlib_disconnect(&conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + __ASSERT_NO_MSG(!err); + } + } +} + +/* 📜: + * 🚧Setup + * ✨Setup / Cleanup ok + * 👉Test step + * ✅Test step passed + * 🚩Likely triggers problematic behavior + * 💣Checks for the bad behavior + * 💥Bad behavior + * 🧹Clean up + * 🌈Test complete + */ + +int main(void) +{ + int err; + bt_addr_le_t connectable_addr; + struct bt_conn *conn = NULL; + + bst_result = In_progress; + + err = bt_enable(NULL); + __ASSERT_NO_MSG(!err); + + err = bt_set_name("dut"); + __ASSERT_NO_MSG(!err); + + + LOG_INF("👉 Preflight test: Advertiser fills connection capacity."); + + /* `bt_le_adv_start` is invoked once, and.. */ + + err = bt_le_adv_start(BT_LE_ADV_CONN_NAME_AD, NULL, 0, NULL, 0); + __ASSERT_NO_MSG(!err); + + /* .. the advertiser shall autoresume. Since it's not + * stopped, it shall continue receivng connections until + * the stack runs out of connection objects. + */ + + LOG_INF("Waiting for connections..."); + while (atomic_get(&connected_count) < CONFIG_BT_MAX_CONN) { + k_msleep(1000); + } + + LOG_INF("✅ Ok"); + + + LOG_INF("👉 Disconnect one to see that it comes back"); + + /* Disconnect one of the connections. It does matter + * which, but object with index 0 is chosen for + * simplicity. + */ + + conn = bt_testlib_conn_unindex(BT_CONN_TYPE_LE, 0); + __ASSERT_NO_MSG(conn); + + /* Disconnect, but wait with unreffing.. */ + + err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + __ASSERT_NO_MSG(!err); + bt_testlib_wait_disconnected(conn); + + /* Simulate a delayed unref. We delay to make sure the resume is not + * triggered by disconnection, but by a connection object becoming + * available. + */ + + k_sleep(K_SECONDS(10)); + + bt_testlib_conn_unref(&conn); + + /* Since there is a free connection object again, the + * advertiser shall automatically resume and receive a + * new connection. + */ + + LOG_INF("Waiting for connections..."); + while (atomic_get(&connected_count) < CONFIG_BT_MAX_CONN) { + k_msleep(1000); + } + + LOG_INF("✅ Ok"); + + + LOG_INF("🧹 Clean up"); + + err = bt_le_adv_stop(); + __ASSERT_NO_MSG(!err); + + disconnect_all(); + + LOG_INF("✨ Ok"); + + + LOG_INF("🚧 Setup: Connect one central connection"); + + err = bt_testlib_scan_find_name(&connectable_addr, "connectable"); + __ASSERT_NO_MSG(!err); + + err = bt_testlib_connect(&connectable_addr, &conn); + __ASSERT_NO_MSG(!err); + + LOG_INF("✅ Ok"); + + + LOG_INF("🚧 Setup: Start advertiser. Let it fill the connection limit."); + + /* With one connection slot taken by the central role, we fill the rest. */ + + err = bt_le_adv_start(BT_LE_ADV_CONN_NAME_AD, NULL, 0, NULL, 0); + __ASSERT_NO_MSG(!err); + + LOG_INF("Waiting for connections..."); + while (atomic_get(&connected_count) < CONFIG_BT_MAX_CONN) { + k_sleep(K_SECONDS(1)); + } + + LOG_INF("✅ Ok"); + + + LOG_INF("👉 Main test: Disconnect, wait and connect the central connection."); + + /* In this situation, disconnecting the central role should not allow + * the advertiser to resume. This behavior was introduced in 372c8f2d92. + */ + + LOG_INF("🚩 Disconnect"); + err = bt_testlib_disconnect(&conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + __ASSERT_NO_MSG(!err); + + LOG_INF("🚩 Wait to bait the advertiser"); + k_sleep(K_SECONDS(5)); + + LOG_INF("💣 Connect"); + err = bt_testlib_connect(&connectable_addr, &conn); + if (err) { + /* If the test fails, it's because the advertiser 'stole' the + * central's connection slot. + */ + __ASSERT_NO_MSG(err == -ENOMEM); + + LOG_ERR("💥 Advertiser stole the connection slot"); + bs_trace_silent_exit(1); + } + + LOG_INF("✅ Ok"); + + bst_result = Passed; + LOG_INF("🌈 Test complete"); + bs_trace_silent_exit(0); + + return 0; +} diff --git a/tests/bsim/bluetooth/host/adv/resume2/dut/prj.conf b/tests/bsim/bluetooth/host/adv/resume2/dut/prj.conf new file mode 100644 index 00000000000..7553df7062b --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/dut/prj.conf @@ -0,0 +1,22 @@ +CONFIG_TEST=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_CENTRAL=y + +CONFIG_BT_DEVICE_NAME_DYNAMIC=y + +CONFIG_ASSERT=y +CONFIG_BT_TESTING=y +CONFIG_LOG=y + +CONFIG_BT_EXT_ADV=n +CONFIG_BT_PRIVACY=n +CONFIG_BT_SCAN_WITH_IDENTITY=n + +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=3 +CONFIG_BT_ID_MAX=1 diff --git a/tests/bsim/bluetooth/host/adv/resume2/run.py b/tests/bsim/bluetooth/host/adv/resume2/run.py new file mode 100755 index 00000000000..5ff98368555 --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/run.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +import os +import subprocess +from pathlib import Path +import sys +from west.util import west_topdir +import concurrent.futures + +dotslash = Path(__file__).parent.absolute() +os.chdir(dotslash) + +BSIM_OUT_PATH = os.environ.get("BSIM_OUT_PATH") +if BSIM_OUT_PATH is None: + print("ERROR: BSIM_OUT_PATH environment variable not set") + exit(1) + +BOARD = os.environ.get("BOARD", "nrf52_bsim") + +ZEPHYR_BASE = west_topdir() + "/zephyr" + + +def bsim_exe_name(prj_conf: Path) -> str: + rel_path = prj_conf.absolute().relative_to(ZEPHYR_BASE) + return f"bs_{BOARD}_" + rel_path.as_posix().replace("/", "_").replace(".", "_") + + +def bsim_exe_path(prj_conf: Path) -> Path: + return Path(BSIM_OUT_PATH) / "bin" / bsim_exe_name(prj_conf) + + +devices = [ + "dut/prj.conf", + "connecter/prj.conf", + "connecter/prj.conf", + "connecter/prj.conf", + "connectable/prj.conf", +] + +simulation_id = "resume2" + +args_all = [f"-s={simulation_id}"] +args_dev = ["-v=2", "-RealEncryption=1"] +sim_seconds = 120 + +print(f"Simulation time: {sim_seconds} seconds") + +realworld_timeout = 60 + + +def subprocess_checked_wait(p: subprocess.Popen): + returncode = p.wait() + if returncode != 0: + raise subprocess.CalledProcessError(returncode, p.args) + + +bsim_bin_path = Path(BSIM_OUT_PATH) / "bin" +with concurrent.futures.ThreadPoolExecutor() as executor: + ps = set() + for i, dev in enumerate(devices): + compile_path = bsim_exe_path(dotslash / dev) + p = subprocess.Popen( + [ + compile_path, + *args_all, + *args_dev, + f"-d={i}", + ], + ) + ps.add(p) + p = subprocess.Popen( + [ + "./bs_2G4_phy_v1", + *args_all, + f"-D={i+1}", + "-v=6", + f"-sim_length={sim_seconds * 10 ** 6}", + ], + cwd=bsim_bin_path, + ) + ps.add(p) + done, not_done = concurrent.futures.wait( + (executor.submit(lambda: subprocess_checked_wait(p)) for p in ps), + timeout=realworld_timeout, + return_when="FIRST_EXCEPTION", + ) + for f in done: + try: + f.result() + except subprocess.CalledProcessError as e: + for p in ps: + p.kill() + print(f"ERROR: Simulation failed with return code {e.returncode}") + sys.exit(1) + + if not_done: + for p in ps: + p.kill() + print(f"ERROR: Real-world timeout occurred after {realworld_timeout} seconds") + sys.exit(1) + + print("End of simulation") diff --git a/tests/bsim/bluetooth/host/adv/resume2/run.sh b/tests/bsim/bluetooth/host/adv/resume2/run.sh new file mode 100755 index 00000000000..21dc7f3caac --- /dev/null +++ b/tests/bsim/bluetooth/host/adv/resume2/run.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +exec "$(dirname "${BASH_SOURCE[0]}")/run.py"