refactor: Add util::Result multiple error and warning messages

Add util::Result support for returning warning messages and multiple errors,
not just a single error string. This provides a way for functions to report
errors and warnings in a standard way, and simplifies interfaces.

The functionality is unit tested here, and put to use in followup PR
https://github.com/bitcoin/bitcoin/pull/25722
This commit is contained in:
Ryan Ofsky 2024-03-26 14:17:01 -04:00
parent 48949d1c60
commit 4599760cc6
5 changed files with 244 additions and 22 deletions

View file

@ -68,6 +68,7 @@ add_library(bitcoinkernel
../util/hasher.cpp ../util/hasher.cpp
../util/moneystr.cpp ../util/moneystr.cpp
../util/rbf.cpp ../util/rbf.cpp
../util/result.cpp
../util/serfloat.cpp ../util/serfloat.cpp
../util/signalinterrupt.cpp ../util/signalinterrupt.cpp
../util/strencodings.cpp ../util/strencodings.cpp

View file

@ -2,10 +2,17 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <tinyformat.h>
#include <util/check.h> #include <util/check.h>
#include <util/result.h> #include <util/result.h>
#include <util/translation.h>
#include <algorithm>
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
#include <memory>
#include <ostream>
#include <string>
#include <utility>
inline bool operator==(const bilingual_str& a, const bilingual_str& b) inline bool operator==(const bilingual_str& a, const bilingual_str& b)
{ {
@ -90,15 +97,60 @@ enum FnError { ERR1, ERR2 };
util::Result<int, FnError> IntFailFn(int i, bool success) util::Result<int, FnError> IntFailFn(int i, bool success)
{ {
if (success) return i; if (success) return {util::Warning{Untranslated(strprintf("int %i warn.", i))}, i};
return {util::Error{Untranslated(strprintf("int %i error.", i))}, i % 2 ? ERR1 : ERR2}; return {util::Error{Untranslated(strprintf("int %i error.", i))}, i % 2 ? ERR1 : ERR2};
} }
util::Result<std::string, FnError> StrFailFn(int i, bool success)
{
util::Result<std::string, FnError> result;
if (auto int_result{IntFailFn(i, success) >> result}) {
result.Update(strprintf("%i", *int_result));
} else {
result.Update({util::Error{Untranslated("str error")}, int_result.GetFailure()});
}
return result;
}
util::Result<NoCopyNoMove, FnError> EnumFailFn(FnError ret) util::Result<NoCopyNoMove, FnError> EnumFailFn(FnError ret)
{ {
return {util::Error{Untranslated("enum fail.")}, ret}; return {util::Error{Untranslated("enum fail.")}, ret};
} }
util::Result<void> WarnFn()
{
return {util::Warning{Untranslated("warn.")}};
}
util::Result<int> MultiWarnFn(int ret)
{
util::Result<int> result;
for (int i = 0; i < ret; ++i) {
result.AddWarning(Untranslated(strprintf("warn %i.", i)));
}
result.Update(ret);
return result;
}
util::Result<void, int> ChainedFailFn(FnError arg, int ret)
{
util::Result<void, int> result{util::Error{Untranslated("chained fail.")}, ret};
EnumFailFn(arg) >> result;
WarnFn() >> result;
return result;
}
util::Result<int, FnError> AccumulateFn(bool success)
{
util::Result<int, FnError> result;
util::Result<int> x = MultiWarnFn(1) >> result;
BOOST_REQUIRE(x);
util::Result<int> y = MultiWarnFn(2) >> result;
BOOST_REQUIRE(y);
result.Update(IntFailFn(*x + *y, success));
return result;
}
util::Result<int, int> TruthyFalsyFn(int i, bool success) util::Result<int, int> TruthyFalsyFn(int i, bool success)
{ {
if (success) return i; if (success) return i;
@ -142,7 +194,13 @@ BOOST_AUTO_TEST_CASE(check_returned)
ExpectFail(NoCopyNoMoveFn(5, false), Untranslated("nocopynomove 5 error."), 5); ExpectFail(NoCopyNoMoveFn(5, false), Untranslated("nocopynomove 5 error."), 5);
ExpectSuccess(StrFn(Untranslated("S"), true), {}, Untranslated("S")); ExpectSuccess(StrFn(Untranslated("S"), true), {}, Untranslated("S"));
ExpectResult(StrFn(Untranslated("S"), false), false, Untranslated("str S error.")); ExpectResult(StrFn(Untranslated("S"), false), false, Untranslated("str S error."));
ExpectSuccess(StrFailFn(1, true), Untranslated("int 1 warn."), "1");
ExpectFail(StrFailFn(2, false), Untranslated("int 2 error. str error"), ERR2);
ExpectFail(EnumFailFn(ERR2), Untranslated("enum fail."), ERR2); ExpectFail(EnumFailFn(ERR2), Untranslated("enum fail."), ERR2);
ExpectFail(ChainedFailFn(ERR1, 5), Untranslated("chained fail. enum fail. warn."), 5);
ExpectSuccess(MultiWarnFn(3), Untranslated("warn 0. warn 1. warn 2."), 3);
ExpectSuccess(AccumulateFn(true), Untranslated("warn 0. warn 0. warn 1. int 3 warn."), 3);
ExpectFail(AccumulateFn(false), Untranslated("int 3 error. warn 0. warn 0. warn 1."), ERR1);
ExpectSuccess(TruthyFalsyFn(0, true), {}, 0); ExpectSuccess(TruthyFalsyFn(0, true), {}, 0);
ExpectFail(TruthyFalsyFn(0, false), Untranslated("failure value 0."), 0); ExpectFail(TruthyFalsyFn(0, false), Untranslated("failure value 0."), 0);
ExpectSuccess(TruthyFalsyFn(1, true), {}, 1); ExpectSuccess(TruthyFalsyFn(1, true), {}, 1);
@ -158,7 +216,7 @@ BOOST_AUTO_TEST_CASE(check_update)
result.Update({util::Error{Untranslated("error")}, ERR1}); result.Update({util::Error{Untranslated("error")}, ERR1});
ExpectFail(result, Untranslated("error"), ERR1); ExpectFail(result, Untranslated("error"), ERR1);
result.Update(2); result.Update(2);
ExpectSuccess(result, Untranslated(""), 2); ExpectSuccess(result, Untranslated("error"), 2);
// Test the same thing but with non-copyable success and failure types. // Test the same thing but with non-copyable success and failure types.
util::Result<NoCopy, NoCopy> result2{0}; util::Result<NoCopy, NoCopy> result2{0};
@ -166,7 +224,7 @@ BOOST_AUTO_TEST_CASE(check_update)
result2.Update({util::Error{Untranslated("error")}, 3}); result2.Update({util::Error{Untranslated("error")}, 3});
ExpectFail(result2, Untranslated("error"), 3); ExpectFail(result2, Untranslated("error"), 3);
result2.Update(4); result2.Update(4);
ExpectSuccess(result2, Untranslated(""), 4); ExpectSuccess(result2, Untranslated("error"), 4);
} }
BOOST_AUTO_TEST_CASE(check_dereference_operators) BOOST_AUTO_TEST_CASE(check_dereference_operators)
@ -190,6 +248,16 @@ BOOST_AUTO_TEST_CASE(check_value_or)
BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), Untranslated("B")); BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), Untranslated("B"));
} }
BOOST_AUTO_TEST_CASE(check_message_accessors)
{
util::Result<void> result{util::Error{Untranslated("Error.")}, util::Warning{Untranslated("Warning.")}};
BOOST_CHECK_EQUAL(Assert(result.GetMessages())->errors.size(), 1);
BOOST_CHECK_EQUAL(Assert(result.GetMessages())->errors[0], Untranslated("Error."));
BOOST_CHECK_EQUAL(Assert(result.GetMessages())->warnings.size(), 1);
BOOST_CHECK_EQUAL(Assert(result.GetMessages())->warnings[0], Untranslated("Warning."));
BOOST_CHECK_EQUAL(util::ErrorString(result), Untranslated("Error. Warning."));
}
struct Derived : NoCopyNoMove { struct Derived : NoCopyNoMove {
using NoCopyNoMove::NoCopyNoMove; using NoCopyNoMove::NoCopyNoMove;
}; };

