Skip to content

test: cover the remaining unexercised members and copy-assignment branches - #101

Merged
steve-downey merged 4 commits into
bemanproject:mainfrom
steve-downey:cover-unexercised-members
Oct 18, 2026
Merged

steve-downey merged 4 commits into
bemanproject:mainfrom
steve-downey:cover-unexercised-members

Conversation

@steve-downey

Copy link
Copy Markdown
Member

Supersedes #80 and #82, rebuilt on current main. Both had fallen behind the switch from static_assert to runtime CHECKs and the conversion tests added since.

What's here

  • Remaining unexercised members (from test: cover the remaining unexercised expected members #80). Tests for the out-of-line definitions no test reached. Most are const& overloads shadowed by their && twins: const unexpected<G>& construction and assignment, converting move constructors, error_or() &&, the const& monadic overloads of expected<void, E> and expected<T&, E>, the ref-qualified error() overloads of expected<T&, E>, its unexpect_t + initializer_list constructor, and bad_expected_access<void>::what(). Also excludes BEMAN_EXPECTED_TRAP() lines from the gcovr report: reaching one kills the process before libgcov flushes, so they can't be covered by construction.
  • Reference specializations in the hardened build (from test: instantiate the reference specializations in the hardened build #82). expected_hardened.test.cpp never instantiated expected<T&, E> or any reference E, 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 the expected<void, G&> converting constructors used the same type on both sides, so they ran the copy and move constructors instead. Their destinations now bind const int&.
  • Copy assignment with a non-trivial E. The existing copy-assignment tests for expected<void, E> and expected<T&, E> all use int, which takes the trivial path, so the non-trivial operator's mixed-state branches never ran. These tests use std::string.

Where the PRs had static_asserts on types or traits, they're now CHECKs, matching the rest of the suite. The one exception asserts the initializer_list constructor under constant evaluation, which is the thing being tested.

Results

  • gcc-16: 1248/1248 tests pass (1178 on main).
  • Every instrumented line of expected.hpp (786/786) and unexpected.hpp (28/28) now runs, not counting trap lines. On main, 18 instrumented lines of expected.hpp never ran. These figures come from gcovr's JSON output, without the coveralls line counting.
  • clang-22 and clang-23 against libstdc++ 16: all pass except beman::expected type traits match std::expected. That failure is already on main, in a file this PR doesn't touch, and Keep expected trivially copyable under clang when no copy assignment is eligible #100 fixes it.

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.
@steve-downey
steve-downey merged commit bac8cd4 into bemanproject:main Oct 18, 2026
78 checks passed
@steve-downey
steve-downey deleted the cover-unexercised-members branch October 19, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant