Skip to content
Merged
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\(\).*
4 changes: 4 additions & 0 deletions tests/beman/expected/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ add_fail_test(expected_void_ref_e_initializer_list_fail

add_executable(beman.expected.tests.hardened)
target_sources(beman.expected.tests.hardened PRIVATE expected_hardened.test.cpp)
target_include_directories(
beman.expected.tests.hardened
PRIVATE ${PROJECT_SOURCE_DIR}/tests
)
target_link_libraries(
beman.expected.tests.hardened
PRIVATE beman::expected Catch2::Catch2WithMain
Expand Down
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 @@ -153,3 +153,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 @@ -515,6 +515,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");
CHECK(type_name<decltype(std::move(e).value())>() == type_name<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
131 changes: 131 additions & 0 deletions tests/beman/expected/expected_hardened.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@

#include <catch2/catch_test_macros.hpp>

#include <beman/expected/testing/type_name.hpp>

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

using namespace beman::expected;

using beman::expected::testing::type_name;

// ---------------------------------------------------------------------------
// Primary template: operator-> happy path
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -90,6 +95,132 @@ TEST_CASE("hardened: error() on error-state expected<void,int>", "[hardened]") {
CHECK(std::move(ce2).error() == 13);
}

// ---------------------------------------------------------------------------
// Reference specializations
//
// Before this section the hardened build only ever instantiated expected<T, E>
// and expected<void, E> with a value E, so the precondition checks in the
// expected<T&, E> specialization were never compiled at all, and neither the
// primary nor the void specialization was ever instantiated with a reference E.
// The checks themselves are the same `if (!has_val_) TRAP` shape throughout;
// what these add is proof that the guarded bodies still compile and return the
// right thing once T or E is a reference -- in particular that the shallow-const
// observers (T*, T&, E& from a const expected) are unaffected by hardening.
//
// Happy paths only, as above: a violated precondition traps, which cannot be
// observed from inside the process.
// ---------------------------------------------------------------------------

// --- expected<T&, E>: operator-> and operator* (one const overload each) ---

TEST_CASE("hardened: observers on value-state expected<T&,E>", "[hardened]") {
std::string s = "hello";
expected<std::string&, int> e(s);
CHECK(e->size() == 5);
CHECK(*e == "hello");
CHECK(&*e == &s);

// Shallow const: a const expected<T&, E> still hands out a mutable T& / T*.
const expected<std::string&, int> ce(s);
CHECK(type_name<decltype(ce.operator->())>() == type_name<std::string*>());
CHECK(type_name<decltype(*ce)>() == type_name<std::string&>());
CHECK(ce->size() == 5);
CHECK(&*ce == &s);
}

// --- expected<T&, E>: error() ---

TEST_CASE("hardened: error() on error-state expected<T&,E>", "[hardened]") {
expected<int&, int> e(unexpect, 42);
CHECK(e.error() == 42);

const expected<int&, int> ce(unexpect, 99);
CHECK(ce.error() == 99);

expected<int&, int> e2(unexpect, 7);
CHECK(std::move(e2).error() == 7);

const expected<int&, int> ce2(unexpect, 13);
CHECK(std::move(ce2).error() == 13);
}

// --- expected<T, E&>: primary template instantiated with a reference E ---

TEST_CASE("hardened: observers on expected<T,E&>", "[hardened]") {
expected<std::string, int&> e(std::in_place, "world");
CHECK(e->size() == 5);
CHECK(*e == "world");

const expected<std::string, int&> ce(std::in_place, "there");
CHECK(ce->size() == 5);
CHECK(*ce == "there");

expected<std::string, int&> e2(std::in_place, "moved");
CHECK(*std::move(e2) == "moved");
}

TEST_CASE("hardened: error() on error-state expected<T,E&>", "[hardened]") {
int err = 42;
expected<int, int&> e(unexpect, err);
// Shallow const: error() yields int&, not const int&, even from a const
// expected -- `const E&` with E = int& collapses back to int&.
CHECK(type_name<decltype(e.error())>() == type_name<int&>());
CHECK(&e.error() == &err);

const expected<int, int&> ce(unexpect, err);
CHECK(type_name<decltype(ce.error())>() == type_name<int&>());
CHECK(&ce.error() == &err);

CHECK(&std::move(e).error() == &err);
CHECK(&std::move(ce).error() == &err);
}

// --- expected<T&, E&>: T& specialization instantiated with a reference E ---

TEST_CASE("hardened: observers on expected<T&,E&>", "[hardened]") {
int x = 5;
expected<int&, int&> e(x);
CHECK(*e == 5);
CHECK(&*e == &x);

const expected<int&, int&> ce(x);
CHECK(&*ce == &x);
}

TEST_CASE("hardened: error() on error-state expected<T&,E&>", "[hardened]") {
int err = 7;
expected<int&, int&> e(unexpect, err);
CHECK(&e.error() == &err);

const expected<int&, int&> ce(unexpect, err);
CHECK(&ce.error() == &err);

CHECK(&std::move(e).error() == &err);
CHECK(&std::move(ce).error() == &err);
}

// --- expected<void, E&>: void specialization with a reference E ---

TEST_CASE("hardened: operator* on value-state expected<void,E&>", "[hardened]") {
expected<void, int&> e;
*e;

const expected<void, int&> ce;
*ce;
}

TEST_CASE("hardened: error() on error-state expected<void,E&>", "[hardened]") {
int err = 42;
expected<void, int&> e(unexpect, err);
CHECK(&e.error() == &err);

const expected<void, int&> ce(unexpect, err);
CHECK(&ce.error() == &err);

CHECK(&std::move(e).error() == &err);
CHECK(&std::move(ce).error() == &err);
}

// ---------------------------------------------------------------------------
// unexpected friend swap: constraint check (beman-only)
//
Expand Down
Loading
Loading