View file

@ -17,6 +17,7 @@ add_library(bitcoin_util STATIC EXCLUDE_FROM_ALL
moneystr.cpp moneystr.cpp
rbf.cpp rbf.cpp
readwritefile.cpp readwritefile.cpp
result.cpp
serfloat.cpp serfloat.cpp
signalinterrupt.cpp signalinterrupt.cpp
sock.cpp sock.cpp

33
src/util/result.cpp Normal file
View file

@ -0,0 +1,33 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
#include <util/result.h>
#include <algorithm>
#include <initializer_list>
#include <iterator>
#include <util/translation.h>
namespace util {
namespace detail {
bilingual_str JoinMessages(const Messages& messages)
{
bilingual_str result;
for (const auto& messages : {messages.errors, messages.warnings}) {
for (const auto& message : messages) {
if (!result.empty()) result += Untranslated(" ");
result += message;
}
}
return result;
}
} // namespace detail
void ResultTraits<Messages>::Update(Messages& dst, Messages& src) {
dst.errors.insert(dst.errors.end(), std::make_move_iterator(src.errors.begin()), std::make_move_iterator(src.errors.end()));
dst.warnings.insert(dst.warnings.end(), std::make_move_iterator(src.warnings.begin()), std::make_move_iterator(src.warnings.end()));
src.errors.clear();
src.warnings.clear();
}
} // namespace util

View file

@ -17,9 +17,15 @@
#include <vector> #include <vector>
namespace util { namespace util {
//! Default MessagesType, simple list of errors and warnings.
struct Messages {
std::vector<bilingual_str> errors{};
std::vector<bilingual_str> warnings{};
};
//! The Result<SuccessType, FailureType, MessagesType> class provides //! The Result<SuccessType, FailureType, MessagesType> class provides
//! an efficient way for functions to return structured result information, as //! an efficient way for functions to return structured result information, as
//! well as descriptive error messages. //! well as descriptive error and warning messages.
//! //!
//! Logically, a result object is equivalent to: //! Logically, a result object is equivalent to:
//! //!
@ -58,15 +64,67 @@ namespace util {
//! return function results. //! return function results.
//! //!
//! Usage examples can be found in \example ../test/result_tests.cpp. //! Usage examples can be found in \example ../test/result_tests.cpp.
template <typename SuccessType = void, typename FailureType = void, typename MessagesType = bilingual_str> template <typename SuccessType = void, typename FailureType = void, typename MessagesType = Messages>
class Result; class Result;
//! Wrapper object to pass an error string to the Result constructor. //! Wrapper object to pass an error string to the Result constructor.
struct Error { struct Error {
bilingual_str message; bilingual_str message;
}; };
//! Wrapper object to pass a warning string to the Result constructor.
struct Warning {
bilingual_str message;
};
//! Type trait that can be specialized to control the way SuccessType /
//! FailureType / MessagesType values are combined. Default behavior
//! is for new values to overwrite existing ones, but this can be specialized
//! for custom behavior when Result::Update() method is called or << operator is
//! used. For example, this is specialized for Messages struct below to append
//! error and warning messages instead of overwriting them. It can also be used,
//! for example, to merge FailureType values when a function can return multiple
//! failures.
template<typename T>
struct ResultTraits {
template<typename O>
static void Update(O& dst, T& src)
{
dst = std::move(src);
}
};
//! Type trait that can be specialized to let the Result class work with custom
//! MessagesType types holding error and warning messages.
template<typename MessagesType>
struct MessagesTraits;
//! ResultTraits specialization for Messages struct.
template<>
struct ResultTraits<Messages> {
static void Update(Messages& dst, Messages& src);
};
//! MessagesTraits specialization for Messages struct.
template<>
struct MessagesTraits<Messages> {
static void AddError(Messages& messages, bilingual_str error)
{
messages.errors.emplace_back(std::move(error));
}
static void AddWarning(Messages& messages, bilingual_str warning)
{
messages.warnings.emplace_back(std::move(warning));
}
static bool HasMessages(const Messages& messages)
{
return messages.errors.size() || messages.warnings.size();
}
};
namespace detail { namespace detail {
//! Helper function to join messages in space separated string.
bilingual_str JoinMessages(const Messages& messages);
//! Substitute for std::monostate that doesn't depend on std::variant. //! Substitute for std::monostate that doesn't depend on std::variant.
struct Monostate{}; struct Monostate{};
@ -157,9 +215,9 @@ public:
static constexpr bool is_result{true}; static constexpr bool is_result{true};
//! Construct a Result object setting a success or failure value and //! Construct a Result object setting a success or failure value and
//! optional error messages. Initial util::Error arguments are processed //! optional warning and error messages. Initial util::Error and
//! first to add error messages. //! util::Warning arguments are processed first to add warning and error
//! Then, any remaining arguments are passed to the SuccessType //! messages. Then, any remaining arguments are passed to the SuccessType
//! constructor and used to construct a success value in the success case. //! constructor and used to construct a success value in the success case.
//! In the failure case, if any util::Error arguments were passed, any //! In the failure case, if any util::Error arguments were passed, any
//! remaining arguments are passed to the FailureType constructor and used //! remaining arguments are passed to the FailureType constructor and used
@ -171,7 +229,7 @@ public:
} }
//! Move-construct a Result object from another Result object, moving the //! Move-construct a Result object from another Result object, moving the
//! success or failure value and any error message. //! success or failure value and any error or warning messages.
template <typename O> template <typename O>
requires (std::decay_t<O>::is_result) requires (std::decay_t<O>::is_result)
Result(O&& other) Result(O&& other)
@ -179,7 +237,10 @@ public:
Move</*Constructed=*/false>(*this, other); Move</*Constructed=*/false>(*this, other);
} }
//! Update this result by moving from another result object. //! Update this result by moving from another result object. Existing
//! success, failure, and messages values are updated (using
//! ResultTraits::Update specializations), so errors and warning messages
//! get appended instead of overwriting existing ones.
Result& Update(Result&& other) LIFETIMEBOUND Result& Update(Result&& other) LIFETIMEBOUND
{ {
Move</*Constructed=*/true>(*this, other); Move</*Constructed=*/true>(*this, other);
@ -187,11 +248,18 @@ public:
} }
//! Disallow operator= and require explicit Result::Update() calls to avoid //! Disallow operator= and require explicit Result::Update() calls to avoid
//! confusion in the future when the Result class gains support for richer //! accidentally clearing error and warning messages when combining results.
//! error reporting, and callers should have ability to set a new result
//! value without clearing existing error messages.
Result& operator=(Result&&) = delete; Result& operator=(Result&&) = delete;
void AddError(bilingual_str error)
{
if (!error.empty()) MessagesTraits<MessagesType>::AddError(this->EnsureFailData().messages, std::move(error));
}
void AddWarning(bilingual_str warning)
{
if (!warning.empty()) MessagesTraits<MessagesType>::AddWarning(this->EnsureFailData().messages, std::move(warning));
}
protected: protected:
template <typename, typename, typename> template <typename, typename, typename>
friend class Result; friend class Result;
@ -216,12 +284,22 @@ protected:
template <bool Failure, typename Result, typename... Args> template <bool Failure, typename Result, typename... Args>
static void Construct(Result& result, util::Error error, Args&&... args) static void Construct(Result& result, util::Error error, Args&&... args)
{ {
result.EnsureFailData().messages = std::move(error.message); result.AddError(std::move(error.message));
Construct</*Failure=*/true>(result, std::forward<Args>(args)...); Construct</*Failure=*/true>(result, std::forward<Args>(args)...);
} }
//! Construct() overload peeling off a util::Warning constructor argument.
template <bool Failure, typename Result, typename... Args>
static void Construct(Result& result, util::Warning warning, Args&&... args)
{
result.AddWarning(std::move(warning.message));
Construct<Failure>(result, std::forward<Args>(args)...);
}
//! Move success, failure, and messages from source Result object to //! Move success, failure, and messages from source Result object to
//! destination object. The source and //! destination object. Existing values are updated (using
//! ResultTraits::Update specializations), so destination errors and warning
//! messages get appended to instead of overwritten. The source and
//! destination results are not required to have the same types, and //! destination results are not required to have the same types, and
//! assigning void source types to non-void destinations type is allowed, //! assigning void source types to non-void destinations type is allowed,
//! since no source information is lost. But assigning non-void source types //! since no source information is lost. But assigning non-void source types
@ -230,16 +308,27 @@ protected:
template <bool DstConstructed, typename DstResult, typename SrcResult> template <bool DstConstructed, typename DstResult, typename SrcResult>
static void Move(DstResult& dst, SrcResult& src) static void Move(DstResult& dst, SrcResult& src)
{ {
// Move messages values first, then move success or failure value below. // Use operator>> to move messages value first, then move
if (src.GetMessages() && !src.GetMessages()->empty()) { // success or failure value below.
dst.EnsureMessages() = std::move(*src.GetMessages()); src >> dst;
}
// If DstConstructed is true, it means dst has either a success value or // If DstConstructed is true, it means dst has either a success value or
// a failure value set, which needs to be destroyed and replaced. If // a failure value set, which needs to be updated or replaced. If
// DstConstructed is false, then dst is being constructed now and has no // DstConstructed is false, then dst is being constructed now and has no
// values set. // values set.
if constexpr (DstConstructed) { if constexpr (DstConstructed) {
if (dst) { if (dst && src) {
// dst and src both hold success values, so update dst from src and return
if constexpr (!std::is_same_v<typename SrcResult::SuccessType, void>) {
ResultTraits<typename SrcResult::SuccessType>::Update(*dst, *src);
}
return;
} else if (!dst && !src) {
// dst and src both hold failure values, so update dst from src and return
if constexpr (!std::is_same_v<typename SrcResult::FailureType, void>) {
ResultTraits<typename SrcResult::FailureType>::Update(dst.GetFailure(), src.GetFailure());
}
return;
} else if (dst) {
// dst has a success value, so destroy it before replacing it with src failure value // dst has a success value, so destroy it before replacing it with src failure value
if constexpr (!std::is_same_v<typename DstResult::SuccessType, void>) { if constexpr (!std::is_same_v<typename DstResult::SuccessType, void>) {
using DstSuccess = typename DstResult::SuccessType; using DstSuccess = typename DstResult::SuccessType;
@ -277,10 +366,40 @@ protected:
} }
}; };
//! Move information from a source Result object to a destination object. It
//! only moves MessagesType values without affecting SuccessType or
//! FailureType values of either Result object.
//!
//! This is useful for combining error and warning messages from multiple result
//! objects into a single object, e.g.:
//!
//! util::Result<void> result;
//! auto r1 = DoSomething() >> result;
//! auto r2 = DoSomethingElse() >> result;
//! ...
//! return result;
//!
template <typename SrcResult, typename DstResult>
requires (std::decay_t<SrcResult>::is_result)
decltype(auto) operator>>(SrcResult&& src LIFETIMEBOUND, DstResult&& dst)
{
using SrcType = std::decay_t<SrcResult>;
if (src.GetMessages() && MessagesTraits<typename SrcType::MessagesType>::HasMessages(*src.GetMessages())) {
ResultTraits<typename SrcType::MessagesType>::Update(dst.EnsureMessages(), *src.GetMessages());
}
return static_cast<SrcType&&>(src);
}
//! Join error and warning messages in a space separated string. This is
//! intended for simple applications where there's probably only one error or
//! warning message to report, but multiple messages should not be lost if they
//! are present. More complicated applications should use Result::GetMessages()
//! method directly.
template <typename Result> template <typename Result>
bilingual_str ErrorString(const Result& result) bilingual_str ErrorString(const Result& result)
{ {
return !result && result.GetMessages() ? *result.GetMessages() : bilingual_str{}; const auto* messages{result.GetMessages()};
return messages ? detail::JoinMessages(*messages) : bilingual_str{};
} }
} // namespace util } // namespace util