diff --git a/mock_pdi/pdi.h b/mock_pdi/pdi.h index 5515ddbdf..3b61871f6 100644 --- a/mock_pdi/pdi.h +++ b/mock_pdi/pdi.h @@ -48,9 +48,18 @@ typedef enum PDI_status_e { PDI_ERR_PERMISSION, PDI_ERR_RIGHT = PDI_ERR_PERMISSION, PDI_ERR_TYPE, + PDI_ERR_INVALIDACTION, + PDI_ERR_MULTIPLE, PDI_NB_STATUSES_DEFINED } PDI_status_t; +static +#ifdef __cplusplus + constexpr +#endif + char const * const PDI_STATUS_MSG[PDI_NB_STATUSES_DEFINED] + = {"", "", "", "", "", "", "", "", "", "", "", ""}; + typedef void (*PDI_errfunc_f)(PDI_status_t status, const char* message, void* context); typedef struct PDI_errhandler_s { diff --git a/pdi/AUTHORS b/pdi/AUTHORS index 918e59df0..eef83bbb7 100644 --- a/pdi/AUTHORS +++ b/pdi/AUTHORS @@ -15,7 +15,8 @@ Benoit Martin - CEA (bmartin@cea.fr) * Add support for const data in `PDI_share`, `PDI_expose` and `PDI_multi_expose` Jacques Morice - CEA (jacques.morice@cea.fr) -* Add Add operator== in Context::Iterator() +* Add operator== in Context::Iterator() +* Delay data events in multi_expose when the last data is exposed François-Xavier Mordant - CEA (francois-xavier.mordant@cea.fr) * Fixed CMake issues, internal API enhancement diff --git a/pdi/CHANGELOG.md b/pdi/CHANGELOG.md index 16035306e..ef22564e0 100644 --- a/pdi/CHANGELOG.md +++ b/pdi/CHANGELOG.md @@ -20,6 +20,16 @@ and this project adheres to * added a `ENABLE_BENCHMARKING` flag to cmake to enable running the benchmarks as part of the tests (off by default) [#679](https://github.com/pdidev/pdi/issues/679) +* Improved messages for specification tree errors, with file & line numbers and + support for file names from Paraconf 1.1, + [#657](https://github.com/pdidev/pdi/issues/657) +* A new error code `PDI_ERR_INVALIDACTION` has been added when an action + requested in the yaml specification tree makes no sense (but the specification + tree in syntaxically correct) +* A new error code `PDI_ERR_MULTIPLE` has been added when multiple errors of + different kind happen +* `PDI_STATUS_MSG` has been added to offer an english description of error + codes. #### Changed * The minimum version of C required is now C17 (ISO/IEC 9899:2018) instead of @@ -28,6 +38,8 @@ and this project adheres to usages [#675](https://github.com/pdidev/pdi/issues/675) * benchmarks are not run as part of the test suite by default anymore, one must set `ENABLE_BENCHMARKING` to `ON` in Cmake to re-enable them +* Delay data events in multi_expose when the last data is exposed + [#514](https://github.com/pdidev/pdi/issues/514) #### Deprecated * Error names have been improved to fix @@ -60,6 +72,7 @@ and this project adheres to #### Removed * `PDI::Unavailable_error` was never used and has been removed. +* `PDI::Error` is now an abstract class and should never be used directly. #### Fixed diff --git a/pdi/CMakeLists.txt b/pdi/CMakeLists.txt index b8590a3ad..c6b162e40 100644 --- a/pdi/CMakeLists.txt +++ b/pdi/CMakeLists.txt @@ -139,6 +139,7 @@ set(PDI_C_SRC src/data_descriptor_impl.cxx src/datatype.cxx src/datatype_template.cxx + src/delayed_data_callbacks.cxx src/error.cxx src/expression.cxx src/expression/impl.cxx diff --git a/pdi/docs/Doxyfile b/pdi/docs/Doxyfile index 8bcc83652..e2eb8080a 100644 --- a/pdi/docs/Doxyfile +++ b/pdi/docs/Doxyfile @@ -1,5 +1,5 @@ #============================================================================= -# Copyright (C) 2024 Commissariat a l'energie atomique et aux energies alternatives (CEA) +# Copyright (C) 2024-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) # # All rights reserved. # @@ -33,7 +33,7 @@ ALLEXTERNALS = NO ALLOW_UNICODE_NAMES = NO ALPHABETICAL_INDEX = YES ALWAYS_DETAILED_SEC = NO -AUTOLINK_IGNORE_WORDS = +AUTOLINK_IGNORE_WORDS = PDI AUTOLINK_SUPPORT = YES BINARY_TOC = NO BRIEF_MEMBER_DESC = YES @@ -90,7 +90,7 @@ EXAMPLE_PATTERNS = * EXAMPLE_RECURSIVE = NO EXCLUDE = EXCLUDE_PATTERNS = -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = *::impl, *::impl::* EXCLUDE_SYMLINKS = NO EXPAND_AS_DEFINED = EXPAND_ONLY_PREDEF = YES @@ -175,7 +175,7 @@ HTML_OUTPUT = @CMAKE_CURRENT_BINARY_DIR@/html HTML_PROJECT_COOKIE = HTML_STYLESHEET = @CMAKE_CURRENT_SOURCE_DIR@/_template/style.css IDL_PROPERTY_SUPPORT = YES -IGNORE_PREFIX = +IGNORE_PREFIX = IMAGE_PATH = @CMAKE_CURRENT_SOURCE_DIR@/images IMPLICIT_DIR_DOCS = NO INCLUDED_BY_GRAPH = NO diff --git a/pdi/include/pdi.h b/pdi/include/pdi.h index 4e2bf5f3c..ef6aec1fa 100644 --- a/pdi/include/pdi.h +++ b/pdi/include/pdi.h @@ -103,10 +103,33 @@ typedef enum PDI_status_e { PDI_ERR_RIGHT PDI_ERR_RIGHT_DEPRECATED = PDI_ERR_PERMISSION, /// Invalid type error PDI_ERR_TYPE, - /// The amount of distinct error codes defined. This should always remain last and not be used as an error code + /// Action described in the specification tree is invalid + PDI_ERR_INVALIDACTION, + /// Multiple errors of different types append + PDI_ERR_MULTIPLE, + /// The amount of distinct error codes defined. This should not be used as an error code + // it should also always remain last PDI_NB_STATUSES_DEFINED } PDI_status_t; +static +#ifdef __cplusplus + constexpr +#endif + char const * const PDI_STATUS_MSG[] + = {"Not an error", + "Data unavailable", + "Invalid entry in specification tree", + "Invalid value expression", + "Invalid plugin", + "Missing feature", + "System error", + "Precondition not respected", + "Permission issue", + "Incorrect type", + "Invalid action requested", + "Multiple errors"}; + /** Type of a callback function used when an error occurs * \param status the error code * \param message the human-readable error message diff --git a/pdi/include/pdi/data_descriptor.h b/pdi/include/pdi/data_descriptor.h index 5870597f0..fccff9e3c 100644 --- a/pdi/include/pdi/data_descriptor.h +++ b/pdi/include/pdi/data_descriptor.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2015-2024 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2015-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -78,14 +78,23 @@ class PDI_EXPORT Data_descriptor */ virtual bool empty() = 0; - /** Shares some data with PDI + /** Shares some data with PDI and callbacks on the data will be trigger inside this function * \param[in,out] data the shared data * \param read whether read access is granted to other references * \param write whether write access is granted to other references */ virtual void share(void* data, bool read, bool write) = 0; - /** Shares some data with PDI + /** Shares some data with PDI and callbacks on the data will be trigger outside this function + * \param[in,out] data the shared data + * \param read whether read access is granted to other references + * \param write whether write access is granted to other references + * \param delayed_callbacks a list of callbacks where the callback for this data will be added, + * instead of being triggered. So that one can delay the trigger + */ + virtual void share(void* data, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) = 0; + + /** Shares some data with PDI and callbacks on the data will be trigger inside this function * \param[in,out] ref a reference to the shared data * \param read whether the stored reference should have read access * \param write whether the stored reference should have write access @@ -93,6 +102,16 @@ class PDI_EXPORT Data_descriptor */ virtual void* share(Ref ref, bool read, bool write) = 0; + /** Shares some data with PDI and callbacks on the data will be trigger outside this function + * \param[in,out] ref a reference to the shared data + * \param read whether the stored reference should have read access + * \param write whether the stored reference should have write access + * \param delayed_callbacks a list of callbacks where the callback for this data will be added, + * instead of being triggered. So that one can delay the trigger + * \return the just shared buffer + */ + virtual void* share(Ref ref, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) = 0; + /** Releases ownership of a data shared with PDI. PDI is then responsible to * free the associated memory whenever necessary. */ diff --git a/pdi/include/pdi/delayed_data_callbacks.h b/pdi/include/pdi/delayed_data_callbacks.h new file mode 100644 index 000000000..029e788ef --- /dev/null +++ b/pdi/include/pdi/delayed_data_callbacks.h @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (C) 2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of CEA nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific + * prior written permission. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#ifndef PDI_DELAYED_DATA_CALLBACK_H_ +#define PDI_DELAYED_DATA_CALLBACK_H_ + +#include +#include + +#include +#include "pdi/context.h" + +#include "global_context.h" + +namespace PDI { + +class PDI_EXPORT Delayed_data_callbacks +{ + /// list of names of the data + std::vector m_datanames; + + /// The context where the list of data is a part of + Global_context& m_context; + +public: + /// constructor + Delayed_data_callbacks(Global_context& ctx); + + Delayed_data_callbacks(const Delayed_data_callbacks&) = delete; + + Delayed_data_callbacks(Delayed_data_callbacks&&) = delete; + + /// In the destructor, we need to throw an error message in case the callback on the data doesn't work (trigger function) + /// (example: error in the config.yml for a plugin, error due to external library incompatibility) + ~Delayed_data_callbacks() noexcept(false); + + /// add element "name" to "m_datanames" + void add_dataname(const std::string& name); + + /// Trigger data callback for all elements in "m_datanames" + void trigger(); + + /// clear m_datanames + void cancel(); + +}; // class Delayed_data_callbacks + +} // namespace PDI +#endif // PDI_DELAYED_DATA_CALLBACK_H_ diff --git a/pdi/include/pdi/error.h b/pdi/include/pdi/error.h index 9e5243e42..65a38383f 100644 --- a/pdi/include/pdi/error.h +++ b/pdi/include/pdi/error.h @@ -26,176 +26,292 @@ #ifndef PDI_ERROR_H_ #define PDI_ERROR_H_ +#include #include +#include +#include #include +#include #include -#include - #include #include +#include namespace PDI { -class PDI_EXPORT Error: public std::exception +/// A concept that represent a "range" (list) of errors, in any kind of storage +template +concept range_of_exception_ptrs + = std::ranges::input_range && std::ranges::sized_range && std::convertible_to, std::exception_ptr>; + +/** An error class from which all PDI error are children. + * + * Offers access to + * - an error message (`what`) from std::exception + * - an error code (`status`) + * - a longer error message for verbose output (`full_msg`) + * - the ability to be rethrown with added context (`rethrow_with_context`) + */ +class PDI_EXPORT Error: virtual public std::exception { -protected: - /// status of the error - PDI_status_t m_status; - - /// message of the error - std::string m_what; - public: - /** Creates a PDI error without a message - * \param[in] errcode the error code of the error to create - */ - Error(PDI_status_t errcode); + virtual ~Error() noexcept; - /** Creates a PDI error - * \param[in] errcode the error code of the error to create - * \param[in] format_str an errror message as a python-style format - * \param[in] args the python-style parameters for the message - * \see printf + /** Gives access to the status of the error + * \returns the status of the error */ - template - inline constexpr Error(PDI_status_t errcode, fmt::format_string format_str, Args&&... args) - : m_status{errcode} - , m_what{fmt::format(format_str, std::forward(args)...)} - {} + virtual PDI_status_t status() const noexcept = 0; - /** Creates a PDI error - * \param[in] errcode the error code of the error to create - * \param[in] message an errror message + /** Rethrow the error with some context prepended to its description + * \param context the context to prepend to the error message */ - Error(PDI_status_t errcode, const char* message); - - const char* what() const noexcept override; + [[noreturn]] virtual void rethrow_with_context(std::string context) const = 0; - /** Returns status of the error - * \return status of the error + /** Gives access to a full error message + * + * The message includes all available context as well as the info about error type + * + * \returns the full message */ - PDI_status_t status() const noexcept; -}; + virtual std::string full_msg() const = 0; -class PDI_EXPORT Spectree_error: public Error -{ -public: + /** Rethrow the error with some context prepended to its description + * \param[in] format_str the context to prepend to the error message as a python-style {fmt} format + * \param[in] args the fmt parameters for the message + */ template - Spectree_error(PC_tree_t tree, fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_SPECTREE) + [[noreturn]] void inline rethrow_with_context(fmt::format_string format_str, Args&&... args) const { - std::ostringstream err_msg; - if (!PC_status(tree) && tree.node) { - if (tree.node->start_mark.line == tree.node->end_mark.line) { - err_msg << "Spectree_error in line " << tree.node->start_mark.line + 1 << ": "; - } else { - err_msg << "Spectree_error in lines " << tree.node->start_mark.line + 1 << " - " << tree.node->end_mark.line << ": "; - } - } else { - err_msg << "Spectree_error: "; - } - err_msg << fmt::format(format_str, std::forward(args)...); - m_what = err_msg.str(); + rethrow_with_context(fmt::format(format_str, std::forward(args)...)); + std::abort(); // rethrow_with_context should never return, this is just to silence compiler warnings } - - Spectree_error(Spectree_error&&) = default; - - Spectree_error(const Spectree_error&) = default; }; -class PDI_EXPORT Value_error: public Error +namespace impl { + +/// a "trait" that implements `what` from std::exception +class PDI_EXPORT what_impl: virtual public std::exception { -public: - template - inline constexpr Value_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_VALUE, "Value_error: {}", fmt::format(format_str, std::forward(args)...)) - {} +private: + /// message of the error + std::string m_what; - Value_error(Value_error&&) = default; +protected: + /** build a new what_impl by specifying the message of the error + * \param what the message of the error + */ + what_impl(std::string what) noexcept; - Value_error(const Value_error&) = default; +public: + const char* what() const noexcept override; }; -class PDI_EXPORT Plugin_error: public Error +/// a "trait" that implements `status` from PDI::Error +template +class PDI_EXPORT status_impl: virtual public Error { public: - template - inline constexpr Plugin_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_PLUGIN, "Plugin_error: {}", fmt::format(format_str, std::forward(args)...)) - {} - - Plugin_error(Plugin_error&&) = default; - - Plugin_error(const Plugin_error&) = default; + PDI_status_t status() const noexcept override { return STATUS; } }; -class PDI_EXPORT Impl_error: public Error +/// a basic implementation of PDI:Error +template +class PDI_EXPORT Error_impl + : public what_impl + , public status_impl { public: + /** build a new Error_impl by specifying the message of the error + * \param what the message of the error + */ + Error_impl(std::string what) noexcept; + + /** build a new Error_impl by specifying the message of the error + * \param[in] format_str an error message as a python-style {fmt} format + * \param[in] args the fmt parameters for the message + */ template - inline constexpr Impl_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_IMPL, "Impl_error: {}", fmt::format(format_str, std::forward(args)...)) + inline Error_impl(fmt::format_string format_str, Args&&... args) noexcept + : Error_impl(fmt::format(format_str, std::forward(args)...)) {} - Impl_error(Impl_error&&) = default; + using what_impl::what; - Impl_error(const Impl_error&) = default; -}; + using status_impl::status; -class PDI_EXPORT System_error: public Error -{ -public: - template - inline constexpr System_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_SYSTEM, "System_error: {}", fmt::format(format_str, std::forward(args)...)) - {} + std::string full_msg() const override; - System_error(System_error&&) = default; - - System_error(const System_error&) = default; + [[noreturn]] void rethrow_with_context(std::string msg) const override; }; -class PDI_EXPORT State_error: public Error +} // namespace impl + +/** An error class to use when a value expression is invalid + */ +using Value_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when trying to load a non-existing plugin + */ +using Plugin_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use for implementation limitations (typically an unimplemented feature) + */ +using Impl_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when a call to a function has been made at a wrong time (e.g. closing an + * unopened transaction) + */ +using State_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when a conflict of ownership over a content has been raised + */ +using Permission_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when a system error occurred (OS, etc.) + */ +using System_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use for invalid types + */ +using Type_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when an action described in the specification tree is invalid + */ +using Invalid_action_error = impl::Error_impl; +extern template class PDI_EXPORT impl::Error_impl; + +/** An error class to use when there is an invalid entry in the specification tree + */ +class PDI_EXPORT Spectree_error + : public impl::what_impl + , public impl::status_impl { +private: + std::optional m_location; + + Spectree_error(std::optional location, std::string what); + public: + /** Creates a new Spectree_error + * \param[in] tree the subtree that's in error + * \param[in] what an error message + */ + Spectree_error(PC_tree_t tree, std::string what); + + /** Creates a new Spectree_error + * \param[in] tree the subtree that's in error + * \param[in] format_str an error message as a python-style format + * \param[in] args the fmt parameters for the message + */ template - inline constexpr State_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_STATE, "State_error: {}", fmt::format(format_str, std::forward(args)...)) + inline Spectree_error(PC_tree_t tree, fmt::format_string format_str, Args&&... args) + : Spectree_error(tree, fmt::format(format_str, std::forward(args)...)) {} - State_error(State_error&&) = default; + using what_impl::what; + + using status_impl::status; - State_error(const State_error&) = default; + std::string full_msg() const; + + [[noreturn]] void rethrow_with_context(std::string msg) const; }; -class PDI_EXPORT Permission_error: public Error +/** An error class to use when multiple errors of different kind have happened + */ +class PDI_EXPORT Multiple_errors + : public impl::status_impl + , public impl::what_impl { +private: + /// The list or original errors + std::vector m_nested_ptrs; + public: + /** Creates a new Multiple_errors + * \param[in] causes the list or original errors + * \param[in] what an error message + */ + Multiple_errors(std::vector causes, std::string what) noexcept; + + /** Creates a new Multiple_errors + * \param[in] causes the list or original errors + * \param[in] format_str an error message as a python-style format + * \param[in] args the fmt parameters for the message + */ template - inline constexpr Permission_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_PERMISSION, "Permission_error: {}", fmt::format(format_str, std::forward(args)...)) + inline Multiple_errors(range_of_exception_ptrs auto&& causes, fmt::format_string format_str, Args&&... args) noexcept + : Multiple_errors(std::vector(causes.begin(), causes.end()), fmt::format(format_str, std::forward(args)...)) {} - Permission_error(Permission_error&&) = default; + /** Gives access to the list or original errors + * \return the list or original errors + */ + std::vector const & nested_ptrs() const; - Permission_error(const Permission_error&) = default; -}; + using status_impl::status; -class PDI_EXPORT Type_error: public Error -{ -public: - template - inline constexpr Type_error(fmt::format_string format_str, Args&&... args) - : Error(PDI_ERR_TYPE, "Type_error: {}", fmt::format(format_str, std::forward(args)...)) - {} + using what_impl::what; - Type_error(Type_error&&) = default; + std::string full_msg() const override; - Type_error(const Type_error&) = default; + [[noreturn]] void rethrow_with_context(std::string msg) const override; }; -} // namespace PDI +/** Throws a new exception by adding context to an existing exception + * + * \param[in] err original error + * \param[in] msg some context to prepend to the error + */ +[[noreturn]] void PDI_EXPORT rethrow_with_simple_context(std::exception_ptr err, std::string msg); + +/** Throws a new exception by adding context to an existing exception + * + * \param[in] err original error + * \param[in] format_str some context to prepend to the error as a python-style {fmt} format + * \param[in] args the fmt parameters for the message + */ +template +[[noreturn]] static inline void rethrow_with_context(std::exception_ptr err, fmt::format_string format_str, Args&&... args) +{ + rethrow_with_simple_context(err, fmt::format(format_str, std::forward(args)...)); +} + +/** Throws a new exception by adding context to an existing set of exceptions + * + * - if the set of exception (errors) is empty, do nothing, + * - if it contains a single exception, throw it with additional context + * - if it contains multiple exceptions, throw a `Multiple_errors` + * + * \param[in] errors a list of original errors + * \param[in] msg some context to prepend to the error + */ +void PDI_EXPORT rethrow_with_simple_context(std::vector errors, std::string msg); + +/** Throws a new exception by adding context to an existing set of exceptions + * + * - if the set of exception (errors) is empty, do nothing, + * - if it contains a single exception, throw it with additional context + * - if it contains multiple exceptions, throw a `Multiple_errors` + * + * \param[in] errors a list of original errors + * \param[in] format_str some context to prepend to the error as a python-style {fmt} format + * \param[in] args the fmt parameters for the message + */ +template +static inline void rethrow_with_context(range_of_exception_ptrs auto&& errors, fmt::format_string format_str, Args&&... args) +{ + rethrow_with_simple_context(std::vector(errors.begin(), errors.end()), fmt::format(format_str, std::forward(args)...)); +} +} // namespace PDI #endif // PDI_ERROR_H_ diff --git a/pdi/include/pdi/fmt.h b/pdi/include/pdi/fmt.h index a42fe609f..7492b3882 100644 --- a/pdi/include/pdi/fmt.h +++ b/pdi/include/pdi/fmt.h @@ -33,13 +33,9 @@ #endif #include #include -#if FMT_VERSION >= 110000 #include -#endif #else // SPDLOG_FMT_EXTERNAL is defined - use external fmtlib #include #include -#if FMT_VERSION >= 110000 #include #endif -#endif diff --git a/pdi/include/pdi/paraconf_wrapper.h b/pdi/include/pdi/paraconf_wrapper.h index 806071be4..96ca50402 100644 --- a/pdi/include/pdi/paraconf_wrapper.h +++ b/pdi/include/pdi/paraconf_wrapper.h @@ -30,6 +30,7 @@ #define PDI_PARACONF_WRAPPER_H_ #include +#include #include #include @@ -44,12 +45,69 @@ namespace PDI { struct PDI_EXPORT Paraconf_wrapper { PC_errhandler_t m_handler; - Paraconf_wrapper(); ~Paraconf_wrapper(); }; +/** A region in a YAML "file", from start to end + */ +class Yaml_region +{ +public: + /// a location in a YAML stream + struct Yaml_mark { + /// the line of the location + size_t line; + /// the column of the location + size_t column; + }; + +private: + /// the "file" where this region resides (might be "" if not in a file + std::string m_file; + + /// the start location of the region + Yaml_mark m_start; + + /// the end location of the region + Yaml_mark m_end; + + /** Builds a new YAML region that contains a given YAML subtree + * + * \warning the subtree must be valid + * \pre PC_status(tree) == PC_OK + * \param tree the subtree + */ + Yaml_region(PC_tree_t tree); + +public: + /** Gives access to the "file" where this region resides + * + * \return the "file" where this region resides (might be "" if not in a file) + */ + inline const std::string& file() const { return m_file; } + + /** Gives access to the start location of the region + * + * \return the start location of the region + */ + inline const Yaml_mark& start() const { return m_start; } + + /** Gives access to the end location of the region + * + * \return the end location of the region + */ + inline const Yaml_mark& end() const { return m_end; } + + /** Builds a YAML region that contains a given YAML subtree or nothing if the tree is invalid + * + * \param tree the subtree + * \return the YAML region or an empty optional if the tree was invalid or had no region specified + */ + static std::optional make(PC_tree_t tree); +}; + /** Returns the length of a node. * * - for a sequence: the number of nodes, diff --git a/pdi/include/pdi/pdi_fwd.h b/pdi/include/pdi/pdi_fwd.h index b3634b737..acd313205 100644 --- a/pdi/include/pdi/pdi_fwd.h +++ b/pdi/include/pdi/pdi_fwd.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2015-2025 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2015-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * Copyright (C) 2021 Institute of Bioorganic Chemistry Polish Academy of Science (PSNC) * All rights reserved. * @@ -79,6 +79,11 @@ using Datatype_template_sptr = std::shared_ptr; using Datatype_sptr = std::shared_ptr; +/** + * A class to delay the moment to call call_data_callback +*/ +class Delayed_data_callbacks; + /** A data descriptors with a name and a value, it contains an implicit type * template that is used when exposing untyped data */ diff --git a/pdi/src/callbacks.cxx b/pdi/src/callbacks.cxx index 8d33606ed..08d13bd41 100644 --- a/pdi/src/callbacks.cxx +++ b/pdi/src/callbacks.cxx @@ -125,40 +125,27 @@ void Callbacks::call_init_callbacks() const void Callbacks::call_data_callbacks(const string& name, Ref ref) const { std::vector>> data_callbacks; - //add named callbacks + // add named callbacks auto callback_it_pair = m_named_data_callbacks.equal_range(name); for (auto it = callback_it_pair.first; it != callback_it_pair.second; it++) { data_callbacks.emplace_back(std::cref(it->second)); } - //add the unnamed callbacks + // add the unnamed callbacks for (auto it = m_data_callbacks.begin(); it != m_data_callbacks.end(); it++) { data_callbacks.emplace_back(std::cref(*it)); } m_context.logger().trace("Calling `{}' share. Callbacks to call: {}", name, data_callbacks.size()); - //call gathered callbacks - vector errors; + // call gathered callbacks + vector errors; for (const std::function& callback: data_callbacks) { try { callback(name, ref); //TODO: remove the faulty plugin in case of error? - } catch (const Error& e) { - errors.emplace_back(e); - } catch (const exception& e) { - errors.emplace_back(PDI_ERR_SYSTEM, e.what()); } catch (...) { - errors.emplace_back(PDI_ERR_SYSTEM, "Not std::exception based error"); - } - } - if (!errors.empty()) { - if (1 == errors.size()) { - throw Error{errors.front().status(), "Error while triggering data share `{}': {}", name, errors.front().what()}; - } - string errmsg = "Multiple (" + std::to_string(errors.size()) + ") errors while triggering data share `" + name + "':\n"; - for (auto&& err: errors) { - errmsg += string(err.what()) + "\n"; + errors.emplace_back(std::current_exception()); } - throw System_error{"{}", errmsg.c_str()}; } + rethrow_with_context(errors, "while sharing `{}', ", name); } void Callbacks::call_data_remove_callbacks(const string& name, Ref ref) const @@ -175,29 +162,16 @@ void Callbacks::call_data_remove_callbacks(const string& name, Ref ref) const } m_context.logger().trace("Calling `{}' data remove. Callbacks to call: {}", name, data_remove_callbacks.size()); //call gathered callbacks - vector errors; + vector errors; for (const std::function& callback: data_remove_callbacks) { try { callback(name, ref); //TODO: remove the faulty plugin in case of error? - } catch (const Error& e) { - errors.emplace_back(e); - } catch (const exception& e) { - errors.emplace_back(PDI_ERR_SYSTEM, e.what()); } catch (...) { - errors.emplace_back(PDI_ERR_SYSTEM, "Not std::exception based error"); + errors.emplace_back(std::current_exception()); } } - if (!errors.empty()) { - if (1 == errors.size()) { - throw Error{errors.front().status(), "Error while triggering data share `{}': {}", name, errors.front().what()}; - } - string errmsg = "Multiple (" + std::to_string(errors.size()) + ") errors while triggering data share `" + name + "':\n"; - for (auto&& err: errors) { - errmsg += string(err.what()) + "\n"; - } - throw System_error{"{}", errmsg.c_str()}; - } + rethrow_with_context(errors, "while removing `{}', ", name); } void Callbacks::call_event_callbacks(const string& name) const @@ -214,29 +188,16 @@ void Callbacks::call_event_callbacks(const string& name) const } m_context.logger().trace("Calling `{}' event. Callbacks to call: {}", name, event_callbacks.size()); //call gathered callbacks - std::vector errors; + vector errors; for (const function& callback: event_callbacks) { try { callback(name); //TODO: remove the faulty plugin in case of error? - } catch (const Error& e) { - errors.emplace_back(e); - } catch (const exception& e) { - errors.emplace_back(PDI_ERR_SYSTEM, e.what()); } catch (...) { - errors.emplace_back(PDI_ERR_SYSTEM, "Not std::exception based error"); - } - } - if (!errors.empty()) { - if (1 == errors.size()) { - throw Error{errors.front().status(), "Error while triggering event `{}': {}", name, errors.front().what()}; - } - string errmsg = "Multiple (" + std::to_string(errors.size()) + ") errors while triggering event `" + string(name) + "':\n"; - for (auto&& err: errors) { - errmsg += string(err.what()) + "\n"; + errors.emplace_back(std::current_exception()); } - throw System_error{"{}", errmsg.c_str()}; } + rethrow_with_context(errors, "while triggering `{}', ", name); } void Callbacks::call_empty_desc_access_callbacks(const string& name) const @@ -253,29 +214,16 @@ void Callbacks::call_empty_desc_access_callbacks(const string& name) const } m_context.logger().trace("Calling `{}' empty desc access. Callbacks to call: {}", name, empty_desc_callbacks.size()); //call gathered callbacks - vector errors; + vector errors; for (const std::function& callback: empty_desc_callbacks) { try { callback(name); //TODO: remove the faulty plugin in case of error? - } catch (const Error& e) { - errors.emplace_back(e); - } catch (const exception& e) { - errors.emplace_back(PDI_ERR_SYSTEM, e.what()); } catch (...) { - errors.emplace_back(PDI_ERR_SYSTEM, "Not std::exception based error"); - } - } - if (!errors.empty()) { - if (1 == errors.size()) { - throw Error{errors.front().status(), "Error while triggering empty desc access `{}': {}", name, errors.front().what()}; - } - string errmsg = "Multiple (" + std::to_string(errors.size()) + ") errors while triggering empty desc access `" + name + "':\n"; - for (auto&& err: errors) { - errmsg += string(err.what()) + "\n"; + errors.emplace_back(std::current_exception()); } - throw System_error{"{}", errmsg.c_str()}; } + rethrow_with_context(errors, "while populating `{}', ", name); } } // namespace PDI diff --git a/pdi/src/data_descriptor_impl.cxx b/pdi/src/data_descriptor_impl.cxx index 5916db179..13dbea1ec 100644 --- a/pdi/src/data_descriptor_impl.cxx +++ b/pdi/src/data_descriptor_impl.cxx @@ -25,6 +25,7 @@ #include "config.h" +#include #include #include #include @@ -32,6 +33,7 @@ #include "pdi/context.h" #include "pdi/datatype.h" +#include "pdi/delayed_data_callbacks.h" #include "pdi/error.h" #include "pdi/plugin.h" #include "pdi/ref_any.h" @@ -152,12 +154,20 @@ bool Data_descriptor_impl::empty() } void Data_descriptor_impl::share(void* data, bool read, bool write) +try { + share(data, read, write, Delayed_data_callbacks(m_context)); +} catch (...) { + // need this line to catch exception in the destructor of Delayed_data_callback + rethrow_with_context(std::current_exception(), "Unable to share `{}', ", name()); +} + +void Data_descriptor_impl::share(void* data, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) try { assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); Ref r{data, &free, m_type->evaluate(m_context), read, write}; try { m_context.logger().trace("Sharing `{}' Ref with rights: R = {}, W = {}", m_name, read, write); - share(r, false, false); + share(r, false, false, std::move(delayed_callbacks)); } catch (...) { // on error, do not free the data as would be done automatically otherwise r.release(); @@ -165,11 +175,20 @@ try { throw; } assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); -} catch (Error& e) { - throw Error(e.status(), "Unable to share `{}', {}", name(), e.what()); +} catch (...) { + rethrow_with_context(std::current_exception(), "Unable to share `{}', ", name()); } void* Data_descriptor_impl::share(Ref data_ref, bool read, bool write) +try { + assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); + return share(data_ref, read, write, Delayed_data_callbacks(m_context)); +} catch (...) { + // need this line to catch exception in the destructor of Delayed_data_callback + rethrow_with_context(std::current_exception(), "Unable to share `{}', ", name()); +} + +void* Data_descriptor_impl::share(Ref data_ref, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) try { assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); // metadata must provide read access @@ -201,17 +220,12 @@ try { throw Permission_error{"Unable to grant requested rights"}; } - try { - m_context.callbacks().call_data_callbacks(m_name, ref()); - } catch (...) { - m_refs.pop(); - throw; - } + delayed_callbacks.add_dataname(m_name); assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); return result; -} catch (Error& e) { - throw Error(e.status(), "Unable to share `{}', {}", name(), e.what()); +} catch (...) { + rethrow_with_context(std::current_exception(), "Unable to share `{}', ", name()); } void Data_descriptor_impl::release() @@ -231,8 +245,8 @@ try { m_refs.emplace(new Ref_holder::Impl(oldref)); } assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); -} catch (Error& e) { - throw Error(e.status(), "Unable to release `{}', {}", name(), e.what()); +} catch (...) { + rethrow_with_context(std::current_exception(), "Unable to release `{}', ", name()); } void* Data_descriptor_impl::reclaim() @@ -254,8 +268,8 @@ try { assert((!metadata() || !m_refs.empty()) && "metadata descriptors should always keep a placeholder"); // finally release the data behind the ref return oldref.release(); -} catch (Error& e) { - throw Error(e.status(), "Unable to reclaim `{}', {}", name(), e.what()); +} catch (...) { + rethrow_with_context(std::current_exception(), "Unable to reclaim `{}', ", name()); } } // namespace PDI diff --git a/pdi/src/data_descriptor_impl.h b/pdi/src/data_descriptor_impl.h index 5f2119c11..15795f861 100644 --- a/pdi/src/data_descriptor_impl.h +++ b/pdi/src/data_descriptor_impl.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2015-2024 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2015-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -59,7 +59,6 @@ class PDI_EXPORT Data_descriptor_impl: public Data_descriptor bool m_metadata; - /** Create an empty descriptor */ Data_descriptor_impl(Global_context& ctx, const char* name); @@ -91,12 +90,15 @@ class PDI_EXPORT Data_descriptor_impl: public Data_descriptor void share(void* data, bool read, bool write) override; + void share(void* data, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) override; + void* share(Ref ref, bool read, bool write) override; + void* share(Ref ref, bool read, bool write, Delayed_data_callbacks&& delayed_callbacks) override; + void release() override; void* reclaim() override; - }; // class Data_descriptor } // namespace PDI diff --git a/pdi/src/delayed_data_callbacks.cxx b/pdi/src/delayed_data_callbacks.cxx new file mode 100644 index 000000000..1c675863b --- /dev/null +++ b/pdi/src/delayed_data_callbacks.cxx @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (C) 2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of CEA nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific + * prior written permission. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "pdi/delayed_data_callbacks.h" + +namespace PDI { + +Delayed_data_callbacks::Delayed_data_callbacks(Global_context& ctx) + : m_context{ctx} + , m_datanames{} +{} + +// In the destructor, we need to throw an error message in case the callback doesn't work on a data(trigger function) +// (example: error in the config.yml for a plugin, error due to external library incompatibility) +Delayed_data_callbacks::~Delayed_data_callbacks() noexcept(false) +{ + try { + this->trigger(); + } catch (const std::exception& e) { + if (std::uncaught_exceptions()) { + // An exception is throwing before. Print simple message to avoid std::terminate. + m_context.logger().error("Error in the destructor of Delayed_data_callbacks, {}", e.what()); + } else { + m_context.logger().info("{}", "Error in the destructor of Delayed_data_callbacks"); + throw; + } + } catch (...) { + if (std::uncaught_exceptions()) { + // An exception is throwing before. Print simple message to avoid std::terminate. + m_context.logger().error("Error (no std::exception) in the destructor of Delayed_data_callbacks."); + } else { + m_context.logger().info("{}", "Error (no std::exception) in the destructor of Delayed_data_callbacks."); + throw; + } + } +} + +void Delayed_data_callbacks::add_dataname(const std::string& name) +{ + // Comment: In case of a multi_expose, if the data is defined twice then the callback "on_data" are called twice also (in PDI v1.10) + // Therefore, we need to add the name to have the same behaviour. + m_datanames.emplace_back(name); +} + +void Delayed_data_callbacks::trigger() +{ + int i = 0; + size_t number_of_elements = m_datanames.size(); + std::vector trigger_data_errors; + + for (auto&& element_name: m_datanames) { + try { + m_context.logger().trace("Trigger data callback `{}' ({}/{})", element_name.c_str(), ++i, number_of_elements); + m_context.callbacks().call_data_callbacks(element_name, m_context[element_name].ref()); + } catch (...) { + trigger_data_errors.emplace_back(std::current_exception()); + } + } + + m_datanames.clear(); + + rethrow_with_context(trigger_data_errors, "`{}' error(s) while triggering data callbacks, ", number_of_elements); +} + +void Delayed_data_callbacks::cancel() +{ + m_datanames.clear(); +} + +} // namespace PDI diff --git a/pdi/src/error.cxx b/pdi/src/error.cxx index 287f72814..484773792 100644 --- a/pdi/src/error.cxx +++ b/pdi/src/error.cxx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2015-2019 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2015-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,29 +31,146 @@ #include #include "pdi/error.h" +#include "pdi/fmt.h" + +template <> +struct fmt::formatter: formatter { + // parse is inherited from formatter. + + format_context::iterator format(std::exception_ptr p, format_context& ctx) const + { + try { + std::rethrow_exception(p); + } catch (PDI::Error const & err) { + return formatter::format(err.full_msg(), ctx); + } catch (std::exception const & err) { + return formatter::format(err.what(), ctx); + } catch (...) { + return formatter::format("unknown error", ctx); + } + } +}; namespace PDI { -Error::Error(PDI_status_t errcode) - : m_status{errcode} -{} +Error::~Error() noexcept = default; -Error::Error(PDI_status_t errcode, const char* fmt) - : m_status{errcode} - , m_what{fmt} +namespace impl { + +what_impl::what_impl(std::string what) noexcept + : m_what{std::move(what)} { // force inclusion of dtor fmt::format_error(""); } -const char* Error::what() const noexcept +const char* what_impl::what() const noexcept { return m_what.c_str(); } -PDI_status_t Error::status() const noexcept +template +Error_impl::Error_impl(std::string what) noexcept + : what_impl(what) +{} + +template +std::string Error_impl::full_msg() const +{ + return fmt::format("{}: {}", PDI_STATUS_MSG[status()], what()); +} + +template +[[noreturn]] void Error_impl::rethrow_with_context(std::string msg) const +{ + throw Error_impl("{}{}", msg, what()); +} + +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; +template class PDI_EXPORT impl::Error_impl; + +} // namespace impl + +Spectree_error::Spectree_error(std::optional location, std::string what) + : impl::what_impl(what) + , m_location(location) +{} + +Spectree_error::Spectree_error(PC_tree_t tree, std::string what) + : Spectree_error(Yaml_region::make(tree), std::move(what)) +{} + +std::string Spectree_error::full_msg() const +{ + if (!!m_location) { + auto& location = *m_location; + return fmt::format( + "{}: {}({}:{} -> {}:{}) {}", + PDI_STATUS_MSG[status()], + location.file(), + location.start().line, + location.start().column, + location.end().line, + location.end().column, + what() + ); + } + return fmt::format("{}: {}", PDI_STATUS_MSG[status()], what()); +} + +[[noreturn]] void Spectree_error::rethrow_with_context(std::string msg) const +{ + throw Spectree_error(m_location, fmt::format("{}{}", msg, what())); +} + +Multiple_errors::Multiple_errors(std::vector causes, std::string what) noexcept + : impl::what_impl(std::move(what)) + , m_nested_ptrs(std::move(causes)) +{} + +std::string Multiple_errors::full_msg() const +{ + return fmt::format("{}: {}\n * {}", PDI_STATUS_MSG[status()], what(), fmt::join(m_nested_ptrs, "\n * ")); +} + +std::vector const & Multiple_errors::nested_ptrs() const +{ + return m_nested_ptrs; +} + +[[noreturn]] void Multiple_errors::rethrow_with_context(std::string msg) const +{ + throw Multiple_errors(nested_ptrs(), "{}{}", msg, what()); +} + +[[noreturn]] void rethrow_with_simple_context(std::exception_ptr err, std::string msg) +{ + try { + std::rethrow_exception(err); + } catch (Error const & e) { + e.rethrow_with_context(std::move(msg)); + } catch (...) { + std::throw_with_nested(System_error(std::move(msg))); + } + std::abort(); // one of the above should have thrown, this is just to silence compiler warnings +} + +void rethrow_with_simple_context(std::vector errors, std::string msg) { - return m_status; + switch (errors.size()) { + case 0: + return; + case 1: + rethrow_with_simple_context(errors[0], std::move(msg)); + default: + throw Multiple_errors(errors, std::move(msg)); + } } } // namespace PDI diff --git a/pdi/src/expression/impl/reference_expression.cxx b/pdi/src/expression/impl/reference_expression.cxx index c89f9ee93..b1e06e628 100644 --- a/pdi/src/expression/impl/reference_expression.cxx +++ b/pdi/src/expression/impl/reference_expression.cxx @@ -23,6 +23,7 @@ * THE SOFTWARE. ******************************************************************************/ +#include #include #include #include @@ -154,8 +155,8 @@ long Expression::Impl::Reference_expression::to_long(Context& ctx) const return ref.scalar_value(); } throw Permission_error{"Unable to grant access for value reference"}; - } catch (const Error& e) { - throw Error{e.status(), "while referencing `{}': {}", m_referenced, e.what()}; + } catch (...) { + rethrow_with_context(std::current_exception(), "while referencing `{}', ", m_referenced); } } @@ -166,8 +167,8 @@ double Expression::Impl::Reference_expression::to_double(Context& ctx) const return ref.scalar_value(); } throw Permission_error{"Unable to grant read access for value reference"}; - } catch (const Error& e) { - throw Error{e.status(), "while referencing `{}': {}", m_referenced, e.what()}; + } catch (...) { + rethrow_with_context(std::current_exception(), "while referencing `{}', ", m_referenced); } } diff --git a/pdi/src/paraconf_wrapper.cxx b/pdi/src/paraconf_wrapper.cxx index 6840165a3..9fc9e8e21 100644 --- a/pdi/src/paraconf_wrapper.cxx +++ b/pdi/src/paraconf_wrapper.cxx @@ -24,6 +24,8 @@ #include "config.h" +#include + #include "pdi/error.h" #include "pdi/paraconf_wrapper.h" @@ -43,6 +45,33 @@ void do_pc(PC_tree_t tree, PC_status_t status) } // namespace +Yaml_region::Yaml_region(PC_tree_t tree) + : m_file( +#ifdef PARACONF_VERSION +#if PARACONF_UNTYPED_VERSION >= PARACONF_UNTYPED_COMPUTE_VERSION(1, 1, 0) + PC_path(tree) +#else + "" +#endif +#else + "" +#endif + ) + , m_start{tree.node->start_mark.line + 1, tree.node->start_mark.column + 1} + , m_end{tree.node->end_mark.line + 1, tree.node->end_mark.column + 1} +{ + assert(!PC_status(tree) && "building a region from an invalid tree is unspecified"); +} + +std::optional Yaml_region::make(PC_tree_t tree) +{ + if (PC_status(tree) == PC_OK && tree.node) { + return Yaml_region(tree); + } else { + return {}; + } +} + Paraconf_wrapper::Paraconf_wrapper() : m_handler{PC_errhandler(PC_NULL_HANDLER)} {} diff --git a/pdi/src/pdi.cxx b/pdi/src/pdi.cxx index 03ed040f7..5a6834569 100644 --- a/pdi/src/pdi.cxx +++ b/pdi/src/pdi.cxx @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include "pdi/context.h" #include "pdi/data_descriptor.h" #include "pdi/datatype.h" +#include "pdi/delayed_data_callbacks.h" #include "pdi/error.h" #include "pdi/paraconf_wrapper.h" #include "pdi/plugin.h" @@ -45,6 +47,8 @@ #include "global_context.h" +static_assert(std::size(PDI_STATUS_MSG) == PDI_NB_STATUSES_DEFINED, "The signification of each error code should be listed in PDI_STATUS_MSG"); + namespace { using namespace PDI; @@ -66,6 +70,8 @@ struct Error_context { string errmsg; + std::ostringstream verbosemsg; + Error_context() : handler{PDI_ASSERT_HANDLER} {} @@ -74,7 +80,9 @@ struct Error_context { */ PDI_status_t return_err(const Error& err) { - errmsg = err.what(); + verbosemsg = std::ostringstream(); + build_verbosemsg(err, 0); + errmsg = err.full_msg(); if (handler.func) handler.func(err.status(), errmsg.c_str(), handler.context); return err.status(); } @@ -97,6 +105,20 @@ struct Error_context { return PDI_ERR_SYSTEM; } + void build_verbosemsg(const exception& err, int indent) + { + static std::regex const NEWLINE("\n"); + std::string const indstr = std::string(' ', 2 * indent); + std::string const rplstr = "\n" + indstr; + verbosemsg << indstr << "* " << std::regex_replace(err.what(), NEWLINE, rplstr) << "\n"; + try { + std::rethrow_if_nested(err); + } catch (const exception& ne) { + build_verbosemsg(ne, indent + 1); + } catch (...) { + } + } + }; // struct Error_context /// The thread-local error context @@ -142,6 +164,63 @@ void warn_status(PDI_status_t status, const char* message, void*) } } +/** A structure to reclaim the datas properly in case of error + */ +struct Var_to_reclaim { + std::vector m_varnames; + const string m_event_name; + + Var_to_reclaim(const char* event_name) + : m_event_name{event_name} + {} + + ~Var_to_reclaim() noexcept(false) + { + try { + this->trigger_reclaim(); + } catch (std::exception& e) { + if (std::uncaught_exceptions() == 0) { + throw; + } else { + Global_context::context().logger().error("{}", e.what()); + } + } catch (...) { + if (std::uncaught_exceptions() == 0) { + throw; + } else { + Global_context::context().logger().error("Error when triggering reclaim in multi expose for event `{}'", m_event_name); + } + } + } + + void trigger_reclaim() + { + std::vector reclaim_data_errors; + int counter = 0; + for (auto&& it = m_varnames.rbegin(); it != m_varnames.rend(); it++) { + try { + Global_context::context().logger().trace("Multi expose: Reclaiming `{}' ({}/{})", it->c_str(), ++counter, m_varnames.size()); + Global_context::context()[it->c_str()].reclaim(); + } catch (...) { + reclaim_data_errors.emplace_back(std::current_exception()); + } + } + rethrow_with_context( + reclaim_data_errors, + "`{}' error(s) when triggering reclaim(s) in multi expose for event `{}': ", + m_varnames.size(), + m_event_name + ); + + m_varnames.clear(); + } + + size_t size() const { return m_varnames.size(); } + + void emplace_back(const string& name) { m_varnames.emplace_back(name); } +}; + + } // namespace extern "C" { @@ -323,37 +402,31 @@ PDI_status_t PDI_multi_expose(const char* event_name, const char* name, const vo try { Paraconf_wrapper fw; va_list ap; - list transaction_data; - PDI_status_t status; - if ((status = PDI_share(name, data, access))) return status; - transaction_data.emplace_back(name); + + Var_to_reclaim list_names{event_name}; // list of variable that will be reclaimed at the end of this function + Delayed_data_callbacks delayed_callbacks(Global_context::context()); + int i = -1; + Global_context::context().logger().trace("Multi expose: Sharing `{}' ({}/{})", name, ++i, list_names.size()); + Global_context::context()[name].share(const_cast(data), access & PDI_OUT, access & PDI_IN, std::move(delayed_callbacks)); + list_names.emplace_back(name); va_start(ap, access); - int i = 0; while (const char* v_name = va_arg(ap, const char*)) { void* v_data = va_arg(ap, void*); PDI_inout_t v_access = static_cast(va_arg(ap, int)); - Global_context::context().logger().trace("Multi expose: Sharing `{}' ({}/{})", v_name, ++i, transaction_data.size()); - if ((status = PDI_share(v_name, v_data, v_access))) { - break; - } - transaction_data.emplace_back(v_name); + Global_context::context().logger().trace("Multi expose: Sharing `{}' ({}/{})", v_name, ++i, list_names.size()); + Global_context::context()[v_name].share(v_data, v_access & PDI_OUT, v_access & PDI_IN, std::move(delayed_callbacks)); + list_names.emplace_back(v_name); } va_end(ap); - if (!status) { //trigger event only when all data is available - Global_context::context().logger().trace("Multi expose: Calling event `{}'", event_name); - status = PDI_event(event_name); - } + delayed_callbacks.trigger(); - i = 0; - for (auto&& it = transaction_data.rbegin(); it != transaction_data.rend(); it++) { - Global_context::context().logger().trace("Multi expose: Reclaiming `{}' ({}/{})", it->c_str(), ++i, transaction_data.size()); - PDI_status_t r_status = PDI_reclaim(it->c_str()); - status = !status ? r_status : status; //if it is first error, save its status (try to reclaim other desc anyway) - } - //the status of the first error is returned - return status; + Global_context::context().logger().trace("Multi expose: Calling event `{}'", event_name); + Global_context::context().event(event_name); + + // remark: The reclaim of the data is done in the destructor of the Var_to_reclaim (see struct Var_to_reclaim) + return PDI_OK; } catch (const Error& e) { return g_error_context.return_err(e); } catch (const exception& e) { diff --git a/pdi/src/plugin_store.cxx b/pdi/src/plugin_store.cxx index 95cb4ea05..63cdb4987 100644 --- a/pdi/src/plugin_store.cxx +++ b/pdi/src/plugin_store.cxx @@ -253,16 +253,20 @@ Plugin_store::Plugin_store(Context& ctx, PC_tree_t conf) void Plugin_store::load_plugins() { - try { - for (auto&& plugin: m_plugins) { + std::vector errors; + for (auto&& plugin: m_plugins) { + try { //TODO: what to do if a single plugin fails to load? plugin.second->ensure_loaded(m_plugins); + } catch (...) { + try { + rethrow_with_context(std::current_exception(), "while loading plugin `{}', ", plugin.first); + } catch (...) { + errors.emplace_back(std::current_exception()); + } } - } catch (const Error& e) { - throw; - } catch (const exception& e) { - throw System_error{"Error while loading plugins: {}", e.what()}; } + rethrow_with_context(errors, ""); } } // namespace PDI diff --git a/pdi/tests/CMakeLists.txt b/pdi/tests/CMakeLists.txt index 1cf7cb83f..b10806864 100644 --- a/pdi/tests/CMakeLists.txt +++ b/pdi/tests/CMakeLists.txt @@ -62,6 +62,7 @@ add_executable(PDI_unit_tests PDI_context.cxx PDI_data_descriptor.cxx PDI_datatype_attributes.cxx + PDI_delayed_data_callbacks.cxx PDI_error.cxx PDI_expression.cxx # PDI_initialize_plugins.cxx diff --git a/pdi/tests/PDI_callbacks.cxx b/pdi/tests/PDI_callbacks.cxx index d99af3e98..cc84c5467 100644 --- a/pdi/tests/PDI_callbacks.cxx +++ b/pdi/tests/PDI_callbacks.cxx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2020-2021 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2020-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * Copyright (C) 2018 Institute of Bioorganic Chemistry Polish Academy of Science (PSNC) * All rights reserved. * @@ -165,6 +165,7 @@ TEST_F(CallbacksTest, add_data_callback) }); ASSERT_EQ(x, 0); this->test_context->desc("data_x").share(&x, true, true); + ASSERT_EQ(x, 42); this->test_context->desc("data_x").reclaim(); ASSERT_EQ(x, 42); } diff --git a/pdi/tests/PDI_data_descriptor.cxx b/pdi/tests/PDI_data_descriptor.cxx index 3cb7760c5..dc057afcb 100644 --- a/pdi/tests/PDI_data_descriptor.cxx +++ b/pdi/tests/PDI_data_descriptor.cxx @@ -41,18 +41,15 @@ using namespace std; namespace PDI { //handler to private fields of Descriptor struct Descriptor_test_handler { - static unique_ptr default_desc(Global_context& global_ctx) - { - return unique_ptr{new Data_descriptor_impl{global_ctx, "default_desc"}}; - } + static Data_descriptor* default_desc(Global_context& global_ctx) { return &global_ctx.desc("default_desc"); } - static Datatype_sptr desc_get_type(unique_ptr& desc, Global_context& global_ctx) + static Datatype_sptr desc_get_type(Data_descriptor* desc, Global_context& global_ctx) { - Datatype_template_sptr desc_template = dynamic_cast(desc.get())->m_type; + Datatype_template_sptr desc_template = dynamic_cast(desc)->m_type; return desc_template->evaluate(global_ctx); } - static int desc_get_refs_number(unique_ptr& desc) { return dynamic_cast(desc.get())->m_refs.size(); } + static int desc_get_refs_number(Data_descriptor* desc) { return dynamic_cast(desc)->m_refs.size(); } }; } // namespace PDI @@ -64,8 +61,8 @@ struct DataDescTest: public ::testing::Test { PC_tree_t array_config{PC_parse_string("{ size: 10, type: array, subtype: int }")}; shared_ptr array_datatype{Array_datatype::make(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int)), 10)}; PDI::Paraconf_wrapper fw; - Global_context global_ctx{PC_parse_string("")}; - unique_ptr m_desc_default = Descriptor_test_handler::default_desc(global_ctx); + Global_context global_ctx{PC_parse_string("logging: debug")}; + Data_descriptor* m_desc_default = Descriptor_test_handler::default_desc(global_ctx); }; /* diff --git a/pdi/tests/PDI_delayed_data_callbacks.cxx b/pdi/tests/PDI_delayed_data_callbacks.cxx new file mode 100644 index 000000000..2670e3a84 --- /dev/null +++ b/pdi/tests/PDI_delayed_data_callbacks.cxx @@ -0,0 +1,314 @@ +/******************************************************************************* + * Copyright (C) 2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2018 Institute of Bioorganic Chemistry Polish Academy of Science (PSNC) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of CEA nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific + * prior written permission. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "global_context.h" + +using namespace PDI; +using namespace std; + +/* + * Struct prepared for DataDescDelayed + */ +struct DataDescDelayed: public ::testing::Test { + DataDescDelayed() + : test_conf{PC_parse_string("logging: trace")} + {} + + void SetUp() override { context.reset(new Global_context{test_conf}); } + + Paraconf_wrapper fw; + PC_tree_t test_conf; + unique_ptr context; +}; + +/* + * Name: DataDescDelayed.multiple_data_callbacks + * + * Description: Check, in case of multiple data, each data callbacks are called + * + */ +TEST_F(DataDescDelayed, multiple_delayed_data_callbacks) +{ + string data_x{"data_x"}; + string data_y{"data_y"}; + Data_descriptor& desc_x = context->desc(data_x); + Data_descriptor& desc_y = context->desc(data_y); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + context->desc(data_y).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + int y = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* y = static_cast(ref_write.get()); + *y += 53; + ASSERT_STREQ(name.c_str(), "data_y"); + }, + "data_y" + ); + + ASSERT_EQ(x, 0); + ASSERT_EQ(y, 0); + + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + context->desc("data_y").share(&y, true, true, std::move(delayed_callbacks)); + ASSERT_EQ(x, 0); + ASSERT_EQ(y, 0); + + delayed_callbacks.trigger(); + + ASSERT_EQ(x, 42); + ASSERT_EQ(y, 53); + + context->desc("data_y").reclaim(); + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 42); + ASSERT_EQ(y, 53); +} + +/* + * Name: DataDescDelayed.test_scope_guard + * + * Tested functions: ~Delayed_data_callbacks() + * + * Description: Check the destructor is called at the end of the scope + * + */ +TEST_F(DataDescDelayed, test_scope_guard) +{ + string data_x{"data_x"}; + Data_descriptor& desc_x = context->desc(data_x); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + { + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + ASSERT_EQ(x, 0); + } + // check that delayed_callbacks.trigger() is called in dtor of delayed_callbacks + ASSERT_EQ(x, 42); + + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 42); +} + +/* + * Name: DataDescDelayed.test_scope_guard_2_callback + * + * Tested functions: ~Delayed_data_callbacks() + * + * Description: Check the destructor is called at the end of the scope with two callbacks + * + */ +TEST_F(DataDescDelayed, test_scope_guard_2_callback) +{ + string data_x{"data_x"}; + Data_descriptor& desc_x = context->desc(data_x); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + { + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 10; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + } + // check that delayed_callbacks.trigger() is called in the end of dtor of delayed_callbacks + ASSERT_EQ(x, 52); + + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 52); +} + +/* + * Name: DataDescDelayed.reclaim_before_trigger + * + * Description: Check the behavior of trigger when a data name is not shared + * + */ +TEST_F(DataDescDelayed, reclaim_before_trigger) +{ + string data_x{"data_x"}; + Data_descriptor& desc_x = context->desc(data_x); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + ASSERT_EQ(x, 0); + + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 0); + + + // Check the error message is trigger is called before reclaim + try { + delayed_callbacks.trigger(); + } catch (Error& e) { + ASSERT_STREQ("`1' error(s) while triggering data callbacks, Cannot access a non shared value: `data_x'", e.what()); + } catch (...) { + FAIL(); + } +} + +/* + * Name: DataDescDelayed.reclaim_before_trigger + * + * Description: Check the behavior of trigger when a data name is defined twice. + * + */ +TEST_F(DataDescDelayed, same_name_added) +{ + string data_x{"data_x"}; + Data_descriptor& desc_x = context->desc(data_x); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + delayed_callbacks.add_dataname("data_x"); // the list of data to trigger contains two times "data_x" + + delayed_callbacks.trigger(); + ASSERT_EQ(x, 84); // the callback on "data_x" is called twice + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 84); +} + +/* + * Name: DataDescDelayed.two_trigger_calls + * + * Description: Check the second call of trigger doesn't change the data + * + */ +TEST_F(DataDescDelayed, two_trigger_calls) +{ + string data_x{"data_x"}; + Data_descriptor& desc_x = context->desc(data_x); + context->desc(data_x).default_type(Scalar_datatype::make(Scalar_kind::SIGNED, sizeof(int))); + int x = 0; + + context->callbacks().add_data_callback( + [](const std::string& name, Ref ref) { + Ref_w ref_write{ref}; + int* x = static_cast(ref_write.get()); + *x += 42; + ASSERT_STREQ(name.c_str(), "data_x"); + }, + "data_x" + ); + + ASSERT_EQ(x, 0); + { + Delayed_data_callbacks delayed_callbacks(*context); + context->desc("data_x").share(&x, true, true, std::move(delayed_callbacks)); + ASSERT_EQ(x, 0); + + delayed_callbacks.trigger(); + ASSERT_EQ(x, 42); + } // the second call of trigger at the end of this scope + context->desc("data_x").reclaim(); + ASSERT_EQ(x, 42); // Check the list of data in second trigger is empty +} diff --git a/pdi/tests/PDI_error.cxx b/pdi/tests/PDI_error.cxx index abe0a460d..feb21b85b 100644 --- a/pdi/tests/PDI_error.cxx +++ b/pdi/tests/PDI_error.cxx @@ -29,75 +29,11 @@ using namespace PDI; -/* - * Name: ErrorTest.call_constructor_no_vargs - * - * Tested functions: PDI::Error::Error() - * PDI::Error::status() - * PDI::Error::what() - * - * Description: Test calls Error constructor without vargs - * and checks what() and status() values. - * - */ -TEST(ErrorTest, call_constructor_no_vargs) -{ - Error error(PDI_OK, "No error."); - ASSERT_STREQ("No error.", error.what()); - ASSERT_EQ(error.status(), PDI_OK); -} - -/* - * Name: ErrorTest.call_constructor_vargs - * - * Tested functions: PDI::Error::Error() - * PDI::Error::status() - * PDI::Error::what() - * - * Description: Test calls Error constructor with 2 vargs - * and checks what() and status() values. - * - */ -TEST(ErrorTest, call_constructor_vargs) -{ - Error error(PDI_ERR_SYSTEM, "{} errors in {}?", 0, "ErrorTest"); - ASSERT_STREQ("0 errors in ErrorTest?", error.what()); - ASSERT_EQ(error.status(), PDI_ERR_SYSTEM); -} - -/* - * Name: ErrorTest.call_constructor_va_list - * - * Tested functions: PDI::Error::Error() - * PDI::Error::status() - * PDI::Error::what() - * - * Description: Test calls Error constructor with va_list - * (containing 2 strings) and checks what() and - * status() values. - * - */ -TEST(ErrorTest, call_constructor_va_list) -{ - Error error{PDI_OK, "Testing {} in {}", "what", "ErrorTest"}; - ASSERT_STREQ("Testing what in ErrorTest", error.what()); - ASSERT_EQ(error.status(), PDI_OK); -} - -/* - * Name: ErrorTest.call_constructor_empty_va_list - * - * Tested functions: PDI::Error::Error() - * PDI::Error::status() - * PDI::Error::what() - * - * Description: Test calls Error constructor with empty va_list - * and checks what() and status() values. - * +/* Checks that the constructor works correctly */ -TEST(ErrorTest, call_constructor_empty_va_list) +TEST(ErrorTest, Constructor) { - Error error{PDI_OK, "This is some text."}; - ASSERT_STREQ("This is some text.", error.what()); - ASSERT_EQ(error.status(), PDI_OK); + Type_error error("{} errors in {}", 0, "ErrorTest"); + ASSERT_STREQ("0 errors in ErrorTest", error.what()); + ASSERT_EQ(error.status(), PDI_ERR_TYPE); } diff --git a/pdi/tests/mocks/data_descriptor_mock.h b/pdi/tests/mocks/data_descriptor_mock.h index 32f8f263b..54c053926 100644 --- a/pdi/tests/mocks/data_descriptor_mock.h +++ b/pdi/tests/mocks/data_descriptor_mock.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2021-2024 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * Copyright (C) 2021-2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) * Copyright (C) 2018 Institute of Bioorganic Chemistry Polish Academy of Science (PSNC) * All rights reserved. * @@ -29,6 +29,7 @@ #include #include #include +#include struct MockDataDescriptor: public PDI::Data_descriptor { MOCK_METHOD1(default_type, void(PDI::Datatype_template_sptr)); @@ -40,6 +41,8 @@ struct MockDataDescriptor: public PDI::Data_descriptor { MOCK_METHOD0(empty, bool()); MOCK_METHOD3(share, void(void*, bool, bool)); MOCK_METHOD3(share, void*(PDI::Ref, bool, bool)); + MOCK_METHOD4(share, void(void*, bool, bool, PDI::Delayed_data_callbacks&&)); + MOCK_METHOD4(share, void*(PDI::Ref, bool, bool, PDI::Delayed_data_callbacks&&)); MOCK_METHOD0(release, void()); MOCK_METHOD0(reclaim, void*()); }; diff --git a/plugins/decl_hdf5/dataset_op.cxx b/plugins/decl_hdf5/dataset_op.cxx index 66aba7f0b..93f3258ba 100644 --- a/plugins/decl_hdf5/dataset_op.cxx +++ b/plugins/decl_hdf5/dataset_op.cxx @@ -392,17 +392,8 @@ void Dataset_op::do_write(Context& ctx, hid_t h5_file, hid_t write_lst, const st } } - // Remark: message error is defined outside Config_error because is too long. - static constexpr char const * const msg_config_error - = "Found `{0}' match(es) in the list of datasets section for `{1}'." - " Cannot choose the right element in datasets.\n" - "The elements that match `{1}' are:\n" - " - {2}\n" - "Attention: The elements are considered as a regex."; - - throw Spectree_error{ - m_dataset_selection.selection_tree(), - msg_config_error, + throw PDI::Invalid_action_error{ + "found {} regexes in the \"datasets:\" list that match the dataset `{}' to write:\n - {}", list_dataset_found.size(), dataset_name, fmt::join(list_dataset_found, "\n - ") diff --git a/plugins/decl_hdf5/tests/decl_hdf5_tests.cxx b/plugins/decl_hdf5/tests/decl_hdf5_tests.cxx index 73d270acf..76762740e 100644 --- a/plugins/decl_hdf5/tests/decl_hdf5_tests.cxx +++ b/plugins/decl_hdf5/tests/decl_hdf5_tests.cxx @@ -32,7 +32,9 @@ using PDI::make_random; using PDI::random_init; +using testing::AllOf; using testing::Eq; +using testing::HasSubstr; using testing::StartsWith; using testing::StrEq; @@ -599,8 +601,7 @@ logging: trace *this, PdiError( Eq(PDI_ERR_SYSTEM), - StartsWith("Error while triggering event `read_scalar': " - "System_error: Cannot open `scalar_data' dataset object 'scalar_data' doesn't exist") + StartsWith("System error: while triggering `read_scalar', Cannot open `scalar_data' dataset object 'scalar_data' doesn't exist") ) ); PDI_multi_expose("read_scalar", "scalar_data", &scalar_data_read, PDI_IN, nullptr); @@ -624,10 +625,7 @@ logging: trace // APPEND => error (scalar_data already exists) EXPECT_CALL( *this, - PdiError( - Eq(PDI_ERR_SYSTEM), - StrEq("Error while triggering event `append': System_error: Dataset collision `scalar_data': Dataset already exists") - ) + PdiError(Eq(PDI_ERR_SYSTEM), StrEq("System error: while triggering `append', Dataset collision `scalar_data': Dataset already exists")) ); auto const scalar_data_2 = make_a(); PDI_multi_expose("append", "scalar_data", &scalar_data_2, PDI_OUT, nullptr); @@ -637,7 +635,7 @@ logging: trace *this, PdiError( Eq(PDI_ERR_SYSTEM), - StrEq("Error while triggering event `error': System_error: Filename collision `decl_hdf5_test_collision_policy.h5': File already exists") + StrEq("System error: while triggering `error', Filename collision `decl_hdf5_test_collision_policy.h5': File already exists") ) ); auto const array_data_5 = make_a, 4>>(); @@ -799,16 +797,15 @@ TEST_F(DeclHdf5, ConfigErrorForMultipleRegexes) EXPECT_CALL( *this, PdiError( - Eq(PDI_ERR_SPECTREE), - StrEq("Error while triggering event `write_event': Spectree_error: " - "Found `4' match(es) in the list of datasets section for `group123/array_data'. " - "Cannot choose the right element in datasets.\n" - "The elements that match `group123/array_data' are:\n" - " - group[0-9]+/array_data defined in line 13\n" - " - group.*/array_data defined in line 14\n" - " - group1.*/array_data defined in line 15\n" - " - group12.*/array_data defined in line 17\n" - "Attention: The elements are considered as a regex.") + Eq(PDI_ERR_INVALIDACTION), + AllOf( + StartsWith("Invalid action requested: while triggering `write_event'"), + HasSubstr(R"(found 4 regexes in the "datasets:" list that match the dataset `group123/array_data' to write:)"), + HasSubstr("- group[0-9]+/array_data defined in line "), + HasSubstr("- group.*/array_data defined in line "), + HasSubstr("- group1.*/array_data defined in line "), + HasSubstr("- group12.*/array_data defined in line ") + ) ) ); PDI_multi_expose("write_event", "array_data", array_data.data(), PDI_OUT, nullptr); @@ -841,8 +838,10 @@ logging: trace *this, PdiError( Eq(PDI_ERR_SPECTREE), - StrEq("Unable to share `array_data', Unable to share `array_data', Error while triggering data share `array_data': " - "Spectree_error in line 13: Dataset selection is invalid for implicit dataset `group123/array_data'") + StrEq("Invalid entry in specification tree: (13:28 -> 13:49) Unable to share `array_data', " + "`1' error(s) while triggering data callbacks, " + "while sharing `array_data', " + "Dataset selection is invalid for implicit dataset `group123/array_data'") ) ); PDI_expose("array_data", array_data.data(), PDI_OUT); diff --git a/plugins/user_code/user_code.cxx b/plugins/user_code/user_code.cxx index d438f29ad..7b25626bb 100644 --- a/plugins/user_code/user_code.cxx +++ b/plugins/user_code/user_code.cxx @@ -112,8 +112,8 @@ ExposedAlias::ExposedAlias(Context& ctx, const Alias& alias) { try { m_desc->share(alias.m_value.to_ref(ctx), false, false); - } catch (const Error& e) { - throw Error{e.status(), "Could not alias `{}' because {}", alias.m_name, e.what()}; + } catch (...) { + PDI::rethrow_with_context(std::current_exception(), "Could not alias `{}' because ", alias.m_name); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 887f0fc97..445193964 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -24,7 +24,13 @@ #============================================================================= cmake_minimum_required(VERSION 3.22...4.2) -project(pdi_tests LANGUAGES C) +project(pdi_tests LANGUAGES C CXX) + +if(NOT TARGET GTest::gtest) + option(INSTALL_GTEST "Enable installation of googletest. (Projects embedding googletest may want to turn this OFF.)" OFF) + add_subdirectory("../vendor/googletest-56efe39/" "googletest" EXCLUDE_FROM_ALL) +endif() +include(GoogleTest) set(RUNTEST_DIR "${CMAKE_CURRENT_LIST_DIR}/../cmake/runtest-dir") @@ -64,3 +70,12 @@ add_test(NAME test_05_C COMMAND "$" "${CMAKE_CURRENT_SOUR set_tests_properties(test_05_C PROPERTIES DISABLED TRUE) endif("${BUILD_DECL_NETCDF_PLUGIN}" AND "${BUILD_SERIALIZE_PLUGIN}") + +if("${BUILD_USER_CODE_PLUGIN}") + +add_executable(test_06_user_code_multi_expose_CXX test_06_user_code_multi_expose.cxx) +target_link_libraries(test_06_user_code_multi_expose_CXX PDI::PDI_C GTest::gmock GTest::gmock_main GTest::gtest GTest::gtest_main) +set_target_properties(test_06_user_code_multi_expose_CXX PROPERTIES ENABLE_EXPORTS TRUE) +gtest_discover_tests(test_06_user_code_multi_expose_CXX) + +endif("${BUILD_USER_CODE_PLUGIN}") \ No newline at end of file diff --git a/tests/test_06_user_code_multi_expose.cxx b/tests/test_06_user_code_multi_expose.cxx new file mode 100644 index 000000000..3fa8b7153 --- /dev/null +++ b/tests/test_06_user_code_multi_expose.cxx @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (C) 2026 Commissariat a l'energie atomique et aux energies alternatives (CEA) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of CEA nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific + * prior written permission. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include +#include +#include +#include + +#include + +class CheckMultiExpose: public ::PDI::PdiTest +{}; + +/* + * Name: CheckMultiExpose.AccesSecondData + * + * Description: Acces to second data on the event "on_data" for the first data of a multi expose + */ + +// Function to check the value inside the user_code function +void check_value(const char* var_name, int& var, const int expected_value) +{ + EXPECT_EQ(var, expected_value) << "Wrong value of " << var_name << ": " << var << " != " << expected_value; +} + +// Define user_code function +extern "C" { + +// Test access of the last argument var2 +void test_access_var2(void) +{ + int* value; + PDI_access("value", (void**)&value, PDI_IN); // Read something from input + PDI_release("value"); + check_value("second", *value, 3); +} + +} // end extern "C" + +TEST_F(CheckMultiExpose, AccesSecondData) +{ + InitPdi(PC_parse_string(R"==( +logging: trace +data: + first: int + second: int +plugins: + user_code: + on_data: + first: + test_access_var2: { value: $second } +)==")); + + const int var1 = -4; + const int var2 = 3; + + PDI_multi_expose("my_test", "first", &var1, PDI_OUT, "second", &var2, PDI_OUT, NULL); +} + +/* + * Name: CheckMultiExpose, DataWithSameName + * + * Description: Verify the behavior of multi_expose when we shared two different + * data in the same place in PDI store. + */ + +// Define user_code function +extern "C" { + +void add_2(void) +{ + int* value; + PDI_access("value", (void**)&value, PDI_IN); // Read something from input + PDI_release("value"); + *value = *value + 2; +} + +} // end extern "C" + +TEST_F(CheckMultiExpose, DataWithSameName) +{ + InitPdi(PC_parse_string(R"==( +logging: trace +data: + pdi_var1: int +plugins: + user_code: + on_data: + pdi_var1: + add_2: { value: $pdi_var1 } +)==")); + + const int var1 = 3; + const int var2 = 11; + + PDI_multi_expose("my_test", "pdi_var1", &var1, PDI_OUT, "pdi_var1", &var2, PDI_OUT, NULL); + + EXPECT_EQ(var1, 3) << "Wrong value of var1"; // the reference of pdi_var1 in the store is &var2 => no change in the value + EXPECT_EQ(var2, 15) << "Wrong value of var2"; // the add_2 function is called two times on reference &var2. +}