diff --git a/.evergreen/scripts/test.sh b/.evergreen/scripts/test.sh index 9edb512e40..59d8373795 100755 --- a/.evergreen/scripts/test.sh +++ b/.evergreen/scripts/test.sh @@ -289,8 +289,23 @@ else command -V valgrind valgrind --version run_test() { + valgrind_args=( + "--leak-check=full" + "--track-origins=yes" + "--num-callers=50" + "--error-exitcode=1" + "--error-limit=no" + "--read-var-info=yes" + "--suppressions=../etc/memcheck.suppressions" + ) + + # Avoid noisy diagnostics caused by deliberate subprocess termination. + if [[ "${1:?}" =~ test_instance ]]; then + valgrind_args+=("--trace-children=no") + fi + echo "Running ${1:?}..." - valgrind --leak-check=full --track-origins=yes --num-callers=50 --error-exitcode=1 --error-limit=no --read-var-info=yes --suppressions=../etc/memcheck.suppressions "${1:?}" "${test_args[@]:?}" || return + valgrind "${1:?}" "${test_args[@]:?}" || return echo "Running ${1:?}... done." } fi @@ -304,7 +319,6 @@ else run_test ./src/mongocxx/test/test_command_monitoring_specs run_test ./src/mongocxx/test/test_instance run_test ./src/mongocxx/test/test_transactions_specs - run_test ./src/mongocxx/test/test_logging run_test ./src/mongocxx/test/test_retryable_reads_specs run_test ./src/mongocxx/test/test_read_write_concern_specs run_test ./src/mongocxx/test/test_unified_format_specs diff --git a/src/bsoncxx/lib/bsoncxx/v1/exception.cpp b/src/bsoncxx/lib/bsoncxx/v1/exception.cpp index a2a601d6bc..2d655243bc 100644 --- a/src/bsoncxx/lib/bsoncxx/v1/exception.cpp +++ b/src/bsoncxx/lib/bsoncxx/v1/exception.cpp @@ -17,6 +17,7 @@ // #include +#include #include #include diff --git a/src/mongocxx/include/mongocxx/v1/exception.hpp b/src/mongocxx/include/mongocxx/v1/exception.hpp index 25c686197e..77c63dc08b 100644 --- a/src/mongocxx/include/mongocxx/v1/exception.hpp +++ b/src/mongocxx/include/mongocxx/v1/exception.hpp @@ -20,6 +20,13 @@ #include +#include + +#include + +#include +#include + namespace mongocxx { namespace v1 { @@ -28,14 +35,60 @@ namespace v1 { /// /// @attention This feature is experimental! It is not ready for use! /// -enum class source_errc {}; +enum class source_errc { + zero, ///< Zero. + mongocxx, ///< From the mongocxx library. + mongoc, ///< From the mongoc library. + mongocrypt, ///< From the mongocrypt library. + server, ///< From the MongoDB server. +}; + +/// +/// The error category for @ref mongocxx::v1::source_errc. +/// +/// @attention This feature is experimental! It is not ready for use! +/// +MONGOCXX_ABI_EXPORT_CDECL(std::error_category const&) source_error_category(); + +/// +/// Support implicit conversion to `std::error_condition`. +/// +/// @attention This feature is experimental! It is not ready for use! +/// +inline std::error_condition make_error_condition(source_errc code) { + return {static_cast(code), v1::source_error_category()}; +} /// /// Enumeration identifying the type (cause) of a @ref mongocxx::v1 error. /// /// @attention This feature is experimental! It is not ready for use! /// -enum class type_errc {}; +enum class type_errc { + zero, ///< Zero. + invalid_argument, ///< An invalid argument passed to the throwing function. + runtime_error, ///< An erroneous condition was detected at runtime. +}; + +/// +/// The error category for @ref mongocxx::v1::type_errc. +/// +/// @attention This feature is experimental! It is not ready for use! +/// +MONGOCXX_ABI_EXPORT_CDECL(std::error_category const&) type_error_category(); + +/// +/// Support implicit conversion to `std::error_condition`. +/// +/// @attention This feature is experimental! It is not ready for use! +/// +inline std::error_condition make_error_condition(type_errc code) { + return {static_cast(code), v1::type_error_category()}; +} + +BSONCXX_PRIVATE_WARNINGS_PUSH(); +BSONCXX_PRIVATE_WARNINGS_DISABLE(MSVC(4251)); +BSONCXX_PRIVATE_WARNINGS_DISABLE(MSVC(4275)); /// /// Base class for all exceptions thrown by @ref mongocxx::v1. @@ -45,11 +98,29 @@ enum class type_errc {}; /// /// @attention This feature is experimental! It is not ready for use! /// -class exception {}; +class exception : public std::system_error { + public: + ~exception() override; + + exception(exception&&) noexcept = default; + exception& operator=(exception&&) noexcept = default; + exception(exception const&) = default; + exception& operator=(exception const&) = default; + + using std::system_error::system_error; +}; + +BSONCXX_PRIVATE_WARNINGS_POP(); } // namespace v1 } // namespace mongocxx +template <> +struct std::is_error_condition_enum : true_type {}; + +template <> +struct std::is_error_condition_enum : true_type {}; + #include /// diff --git a/src/mongocxx/include/mongocxx/v1/instance.hpp b/src/mongocxx/include/mongocxx/v1/instance.hpp index 90f276f9b4..cfd2236b25 100644 --- a/src/mongocxx/include/mongocxx/v1/instance.hpp +++ b/src/mongocxx/include/mongocxx/v1/instance.hpp @@ -20,6 +20,14 @@ #include +#include + +#include + +#include +#include +#include + namespace mongocxx { namespace v1 { @@ -50,15 +58,123 @@ namespace v1 { /// @par Special exemptions /// Only the following API are permitted to be used outside the lifetime of an instance object: /// - @ref mongocxx::v1::logger +/// - @ref mongocxx::v1::default_logger /// /// @see /// - [Initialization and Cleanup (mongoc)](https://mongoc.org/libmongoc/current/init-cleanup.html) /// -class instance {}; +class instance { + private: + class impl; + std::unique_ptr _impl; + + public: + /// + /// Cleanup the mongocxx (and mongoc) library. + /// + /// Calls [`mongoc_cleanup()`](https://mongoc.org/libmongoc/current/mongoc_cleanup.html). + /// + MONGOCXX_ABI_EXPORT_CDECL() ~instance(); + + /// + /// This class is not moveable. + /// + instance(instance&&) = delete; + + /// + /// This class is not moveable. + /// + instance& operator=(instance&&) = delete; + + /// + /// This class is not copyable. + /// + instance(instance const&) = delete; + + /// + /// This class is not copyable. + /// + instance& operator=(instance const&) = delete; + + /// + /// Initialize the mongoc library with unstructured log messages disabled. + /// + /// Calls [`mongoc_init()`](https://mongoc.org/libmongoc/current/mongoc_init.html) after disabling unstructured + /// log messages by calling `mongoc_log_set_handler(nullptr, nullptr)`. + /// + /// @important To use mongoc's default log message handler, construct this object with + /// @ref instance(v1::default_logger tag) instead. + /// + /// @throws mongocxx::v1::exception with @ref mongocxx::v1::instance::errc::multiple_instances if an `instance` + /// object has already been created. + /// + /// @see + /// - [Custom Log Handlers (mongoc)](https://mongoc.org/libmongoc/current/unstructured_log.html#custom-log-handlers) + /// + MONGOCXX_ABI_EXPORT_CDECL() instance(); + + /// + /// Initialize the mongoc library with the custom unstructured log message handler. + /// + /// Calls [`mongoc_init`](https://mongoc.org/libmongoc/current/mongoc_init.html) after registering the custom + /// unstructured log handler by calling `mongoc_log_set_handler()`. + /// + /// @param handler Disable unstructured logging when null. + /// + /// @throws mongocxx::v1::exception with @ref mongocxx::v1::instance::errc::multiple_instances if an `instance` + /// object has already been created. + /// + /// @see + /// - [Custom Log Handlers (mongoc)](https://mongoc.org/libmongoc/current/unstructured_log.html#custom-log-handlers) + /// + explicit MONGOCXX_ABI_EXPORT_CDECL() instance(std::unique_ptr handler); + + /// + /// Initialize the mongoc library with its default unstructured log handler. + /// + /// Calls [`mongoc_init`](https://mongoc.org/libmongoc/current/mongoc_init.html) without registering any custom + /// unstructured log handler. + /// + /// @param tag Unused: only for overload resolution. + /// + /// @throws mongocxx::v1::exception with @ref mongocxx::v1::instance::errc::multiple_instances if an `instance` + /// object has already been created. + /// + explicit MONGOCXX_ABI_EXPORT_CDECL() instance(v1::default_logger tag); + + /// + /// Errors codes which may be returned by @ref mongocxx::v1::instance. + /// + /// @attention This feature is experimental! It is not ready for use! + /// + enum class errc { + zero, ///< Zero. + multiple_instances, ///< Cannot construct multiple instance objects in a given process. + }; + + /// + /// The error category for @ref mongocxx::v1::instance::errc. + /// + /// @attention This feature is experimental! It is not ready for use! + /// + static MONGOCXX_ABI_EXPORT_CDECL(std::error_category const&) error_category(); + + /// + /// Support implicit conversion to `std::error_code`. + /// + /// @attention This feature is experimental! It is not ready for use! + /// + friend std::error_code make_error_code(errc v) { + return {static_cast(v), error_category()}; + } +}; } // namespace v1 } // namespace mongocxx +template <> +struct std::is_error_code_enum : true_type {}; + #include /// diff --git a/src/mongocxx/include/mongocxx/v1/logger-fwd.hpp b/src/mongocxx/include/mongocxx/v1/logger-fwd.hpp index c4a155ba23..2b57d2b06a 100644 --- a/src/mongocxx/include/mongocxx/v1/logger-fwd.hpp +++ b/src/mongocxx/include/mongocxx/v1/logger-fwd.hpp @@ -16,12 +16,17 @@ #include +// + +#include + namespace mongocxx { namespace v1 { enum class log_level; -class logger; +class MONGOCXX_ABI_EXPORT logger; + class default_logger; } // namespace v1 diff --git a/src/mongocxx/include/mongocxx/v1/logger.hpp b/src/mongocxx/include/mongocxx/v1/logger.hpp index 8f35665b92..5aba2e9316 100644 --- a/src/mongocxx/include/mongocxx/v1/logger.hpp +++ b/src/mongocxx/include/mongocxx/v1/logger.hpp @@ -20,6 +20,11 @@ #include +#include +#include + +#include + namespace mongocxx { namespace v1 { @@ -28,7 +33,21 @@ namespace v1 { /// /// @attention This feature is experimental! It is not ready for use! /// -enum class log_level {}; +enum class log_level { + k_error, ///< MONGOC_LOG_LEVEL_ERROR + k_critical, ///< MONGOC_LOG_LEVEL_CRITICAL + k_warning, ///< MONGOC_LOG_LEVEL_WARNING + k_message, ///< MONGOC_LOG_LEVEL_MESSAGE + k_info, ///< MONGOC_LOG_LEVEL_INFO + k_debug, ///< MONGOC_LOG_LEVEL_DEBUG + k_trace, ///< MONGOC_LOG_LEVEL_TRACE +}; + +MONGOCXX_ABI_EXPORT_CDECL(bsoncxx::v1::stdx::string_view) to_string(log_level level); + +BSONCXX_PRIVATE_WARNINGS_PUSH(); +BSONCXX_PRIVATE_WARNINGS_DISABLE(MSVC(4251)); +BSONCXX_PRIVATE_WARNINGS_DISABLE(MSVC(4275)); /// /// The interface for an unstructured log message handler. @@ -37,7 +56,58 @@ enum class log_level {}; /// /// @attention This feature is experimental! It is not ready for use! /// -class logger {}; +class logger { + public: + /// + /// Destructor. + /// + virtual ~logger(); + + /// + /// Move constructor. + /// + logger(logger&&) = default; + + /// + /// Move assignment operator. + /// + logger& operator=(logger&&) = default; + + /// + /// Copy constructor. + /// + logger(logger const&) = default; + + /// + /// Copy assignment operator. + /// + logger& operator=(logger const&) = default; + + /// + /// Default constructor. + /// + logger() = default; + + /// + /// Handle an unstructured log message emitted by mongoc. + /// + /// Users may override this function to implement custom log message behavior such as outputting messages to a file + /// or sending messages to a remote server. + /// + /// @param level The log level for the message being handled. + /// @param domain The domain of the message. + /// @param message The contents of the log message. + /// + /// @see + /// - [Custom Log handlers (mongoc)](https://mongoc.org/libmongoc/current/unstructured_log.html#custom-log-handlers) + /// + virtual void operator()( + log_level level, + bsoncxx::v1::stdx::string_view domain, + bsoncxx::v1::stdx::string_view message) noexcept = 0; +}; + +BSONCXX_PRIVATE_WARNINGS_POP(); /// /// A tag type representing mongoc's default unstructured log handler. diff --git a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/instance.hpp b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/instance.hpp index bd99b83b86..e52ecd6c4f 100644 --- a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/instance.hpp +++ b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/instance.hpp @@ -14,9 +14,14 @@ #pragma once +#include + +// + +#include + #include -#include #include #include @@ -24,59 +29,7 @@ namespace mongocxx { namespace v_noabi { -/// -/// An instance of the MongoDB driver. -/// -/// The constructor and destructor initialize and shut down the driver, respectively. Therefore, an -/// instance must be created before using the driver and must remain alive until all other mongocxx -/// objects are destroyed. After the instance destructor runs, the driver may not be used. -/// -/// Exactly one instance must be created in a given program. Not constructing an instance or -/// constructing more than one instance in a program are errors, even if the multiple instances have -/// non-overlapping lifetimes. -/// -/// The following is a correct example of using an instance in a program, as the instance is kept -/// alive for as long as the driver is in use: -/// -/// \code -/// -/// #include -/// #include -/// #include -/// -/// int main() { -/// mongocxx::v_noabi::instance inst{}; -/// mongocxx::v_noabi::client conn{mongocxx::v_noabi::uri{}}; -/// ... -/// } -/// -/// \endcode -/// -/// An example of using instance incorrectly might look as follows: -/// -/// \code -/// -/// #include -/// #include -/// #include -/// -/// client get_client() { -/// mongocxx::v_noabi::instance inst{}; -/// mongocxx::v_noabi::client conn{mongocxx::v_noabi::uri{}}; -/// -/// return client; -/// } // ERROR! The instance is no longer alive after this function returns. -/// -/// int main() { -/// mongocxx::v_noabi::client conn = get_client(); -/// ... -/// } -/// -/// \endcode -/// -/// For examples of more advanced usage of instance, see -/// `examples/mongocxx/instance_management.cpp`. -/// +/// @copydoc mongocxx::v1::instance class instance { public: /// @@ -90,24 +43,37 @@ class instance { /// /// @throws mongocxx::v_noabi::logic_error if an instance already exists. /// - MONGOCXX_ABI_EXPORT_CDECL() instance(std::unique_ptr logger); + MONGOCXX_ABI_EXPORT_CDECL() instance(std::unique_ptr logger); /// /// Move constructs an instance of the driver. /// - MONGOCXX_ABI_EXPORT_CDECL() instance(instance&&) noexcept; + /// @par Postconditions: + /// - `other` is in an assign-or-destroy-only state. + /// + MONGOCXX_ABI_EXPORT_CDECL() instance(instance&& other) noexcept; /// /// Move assigns an instance of the driver. /// - MONGOCXX_ABI_EXPORT_CDECL(instance&) operator=(instance&&) noexcept; + /// @par Postconditions: + /// - `other` is in an assign-or-destroy-only state. + /// + MONGOCXX_ABI_EXPORT_CDECL(instance&) operator=(instance&& other) noexcept; /// /// Destroys an instance of the driver. /// MONGOCXX_ABI_EXPORT_CDECL() ~instance(); + /// + /// This class is not copyable. + /// instance(instance const&) = delete; + + /// + /// This class is not copyable. + /// instance& operator=(instance const&) = delete; /// @@ -129,3 +95,6 @@ class instance { /// @file /// Provides @ref mongocxx::v_noabi::instance. /// +/// @par Includes +/// - @ref mongocxx/v1/instance.hpp +/// diff --git a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger-fwd.hpp b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger-fwd.hpp index 78de780424..32c31affe9 100644 --- a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger-fwd.hpp +++ b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger-fwd.hpp @@ -14,23 +14,29 @@ #pragma once +#include + #include namespace mongocxx { namespace v_noabi { -enum class log_level; +using v1::log_level; + +using v1::logger; -class MONGOCXX_ABI_EXPORT logger; +using v1::default_logger; } // namespace v_noabi } // namespace mongocxx namespace mongocxx { -using ::mongocxx::v_noabi::log_level; +using v1::log_level; -using ::mongocxx::v_noabi::logger; +using v1::logger; + +using v1::default_logger; } // namespace mongocxx @@ -40,3 +46,6 @@ using ::mongocxx::v_noabi::logger; /// @file /// Declares utilities related to mongocxx logging. /// +/// @par Includes +/// - @ref mongocxx/v1/logger-fwd.hpp +/// diff --git a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger.hpp b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger.hpp index 90fd27e3fa..d341bd0d8a 100644 --- a/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger.hpp +++ b/src/mongocxx/include/mongocxx/v_noabi/mongocxx/logger.hpp @@ -14,83 +14,25 @@ #pragma once -#include - #include -#include +// + +#include #include namespace mongocxx { namespace v_noabi { -/// -/// The log level of a log message. -/// -enum class log_level { - k_error, ///< Log Level Error. - k_critical, ///< Log Level Critical. - k_warning, ///< Log Level Warning. - k_message, ///< Log Level Message. - k_info, ///< Log Level Info. - k_debug, ///< Log Level Debug. - k_trace, ///< Log Level Trace. -}; - -/// -/// Returns a stringification of the given log level. -/// -/// @param level -/// The type to stringify. -/// -/// @return a std::string representation of the type. -/// -MONGOCXX_ABI_EXPORT_CDECL(bsoncxx::v_noabi::stdx::string_view) to_string(log_level level); - -/// -/// The interface which user-defined loggers must implement. -/// -/// @see -/// - @ref mongocxx::v_noabi::instance -/// -class logger { - public: - virtual ~logger(); - - logger(logger&&) = default; - logger& operator=(logger&&) = default; - logger(logger const&) = default; - logger& operator=(logger const&) = default; - - /// - /// Handles a log message. User defined logger implementations may do whatever they wish when - /// this is called, such as log the output to a file or send it to a remote server for analysis. - /// - /// @param level - /// The log level of the current log message - /// @param domain - /// The domain of the current log message, such as 'client' - /// @param message - /// The text of the current log message. - virtual void operator()( - log_level level, - bsoncxx::v_noabi::stdx::string_view domain, - bsoncxx::v_noabi::stdx::string_view message) noexcept = 0; - - protected: - /// - /// Default constructor - /// - logger(); -}; +using v1::to_string; } // namespace v_noabi } // namespace mongocxx namespace mongocxx { -using ::mongocxx::v_noabi::to_string; +using v1::to_string; } // namespace mongocxx @@ -100,3 +42,6 @@ using ::mongocxx::v_noabi::to_string; /// @file /// Provides utilities related to mongocxx logging. /// +/// @par Includes +/// - @ref mongocxx/v1/logger.hpp +/// diff --git a/src/mongocxx/lib/mongocxx/v1/exception.cpp b/src/mongocxx/lib/mongocxx/v1/exception.cpp index 38ad005621..ab29a888c3 100644 --- a/src/mongocxx/lib/mongocxx/v1/exception.cpp +++ b/src/mongocxx/lib/mongocxx/v1/exception.cpp @@ -13,3 +13,77 @@ // limitations under the License. #include + +// + +#include +#include + +#include +#include + +namespace mongocxx { +namespace v1 { + +std::error_category const& source_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "mongocxx::v1::source_errc"; + } + + std::string message(int v) const noexcept override { + using code = v1::source_errc; + + switch (static_cast(v)) { + case code::zero: + return "zero"; + case code::mongocxx: + return "mongocxx"; + case code::mongoc: + return "mongoc"; + case code::mongocrypt: + return "mongocrypt"; + case code::server: + return "server"; + default: + return std::string(this->name()) + ':' + std::to_string(v); + } + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +std::error_category const& type_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "mongocxx::v1::type_errc"; + } + + std::string message(int v) const noexcept override { + using code = v1::type_errc; + + switch (static_cast(v)) { + case code::zero: + return "zero"; + case code::invalid_argument: + return "invalid argument"; + case code::runtime_error: + return "runtime error"; + default: + return std::string(this->name()) + ':' + std::to_string(v); + } + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +exception::~exception() = default; + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v1/instance.cpp b/src/mongocxx/lib/mongocxx/v1/instance.cpp index 82b5800947..3c6c147213 100644 --- a/src/mongocxx/lib/mongocxx/v1/instance.cpp +++ b/src/mongocxx/lib/mongocxx/v1/instance.cpp @@ -13,3 +13,195 @@ // limitations under the License. #include + +// + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace mongocxx { +namespace v1 { + +using code = v1::instance::errc; + +static_assert(!std::is_move_constructible::value, "mongocxx::v1::instance must be non-moveable"); +static_assert(!std::is_copy_constructible::value, "mongocxx::v1::instance must be non-copyable"); + +namespace { + +// 0: no instance object has been created. +// 1: there is a single instance object. +// 2: the single instance object has been destroyed. +std::atomic_int instance_state{0}; + +void custom_log_handler( + mongoc_log_level_t log_level, + char const* domain, + char const* message, + void* user_data) noexcept { + (*static_cast(user_data))( + static_cast(log_level), + bsoncxx::v1::stdx::string_view(domain), + bsoncxx::v1::stdx::string_view(message)); +} + +} // namespace + +class instance::impl { + public: + std::unique_ptr _handler; + + ~impl() { + if (_handler) { + libmongoc::log_set_handler(nullptr, nullptr); + } + + libmongoc::cleanup(); + + instance_state.fetch_add(1, std::memory_order_release); + } + + impl(impl&&) = delete; + impl& operator=(impl&&) = delete; + impl(impl const&) = delete; + impl& operator=(impl const&) = delete; + + explicit impl(std::unique_ptr handler) : _handler{std::move(handler)} { + this->init(true); + } + + explicit impl(v1::default_logger tag) { + (void)tag; + this->init(false); + } + + private: + void init(bool set_custom_handler) { + { + int expected = 0; + + if (!instance_state.compare_exchange_strong( + expected, 1, std::memory_order_acquire, std::memory_order_relaxed)) { + throw v1::exception{code::multiple_instances}; + } + } + + if (set_custom_handler) { + if (auto ptr = _handler.get()) { + libmongoc::log_set_handler(&custom_log_handler, ptr); + } else { + libmongoc::log_set_handler(nullptr, nullptr); + } + } + + libmongoc::init(); + + { + // Avoid /Zc:__cplusplus problems with MSVC. +#pragma push_macro("STDCXX") +#undef STDCXX +#if defined(_MSVC_LANG) +#define STDCXX BSONCXX_PRIVATE_STRINGIFY(_MSVC_LANG) +#else +#define STDCXX BSONCXX_PRIVATE_STRINGIFY(__cplusplus) +#endif + + // Handshake data can only be appended once (or it will return false): this is that "once". + // Despite the name, mongoc_handshake_data_append() *prepends* the platform string. + // Use " / " to delimit handshake date for mongocxx and mongoc. + (void)libmongoc::handshake_data_append( + "mongocxx", + MONGOCXX_VERSION_STRING, + "CXX=" MONGOCXX_COMPILER_ID " " MONGOCXX_COMPILER_VERSION " stdcxx=" STDCXX " / "); + +#pragma pop_macro("STDCXX") + } + } +}; + +instance::~instance() = default; + +instance::instance() : instance{v1::default_logger{}} {} + +instance::instance(std::unique_ptr handler) : _impl{bsoncxx::make_unique(std::move(handler))} {} + +instance::instance(v1::default_logger tag) : _impl{bsoncxx::make_unique(tag)} { + (void)tag; +} + +std::error_category const& instance::error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "mongocxx::v1::instance"; + } + + std::string message(int v) const noexcept override { + switch (static_cast(v)) { + case code::zero: + return "zero"; + case code::multiple_instances: + return "cannot construct multiple instance objects in a given process"; + default: + return std::string(this->name()) + ':' + std::to_string(v); + } + } + + bool equivalent(int v, std::error_condition const& ec) const noexcept override { + if (ec.category() == v1::source_error_category()) { + using condition = v1::source_errc; + + auto const source = static_cast(ec.value()); + + switch (static_cast(v)) { + case code::multiple_instances: + return source == condition::mongocxx; + + case code::zero: + default: + return false; + } + } + + if (ec.category() == v1::type_error_category()) { + using condition = v1::type_errc; + + auto const type = static_cast(ec.value()); + + switch (static_cast(v)) { + case code::multiple_instances: + return type == condition::runtime_error; + + case code::zero: + default: + return false; + } + } + + return false; + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v1/logger.cpp b/src/mongocxx/lib/mongocxx/v1/logger.cpp index d1a70367cd..3f297e9abb 100644 --- a/src/mongocxx/lib/mongocxx/v1/logger.cpp +++ b/src/mongocxx/lib/mongocxx/v1/logger.cpp @@ -13,3 +13,46 @@ // limitations under the License. #include + +// + +#include + +#include + +namespace mongocxx { +namespace v1 { + +static_assert(static_cast(log_level::k_error) == static_cast(MONGOC_LOG_LEVEL_ERROR), ""); +static_assert(static_cast(log_level::k_critical) == static_cast(MONGOC_LOG_LEVEL_CRITICAL), ""); +static_assert(static_cast(log_level::k_warning) == static_cast(MONGOC_LOG_LEVEL_WARNING), ""); +static_assert(static_cast(log_level::k_message) == static_cast(MONGOC_LOG_LEVEL_MESSAGE), ""); +static_assert(static_cast(log_level::k_info) == static_cast(MONGOC_LOG_LEVEL_INFO), ""); +static_assert(static_cast(log_level::k_debug) == static_cast(MONGOC_LOG_LEVEL_DEBUG), ""); +static_assert(static_cast(log_level::k_trace) == static_cast(MONGOC_LOG_LEVEL_TRACE), ""); + +bsoncxx::v1::stdx::string_view to_string(log_level level) { + switch (level) { + case log_level::k_error: + return "error"; + case log_level::k_critical: + return "critical"; + case log_level::k_warning: + return "warning"; + case log_level::k_message: + return "message"; + case log_level::k_info: + return "info"; + case log_level::k_debug: + return "debug"; + case log_level::k_trace: + return "trace"; + default: + return "unknown"; + } +} + +logger::~logger() = default; + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cpp b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cpp index 37687ae315..00eec93c24 100644 --- a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cpp +++ b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cpp @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include +#include + +// + +#include +#include +#include #include -#include -#include +#include #include #include #include -#include #include #include -#include #include namespace mongocxx { @@ -35,130 +37,61 @@ namespace v_noabi { namespace { -log_level convert_log_level(::mongoc_log_level_t mongoc_log_level) { - switch (mongoc_log_level) { - case MONGOC_LOG_LEVEL_ERROR: - return log_level::k_error; - case MONGOC_LOG_LEVEL_CRITICAL: - return log_level::k_critical; - case MONGOC_LOG_LEVEL_WARNING: - return log_level::k_warning; - case MONGOC_LOG_LEVEL_MESSAGE: - return log_level::k_message; - case MONGOC_LOG_LEVEL_INFO: - return log_level::k_info; - case MONGOC_LOG_LEVEL_DEBUG: - return log_level::k_debug; - case MONGOC_LOG_LEVEL_TRACE: - return log_level::k_trace; - default: - MONGOCXX_PRIVATE_UNREACHABLE; - } -} +// To support mongocxx::v_noabi::instance::current(). +std::atomic current_instance{nullptr}; -void null_log_handler(::mongoc_log_level_t, char const*, char const*, void*) {} - -void user_log_handler( - ::mongoc_log_level_t mongoc_log_level, - char const* log_domain, - char const* message, - void* user_data) { - (*static_cast(user_data))( - convert_log_level(mongoc_log_level), - bsoncxx::v_noabi::stdx::string_view{log_domain}, - bsoncxx::v_noabi::stdx::string_view{message}); +// Sentinel value denoting the current instance has been destroyed. +instance* sentinel() { + alignas(instance) static unsigned char value[sizeof(instance)]; + return reinterpret_cast(value); } -// A region of memory that acts as a sentintel value indicating that an instance object is being -// destroyed. We only care about the address of this object, never its contents. -alignas(instance) unsigned char sentinel[sizeof(instance)]; - -std::atomic current_instance{nullptr}; -static_assert(std::is_standard_layout::value, "Must be standard layout"); -static_assert(std::is_trivially_destructible::value, "Must be trivially destructible"); - } // namespace class instance::impl { public: - impl(std::unique_ptr logger) : _user_logger(std::move(logger)) { - libmongoc::init(); - if (_user_logger) { - libmongoc::log_set_handler(user_log_handler, _user_logger.get()); - // The libmongoc namespace mocking system doesn't play well with varargs - // functions, so we use a bare mongoc_log call here. + v1::instance _instance; + + // mongoc does not expose the state of registered custom log handlers. A bit of indirection is needed to + // condition the informational "enabled" message even after `handler` is moved-from. + impl(std::unique_ptr&& handler, bool set_custom_handler) try : _instance{std::move(handler)} { + if (set_custom_handler) { + // Inform the user that a custom log handler has been registered. + // Cannot use `libmongoc::*` mock due to varargs. mongoc_log(MONGOC_LOG_LEVEL_INFO, "mongocxx", "libmongoc logging callback enabled"); - } else { - libmongoc::log_set_handler(null_log_handler, nullptr); } - - // Despite the name, mongoc_handshake_data_append *prepends* the platform string. - // mongoc_handshake_data_append does not add a delimitter, so include the " / " in the - // argument for consistency with the driver_name, and driver_version. - std::stringstream platform; - long stdcxx = __cplusplus; -#ifdef _MSVC_LANG - // Prefer _MSVC_LANG to report the supported C++ standard with MSVC. - // The __cplusplus macro may be incorrect. See: - // https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ - stdcxx = _MSVC_LANG; -#endif - platform << "CXX=" << MONGOCXX_COMPILER_ID << " " << MONGOCXX_COMPILER_VERSION << " " - << "stdcxx=" << stdcxx << " / "; - libmongoc::handshake_data_append("mongocxx", MONGOCXX_VERSION_STRING, platform.str().c_str()); + } catch (v1::exception const&) { + throw v_noabi::logic_error{error_code::k_cannot_recreate_instance}; } - ~impl() { - // If we had a user logger, remove it so that it can't be used by the driver after it goes - // out of scope - if (_user_logger) { - libmongoc::log_set_handler(null_log_handler, nullptr); - } - - libmongoc::cleanup(); - } - - impl(impl&&) noexcept = delete; - impl& operator=(impl&&) noexcept = delete; - - impl(impl const&) = delete; - impl& operator=(impl const&) = delete; - - std::unique_ptr const _user_logger; + explicit impl(std::unique_ptr handler) : impl{std::move(handler), static_cast(handler)} {} }; instance::instance() : instance(nullptr) {} -instance::instance(std::unique_ptr logger) { - instance* expected = nullptr; - - if (!current_instance.compare_exchange_strong(expected, this)) { - throw logic_error{error_code::k_cannot_recreate_instance}; - } - - _impl = bsoncxx::make_unique(std::move(logger)); +instance::instance(std::unique_ptr logger) : _impl{bsoncxx::make_unique(std::move(logger))} { + current_instance.store(this, std::memory_order_relaxed); } -instance::instance(instance&&) noexcept = default; -instance& instance::operator=(instance&&) noexcept = default; +instance::instance(instance&& other) noexcept = default; +instance& instance::operator=(instance&& other) noexcept = default; instance::~instance() { - current_instance.store(reinterpret_cast(&sentinel)); - _impl.reset(); + current_instance.store(sentinel(), std::memory_order_relaxed); } instance& instance::current() { - if (!current_instance.load()) { - static instance the_instance; - } - - instance* curr = current_instance.load(); + if (auto const p = current_instance.load(std::memory_order_relaxed)) { + if (p == sentinel()) { + throw v_noabi::logic_error{error_code::k_instance_destroyed}; + } else { + return *p; + } - if (curr == reinterpret_cast(&sentinel)) { - throw logic_error{error_code::k_instance_destroyed}; + } else { + static instance the_instance; + return the_instance; } - - return *curr; } } // namespace v_noabi diff --git a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/logger.cpp b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/logger.cpp index 69fbc72912..ec4c30f0a5 100644 --- a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/logger.cpp +++ b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/logger.cpp @@ -13,33 +13,3 @@ // limitations under the License. #include - -namespace mongocxx { -namespace v_noabi { - -bsoncxx::v_noabi::stdx::string_view to_string(log_level level) { - switch (level) { - case log_level::k_error: - return "error"; - case log_level::k_critical: - return "critical"; - case log_level::k_warning: - return "warning"; - case log_level::k_message: - return "message"; - case log_level::k_info: - return "info"; - case log_level::k_debug: - return "debug"; - case log_level::k_trace: - return "trace"; - default: - return "unknown"; - } -} - -logger::logger() = default; -logger::~logger() = default; - -} // namespace v_noabi -} // namespace mongocxx diff --git a/src/mongocxx/test/CMakeLists.txt b/src/mongocxx/test/CMakeLists.txt index ace3575102..abc5bf1e61 100644 --- a/src/mongocxx/test/CMakeLists.txt +++ b/src/mongocxx/test/CMakeLists.txt @@ -94,6 +94,11 @@ set(mongocxx_test_sources_v_noabi v_noabi/write_concern.cpp ) +set(mongocxx_test_sources_v1 + v1/exception.cpp + v1/logger.cpp +) + set(mongocxx_test_sources_spec spec/monitoring.cpp spec/operation.cpp @@ -113,10 +118,12 @@ set(mongocxx_test_sources_extra spec/transactions.cpp spec/unified_tests/operations.cpp spec/unified_tests/runner.cpp + subprocess.cpp v_noabi/catch_helpers.cpp v_noabi/client_helpers.cpp v_noabi/instance.cpp v_noabi/logging.cpp + v1/instance.cpp ) file(GLOB_RECURSE mongocxx_test_headers @@ -132,14 +139,17 @@ add_library(spec_test_common OBJECT ${mongocxx_test_sources_spec}) add_executable(test_driver ${mongocxx_test_sources_private} ${mongocxx_test_sources_v_noabi} + ${mongocxx_test_sources_v1} v_noabi/catch_helpers.cpp ) target_link_libraries(test_driver PRIVATE spec_test_common client_helpers) -add_executable(test_logging v_noabi/logging.cpp) -target_compile_definitions(test_logging PRIVATE MONGOCXX_TEST_DISABLE_MAIN_INSTANCE) - -add_executable(test_instance v_noabi/instance.cpp) +add_executable(test_instance + subprocess.cpp + v_noabi/instance.cpp + v_noabi/logging.cpp + v1/instance.cpp +) target_compile_definitions(test_instance PRIVATE MONGOCXX_TEST_DISABLE_MAIN_INSTANCE) add_executable(test_crud_specs spec/crud.cpp) @@ -202,7 +212,6 @@ set_property( test_driver test_gridfs_specs test_instance - test_logging test_read_write_concern_specs test_retryable_reads_specs test_transactions_specs @@ -231,7 +240,6 @@ foreach(test_name driver gridfs_specs instance - logging read_write_concern_specs retryable_reads_specs transactions_specs @@ -338,6 +346,7 @@ set_dist_list(src_mongocxx_test_DIST CMakeLists.txt ${mongocxx_test_sources_private} ${mongocxx_test_sources_v_noabi} + ${mongocxx_test_sources_v1} ${mongocxx_test_sources_spec} ${mongocxx_test_sources_extra} ${mongocxx_test_headers} diff --git a/src/mongocxx/test/subprocess.cpp b/src/mongocxx/test/subprocess.cpp new file mode 100644 index 0000000000..743fe2f82f --- /dev/null +++ b/src/mongocxx/test/subprocess.cpp @@ -0,0 +1,189 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +// + +#include +#include + +#include + +#if !defined(_MSC_VER) + +#include // exit(), strsignal(), etc. +#include // strsignal() +#include // fork(), close(), etc. + +#include +#include // waitpid() + +namespace mongocxx { +namespace test { + +int subprocess(std::function fn, bool* is_signal_ptr) { + auto is_signal_local = false; + auto& is_signal = is_signal_ptr ? *is_signal_ptr : is_signal_local; + + pid_t const pid = ::fork(); + + // Child: do nothing more than call `fn`. + if (pid == 0) { + // Use `std::_Exit()` and `std::terminate()` to prevent continued execution of the Catch2 test suite. + try { + fn(); + std::_Exit(EXIT_SUCCESS); + } catch (Catch::TestFailureException) { + // Assertion failure already output its diagnostic message. + std::_Exit(EXIT_FAILURE); + } catch (Catch::TestSkipException) { + // SKIP() already output its diagnostic message. + // Don't try to propagate the "skip", just treat as equivalent to success. + std::_Exit(EXIT_SUCCESS); + } catch (std::exception const& ex) { + // Trigger output of Catch2 diagnostic messages. + FAIL_CHECK("uncaught exception in subprocess: " << ex.what()); + std::_Exit(EXIT_FAILURE); + } catch (...) { + // Allow default termination handler to translate the unknown exception type. + // This should also trigger the output of Catch2 diagnostic messages. + std::terminate(); + } + } + + // Parent: wait for child and handle returned status values. + else { + int status; + + int const ret = ::waitpid(pid, &status, 0); + + if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS) { + UNSCOPED_INFO("subprocess exited with a non-zero exit code: " << WEXITSTATUS(status)); + is_signal = false; + return WEXITSTATUS(status); + } + + // For unexpected signals, stop immediately. + else if (WIFSIGNALED(status)) { + int const signal = WTERMSIG(status); + char const* const sigstr = ::strsignal(signal); + UNSCOPED_INFO("subprocess was killed by signal: " << signal << " (" << (sigstr ? sigstr : "") << ")"); + is_signal = true; + return signal; + } + + // We don't expect any other failure condition. + else { + REQUIRE(ret != -1); + return 0; + } + } +} + +} // namespace test +} // namespace mongocxx + +#else + +namespace mongocxx { +namespace test { + +int subprocess(std::function fn, bool* is_signal_ptr) { + (void)fn; + (void)is_signal_ptr; + + SKIP("mongocxx::test::subprocess() is not supported"); + + return 0; // Unused. +} + +} // namespace test +} // namespace mongocxx + +#endif // !defined(_MSC_VER) + +TEST_CASE("counter", "[mongocxx][test][subprocess]") { + std::atomic_int counter{0}; + + CHECK_SUBPROCESS([&counter] { counter.fetch_add(1); }); + CHECK_SUBPROCESS([&counter] { counter.fetch_add(2); }); + CHECK_SUBPROCESS([&counter] { counter.fetch_add(3); }); + + // State of a subprocess must not be observable by the original process. + REQUIRE(counter.load() == 0); +} + +TEST_CASE("failure", "[mongocxx][test][subprocess]") { + auto is_signal = false; + CHECK_FALSE_SUBPROCESS([] { + // Try to silence noisy Catch2 output. + (void)::close(1); // stdout + (void)::close(2); // stderr + + FAIL("subprocess"); + }); + CHECK_FALSE(is_signal); +} + +TEST_CASE("skip", "[mongocxx][test][subprocess]") { + auto is_signal = false; + CHECK_SUBPROCESS( + [] { + // Try to silence noisy Catch2 output. + (void)::close(1); // stdout + (void)::close(2); // stderr + + SKIP("subprocess"); + }, + &is_signal); + CHECK_FALSE(is_signal); +} + +TEST_CASE("exception", "[mongocxx][test][subprocess]") { + auto is_signal = false; + CHECK_FALSE_SUBPROCESS( + [] { + // Try to silence noisy Catch2 output. + (void)::close(1); // stdout + (void)::close(2); // stderr + + throw std::runtime_error("subprocess"); + }, + &is_signal); + CHECK_FALSE(is_signal); +} + +TEST_CASE("unknown exception", "[mongocxx][test][subprocess]") { +#if !defined(_MSC_VER) + auto is_signal = false; + auto const ret = mongocxx::test::subprocess( + [] { + // Try to silence noisy Catch2 output. + (void)::close(1); // stdout + (void)::close(2); // stderr + + throw "subprocess"; + }, + &is_signal); + CHECK(is_signal); + CHECK(ret != 0); + CHECK(ret != SIGTERM); // std::terminate() + +#else + + CHECK_SUBPROCESS([] {}); + +#endif // !defined(_MSC_VER) +} diff --git a/src/mongocxx/test/subprocess.hh b/src/mongocxx/test/subprocess.hh new file mode 100644 index 0000000000..9c75bf3f41 --- /dev/null +++ b/src/mongocxx/test/subprocess.hh @@ -0,0 +1,60 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include + +namespace mongocxx { +namespace test { + +// Execute `fn` in a forked subprocess. +// +// Should be invoked directly from the body of a TEST_CASE(), not within a SECTION(). Keep it simple! +// +// @param fn The function to be invoked within a subprocess. +// @param is_signal_ptr When not null, `*is_signal_ptr` is set to `true` if the return value is a signal rather than an +// exit code. +// +// @returns The exit code of the subprocess (`*is_signal_ptr` is `false`) or the signal used to kill the subprocess +// (`*is_signal_ptr` is `true`) . +int subprocess(std::function fn, bool* is_signal_ptr = nullptr); + +#if !defined(_MSC_VER) + +#define CHECK_SUBPROCESS(...) \ + if (1) { \ + int const ret = mongocxx::test::subprocess(__VA_ARGS__); \ + CHECK(ret == 0); \ + } else \ + ((void)0) + +#define CHECK_FALSE_SUBPROCESS(...) \ + if (1) { \ + int const ret = mongocxx::test::subprocess(__VA_ARGS__); \ + CHECK(ret != 0); \ + } else \ + ((void)0) + +#else + +#define CHECK_SUBPROCESS(...) SKIP("mongocxx::test::subprocess() is not supported") +#define CHECK_FALSE_SUBPROCESS(...) SKIP("mongocxx::test::subprocess() is not supported") + +#endif // !defined(_MSC_VER) + +} // namespace test +} // namespace mongocxx diff --git a/src/mongocxx/test/v1/exception.cpp b/src/mongocxx/test/v1/exception.cpp new file mode 100644 index 0000000000..875cc8926a --- /dev/null +++ b/src/mongocxx/test/v1/exception.cpp @@ -0,0 +1,62 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +// + +#include + +#include +#include + +namespace mongocxx { +namespace v1 { + +TEST_CASE("source", "[mongocxx][v1][error]") { + auto const& c = source_error_category(); + + SECTION("name") { + CHECK_THAT(c.name(), Catch::Matchers::Equals("mongocxx::v1::source_errc")); + } + + SECTION("message") { + CHECK(c.message(-1) == "mongocxx::v1::source_errc:-1"); + CHECK(c.message(0) == "zero"); + CHECK(c.message(1) == "mongocxx"); + CHECK(c.message(2) == "mongoc"); + CHECK(c.message(3) == "mongocrypt"); + CHECK(c.message(4) == "server"); + CHECK(c.message(5) == "mongocxx::v1::source_errc:5"); + } +} + +TEST_CASE("type", "[mongocxx][v1][error]") { + auto const& c = type_error_category(); + + SECTION("name") { + CHECK_THAT(c.name(), Catch::Matchers::Equals("mongocxx::v1::type_errc")); + } + + SECTION("message") { + CHECK(c.message(-1) == "mongocxx::v1::type_errc:-1"); + CHECK(c.message(0) == "zero"); + CHECK(c.message(1) == "invalid argument"); + CHECK(c.message(2) == "runtime error"); + CHECK(c.message(3) == "mongocxx::v1::type_errc:3"); + } +} + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/test/v1/exception.hh b/src/mongocxx/test/v1/exception.hh new file mode 100644 index 0000000000..4d1accb903 --- /dev/null +++ b/src/mongocxx/test/v1/exception.hh @@ -0,0 +1,35 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +// + +#include + +CATCH_REGISTER_ENUM( + mongocxx::v1::source_errc, + mongocxx::v1::source_errc::zero, + mongocxx::v1::source_errc::mongocxx, + mongocxx::v1::source_errc::mongoc, + mongocxx::v1::source_errc::mongocrypt, + mongocxx::v1::source_errc::server) + +CATCH_REGISTER_ENUM( + mongocxx::v1::type_errc, + mongocxx::v1::type_errc::zero, + mongocxx::v1::type_errc::invalid_argument, + mongocxx::v1::type_errc::runtime_error) diff --git a/src/mongocxx/test/v1/instance.cpp b/src/mongocxx/test/v1/instance.cpp new file mode 100644 index 0000000000..7c80662b2c --- /dev/null +++ b/src/mongocxx/test/v1/instance.cpp @@ -0,0 +1,206 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +// + +#include + +#include + +#include +#include + +#include + +#include + +#include + +#include + +#include +#include + +#if !defined(_MSC_VER) +#include +#endif + +namespace mongocxx { +namespace v1 { + +namespace { + +class custom_logger : public logger { + private: + using fn_type = std::function; + + fn_type _fn; + + public: + /* explicit(false) */ custom_logger(fn_type fn) : _fn{std::move(fn)} {} + + void operator()( + log_level level, + bsoncxx::v1::stdx::string_view domain, + bsoncxx::v1::stdx::string_view message) noexcept override { + _fn(level, domain, message); + } +}; + +} // namespace + +TEST_CASE("logger", "[mongocxx][v1][instance]") { +#if !defined(_MSC_VER) + class capture_stderr { + private: + int _pipes[2]; + int _stderr; // stderr + + public: + ~capture_stderr() { + ::dup2(_stderr, STDERR_FILENO); // Restore original stderr. + } + + capture_stderr(capture_stderr&&) = delete; + capture_stderr& operator=(capture_stderr&&) = delete; + capture_stderr(capture_stderr const&) = delete; + capture_stderr& operator=(capture_stderr const&) = delete; + + capture_stderr() + : _stderr{::dup(STDERR_FILENO)} // Save original stderr. + { + ::fflush(stderr); + REQUIRE(::pipe(_pipes) == 0); // Open redirection pipes. + REQUIRE(::dup2(_pipes[1], STDERR_FILENO) != -1); // Copy stderr to input pipe. + REQUIRE(::close(_pipes[1]) != -1); // Close original stderr. + + REQUIRE( + ::fcntl(_pipes[0], F_SETFL, ::fcntl(_pipes[0], F_GETFL) | O_NONBLOCK) != -1); // Do not block on read. + } + + std::string read() { + std::string res; + + { + // Capture everything up to this point. + // Avoid recursive macro expansion of `stderr` due to Catch expression decomposition. + auto const res = ::fflush(stderr); + REQUIRE(res == 0); + } + + while (true) { + char buf[BUFSIZ]; + auto const n = ::read(_pipes[0], buf, std::size_t{BUFSIZ - 1}); + + CHECKED_IF(n < 0) { + REQUIRE(errno == EAGAIN); // No data left in stream. + break; + } + else { + res.append(buf, static_cast(n)); + } + } + + return res; + } + }; + +#else + + class capture_stderr { + public: + std::string read() { + return {}; + } + }; + +#endif // !defined(_MSC_VER) + + { + auto const test_default = [] { + instance i; + + auto const output = [&] { + capture_stderr guard; + mongoc_log(MONGOC_LOG_LEVEL_WARNING, "mongocxx::v1::instance", "mongoc_log_default_handler"); + return guard.read(); + }(); + + CHECK_THAT( + output, + Catch::Matchers::ContainsSubstring("mongocxx::v1::instance") && + Catch::Matchers::ContainsSubstring("mongoc_log_default_handler")); + }; + CHECK_SUBPROCESS(test_default); + } + + { + auto const test_noop = [] { + instance i{nullptr}; + + auto const output = [&] { + capture_stderr guard; + mongoc_log(MONGOC_LOG_LEVEL_WARNING, "mongocxx::v1::instance", "SHOULD NOT BE LOGGED"); + return guard.read(); + }(); + + CHECK_THAT( + output, + !Catch::Matchers::ContainsSubstring("mongocxx::v1::instance") && + !Catch::Matchers::ContainsSubstring("SHOULD NOT BE LOGGED")); + }; + CHECK_SUBPROCESS(test_noop); + } + + { + auto const test_custom = [] { + int count = 0; + log_level level; + std::string domain; + std::string message; + + auto const fn = + [&](log_level _level, bsoncxx::v1::stdx::string_view _domain, bsoncxx::v1::stdx::string_view _message) { + ++count; + level = _level; + domain = std::string{_domain}; + message = std::string{_message}; + }; + + instance i{bsoncxx::make_unique(fn)}; + + mongoc_log(MONGOC_LOG_LEVEL_WARNING, "mongocxx::v1::instance", "custom_logger"); + + CHECK(count == 1); + CHECK(level == log_level::k_warning); + CHECK(domain == "mongocxx::v1::instance"); + CHECK(message == "custom_logger"); + }; + CHECK_SUBPROCESS(test_custom); + } +} + +TEST_CASE("stringify", "[mongocxx][test][v1][instance]") { + auto const test = [] { + instance i; + + REQUIRE(bsoncxx::test::stringify(i) == "mongocxx::v1::instance"); + }; + CHECK_SUBPROCESS(test); +} + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/test/v1/instance.hh b/src/mongocxx/test/v1/instance.hh new file mode 100644 index 0000000000..2faac1a5f9 --- /dev/null +++ b/src/mongocxx/test/v1/instance.hh @@ -0,0 +1,32 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +// + +#include + +namespace Catch { + +template <> +struct StringMaker { + static std::string convert(mongocxx::v1::instance const&) { + return "mongocxx::v1::instance"; + } +}; + +} // namespace Catch diff --git a/src/mongocxx/test/v1/logger.cpp b/src/mongocxx/test/v1/logger.cpp new file mode 100644 index 0000000000..bd9f615660 --- /dev/null +++ b/src/mongocxx/test/v1/logger.cpp @@ -0,0 +1,55 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +// + +#include + +#include + +#include + +namespace mongocxx { +namespace v1 { + +namespace { + +struct custom_logger : logger { + void operator()( + log_level level, + bsoncxx::v1::stdx::string_view domain, + bsoncxx::v1::stdx::string_view message) noexcept override { + (void)level; + (void)domain; + (void)message; + } +}; + +} // namespace + +// mongocxx::v1::logger must not unnecessarily impose special requirements on derived classes. +static_assert(std::is_nothrow_destructible::value, ""); +static_assert(std::is_nothrow_move_constructible::value, ""); +static_assert(std::is_copy_constructible::value, ""); +static_assert(std::is_default_constructible::value, ""); + +TEST_CASE("stringify", "[mongocxx][test][v1][logger]") { + // No point specializing StringMaker for abstract class mongocxx::v1::logger. + CHECK(bsoncxx::test::stringify(custom_logger{}) == "{?}"); +} + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/test/v1/logger.hh b/src/mongocxx/test/v1/logger.hh new file mode 100644 index 0000000000..3c8249d60e --- /dev/null +++ b/src/mongocxx/test/v1/logger.hh @@ -0,0 +1,37 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +// + +#include + +CATCH_REGISTER_ENUM( + mongocxx::v1::log_level, + mongocxx::v1::log_level::k_error, + mongocxx::v1::log_level::k_critical, + mongocxx::v1::log_level::k_warning, + mongocxx::v1::log_level::k_message, + mongocxx::v1::log_level::k_info, + mongocxx::v1::log_level::k_debug, + mongocxx::v1::log_level::k_trace) + +namespace Catch { + +// No point specializing StringMaker for abstract class mongocxx::v1::logger. + +} // namespace Catch diff --git a/src/mongocxx/test/v_noabi/instance.cpp b/src/mongocxx/test/v_noabi/instance.cpp index 1e5d5b9212..77fe2212ff 100644 --- a/src/mongocxx/test/v_noabi/instance.cpp +++ b/src/mongocxx/test/v_noabi/instance.cpp @@ -12,34 +12,50 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include -#include +// + +#include +#include + +#include + +#include namespace { -TEST_CASE("instance", "[mongocxx][v_noabi][instance]") { - using namespace mongocxx; +using mongocxx::v_noabi::instance; + +using code = mongocxx::v_noabi::error_code; + +TEST_CASE("basic", "[mongocxx][v_noabi][instance]") { + auto const ret = mongocxx::test::subprocess([] { + using mongocxx::v_noabi::logic_error; - instance* inst = nullptr; + instance* inst = nullptr; - // instance::current creates instance when one has not already been created - REQUIRE_NOTHROW(inst = &instance::current()); + // instance::current creates instance when one has not already been created + REQUIRE_NOTHROW(inst = &instance::current()); - // multiple instances cannot be created" - REQUIRE_THROWS_AS(instance{}, logic_error); + // multiple instances cannot be created" + REQUIRE_THROWS_WITH_CODE(instance{}, code::k_cannot_recreate_instance); + REQUIRE_THROWS_AS(instance{}, logic_error); - // instance::current works when instance is alive - REQUIRE_NOTHROW(instance::current()); + // instance::current works when instance is alive + REQUIRE_NOTHROW(instance::current()); - // an instance cannot be created after one has been destroyed - inst->~instance(); - inst = nullptr; - REQUIRE_THROWS_AS(instance{}, logic_error); + // an instance cannot be created after one has been destroyed + inst->~instance(); + inst = nullptr; + REQUIRE_THROWS_WITH_CODE(instance{}, code::k_cannot_recreate_instance); + REQUIRE_THROWS_AS(instance{}, logic_error); - // instance::current throws if an instance has already been destroyed - REQUIRE_THROWS_AS(instance::current(), logic_error); + // instance::current throws if an instance has already been destroyed + REQUIRE_THROWS_WITH_CODE(instance::current(), code::k_instance_destroyed); + REQUIRE_THROWS_AS(instance::current(), logic_error); + }); + REQUIRE(ret == 0); } } // namespace diff --git a/src/mongocxx/test/v_noabi/logging.cpp b/src/mongocxx/test/v_noabi/logging.cpp index 4d9fb86a67..fb2938f7ec 100644 --- a/src/mongocxx/test/v_noabi/logging.cpp +++ b/src/mongocxx/test/v_noabi/logging.cpp @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + +// + #include #include -#include #include @@ -23,6 +26,8 @@ #include +#include + namespace { using namespace mongocxx; @@ -49,18 +54,21 @@ class reset_log_handler_when_done { } }; -TEST_CASE("a user-provided log handler will be used for logging output", "[instance]") { - reset_log_handler_when_done rlhwd; +TEST_CASE("custom", "[mongocxx][v_noabi][logger]") { + auto const ret = mongocxx::test::subprocess([] { + reset_log_handler_when_done rlhwd; - std::vector events; - mongocxx::instance driver{bsoncxx::make_unique(&events)}; + std::vector events; + mongocxx::instance driver{bsoncxx::make_unique(&events)}; - // The libmongoc namespace mocking system doesn't play well with varargs - // functions, so we use a bare mongoc_log call here. - mongoc_log(::MONGOC_LOG_LEVEL_ERROR, "foo", "bar"); + // The libmongoc namespace mocking system doesn't play well with varargs + // functions, so we use a bare mongoc_log call here. + mongoc_log(::MONGOC_LOG_LEVEL_ERROR, "foo", "bar"); - REQUIRE(events.size() == 1); - REQUIRE(events[0] == std::make_tuple(log_level::k_error, "foo", "bar")); + REQUIRE(events.size() == 1); + REQUIRE(events[0] == std::make_tuple(log_level::k_error, "foo", "bar")); + }); + REQUIRE(ret == 0); } } // namespace