Bluetooth: tests: bsim: host/adv/resume2

This test is an attempt at formalizing at least part of the behavior
described in commit 6a79c3deae7d3a09965a6e71fc7da863aaa721fa.

Signed-off-by: Aleksander Wasaznik <aleksander.wasaznik@nordicsemi.no>
diff --git a/tests/bsim/bluetooth/host/adv/compile.sh b/tests/bsim/bluetooth/host/adv/compile.sh
index 026b436..7639c08 100755
--- a/tests/bsim/bluetooth/host/adv/compile.sh
+++ b/tests/bsim/bluetooth/host/adv/compile.sh
@@ -21,6 +21,9 @@
 
 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 0000000..dfd2882
--- /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 0000000..3770a46
--- /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 0000000..8c86c14
--- /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 0000000..9347ec5
--- /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 <zephyr/bluetooth/bluetooth.h>
+#include <zephyr/bluetooth/conn.h>
+#include <zephyr/sys/__assert.h>
+
+#include <testlib/adv.h>
+#include <testlib/conn.h>
+
+/* @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 0000000..5b19343
--- /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 0000000..3770a46
--- /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 0000000..8c86c14
--- /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 0000000..ffdcb31
--- /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 <zephyr/bluetooth/bluetooth.h>
+#include <zephyr/bluetooth/hci_types.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/sys/__assert.h>
+
+#include <testlib/conn.h>
+#include <testlib/scan.h>
+
+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 0000000..3aa128d
--- /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 0000000..3770a46
--- /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 0000000..8c86c14
--- /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 0000000..9bf7f88
--- /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 <errno.h>
+
+#include <zephyr/bluetooth/bluetooth.h>
+#include <zephyr/bluetooth/conn.h>
+#include <zephyr/kernel.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/sys/__assert.h>
+#include <zephyr/sys/atomic_builtin.h>
+#include <zephyr/sys/atomic_types.h>
+
+#include <testlib/conn.h>
+#include <testlib/scan.h>
+
+#include <bs_tracing.h>
+#include <bstests.h>
+
+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 0000000..7553df7
--- /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 0000000..5ff9836
--- /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 0000000..21dc7f3
--- /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"