Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmake/gcovr.cfg.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ print-summary = yes
filter = .*/include/beman/expected/.*
coveralls = coverage.json
coveralls-pretty = yes

# BEMAN_EXPECTED_TRAP() expands to __builtin_trap() (or __debugbreak/abort).
# Reaching one of these lines kills the process with SIGILL before libgcov
# flushes, so no .gcda is written at all -- a test that triggers a hardened
# precondition violation would not merely fail to mark the line covered, it
# would discard the whole run's counters. They are uncoverable by construction,
# not untested, so they are excluded rather than left as permanent residue.
exclude-lines-by-pattern = .*BEMAN_EXPECTED_TRAP\(\).*
22 changes: 22 additions & 0 deletions tests/beman/expected/bad_expected_access.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,25 @@ TEST_CASE("bad_expected_access: const rvalue error accessor (string move)", "[Ba
std::string s = std::move(e).error();
CHECK(s == "val");
}

// bad_expected_access<void>::what() is only reachable from a class derived from
// bad_expected_access<void> that does not itself override what(): the special
// members of bad_expected_access<void> are protected, so no standalone object
// can be made, and every bad_expected_access<E> overrides what(), so virtual
// dispatch through a base reference always lands on the derived override.
namespace {
struct derived_bad_access : expt::bad_expected_access<void> {
derived_bad_access() = default;
};
} // namespace

TEST_CASE("bad_expected_access<void>: what() from a derived class", "[BadExpectedAccessTest]") {
derived_bad_access d;
CHECK(d.what() != nullptr);
#ifndef BEMAN_EXPECTED_TEST_STD
CHECK(std::string_view(d.what()) == "bad expected access");
#endif

const std::exception& ex = d;
CHECK(ex.what() != nullptr);
}
9 changes: 9 additions & 0 deletions tests/beman/expected/expected.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,15 @@ TEST_CASE("expected: value() rvalue ref", "[ExpectedTest]") {
CHECK(s == "rval");
}

TEST_CASE("expected: value() const rvalue ref", "[ExpectedTest]") {
const expt::expected<std::string, int> e("crval");
static_assert(std::is_same_v<decltype(std::move(e).value()), const std::string&&>);
std::string s = std::move(e).value();
CHECK(s == "crval");
// const rvalue value() yields a const rvalue: the copy above leaves e intact.
CHECK(*e == "crval");
}

TEST_CASE("expected: value() throws bad_expected_access from lvalue", "[ExpectedTest]") {
expt::expected<int, std::string> e(expt::unexpected<std::string>("bad"));
CHECK_THROWS_AS(e.value(), expt::bad_expected_access<std::string>);
Expand Down
135 changes: 135 additions & 0 deletions tests/beman/expected/expected_ref.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <string>
#include <type_traits>
#include <utility>

using namespace beman::expected;

Expand Down Expand Up @@ -116,6 +117,60 @@ TEST_CASE("expected<T&>: construct from derived expected<U&, G>", "[expected_ref
CHECK(&*dst == static_cast<Base*>(&d));
}

// The rvalue converting constructor is a separate overload from the lvalue one
// above: it moves the source's error rather than copying it.
TEST_CASE("expected<T&>: move-construct from expected<U&, G>&&", "[expected_ref]") {
struct Base {
virtual ~Base() = default;
int v;
};
struct Derived : Base {
Derived(int i) { v = i; }
};

Derived d{7};
expected<Derived&, int> src(d);
expected<Base&, long> dst = std::move(src);
REQUIRE(dst.has_value());
CHECK(&*dst == static_cast<Base*>(&d));
}

TEST_CASE("expected<T&>: move-construct error state from expected<U&, G>&&", "[expected_ref]") {
expected<int&, testing::narrowed> src(unexpect, testing::narrowed(7));
expected<int&, testing::widened> dst = std::move(src);
REQUIRE(!dst.has_value());
CHECK(dst.error().val == 7);
}

// The const& constructor from unexpected<G> is a separate overload from the &&
// one exercised above: it copies the source's error and leaves it intact.
TEST_CASE("expected<T&>: construct from a const unexpected lvalue", "[expected_ref]") {
const unexpected<std::string> u("copied");
expected<int&, std::string> e = u;
REQUIRE(!e.has_value());
CHECK(e.error() == "copied");
CHECK(u.error() == "copied");
}

// The trailing argument is deliberately a runtime lvalue. Every argument being
// a constant expression would make the whole initializer constant-evaluated —
// this constructor and init_list_type's are both constexpr — and GCC folds that
// at compile time even under the Gcov profile's -O0 -fno-inline, emitting no
// runtime code and therefore no coverage record. The constexpr path is asserted
// separately below.
TEST_CASE("expected<T&>: construct from unexpect_t with initializer_list", "[expected_ref]") {
int extra = 10;
expected<int&, testing::init_list_type> e(unexpect, {1, 2, 3}, extra);
REQUIRE(!e.has_value());
CHECK(e.error().sum == 16);
CHECK(e.error().count == 3);
}

static_assert([] {
expected<int&, testing::init_list_type> e(unexpect, {1, 2, 3}, 10);
return e.error().sum;
}() == 16);

// =============================================================================
// Rebind semantics on assignment
// =============================================================================
Expand Down Expand Up @@ -146,6 +201,26 @@ TEST_CASE("expected<T&>: assign from unexpected transitions to error state", "[e
CHECK(x == 5);
}

// The const& assignment operator is a separate overload from the && one above:
// it copies the source's error and leaves it intact.
TEST_CASE("expected<T&>: assign from a const unexpected lvalue when value", "[expected_ref]") {
const unexpected<std::string> u("copied");
int x = 5;
expected<int&, std::string> e(x);
e = u;
REQUIRE(!e.has_value());
CHECK(e.error() == "copied");
CHECK(u.error() == "copied");
}

TEST_CASE("expected<T&>: assign from a const unexpected lvalue when already error", "[expected_ref]") {
const unexpected<std::string> u("second");
expected<int&, std::string> e(unexpect, "first");
e = u;
REQUIRE(!e.has_value());
CHECK(e.error() == "second");
}

TEST_CASE("expected<T&>: assign lvalue rebinds from error state", "[expected_ref]") {
int x = 7;
expected<int&, int> e = unexpected(1);
Expand Down Expand Up @@ -276,11 +351,39 @@ TEST_CASE("expected<T&>: value() throws bad_expected_access on error", "[expecte
REQUIRE_THROWS_AS(e.value(), beman::expected::bad_expected_access<int>);
}

// value() && on expected<T&, E> still yields T& — the referent is external, so
// there is nothing to move out of.
TEST_CASE("expected<T&>: rvalue value() returns T&", "[expected_ref]") {
int x = 1;
expected<int&, int> e(x);
static_assert(std::is_same_v<decltype(std::move(e).value()), int&>);
CHECK(&std::move(e).value() == &x);
}

TEST_CASE("expected<T&>: rvalue value() throws bad_expected_access on error", "[expected_ref]") {
expected<int&, int> e = unexpected(5);
REQUIRE_THROWS_AS(std::move(e).value(), beman::expected::bad_expected_access<int>);
}

TEST_CASE("expected<T&>: error() returns error", "[expected_ref]") {
expected<int&, int> e = unexpected(42);
CHECK(e.error() == 42);
}

TEST_CASE("expected<T&>: error() ref-qualified overloads", "[expected_ref]") {
expected<int&, std::string> e(unexpect, "err");
static_assert(std::is_same_v<decltype(e.error()), std::string&>);
static_assert(std::is_same_v<decltype(std::as_const(e).error()), const std::string&>);
static_assert(std::is_same_v<decltype(std::move(std::as_const(e)).error()), const std::string&&>);
static_assert(std::is_same_v<decltype(std::move(e).error()), std::string&&>);

CHECK(std::as_const(e).error() == "err");
CHECK(std::move(std::as_const(e)).error() == "err");
// Move out last: this leaves e's error in a moved-from state.
std::string moved = std::move(e).error();
CHECK(moved == "err");
}

TEST_CASE("expected<T&>: value_or returns referred value when has value", "[expected_ref]") {
int x = 42;
expected<int&, int> e(x);
Expand All @@ -305,6 +408,17 @@ TEST_CASE("expected<T&>: error_or returns default when has value", "[expected_re
CHECK(e.error_or(99) == 99);
}

TEST_CASE("expected<T&>: rvalue error_or moves the error when has error", "[expected_ref]") {
expected<int&, std::string> e(unexpect, "held");
CHECK(std::move(e).error_or("fallback") == "held");
}

TEST_CASE("expected<T&>: rvalue error_or returns default when has value", "[expected_ref]") {
int x = 5;
expected<int&, std::string> e(x);
CHECK(std::move(e).error_or("fallback") == "fallback");
}

TEST_CASE("expected<T&>: bool conversion", "[expected_ref]") {
int x = 1;
expected<int&, int> val(x);
Expand Down Expand Up @@ -512,6 +626,27 @@ TEST_CASE("expected<T&>: const and_then", "[expected_ref]") {
CHECK(*r == 11);
}

TEST_CASE("expected<T&>: const or_else on error calls F", "[expected_ref]") {
const expected<int&, int> e(unexpect, 5);
auto r = e.or_else([](const int& v) -> expected<int&, int> { return unexpected(v + 1); });
REQUIRE(!r.has_value());
CHECK(r.error() == 6);
CHECK(e.error() == 5);
}

TEST_CASE("expected<T&>: const or_else on value short-circuits", "[expected_ref]") {
int x = 10;
const expected<int&, int> e(x);
bool called = false;
auto r = e.or_else([&](const int&) -> expected<int&, int> {
called = true;
return unexpected(0);
});
CHECK(!called);
REQUIRE(r.has_value());
CHECK(&*r == &x);
}

TEST_CASE("expected<T&>: const transform", "[expected_ref]") {
int x = 3;
const expected<int&, int> e(x);
Expand Down
49 changes: 49 additions & 0 deletions tests/beman/expected/expected_ref_both.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,55 @@ TEST_CASE("expected<T&,E&>: move construct preserves error pointer", "[expected_
CHECK(&b.error() == &err);
}

// Converting constructors from expected<U&, G&>: distinct overloads from the
// copy/move constructors above, selected when U/G differ from T/E. Both the
// value side (Derived& -> Base&) and the error side (int& -> const int&) are
// reference binds, so nothing is copied.
namespace {
struct ConvBase {
virtual ~ConvBase() = default;
int v = 0;
};
struct ConvDerived : ConvBase {
explicit ConvDerived(int i) { v = i; }
};
} // namespace

static_assert(std::is_constructible_v<expected<ConvBase&, const int&>, const expected<ConvDerived&, int&>&>);
static_assert(std::is_constructible_v<expected<ConvBase&, const int&>, expected<ConvDerived&, int&>&&>);

TEST_CASE("expected<T&,E&>: converting copy construct, value state", "[expected_ref_both]") {
ConvDerived d{7};
expected<ConvDerived&, int&> src(d);
expected<ConvBase&, const int&> dst(src);
REQUIRE(dst.has_value());
CHECK(&*dst == static_cast<ConvBase*>(&d));
}

TEST_CASE("expected<T&,E&>: converting copy construct, error state", "[expected_ref_both]") {
int err = 5;
expected<ConvDerived&, int&> src(unexpect, err);
expected<ConvBase&, const int&> dst(src);
REQUIRE(!dst.has_value());
CHECK(&dst.error() == &err);
}

TEST_CASE("expected<T&,E&>: converting move construct, value state", "[expected_ref_both]") {
ConvDerived d{9};
expected<ConvDerived&, int&> src(d);
expected<ConvBase&, const int&> dst(std::move(src));
REQUIRE(dst.has_value());
CHECK(&*dst == static_cast<ConvBase*>(&d));
}

TEST_CASE("expected<T&,E&>: converting move construct, error state", "[expected_ref_both]") {
int err = 11;
expected<ConvDerived&, int&> src(unexpect, err);
expected<ConvBase&, const int&> dst(std::move(src));
REQUIRE(!dst.has_value());
CHECK(&dst.error() == &err);
}

// =============================================================================
// Value rebind semantics
// =============================================================================
Expand Down
54 changes: 54 additions & 0 deletions tests/beman/expected/expected_review_corrections.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,60 @@ TEST_CASE("rebinding assignment from unexpected<E&> repoints the error reference
}
}

// The const& constructor and assignment overloads are separate from the &&
// overloads exercised above. For reference E they behave identically — the
// stored reference is rebound to the source's external referent either way —
// but they are only selected when the source unexpected is an lvalue.

TEST_CASE("reference-error construction from a const unexpected<E&> lvalue", "[ref][unexpected]") {
int err = 41;
const unexpected<int&> u(err);

expected<void, int&> a{u};
expected<int, int&> b{u};
expected<int&, int&> c{u};

REQUIRE(&a.error() == &err);
REQUIRE(&b.error() == &err);
REQUIRE(&c.error() == &err);

err = 99;
REQUIRE(a.error() == 99);
REQUIRE(b.error() == 99);
REQUIRE(c.error() == 99);
}

TEST_CASE("rebinding assignment from a const unexpected<E&> lvalue", "[ref][unexpected][assign]") {
int g1 = 1, g2 = 2;
const unexpected<int&> u(g2);

SECTION("error -> error rebind (expected<int, int&>)") {
expected<int, int&> e{unexpect, g1};
e = u;
REQUIRE(&e.error() == &g2);
REQUIRE(g1 == 1); // previously-referenced object untouched
}
SECTION("value -> error transition (expected<int, int&>)") {
expected<int, int&> e{7};
e = u;
REQUIRE_FALSE(e.has_value());
REQUIRE(&e.error() == &g2);
}
SECTION("both references (expected<int&, int&>)") {
int target = 5;
expected<int&, int&> e{target};
e = u;
REQUIRE_FALSE(e.has_value());
REQUIRE(&e.error() == &g2);
}
SECTION("void value (expected<void, int&>)") {
expected<void, int&> e{};
e = u;
REQUIRE_FALSE(e.has_value());
REQUIRE(&e.error() == &g2);
}
}

// =============================================================================
// F4 / F5 — value constructor and emplace are noexcept only when the reference
// bind cannot throw, so a throwing conversion propagates instead of terminating.
Expand Down
Loading
Loading