diff --git a/README.md b/README.md index bd21f239..bb8ed487 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ # SQLite ORM SQLite ORM light header only library for modern C++. Please read the license precisely. The project has AGPL license for open source project and MIT license after purchasing it for 50$ (using [PayPal](https://paypal.me/fnc12) or any different way (contact using email fnc12@me.com)). +Documentation is found in [docs](docs/home.md). + # Status | Branch | Travis | Appveyor | | :----- | :----- | :------- | diff --git a/dev/backup.h b/dev/backup.h index a2fc533e..243be17c 100644 --- a/dev/backup.h +++ b/dev/backup.h @@ -28,15 +28,15 @@ namespace sqlite_orm { const std::string& zSourceName, std::unique_ptr holder_) : handle(sqlite3_backup_init(to_.get(), zDestName.c_str(), from_.get(), zSourceName.c_str())), - holder(std::move(holder_)), to(to_), from(from_) { + holder(std::move(holder_)), to(std::move(to_)), from(std::move(from_)) { if (!this->handle) { throw std::system_error{orm_error_code::failed_to_init_a_backup}; } } backup_t(backup_t&& other) : - handle(std::exchange(other.handle, nullptr)), holder(std::move(other.holder)), to(other.to), - from(other.from) {} + handle(std::exchange(other.handle, nullptr)), holder(std::move(other.holder)), to(std::move(other.to)), + from(std::move(other.from)) {} ~backup_t() { if (this->handle) { diff --git a/dev/connection_holder.h b/dev/connection_holder.h index b51e8451..c2c9f6c8 100644 --- a/dev/connection_holder.h +++ b/dev/connection_holder.h @@ -2,11 +2,16 @@ #include #ifndef SQLITE_ORM_IMPORT_STD_MODULE -#include +#include // std::atomic_int, memory order flags +#include // std::mutex, std::lock_guard +#include // std::thread::id +#include // std::swap, std::exchange #include // std::function #include // std::string #endif +#include "functional/cxx_new.h" +#include "functional/cxx_scope_guard.h" #include "functional/gsl.h" #include "error_code.h" #include "vfs_name.h" @@ -15,113 +20,288 @@ namespace sqlite_orm { namespace internal { + struct db_arguments { + db_arguments(std::string filename, const connection_control& connectionCtrl = {}) : + filename{std::move(filename)}, vfs_name{connectionCtrl.vfs_name}, open_mode{connectionCtrl.open_mode} {} + std::string filename; + std::string vfs_name; + db_open_mode open_mode; + }; + + /* + The connection holder should be performant in all variants: + 1. single-threaded use + 2. opened permanently (open forever) + 3. concurrent open/close + */ struct connection_holder { - connection_holder(std::string filename, - std::function didOpenDb, - const connection_control& options = {}) : - _didOpenDb{std::move(didOpenDb)}, filename(std::move(filename)), vfs_name(options.vfs_name), - open_mode(options.open_mode) {} + explicit connection_holder(bool openedForeverHint, + db_arguments dbArgs, + std::function didOpenDb) : + _control{openedForeverHint}, dbArgs{std::move(dbArgs)}, _didOpenDb{std::move(didOpenDb)} {} connection_holder(const connection_holder&) = delete; + connection_holder& operator=(const connection_holder&) = delete; - connection_holder(const connection_holder& other, std::function didOpenDb) : - _didOpenDb{std::move(didOpenDb)}, filename{other.filename}, vfs_name(other.vfs_name), - open_mode{other.open_mode} {} + explicit connection_holder(const connection_holder& other, std::function didOpenDb) : + _control{other._control.openedForeverHint}, dbArgs{other.dbArgs}, _didOpenDb{std::move(didOpenDb)} {} - void retain() { - // first one opens the connection. - // we presume that the connection is opened once in a single-threaded context [also open forever]. - // therefore we can just use an atomic increment but don't need sequencing due to `prevCount > 0`. - if (_retainCount.fetch_add(1, std::memory_order_relaxed) == 0) { - int open_flags = internal::db_open_mode_to_int_flags(this->open_mode); -#if SQLITE_VERSION_NUMBER >= 3037002 - open_flags |= SQLITE_OPEN_EXRESCODE; + explicit connection_holder(const connection_holder& other, std::true_type /*openedForeverHint*/) : + _control{true}, dbArgs{other.dbArgs}, _didOpenDb{other._didOpenDb} {} + + /* + Open the database once and for all from a single-threaded context when it should be opened permanently. + */ + void open() { +#ifdef SQLITE_ORM_CONTRACTS_SUPPORTED + contract_assert(_control.openedForeverHint); + contract_assert(!_control.db); #endif + _control.retainCount.fetch_add(1, std::memory_order_relaxed); + _do_open(); - const int rc = - sqlite3_open_v2(this->filename.c_str(), &this->db, open_flags, this->vfs_name.c_str()); + if (_didOpenDb) { + _didOpenDb(_control.db); + } + } - if (rc != SQLITE_OK) SQLITE_ORM_CPP_UNLIKELY /*possible, but unexpected*/ { - throw_translated_sqlite_error(rc); - } + /* + Close the database from a single-threaded context when the database has already been opened permanently. + */ + void close() { +#ifdef SQLITE_ORM_CONTRACTS_SUPPORTED + contract_assert(_control.openedForeverHint); + contract_assert(_control.db); +#endif + _control.retainCount.fetch_sub(1, std::memory_order_relaxed); + _do_close(); + } + + /* + Retain the database handle if the database is already open, `nullptr` otherwise. + */ + sqlite3* retain_if_open() { + return _try_retain_if_open(false); + } + + /* + Retain the database handle if the database is already open, otherwise open the database. + */ + sqlite3* retain() { + // optional fast path: if connection is already open, just try incrementing the counter; + if (sqlite3* db = _try_retain_if_open(true)) { + return db; + } + + // slow path: need to open connection or wait for it + + const std::lock_guard _{_sync}; + // double-check: another thread might have opened it + const bool needsToBeOpened = _control.retainCount == 0; + if (needsToBeOpened) { + _do_open(); if (_didOpenDb) { - _didOpenDb(this->db); + _control.initializingThreadId.store(std::this_thread::get_id(), std::memory_order_release); + const scope_guard threadIdGuard{[&threadId = _control.initializingThreadId] { + threadId.store(std::thread::id{}, std::memory_order_release); + }}; + // note: may incur recursion in user-provided `on_open` callback + _didOpenDb(_control.db); } } + + // attention: only increase the reference count after successful open in order to propagate a fully setup connection to other threads + _control.retainCount.fetch_add(1, std::memory_order_release); + return _control.db; } void release() { - // last one closes the connection. - // we assume that this might happen by any thread, therefore the counter must serve as a synchronization point. - if (_retainCount.fetch_sub(1, std::memory_order_acq_rel) == 1) { - int rc = sqlite3_close_v2(this->db); - if (rc != SQLITE_OK) SQLITE_ORM_CPP_UNLIKELY { - throw_translated_sqlite_error(this->db); - } else { - this->db = nullptr; + // optional optimization for permanently opened connections; + if (_control.openedForeverHint) { +#ifdef SQLITE_ORM_CONTRACTS_SUPPORTED + contract_assert(_control.db); +#endif + return; + } + + // test for recursion from the same thread; + // testing against an empty thread id is sufficient because recursion is only possible while the `_didOpenDb` callback is executing in `retain()` + if (_control.initializingThreadId.load(std::memory_order_acquire) != std::thread::id{}) + SQLITE_ORM_CPP_UNLIKELY { + return; + } + + const int previousCount = _control.retainCount.fetch_sub(1, std::memory_order_release); + if (previousCount == 1) { + // last one closes the connection + + const std::lock_guard _{_sync}; + + // double-check: another thread might have acquired in the meantime + if (_control.retainCount.load(std::memory_order_acquire) == 0) { + _do_close(); } } } - sqlite3* get() const { - // note: ensuring a valid DB handle was already memory ordered with `retain()` - return this->db; + /* + Open from a single-threaded context. + */ + void _do_open() { + int openFlags = db_open_mode_to_int_flags(this->dbArgs.open_mode); +#if SQLITE_VERSION_NUMBER >= 3037002 + openFlags |= SQLITE_OPEN_EXRESCODE; +#endif + + const int rc = sqlite3_open_v2(this->dbArgs.filename.c_str(), + &_control.db, + openFlags, + this->dbArgs.vfs_name.c_str()); + + if (rc != SQLITE_OK) SQLITE_ORM_CPP_UNLIKELY /*possible, but unexpected*/ { + throw_translated_sqlite_error(rc); + } } - /** - * @attention While retrieving the reference count value is atomic it makes only sense at single-threaded points in code. + /* + Close from a single-threaded context. */ - int retain_count() const { - return _retainCount.load(std::memory_order_relaxed); + void _do_close() { + const int rc = sqlite3_close_v2(_control.db); + if (rc != SQLITE_OK) SQLITE_ORM_CPP_UNLIKELY { + throw_translated_sqlite_error(_control.db); + } else { + _control.db = nullptr; + } } - protected: - orm_gsl::owner db = nullptr; + sqlite3* _try_retain_if_open(const bool yieldIfContended) { + // optional optimization for permanently opened connections; + if (_control.openedForeverHint) { +#ifdef SQLITE_ORM_CONTRACTS_SUPPORTED + contract_assert(_control.db); +#endif + return _control.db; + } - private: - std::atomic_int _retainCount{}; + if (int currentCount = _control.retainCount.load(std::memory_order_relaxed)) { + do { + if (_control.retainCount.compare_exchange_weak(currentCount, + currentCount + 1, + std::memory_order_release, + std::memory_order_acquire)) { + // successfully incremented, connection is guaranteed to be open + return _control.db; + } + // CAS failed due to contention; + // 1. !yieldIfContended (compete) : retry until successful or count reaches zero because another thread closed the database; + // 2. yieldIfContended: do not try again; it does not have to be lock-free at all costs, which avoids CPUs competing for incrementation. + } while (!yieldIfContended && currentCount > 0); + } + // test for recursion from the same thread + else /*currentCount == 0*/ { + const std::thread::id threadId = _control.initializingThreadId.load(std::memory_order_acquire); + if (threadId != std::thread::id{} && std::this_thread::get_id() == threadId) + SQLITE_ORM_CPP_UNLIKELY { + return _control.db; + } + } - private: - const std::function _didOpenDb; + return nullptr; + } + + // note: members of the `control_block` are deliberately put on the same cache-line + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + struct control_block { + // Optional optimization hint that also serves to convey logic. + // in a test scenario involving a tight retain()/releae() loop from multiple threads the performance gain is outstanding; + // in a real-world scenario it merely saves all the atomic operations and the CPU cache updates they entail; + bool openedForeverHint = false; + std::atomic_int retainCount{}; + // `db` synchronizes with `retainCount` + orm_gsl::owner db = nullptr; + // we don't know what the user-provided `on_open` callback might do, so we need to track recursion during the `_didOpenDb` callback; + std::atomic initializingThreadId{}; + } _control; - public: - const std::string filename; - const std::string vfs_name; - const db_open_mode open_mode; + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + std::mutex _sync; + const db_arguments dbArgs; + const std::function _didOpenDb; }; + /* + Acquires a database connection upon construction and releases it upon destruction. + + Note: It is important to cache the `sqlite3*` pointer for cache-friendliness (thus avoiding to access the holder on each `get()` call). + */ struct connection_ref { - connection_ref(connection_holder& holder) : holder(&holder) { - this->holder->retain(); + connection_ref(connection_holder& holder) : holder{&holder}, db{holder.retain()} {} + + connection_ref(connection_ref&& other) : holder{other.holder}, db{this->holder->retain()} {} + + /* + Rebind connection reference; + This function is actually unused in the library, but required for concepts compliance (moveable type). + */ + connection_ref& operator=(connection_ref&& other) noexcept { + std::swap(this->holder, other.holder); + std::swap(this->db, other.db); + return *this; } - connection_ref(const connection_ref& other) : holder(other.holder) { - this->holder->retain(); + ~connection_ref() { + this->holder->release(); } - // rebind connection reference - connection_ref& operator=(const connection_ref& other) { - if (other.holder != this->holder) { - this->holder->release(); - this->holder = other.holder; - this->holder->retain(); - } + sqlite3* get() const { + return this->db; + } + + private: + connection_holder* holder; + sqlite3* db; + }; + + /* + Increases the reference count of an existing open connection upon construction and releases it upon destruction. + + Note: It is important to cache the `sqlite3*` pointer for cache-friendliness (thus avoiding to access the holder on each `get()` call). + */ + struct connection_ptr { + connection_ptr(connection_holder& holder) : holder{&holder}, db{holder.retain_if_open()} {} + connection_ptr(connection_ptr&& other) noexcept : + holder{other.holder}, db{std::exchange(other.db, nullptr)} {} + + /* + Rebind connection pointer; + */ + connection_ptr& operator=(connection_ptr&& other) noexcept { + std::swap(this->holder, other.holder); + std::swap(this->db, other.db); return *this; } - ~connection_ref() { - this->holder->release(); + ~connection_ptr() { + if (this->db) { + this->holder->release(); + } + } + + explicit operator bool() const { + return this->db || false; } sqlite3* get() const { - return this->holder->get(); + return this->db; } private: - connection_holder* holder = nullptr; + connection_holder* holder; + sqlite3* db; }; } } diff --git a/dev/functional/cxx_new.h b/dev/functional/cxx_new.h new file mode 100644 index 00000000..c360d9c9 --- /dev/null +++ b/dev/functional/cxx_new.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef SQLITE_ORM_IMPORT_STD_MODULE +#include +#else +#include +#endif + +namespace sqlite_orm { + namespace internal { + namespace polyfill { +#if __cpp_lib_hardware_interference_size >= 201703L + using std::hardware_constructive_interference_size; + using std::hardware_destructive_interference_size; +#else + constexpr size_t hardware_constructive_interference_size = 64; + constexpr size_t hardware_destructive_interference_size = 64; +#endif + } + } + + namespace polyfill = internal::polyfill; +} diff --git a/dev/functional/cxx_scope_guard.h b/dev/functional/cxx_scope_guard.h new file mode 100644 index 00000000..057e101f --- /dev/null +++ b/dev/functional/cxx_scope_guard.h @@ -0,0 +1,23 @@ +#pragma once + +#ifndef SQLITE_ORM_IMPORT_STD_MODULE +#include // std::forward +#endif + +namespace sqlite_orm::internal { + /* + Poor-man's scope (exit) guard until C++29 finally comes with proper standard facilities [Draft D3610]. + */ + template + class scope_guard { + public: + explicit scope_guard(F&& exitFunction) : _exitFunction{std::forward(exitFunction)} {} + + ~scope_guard() { + _exitFunction(); + } + + private: + F _exitFunction; + }; +} diff --git a/dev/limit_accessor.h b/dev/limit_accessor.h index e52f8448..18b8501d 100644 --- a/dev/limit_accessor.h +++ b/dev/limit_accessor.h @@ -3,8 +3,8 @@ #include #ifndef SQLITE_ORM_IMPORT_STD_MODULE #include // std::map -#include // std::function -#include // std::shared_ptr +#include // std::function, std::reference_wrapper +#include // std::move #endif #include "connection_holder.h" @@ -14,9 +14,7 @@ namespace sqlite_orm { namespace internal { struct limit_accessor { - using get_connection_t = std::function; - - limit_accessor(get_connection_t get_connection_) : get_connection(std::move(get_connection_)) {} + limit_accessor(std::unique_ptr& connection) : connection{connection} {} int length() { return this->get(SQLITE_LIMIT_LENGTH); @@ -117,7 +115,7 @@ namespace sqlite_orm { #endif protected: - get_connection_t get_connection; + std::reference_wrapper> connection; friend struct storage_base; @@ -127,14 +125,15 @@ namespace sqlite_orm { std::map limits; int get(int id) { - auto connection = this->get_connection(); + connection_ref connection = *this->connection.get(); return sqlite3_limit(connection.get(), id, -1); } void set(int id, int newValue) { this->limits[id] = newValue; - auto connection = this->get_connection(); - sqlite3_limit(connection.get(), id, newValue); + if (connection_ptr maybeConnection = *this->connection.get()) { + sqlite3_limit(maybeConnection.get(), id, newValue); + } } }; } diff --git a/dev/statement_serializer.h b/dev/statement_serializer.h index 18290978..0409305b 100644 --- a/dev/statement_serializer.h +++ b/dev/statement_serializer.h @@ -12,6 +12,9 @@ #include #include #include // std::list +#ifdef SQLITE_ORM_CPP20_RANGES_SUPPORTED +#include // std::views::transform +#endif #endif #include "functional/cxx_string_view.h" #include "functional/cxx_optional.h" @@ -1501,6 +1504,9 @@ namespace sqlite_orm { const Ctx&) SQLITE_ORM_OR_CONST_CALLOP { std::stringstream ss; ss << "SET "; +#ifdef SQLITE_ORM_CPP20_RANGES_SUPPORTED + ss << streaming_serialized(statement | std::views::transform(&dynamic_set_entry::serialized_value)); +#else int index = 0; for (const dynamic_set_entry& entry: statement) { if (index > 0) { @@ -1509,6 +1515,7 @@ namespace sqlite_orm { ss << entry.serialized_value; ++index; } +#endif return ss.str(); } }; diff --git a/dev/storage.h b/dev/storage.h index 2fc0ec6c..4f0c63bc 100644 --- a/dev/storage.h +++ b/dev/storage.h @@ -146,7 +146,7 @@ namespace sqlite_orm { context_t context{this->db_objects}; statement_serializer serializer; const std::string sql = serializer.serialize(table, context, tableName); - this->executor.perform_void_exec(db, sql.data()); + this->executor.perform_void_exec(db, sql.c_str()); } /** @@ -169,7 +169,7 @@ namespace sqlite_orm { << streaming_identifier(columnName) << std::flush; sql = ss.str(); } - this->executor.perform_void_exec(db, sql.data()); + this->executor.perform_void_exec(db, sql.c_str()); } #endif @@ -278,8 +278,8 @@ namespace sqlite_orm { mapped_view iterate(Args&&... args) { this->assert_mapped_type(); - auto connection = this->get_connection(); - return {*this, std::move(connection), std::forward(args)...}; + auto conRef = this->get_connection(); + return {*this, std::move(conRef), std::forward(args)...}; } #ifdef SQLITE_ORM_WITH_CPP20_ALIASES @@ -313,8 +313,8 @@ namespace sqlite_orm { if constexpr (is_select_v) { expression.highest_level = true; } - auto con = this->get_connection(); - return {this->db_objects, std::move(con), std::move(expression)}; + auto conRef = this->get_connection(); + return {this->db_objects, std::move(conRef), std::move(expression)}; } #ifdef SQLITE_ORM_CPP23_GENERATOR_SUPPORTED @@ -25080,7 +25350,7 @@ namespace sqlite_orm { << serialize(column, context) << std::flush; sql = ss.str(); } - this->executor.perform_void_exec(db, sql.data()); + this->executor.perform_void_exec(db, sql.c_str()); } template @@ -25131,10 +25401,10 @@ namespace sqlite_orm { context.omit_table_name = false; context.replace_bindable_with_question = true; - auto conection = this->get_connection(); const std::string sql = serialize(statement, context); - sqlite3_stmt* stmt = prepare_stmt(conection.get(), sql); - return prepared_statement_t{std::forward(statement), stmt, std::move(conection)}; + auto conRef = this->get_connection(); + sqlite3_stmt* stmt = prepare_stmt(conRef.get(), sql); + return prepared_statement_t{std::forward(statement), stmt, std::move(conRef)}; } public: @@ -25166,9 +25436,9 @@ namespace sqlite_orm { * can be printed out on std::ostream with `operator<<`. */ std::map sync_schema(bool preserve = false) { - auto con = this->get_connection(); + auto conRef = this->get_connection(); std::map result; - iterate_tuple(this->db_objects, [this, db = con.get(), preserve, &result](auto& schemaObject) { + iterate_tuple(this->db_objects, [this, db = conRef.get(), preserve, &result](auto& schemaObject) { sync_schema_result status = this->sync_dbo(schemaObject, db, preserve); result.emplace(schemaObject.name, status); }); @@ -25181,9 +25451,9 @@ namespace sqlite_orm { * what will happen if you sync your schema. */ std::map sync_schema_simulate(bool preserve = false) { - auto con = this->get_connection(); + auto conRef = this->get_connection(); std::map result; - iterate_tuple(this->db_objects, [this, db = con.get(), preserve, &result](auto& schemaObject) { + iterate_tuple(this->db_objects, [this, db = conRef.get(), preserve, &result](auto& schemaObject) { sync_schema_result status = this->schema_status(schemaObject, db, preserve, nullptr); result.emplace(schemaObject.name, status); }); diff --git a/tests/storage_tests.cpp b/tests/storage_tests.cpp index 0d7fc3d3..94cbcd58 100644 --- a/tests/storage_tests.cpp +++ b/tests/storage_tests.cpp @@ -1,12 +1,124 @@ +#include // std::unique_ptr, std::make_unique #include #include #include using namespace sqlite_orm; -TEST_CASE("connection control") { +TEST_CASE("connection holder tests") { + using namespace sqlite_orm::internal; + + struct try_acquire_sync { + try_acquire_sync(std::mutex& mtx) : mtx{mtx}, locked{mtx.try_lock()} {} + ~try_acquire_sync() { + if (locked) { + mtx.unlock(); + } + } + operator bool() const { + return locked; + } + + std::mutex& mtx; + const bool locked; + }; + + const bool openForever = GENERATE(false, true); + { + std::unique_ptr connection; + connection = std::make_unique( + openForever, + db_arguments{""}, + // test whether executed under the lock while opening + [&connection, openForever](sqlite3* db) { + // alias + auto& controlBlock = connection->_control; + + REQUIRE(controlBlock.openedForeverHint == openForever); + REQUIRE(controlBlock.db == db); + + // retain count is still zero while holding the lock + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.initializingThreadId == + (openForever ? std::thread::id{} : std::this_thread::get_id())); + REQUIRE(try_acquire_sync(connection->_sync) == openForever); + + // test re-entrance + + REQUIRE(connection->retain_if_open() == controlBlock.db); + { + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.initializingThreadId == + (openForever ? std::thread::id{} : std::this_thread::get_id())); + REQUIRE(try_acquire_sync(connection->_sync) == openForever); + } + + connection->release(); + { + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.openedForeverHint == openForever); + REQUIRE(controlBlock.initializingThreadId == + (openForever ? std::thread::id{} : std::this_thread::get_id())); + REQUIRE(try_acquire_sync(connection->_sync) == openForever); + } + + REQUIRE(connection->retain() == controlBlock.db); + { + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.initializingThreadId == + (openForever ? std::thread::id{} : std::this_thread::get_id())); + REQUIRE(try_acquire_sync(connection->_sync) == openForever); + } + connection->release(); + }); + + // alias + auto& controlBlock = connection->_control; + + if (openForever) { + connection->open(); + // note: state is tested in `on_open` handler above + } + + REQUIRE(connection->retain_if_open() == (openForever ? controlBlock.db : nullptr)); + { + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.initializingThreadId == std::thread::id{}); + REQUIRE(try_acquire_sync(connection->_sync)); + } + // note: must not release() if not opened + + REQUIRE(connection->retain() == controlBlock.db); + { + REQUIRE(controlBlock.retainCount == 1); + REQUIRE(controlBlock.initializingThreadId == std::thread::id{}); + REQUIRE(try_acquire_sync(connection->_sync)); + } + + connection->release(); + { + if (openForever) { + REQUIRE(controlBlock.db != nullptr); + } else { + REQUIRE(controlBlock.db == nullptr); + } + REQUIRE(controlBlock.retainCount == (openForever ? 1 : 0)); + REQUIRE(controlBlock.initializingThreadId == std::thread::id{}); + } + + if (openForever) { + connection->close(); + + REQUIRE(controlBlock.db == nullptr); + REQUIRE(controlBlock.retainCount == 0); + REQUIRE(controlBlock.initializingThreadId == std::thread::id{}); + } + } +} + +TEST_CASE("connection control tests") { const auto openForever = GENERATE(false, true); - SECTION("") { + { bool onOpenCalled = false; int nOnOpenCalled = 0; SECTION("empty") {