From 331c2095cd1a84e2e19937f3534f72ae62365dc9 Mon Sep 17 00:00:00 2001 From: sentomk Date: Mon, 3 Aug 2026 15:59:58 +0800 Subject: [PATCH] test: exercise previously uncovered feature paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem - Branch coverage sat at 31.9% / line at 42.5%, with several feature paths unused by any test: lit_ci edge cases, alt subpattern paths, the full guard operator set, and the combinator operator-sugar truth table. Implementation - tests_literal_pattern: lit_ci with non-letter characters (tolower_ascii non-letter branch), size mismatch, and against std::string_view / const char* subjects (templated comparison overload). - tests_type_pattern: alt() no-subpattern fast path, alt(sub) subpattern match path, and is(sub) match+bind — covering both branches of type_is/type_alt match()/bind(). - tests_guard: the previously unused arithmetic/comparison guard operators (%, /, <=, >=, and all of != == < > on members). - tests_combinator: a full truth table for the !/||/&& operator sugar (both/one/none-true for || and &&, and ! combined with each). Tests - Full suite: 216/216 (was 204). - Local source-based coverage: lines 42.5% -> 43.2%, branches 31.9% -> 32.4%; lit.hpp 89.5% -> 94.7%. Notes - Two eval.hpp otherwise paths (unreachable_t branch and the Subject& otherwise branch) stay uncovered: they are forward- looking infrastructure with no public-API trigger today (the exhaustiveness-checking work will exercise them). optimize.hpp remains 0% as its dispatch tables are built at compile time and are not credited by runtime coverage; verifying them needs compile-time static_assert checks, not runtime tests. --- tests/tests_combinator.cpp | 27 ++++++++++++++++++ tests/tests_guard.cpp | 46 ++++++++++++++++++++++++++++++ tests/tests_literal_pattern.cpp | 37 ++++++++++++++++++++++++ tests/tests_terminal_semantics.cpp | 2 +- tests/tests_type_pattern.cpp | 44 ++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) diff --git a/tests/tests_combinator.cpp b/tests/tests_combinator.cpp index 4c472a5..1560019 100644 --- a/tests/tests_combinator.cpp +++ b/tests/tests_combinator.cpp @@ -421,3 +421,30 @@ TEST(OperatorSugar, MixedWithTypePatterns) { _ >> 0), 0); } + +// --- Operator sugar truth table: exercise every !/||/&& combination +// over a small predicate set so each operator branch is hit. --- + +TEST(OperatorSugar, FullTruthTable) { + auto pos = [](int n) { return n > 0; }; + auto even = [](int n) { return n % 2 == 0; }; + + auto run = [&](int v, auto pat) { + return ptn::match(v) | ptn::on(pat >> 1, ptn::_ >> 0); + }; + + // (a || b) : both true, one true, none true + EXPECT_EQ(run(4, ptn::pred(pos) || ptn::pred(even)), 1); + EXPECT_EQ(run(-2, ptn::pred(pos) || ptn::pred(even)), 1); + EXPECT_EQ(run(-3, ptn::pred(pos) || ptn::pred(even)), 0); + + // (a && b) : both true, one false, none true + EXPECT_EQ(run(4, ptn::pred(pos) && ptn::pred(even)), 1); + EXPECT_EQ(run(3, ptn::pred(pos) && ptn::pred(even)), 0); + EXPECT_EQ(run(-2, ptn::pred(pos) && ptn::pred(even)), 0); + + // !p combined with && / || + EXPECT_EQ(run(-1, !ptn::pred(pos) && ptn::pred(even)), 0); + EXPECT_EQ(run(-4, !ptn::pred(pos) && ptn::pred(even)), 1); + EXPECT_EQ(run(0, !ptn::pred(pos) || ptn::pred(pos)), 1); +} diff --git a/tests/tests_guard.cpp b/tests/tests_guard.cpp index 985c7ff..d6c0b8b 100644 --- a/tests/tests_guard.cpp +++ b/tests/tests_guard.cpp @@ -145,3 +145,49 @@ TEST(Guard, BlockScopeNamesSupportFiveValues) { EXPECT_EQ(result, 1); } + +// --- Guard operators not previously exercised: %, /, <=, >=. --- + +namespace { + struct Pair { + int a; + int b; + }; + PTN_BIND(Pair, a, b); +} // namespace + +TEST(GuardExpression, ModuloAndDivisionOperators) { + Pair p{9, 4}; + // a % b == 1 and a / b == 2 + auto r = ptn::match(p) + | ptn::on( + ptn::$(ptn::has<&Pair::a, &Pair::b>)[a % b == 1 + && a / b == 2] + >> 1, + ptn::_ >> 0); + EXPECT_EQ(r, 1); +} + +TEST(GuardExpression, LessEqualGreaterEqual) { + int v = 5; + auto r = ptn::match(v) + | ptn::on(ptn::$[_ <= 5 && _ >= 5] >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 1); + + int w = 6; + auto r2 = ptn::match(w) + | ptn::on(ptn::$[_ <= 5] >> 1, ptn::_ >> 0); + EXPECT_EQ(r2, 0); +} + +TEST(GuardExpression, AllComparisonOperatorsOnMembers) { + Pair p{3, 3}; + auto r = ptn::match(p) + | ptn::on( + ptn::$( + ptn::has<&Pair::a, &Pair::b>)[a != b || a == b + || a < b || a > b] + >> 1, + ptn::_ >> 0); + EXPECT_EQ(r, 1); +} diff --git a/tests/tests_literal_pattern.cpp b/tests/tests_literal_pattern.cpp index feb3c3f..5f8edea 100644 --- a/tests/tests_literal_pattern.cpp +++ b/tests/tests_literal_pattern.cpp @@ -170,3 +170,40 @@ TEST(LiteralPattern, ValSupportsFloatingPointInCpp20) { EXPECT_EQ(hit, 7); } #endif + +// --- lit_ci branch coverage: non-letter chars, size mismatch, +// and the generic convertible-to-string_view comparison path. --- + +TEST(LiteralPattern, LitCiNonLetterCharacters) { + // Exercises tolower_ascii's non-letter branch (digits/symbols). + std::string s = "ABC-123_XYZ"; + auto r = ptn::match(s) + | ptn::on(ptn::lit_ci("abc-123_xyz") >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 1); +} + +TEST(LiteralPattern, LitCiSizeMismatch) { + // Different lengths hit the early-return false in iequal_ascii. + std::string s = "ab"; + auto r = ptn::match(s) + | ptn::on(ptn::lit_ci("abc") >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 0); +} + +TEST(LiteralPattern, LitCiAgainstStringViewSubject) { + // Subject as std::string_view exercises the comparison through + // the string_view overload. + std::string_view s = "HeLLo"; + auto r = ptn::match(s) + | ptn::on(ptn::lit_ci("hello") >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 1); +} + +TEST(LiteralPattern, LitCiCharLiteralSubject) { + // const char* subject forces the templated convertible overload + // of iequal_ascii::operator(). + const char *s = "World"; + auto r = ptn::match(s) + | ptn::on(ptn::lit_ci("WORLD") >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 1); +} diff --git a/tests/tests_terminal_semantics.cpp b/tests/tests_terminal_semantics.cpp index dda90f1..fee38ea 100644 --- a/tests/tests_terminal_semantics.cpp +++ b/tests/tests_terminal_semantics.cpp @@ -45,4 +45,4 @@ TEST(TerminalSemantics, FirstMatchingCaseWins) { | ptn::on(ptn::$ >> 1, ptn::$ >> 2, ptn::_ >> 0); EXPECT_EQ(result, 1); -} +} \ No newline at end of file diff --git a/tests/tests_type_pattern.cpp b/tests/tests_type_pattern.cpp index cbaa4cb..09898e9 100644 --- a/tests/tests_type_pattern.cpp +++ b/tests/tests_type_pattern.cpp @@ -151,3 +151,47 @@ TEST(TypePattern, MixedVariantGuardedCaseWinsWhenPredicateTrue) { EXPECT_EQ(guarded_hits, 1); EXPECT_EQ(simple_hits, 0); } + +// --- alt and is: cover both the no-subpattern fast path and +// the subpattern path for match()/bind() in type.hpp. --- + +TEST(TypePattern, AltByIndexNoSubpatternMatchFastPath) { + std::variant v = std::string("x"); + // alt() with no subpattern: match() fast-path returns true; + // handler is nullary (no_subpattern binds nothing). + auto r = ptn::match(v) + | ptn::on(ptn::alt<1>() >> [] { return 1; }, ptn::_ >> 0); + EXPECT_EQ(r, 1); +} + +TEST(TypePattern, AltByIndexWithSubpattern) { + std::variant v = std::string("hi"); + // alt(sub): the subpattern match()/bind() branches. + auto r = ptn::match(v) + | ptn::on(ptn::alt<1>(ptn::pred([](const std::string &s) { + return s.size() == 2; + })) >> 1, + ptn::_ >> 0); + EXPECT_EQ(r, 1); +} + +TEST(TypePattern, AltRejectsWrongIndex) { + std::variant v = 7; + auto r = ptn::match(v) | ptn::on(ptn::alt<1>() >> 1, ptn::_ >> 0); + EXPECT_EQ(r, 0); +} + +TEST(TypePattern, IsWithSubpatternMatchAndBind) { + struct P { + int a; + int b; + }; + std::variant v = P{2, 3}; + // is(sub): exercises the subpattern branch of type_is_pattern. + auto r = ptn::match(v) + | ptn::on( + ptn::is

(ptn::$(ptn::has<&P::a, &P::b>)) >> + [](int a, int b) { return a * b; }, + ptn::_ >> 0); + EXPECT_EQ(r, 6); +}