Repository navigation
test: cover the remaining unexercised members and copy-assignment branches - #101
Merged
steve-downey merged 4 commits intoOct 18, 2026
Merged
Conversation
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<void, E>: 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<T&, E>: 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<T&, E&>: converting copy/move ctors (Derived& -> Base&,
int& -> const int&)
- expected<void, E&>: converting construction of the value state
- reference-E const unexpected<G&> lvalue ctor/assign across all three
specializations
- expected<T, E>::value() const&& on a valued expected
- bad_expected_access<void>::what(), reachable only from a derived class:
its special members are protected and every bad_expected_access<E>
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.
expected_hardened.test.cpp only ever used expected<T, E> and expected<void, E> with a value E. Consequently 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. 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<T&, E> were dead as far as the test suite was concerned. Adds happy-path coverage for expected<T&, E>, expected<T, E&>, expected<T&, E&> and expected<void, E&>, 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.
"convert from expected<void, G&>" and its rvalue counterpart used expected<void, int&> 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.
…ivial 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<T&, E>, with std::string.
This was referenced Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #80 and #82, rebuilt on current
main. Both had fallen behind the switch fromstatic_assertto runtimeCHECKs and the conversion tests added since.What's here
const&overloads shadowed by their&&twins:const unexpected<G>&construction and assignment, converting move constructors,error_or() &&, theconst&monadic overloads ofexpected<void, E>andexpected<T&, E>, the ref-qualifiederror()overloads ofexpected<T&, E>, itsunexpect_t+initializer_listconstructor, andbad_expected_access<void>::what(). Also excludesBEMAN_EXPECTED_TRAP()lines from the gcovr report: reaching one kills the process before libgcov flushes, so they can't be covered by construction.expected_hardened.test.cppnever instantiatedexpected<T&, E>or any referenceE, so 6 of the 21 trap sites were never compiled. It now covers all four reference shapes.expected<void, E&>conversion tests that didn't convert. Two tests named for theexpected<void, G&>converting constructors used the same type on both sides, so they ran the copy and move constructors instead. Their destinations now bindconst int&.E. The existing copy-assignment tests forexpected<void, E>andexpected<T&, E>all useint, which takes the trivial path, so the non-trivial operator's mixed-state branches never ran. These tests usestd::string.Where the PRs had
static_asserts on types or traits, they're nowCHECKs, matching the rest of the suite. The one exception asserts theinitializer_listconstructor under constant evaluation, which is the thing being tested.Results
main).expected.hpp(786/786) andunexpected.hpp(28/28) now runs, not counting trap lines. Onmain, 18 instrumented lines ofexpected.hppnever ran. These figures come from gcovr's JSON output, without the coveralls line counting.beman::expected type traits match std::expected. That failure is already onmain, in a file this PR doesn't touch, and Keep expected trivially copyable under clang when no copy assignment is eligible #100 fixes it.