From f5e601df95e4d8682406f0ccf0d2a0c1ae6609a2 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 2 Aug 2026 15:04:07 -0400 Subject: [PATCH 1/4] test: cover the remaining unexercised expected members A coverage review turned up out-of-line definitions that no test instantiated, and a few more that were instantiated but never called. Nearly all of them had the same shape: a `const&` overload shadowed by its `&&` twin. Several existing tests were misnamed accordingly: "construct from unexpected const&" passed an rvalue and bound the `&&` overload, and the `const` monadic tests all wrote `const expected<...> e; std::move(e).or_else(...)`, which hits `const&&` and leaves every `const&` monadic overload untouched. Adds tests for: - expected: converting move ctor, const unexpected& ctor/assign, error_or() &&, value() && on a valued expected, and the const& overloads of or_else / transform / transform_error - expected: converting move ctor, const unexpected& ctor/assign, unexpect_t + initializer_list ctor, value() &&, all four ref-qualified error() overloads, error_or() &&, or_else const& - expected: converting copy/move ctors (Derived& -> Base&, int& -> const int&) - expected: converting construction of the value state - reference-E const unexpected lvalue ctor/assign across all three specializations - expected::value() const&& on a valued expected - bad_expected_access::what(), reachable only from a derived class: its special members are protected and every bad_expected_access overrides what(), so dispatch through a base reference never lands on it Type and constructibility checks are runtime CHECKs, following the rest of the suite, so that a regression is reported by the test run. The initializer_list test passes its trailing argument as a runtime lvalue on purpose. With every argument a constant expression the whole initializer is constant-evaluated (both that constructor and init_list_type's are constexpr), and GCC folds it at compile time even under the Gcov profile's -O0 -fno-inline, emitting no symbol and no coverage record. The constant-evaluated path is asserted separately, and that one stays a static_assert because constant evaluation is what it tests. Also excludes BEMAN_EXPECTED_TRAP() lines from the gcovr report. The macro expands to __builtin_trap(); reaching one kills the process with SIGILL before libgcov flushes, so no .gcda is written at all. A death test would not merely fail to mark the line covered, it would discard the whole run's counters. Those lines are uncoverable by construction rather than untested. The guarding `if (has_val_)` is deliberately left in, so branch coverage still reports that no test drives the precondition check. --- cmake/gcovr.cfg.in | 8 ++ .../expected/bad_expected_access.test.cpp | 22 +++ tests/beman/expected/expected.test.cpp | 9 ++ tests/beman/expected/expected_ref.test.cpp | 136 ++++++++++++++++++ .../beman/expected/expected_ref_both.test.cpp | 51 +++++++ .../expected_review_corrections.test.cpp | 54 +++++++ tests/beman/expected/expected_void.test.cpp | 56 +++++++- .../expected/expected_void_monadic.test.cpp | 80 +++++++++++ .../expected/expected_void_ref_e.test.cpp | 6 + 9 files changed, 421 insertions(+), 1 deletion(-) diff --git a/cmake/gcovr.cfg.in b/cmake/gcovr.cfg.in index 419096ae..6623cb0f 100644 --- a/cmake/gcovr.cfg.in +++ b/cmake/gcovr.cfg.in @@ -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\(\).* diff --git a/tests/beman/expected/bad_expected_access.test.cpp b/tests/beman/expected/bad_expected_access.test.cpp index c8eef7ac..de2f2710 100644 --- a/tests/beman/expected/bad_expected_access.test.cpp +++ b/tests/beman/expected/bad_expected_access.test.cpp @@ -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::what() is only reachable from a class derived from +// bad_expected_access that does not itself override what(): the special +// members of bad_expected_access are protected, so no standalone object +// can be made, and every bad_expected_access overrides what(), so virtual +// dispatch through a base reference always lands on the derived override. +namespace { +struct derived_bad_access : expt::bad_expected_access { + derived_bad_access() = default; +}; +} // namespace + +TEST_CASE("bad_expected_access: 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); +} diff --git a/tests/beman/expected/expected.test.cpp b/tests/beman/expected/expected.test.cpp index 544ae0ea..dd0d24e1 100644 --- a/tests/beman/expected/expected.test.cpp +++ b/tests/beman/expected/expected.test.cpp @@ -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 e("crval"); + CHECK(type_name() == type_name()); + 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 e(expt::unexpected("bad")); CHECK_THROWS_AS(e.value(), expt::bad_expected_access); diff --git a/tests/beman/expected/expected_ref.test.cpp b/tests/beman/expected/expected_ref.test.cpp index 088a38f9..82664014 100644 --- a/tests/beman/expected/expected_ref.test.cpp +++ b/tests/beman/expected/expected_ref.test.cpp @@ -12,6 +12,7 @@ #include #include +#include using namespace beman::expected; @@ -141,6 +142,61 @@ TEST_CASE("expected: construct from derived expected", "[expected_ref CHECK(&*dst == static_cast(&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: move-construct from expected&&", "[expected_ref]") { + struct Base { + virtual ~Base() = default; + int v; + }; + struct Derived : Base { + Derived(int i) { v = i; } + }; + + Derived d{7}; + expected src(d); + expected dst = std::move(src); + REQUIRE(dst.has_value()); + CHECK(&*dst == static_cast(&d)); +} + +TEST_CASE("expected: move-construct error state from expected&&", "[expected_ref]") { + expected src(unexpect, testing::narrowed(7)); + expected dst = std::move(src); + REQUIRE(!dst.has_value()); + CHECK(dst.error().val == 7); +} + +// The const& constructor from unexpected is a separate overload from the && +// one exercised above: it copies the source's error and leaves it intact. +TEST_CASE("expected: construct from a const unexpected lvalue", "[expected_ref]") { + const unexpected u("copied"); + expected 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, with a static_assert because constant evaluation is the +// thing under test. +TEST_CASE("expected: construct from unexpect_t with initializer_list", "[expected_ref]") { + int extra = 10; + expected e(unexpect, {1, 2, 3}, extra); + REQUIRE(!e.has_value()); + CHECK(e.error().sum == 16); + CHECK(e.error().count == 3); +} + +static_assert([] { + expected e(unexpect, {1, 2, 3}, 10); + return e.error().sum; +}() == 16); + // ============================================================================= // Rebind semantics on assignment // ============================================================================= @@ -171,6 +227,26 @@ TEST_CASE("expected: 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: assign from a const unexpected lvalue when value", "[expected_ref]") { + const unexpected u("copied"); + int x = 5; + expected e(x); + e = u; + REQUIRE(!e.has_value()); + CHECK(e.error() == "copied"); + CHECK(u.error() == "copied"); +} + +TEST_CASE("expected: assign from a const unexpected lvalue when already error", "[expected_ref]") { + const unexpected u("second"); + expected e(unexpect, "first"); + e = u; + REQUIRE(!e.has_value()); + CHECK(e.error() == "second"); +} + TEST_CASE("expected: assign lvalue rebinds from error state", "[expected_ref]") { int x = 7; expected e = unexpected(1); @@ -301,11 +377,39 @@ TEST_CASE("expected: value() throws bad_expected_access on error", "[expecte REQUIRE_THROWS_AS(e.value(), beman::expected::bad_expected_access); } +// value() && on expected still yields T& — the referent is external, so +// there is nothing to move out of. +TEST_CASE("expected: rvalue value() returns T&", "[expected_ref]") { + int x = 1; + expected e(x); + CHECK(type_name() == type_name()); + CHECK(&std::move(e).value() == &x); +} + +TEST_CASE("expected: rvalue value() throws bad_expected_access on error", "[expected_ref]") { + expected e = unexpected(5); + REQUIRE_THROWS_AS(std::move(e).value(), beman::expected::bad_expected_access); +} + TEST_CASE("expected: error() returns error", "[expected_ref]") { expected e = unexpected(42); CHECK(e.error() == 42); } +TEST_CASE("expected: error() ref-qualified overloads", "[expected_ref]") { + expected e(unexpect, "err"); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + + 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: value_or returns referred value when has value", "[expected_ref]") { int x = 42; expected e(x); @@ -330,6 +434,17 @@ TEST_CASE("expected: error_or returns default when has value", "[expected_re CHECK(e.error_or(99) == 99); } +TEST_CASE("expected: rvalue error_or moves the error when has error", "[expected_ref]") { + expected e(unexpect, "held"); + CHECK(std::move(e).error_or("fallback") == "held"); +} + +TEST_CASE("expected: rvalue error_or returns default when has value", "[expected_ref]") { + int x = 5; + expected e(x); + CHECK(std::move(e).error_or("fallback") == "fallback"); +} + TEST_CASE("expected: bool conversion", "[expected_ref]") { int x = 1; expected val(x); @@ -537,6 +652,27 @@ TEST_CASE("expected: const and_then", "[expected_ref]") { CHECK(*r == 11); } +TEST_CASE("expected: const or_else on error calls F", "[expected_ref]") { + const expected e(unexpect, 5); + auto r = e.or_else([](const int& v) -> expected { return unexpected(v + 1); }); + REQUIRE(!r.has_value()); + CHECK(r.error() == 6); + CHECK(e.error() == 5); +} + +TEST_CASE("expected: const or_else on value short-circuits", "[expected_ref]") { + int x = 10; + const expected e(x); + bool called = false; + auto r = e.or_else([&](const int&) -> expected { + called = true; + return unexpected(0); + }); + CHECK(!called); + REQUIRE(r.has_value()); + CHECK(&*r == &x); +} + TEST_CASE("expected: const transform", "[expected_ref]") { int x = 3; const expected e(x); diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index ea1f35b5..2e2da1b7 100644 --- a/tests/beman/expected/expected_ref_both.test.cpp +++ b/tests/beman/expected/expected_ref_both.test.cpp @@ -160,6 +160,57 @@ TEST_CASE("expected: move construct preserves error pointer", "[expected_ CHECK(&b.error() == &err); } +// Converting constructors from expected: 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 + +TEST_CASE("expected: converting constructors are available", "[expected_ref_both]") { + CHECK(std::is_constructible_v, const expected&>); + CHECK(std::is_constructible_v, expected&&>); +} + +TEST_CASE("expected: converting copy construct, value state", "[expected_ref_both]") { + ConvDerived d{7}; + expected src(d); + expected dst(src); + REQUIRE(dst.has_value()); + CHECK(&*dst == static_cast(&d)); +} + +TEST_CASE("expected: converting copy construct, error state", "[expected_ref_both]") { + int err = 5; + expected src(unexpect, err); + expected dst(src); + REQUIRE(!dst.has_value()); + CHECK(&dst.error() == &err); +} + +TEST_CASE("expected: converting move construct, value state", "[expected_ref_both]") { + ConvDerived d{9}; + expected src(d); + expected dst(std::move(src)); + REQUIRE(dst.has_value()); + CHECK(&*dst == static_cast(&d)); +} + +TEST_CASE("expected: converting move construct, error state", "[expected_ref_both]") { + int err = 11; + expected src(unexpect, err); + expected dst(std::move(src)); + REQUIRE(!dst.has_value()); + CHECK(&dst.error() == &err); +} + // ============================================================================= // Value rebind semantics // ============================================================================= diff --git a/tests/beman/expected/expected_review_corrections.test.cpp b/tests/beman/expected/expected_review_corrections.test.cpp index eeb4e45e..8c55b343 100644 --- a/tests/beman/expected/expected_review_corrections.test.cpp +++ b/tests/beman/expected/expected_review_corrections.test.cpp @@ -167,6 +167,60 @@ TEST_CASE("rebinding assignment from unexpected 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 lvalue", "[ref][unexpected]") { + int err = 41; + const unexpected u(err); + + expected a{u}; + expected b{u}; + expected 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 lvalue", "[ref][unexpected][assign]") { + int g1 = 1, g2 = 2; + const unexpected u(g2); + + SECTION("error -> error rebind (expected)") { + expected e{unexpect, g1}; + e = u; + REQUIRE(&e.error() == &g2); + REQUIRE(g1 == 1); // previously-referenced object untouched + } + SECTION("value -> error transition (expected)") { + expected e{7}; + e = u; + REQUIRE_FALSE(e.has_value()); + REQUIRE(&e.error() == &g2); + } + SECTION("both references (expected)") { + int target = 5; + expected e{target}; + e = u; + REQUIRE_FALSE(e.has_value()); + REQUIRE(&e.error() == &g2); + } + SECTION("void value (expected)") { + expected 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. diff --git a/tests/beman/expected/expected_void.test.cpp b/tests/beman/expected/expected_void.test.cpp index 00899df4..d910fddf 100644 --- a/tests/beman/expected/expected_void.test.cpp +++ b/tests/beman/expected/expected_void.test.cpp @@ -106,12 +106,29 @@ TEST_CASE("expected: convert from expected with error", "[expecte CHECK(dst.error() == 7L); } +// The rvalue converting constructor is a distinct overload: it moves out of the +// source's error rather than copying it. +TEST_CASE("expected: convert from expected&& with value", "[expected_void]") { + expected src; + expected dst = std::move(src); + CHECK(dst.has_value()); +} + +TEST_CASE("expected: convert from expected&& with error", "[expected_void]") { + expected src(unexpect, 7); + expected dst = std::move(src); + REQUIRE(!dst.has_value()); + CHECK(dst.error() == 7L); +} + // --- Constructor from unexpected --- TEST_CASE("expected: construct from unexpected const&", "[expected_void]") { - expected e = unexpected("fail"); + const unexpected u("fail"); + expected e = u; REQUIRE(!e.has_value()); CHECK(e.error() == "fail"); + CHECK(u.error() == "fail"); // copied, not moved from } TEST_CASE("expected: construct from unexpected&&", "[expected_void]") { @@ -223,6 +240,26 @@ TEST_CASE("expected: assign from unexpected when already error", "[expecte CHECK(e.error() == 2); } +// The const& assignment operator is a distinct overload from the && one above: +// it copies the source's error instead of moving it. +TEST_CASE("expected: assign from unexpected const& when value", "[expected_void]") { + const unexpected u("copied"); + expected e; + e = u; + REQUIRE(!e.has_value()); + CHECK(e.error() == "copied"); + CHECK(u.error() == "copied"); +} + +TEST_CASE("expected: assign from unexpected const& when already error", "[expected_void]") { + const unexpected u("second"); + expected e(unexpect, "first"); + e = u; + REQUIRE(!e.has_value()); + CHECK(e.error() == "second"); + CHECK(u.error() == "second"); +} + // --- emplace() --- TEST_CASE("expected: emplace from error state", "[expected_void]") { @@ -313,6 +350,13 @@ TEST_CASE("expected: rvalue value() throws on error", "[expected_void]") { REQUIRE_THROWS_AS(std::move(e).value(), bad_expected_access); } +TEST_CASE("expected: rvalue value() on success is no-op", "[expected_void]") { + expected e; + CHECK(type_name() == type_name()); + std::move(e).value(); // should not throw + CHECK(e.has_value()); +} + // --- error() --- TEST_CASE("expected: error() all ref qualifications", "[expected_void]") { @@ -335,6 +379,16 @@ TEST_CASE("expected: error_or with error", "[expected_void]") { CHECK(e.error_or(0) == 7); } +TEST_CASE("expected: rvalue error_or with error moves the error", "[expected_void]") { + expected e(unexpect, "held"); + CHECK(std::move(e).error_or("fallback") == "held"); +} + +TEST_CASE("expected: rvalue error_or with value uses the default", "[expected_void]") { + expected e; + CHECK(std::move(e).error_or("fallback") == "fallback"); +} + // ============================================================================= // [expected.void.eq] Equality operators // ============================================================================= diff --git a/tests/beman/expected/expected_void_monadic.test.cpp b/tests/beman/expected/expected_void_monadic.test.cpp index 3007e6ed..298996c7 100644 --- a/tests/beman/expected/expected_void_monadic.test.cpp +++ b/tests/beman/expected/expected_void_monadic.test.cpp @@ -10,6 +10,7 @@ #include "testing/types.hpp" #include +#include #include using namespace test_ns; @@ -340,6 +341,85 @@ TEST_CASE("transform_error const rvalue on error calls F", "[expected_void_monad CHECK(r.error() == 4); } +// --- Monadic: const lvalue overloads --- +// Distinct from the const rvalue overloads above: these invoke F with a +// `const E&` drawn from a const lvalue expected, without moving out of it. + +TEST_CASE("or_else const lvalue on error calls F", "[expected_void_monadic]") { + const expected e(unexpect, 5); + auto r = e.or_else([](const int& v) -> expected { return unexpected(v + 1); }); + REQUIRE(!r.has_value()); + CHECK(r.error() == 6); + CHECK(e.error() == 5); +} + +TEST_CASE("or_else const lvalue on value short-circuits", "[expected_void_monadic]") { + const expected e; + bool called = false; + auto r = e.or_else([&](const int&) -> expected { + called = true; + return unexpected(0); + }); + CHECK(!called); + REQUIRE(r.has_value()); +} + +TEST_CASE("transform const lvalue on value calls F", "[expected_void_monadic]") { + const expected e; + auto r = e.transform([]() { return 42; }); + REQUIRE(r.has_value()); + CHECK(*r == 42); +} + +TEST_CASE("transform const lvalue on error short-circuits", "[expected_void_monadic]") { + const expected e(unexpect, 9); + bool called = false; + auto r = e.transform([&]() { + called = true; + return 0; + }); + CHECK(!called); + REQUIRE(!r.has_value()); + CHECK(r.error() == 9); +} + +TEST_CASE("transform const lvalue with void result", "[expected_void_monadic]") { + const expected e; + int count = 0; + auto r = e.transform([&]() { ++count; }); + CHECK(count == 1); + REQUIRE(r.has_value()); + CHECK(type_name() == type_name>()); +} + +TEST_CASE("transform const lvalue with void result on error", "[expected_void_monadic]") { + const expected e(unexpect, 4); + int count = 0; + auto r = e.transform([&]() { ++count; }); + CHECK(count == 0); + REQUIRE(!r.has_value()); + CHECK(r.error() == 4); +} + +TEST_CASE("transform_error const lvalue on error calls F", "[expected_void_monadic]") { + const expected e(unexpect, 3); + auto r = e.transform_error([](const int& v) { return v * 2; }); + REQUIRE(!r.has_value()); + CHECK(r.error() == 6); + CHECK(e.error() == 3); +} + +TEST_CASE("transform_error const lvalue on value short-circuits", "[expected_void_monadic]") { + const expected e; + bool called = false; + auto r = e.transform_error([&](const int& v) { + called = true; + return v; + }); + CHECK(!called); + REQUIRE(r.has_value()); +} + // --------------------------------------------------------------------------- // void monadic tests // --------------------------------------------------------------------------- diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index 19cca7c2..f0d9a205 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -158,6 +158,12 @@ TEST_CASE("expected: convert from expected (lvalue and CHECK(&dst2.error() == &err); } +TEST_CASE("expected: convert value state from expected", "[expected_void_ref_e]") { + const expected src; + expected dst(src); + CHECK(dst.has_value()); +} + // --------------------------------------------------------------------------- // Error rebind semantics on assignment // --------------------------------------------------------------------------- From 4c1ed2e29b9c910783d5f0a47042a3c5279310e3 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 2 Aug 2026 16:03:12 -0400 Subject: [PATCH 2/4] test: instantiate the reference specializations in the hardened build expected_hardened.test.cpp only ever used expected and expected with a value E. Consequently the precondition checks in the expected specialization were never compiled at all, and neither the primary nor the void specialization was ever instantiated with a reference E. Of the 21 BEMAN_EXPECTED_TRAP() sites in the header, only 15 were reachable by any translation unit in the hardened build; the six in expected were dead as far as the test suite was concerned. Adds happy-path coverage for expected, expected, expected and expected, in the same style as the existing sections. All 21 trap sites are now compiled. The checks are the same `if (!has_val_) TRAP` shape throughout, so this is mostly about compiling them at all. What it does verify beyond that is that the guarded bodies still return the right thing once T or E is a reference -- the shallow-const observers in particular, where a const expected still hands out a mutable T* / T& / E&, and where `const E&` with E = int& collapses back to int&. Happy paths only, matching the rest of the file: a violated precondition traps, and that cannot be observed from inside the process. Type checks are runtime CHECKs through type_name, as in the rest of the suite, so the hardened target now gets the tests/ include directory for the testing helpers. --- tests/beman/expected/CMakeLists.txt | 4 + .../beman/expected/expected_hardened.test.cpp | 131 ++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/tests/beman/expected/CMakeLists.txt b/tests/beman/expected/CMakeLists.txt index 28ad9568..134c2bce 100644 --- a/tests/beman/expected/CMakeLists.txt +++ b/tests/beman/expected/CMakeLists.txt @@ -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 diff --git a/tests/beman/expected/expected_hardened.test.cpp b/tests/beman/expected/expected_hardened.test.cpp index 34f30e71..118ac1b5 100644 --- a/tests/beman/expected/expected_hardened.test.cpp +++ b/tests/beman/expected/expected_hardened.test.cpp @@ -8,11 +8,16 @@ #include +#include + #include #include +#include using namespace beman::expected; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- // Primary template: operator-> happy path // --------------------------------------------------------------------------- @@ -90,6 +95,132 @@ TEST_CASE("hardened: error() on error-state expected", "[hardened]") { CHECK(std::move(ce2).error() == 13); } +// --------------------------------------------------------------------------- +// Reference specializations +// +// Before this section the hardened build only ever instantiated expected +// and expected with a value E, so the precondition checks in the +// expected 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: operator-> and operator* (one const overload each) --- + +TEST_CASE("hardened: observers on value-state expected", "[hardened]") { + std::string s = "hello"; + expected e(s); + CHECK(e->size() == 5); + CHECK(*e == "hello"); + CHECK(&*e == &s); + + // Shallow const: a const expected still hands out a mutable T& / T*. + const expected ce(s); + CHECK(type_name())>() == type_name()); + CHECK(type_name() == type_name()); + CHECK(ce->size() == 5); + CHECK(&*ce == &s); +} + +// --- expected: error() --- + +TEST_CASE("hardened: error() on error-state expected", "[hardened]") { + expected e(unexpect, 42); + CHECK(e.error() == 42); + + const expected ce(unexpect, 99); + CHECK(ce.error() == 99); + + expected e2(unexpect, 7); + CHECK(std::move(e2).error() == 7); + + const expected ce2(unexpect, 13); + CHECK(std::move(ce2).error() == 13); +} + +// --- expected: primary template instantiated with a reference E --- + +TEST_CASE("hardened: observers on expected", "[hardened]") { + expected e(std::in_place, "world"); + CHECK(e->size() == 5); + CHECK(*e == "world"); + + const expected ce(std::in_place, "there"); + CHECK(ce->size() == 5); + CHECK(*ce == "there"); + + expected e2(std::in_place, "moved"); + CHECK(*std::move(e2) == "moved"); +} + +TEST_CASE("hardened: error() on error-state expected", "[hardened]") { + int err = 42; + expected 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() == type_name()); + CHECK(&e.error() == &err); + + const expected ce(unexpect, err); + CHECK(type_name() == type_name()); + CHECK(&ce.error() == &err); + + CHECK(&std::move(e).error() == &err); + CHECK(&std::move(ce).error() == &err); +} + +// --- expected: T& specialization instantiated with a reference E --- + +TEST_CASE("hardened: observers on expected", "[hardened]") { + int x = 5; + expected e(x); + CHECK(*e == 5); + CHECK(&*e == &x); + + const expected ce(x); + CHECK(&*ce == &x); +} + +TEST_CASE("hardened: error() on error-state expected", "[hardened]") { + int err = 7; + expected e(unexpect, err); + CHECK(&e.error() == &err); + + const expected ce(unexpect, err); + CHECK(&ce.error() == &err); + + CHECK(&std::move(e).error() == &err); + CHECK(&std::move(ce).error() == &err); +} + +// --- expected: void specialization with a reference E --- + +TEST_CASE("hardened: operator* on value-state expected", "[hardened]") { + expected e; + *e; + + const expected ce; + *ce; +} + +TEST_CASE("hardened: error() on error-state expected", "[hardened]") { + int err = 42; + expected e(unexpect, err); + CHECK(&e.error() == &err); + + const expected 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) // From 79878eb28198d7e0f9db9c858f33384015be4679 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 18 Oct 2026 00:44:54 -0400 Subject: [PATCH 3/4] test: make the expected conversion tests convert "convert from expected" and its rvalue counterpart used expected on both sides, so they selected the copy and move constructors and duplicated the tests just above them. The converting constructors they were named for were reached only by the combined const-E test further down. Bind the destination to const int& so each one exercises the overload in its name. --- tests/beman/expected/expected_void_ref_e.test.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index f0d9a205..c49debde 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -127,9 +127,9 @@ TEST_CASE("expected: move construct copies pointer", "[expected_void_re } TEST_CASE("expected: convert from expected", "[expected_void_ref_e]") { - int err = 7; - expected src(unexpect, err); - expected dst(src); + int err = 7; + expected src(unexpect, err); + expected dst(src); REQUIRE(!dst.has_value()); CHECK(&dst.error() == &err); } @@ -138,9 +138,9 @@ TEST_CASE("expected: convert from expected", "[expected_void_ // only the const& form existed. Verify the && form works and still binds the external // referent (never dangles — G is itself a reference to the caller's object). TEST_CASE("expected: convert from expected&& binds external referent", "[expected_void_ref_e]") { - int err = 9; - expected src(unexpect, err); - expected dst(std::move(src)); + int err = 9; + expected src(unexpect, err); + expected dst(std::move(src)); REQUIRE(!dst.has_value()); CHECK(&dst.error() == &err); } From dca8992fa5ae3166869c59f8eabfa00fdf132a59 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 18 Oct 2026 00:52:38 -0400 Subject: [PATCH 4/4] test: copy-assign expected and expected with a non-trivial E The existing copy-assignment tests for both specializations use int, which selects the trivial operator, so the non-trivial one never ran its mixed-state branches: constructing the error when a valued expected takes an error, and destroying it when an errored one takes a value. Repeat those cases, and the value-value rebind for expected, with std::string. --- tests/beman/expected/expected_ref.test.cpp | 32 +++++++++++++++++++++ tests/beman/expected/expected_void.test.cpp | 18 ++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/beman/expected/expected_ref.test.cpp b/tests/beman/expected/expected_ref.test.cpp index 82664014..7d22369d 100644 --- a/tests/beman/expected/expected_ref.test.cpp +++ b/tests/beman/expected/expected_ref.test.cpp @@ -288,6 +288,38 @@ TEST_CASE("expected: copy assignment error-to-value", "[expected_ref]") { CHECK(&*a == &x); } +// With a non-trivial E the non-trivial copy assignment is selected, and the +// mixed-state cases are the ones that construct or destroy the error. +TEST_CASE("expected: copy assignment value-to-error (non-trivial E)", "[expected_ref]") { + int x = 5; + expected a(x); + const expected b(unexpect, "err"); + a = b; + REQUIRE(!a.has_value()); + CHECK(a.error() == "err"); + CHECK(b.error() == "err"); + CHECK(x == 5); +} + +TEST_CASE("expected: copy assignment error-to-value (non-trivial E)", "[expected_ref]") { + int x = 5; + expected a(unexpect, "err"); + const expected b(x); + a = b; + REQUIRE(a.has_value()); + CHECK(&*a == &x); +} + +TEST_CASE("expected: copy assignment value-value rebinds (non-trivial E)", "[expected_ref]") { + int x = 1, y = 2; + expected a(x); + const expected b(y); + a = b; + REQUIRE(a.has_value()); + CHECK(&*a == &y); + CHECK(x == 1); +} + TEST_CASE("expected: move assignment value-to-value (non-trivial E)", "[expected_ref]") { int x1 = 1, x2 = 2; expected a(x1), b(x2); diff --git a/tests/beman/expected/expected_void.test.cpp b/tests/beman/expected/expected_void.test.cpp index d910fddf..dc8dd794 100644 --- a/tests/beman/expected/expected_void.test.cpp +++ b/tests/beman/expected/expected_void.test.cpp @@ -212,6 +212,24 @@ TEST_CASE("expected: copy assign error-to-error", "[expected_void]") { CHECK(a.error() == 2); } +// With a non-trivial E the non-trivial copy assignment is selected, and the +// mixed-state cases are the ones that construct or destroy the error. +TEST_CASE("expected: copy assign error-to-value (non-trivial E)", "[expected_void]") { + expected a; + const expected b(unexpect, "err"); + a = b; + REQUIRE(!a.has_value()); + CHECK(a.error() == "err"); + CHECK(b.error() == "err"); +} + +TEST_CASE("expected: copy assign value-to-error (non-trivial E)", "[expected_void]") { + expected a(unexpect, "err"); + const expected b; + a = b; + CHECK(a.has_value()); +} + // --- Move assignment --- TEST_CASE("expected: move assignment is noexcept", "[expected_void]") {