Files
scylladb/test/boost/exception_container_test.cc
Piotr Dulikowski 742f2abfd8 exception_container: do not throw in accept
This commit changes the behavior of `exception_container::accept`. Now,
instead of throwing an `utils::bad_exception_container_access` exception
when the container is empty, the provided visitor is invoked with that
exception instead. There are two reasons for this change:

- The exception_container is supposed to allow handling exceptions
  without using the costly C++'s exception runtime. Although an empty
  container is an edge case, I think it the new behavior is more aligned
  with the class' purpose. The old behavior can be simulated by
  providing a visitor which throws when called with bad access
  exception.

- The new behavior fixes a bug in `result_try`/`result_futurize_try`.
  Before the change, if the `try` block returned a failed result with an
  empty exception container, a bad access exception would either be
  thrown or returned as an exceptional future without being handled by
  the `catch` clauses. Although nobody is supposed to return such
  result<>s on purpose, a moved out result can be returned by accident
  and it's important for the exception handling logic to be correct in
  such a situation.

Tests: unit(dev)

Closes #10086
2022-02-16 10:06:10 +02:00

98 lines
2.9 KiB
C++

/*
* Copyright (C) 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <stdexcept>
#include "utils/exception_container.hh"
#include <seastar/testing/test_case.hh>
#include <seastar/core/sstring.hh>
using namespace seastar;
class foo_exception : public std::exception {
public:
const char* what() const noexcept override {
return "foo";
}
};
class bar_exception : public std::exception {
public:
const char* what() const noexcept override {
return "bar";
}
};
using foo_bar_container = utils::exception_container<foo_exception, bar_exception>;
static sstring foo_bar_what(const foo_bar_container& fbc) {
return fbc.accept([] (const auto& ex) { return ex.what(); });
}
SEASTAR_TEST_CASE(test_exception_container) {
auto empty = foo_bar_container();
auto foo = foo_bar_container(foo_exception());
auto bar = foo_bar_container(bar_exception());
BOOST_REQUIRE(empty.empty());
BOOST_REQUIRE(!foo.empty());
BOOST_REQUIRE(!bar.empty());
BOOST_REQUIRE(!empty);
BOOST_REQUIRE(foo);
BOOST_REQUIRE(bar);
BOOST_REQUIRE_EQUAL(foo_bar_what(empty), sstring("bad exception container access"));
BOOST_REQUIRE_EQUAL(foo_bar_what(foo), sstring("foo"));
BOOST_REQUIRE_EQUAL(foo_bar_what(bar), sstring("bar"));
BOOST_REQUIRE_THROW(empty.throw_me(), utils::bad_exception_container_access);
BOOST_REQUIRE_THROW(foo.throw_me(), foo_exception);
BOOST_REQUIRE_THROW(bar.throw_me(), bar_exception);
// Construct the futures outside BOOST_REQUIRE_THROW
// otherwise the checks would pass if as_exception_future throwed
// and we don't want that
auto f_empty = empty.as_exception_future();
auto f_foo = foo.as_exception_future();
auto f_bar = bar.as_exception_future();
BOOST_REQUIRE_THROW(f_empty.get(), utils::bad_exception_container_access);
BOOST_REQUIRE_THROW(f_foo.get(), foo_exception);
BOOST_REQUIRE_THROW(f_bar.get(), bar_exception);
// Same reasoning as with as_exception_future
f_empty = std::move(empty).into_exception_future();
f_foo = std::move(foo).into_exception_future();
f_bar = std::move(bar).into_exception_future();
BOOST_REQUIRE_THROW(f_empty.get(), utils::bad_exception_container_access);
BOOST_REQUIRE_THROW(f_foo.get(), foo_exception);
BOOST_REQUIRE_THROW(f_bar.get(), bar_exception);
return make_ready_future<>();
}
SEASTAR_TEST_CASE(test_exception_container_empty_accept) {
auto empty = foo_bar_container();
struct visitor {
sstring operator()(const foo_exception&) {
return "had foo exception";
}
sstring operator()(const bar_exception&) {
return "had bar exception";
}
sstring operator()(const utils::bad_exception_container_access&) {
return "was empty";
}
};
BOOST_REQUIRE_EQUAL(empty.accept(visitor{}), "was empty");
return make_ready_future<>();
}