Skip to content

Fix any() assertion combinator to correctly resolve OR semantics#598

Merged
MrAnno merged 2 commits into
Snaipe:bleedingfrom
94xhn:fix/any-assertion-or-semantics
Jul 17, 2026
Merged

Fix any() assertion combinator to correctly resolve OR semantics#598
MrAnno merged 2 commits into
Snaipe:bleedingfrom
94xhn:fix/any-assertion-or-semantics

Conversation

@94xhn

@94xhn 94xhn commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

As reported in #595, the any() assertion combinator does not actually
combine its sub-assertions with OR semantics. Instead, the result silently
collapses to the truth value of the last sub-argument:

Test(assertion, testing)
{
    cr_expect(any(eq(int, 0, 1), eq(int, 0, 2), eq(int, 3, 3))); // Passes
    cr_expect(any(eq(int, 0, 1), eq(int, 2, 2), eq(int, 0, 3))); // Fails but should pass
    cr_expect(any(eq(int, 1, 1), eq(int, 0, 2), eq(int, 0, 3))); // Fails but should pass
}

Root cause

Two separate issues in include/criterion/internal/new_asserts.h
combine to produce this behavior:

  1. CRI_ASSERT_SPECIFIER_ANY_INDIRECT splices each sub-assertion's raw
    expansion directly into Cond = Cond || <expansion>, instead of first
    assigning it to its own temporary (cri_cond_un) like the sibling
    ALL_INDIRECT/NONE_INDIRECT macros do.

  2. CRI_ASSERT_SPECIFIER_any combines the final accumulated result with
    *cri_pass = *cri_pass && cri_cond; — using && instead of the
    combinator's own || operator. For all()/none() this is
    coincidentally harmless (AND-absorption with 0 holds for an AND-chain),
    but for any() the combinator can be correctly true even when the last
    evaluated leaf is false — exactly the reported scenario — so ANDing the
    correct cri_cond with a stale residual wrongly forces the result to 0.

Both edits are needed together; applying only the first is not sufficient
to fix #595 (verified by testing).

Fix

  • CRI_ASSERT_SPECIFIER_ANY_INDIRECT: assign the sub-assertion result to
    cri_cond_un first, then Cond = Cond || cri_cond_un (mirrors
    ALL_INDIRECT).
  • CRI_ASSERT_SPECIFIER_any: change *cri_pass = *cri_pass && cri_cond;
    to *cri_pass = *cri_pass || cri_cond; (mirrors how all()/none()
    each combine using their own operator).

Testing

Verified against the real, unmodified header by compiling the macro
expansion with a minimal stub runtime (the small set of
cri_assert_node_*/criterion_*_test support functions), since the full
meson build wasn't viable in the sandbox. Before the fix, both
any(1==1, 2==2, 0==3) and any(0==1, 2==2, 0==3) (the exact shapes from
#595) incorrectly reported failure. After the fix, both correctly pass,
and all()/none() sanity cases are unaffected (no regression).

Fixes #595

94xhn and others added 2 commits July 17, 2026 21:11
any() was incorrectly combining sub-assertion results: the last
sub-argument's truth value silently overrode the accumulated OR result,
because CRI_ASSERT_SPECIFIER_ANY_INDIRECT spliced the sub-assertion's raw
expansion directly into the accumulator instead of assigning it to its own
temporary first (unlike the sibling ALL_INDIRECT/NONE_INDIRECT macros), and
CRI_ASSERT_SPECIFIER_any combined the final result with the last leaf's
residual value using && instead of the combinator's own || operator.

As a result, any(eq(int, 1, 1), eq(int, 0, 2), eq(int, 0, 3)) incorrectly
failed even though the first sub-assertion is true, matching the exact
shape reported in Snaipe#595.

Fixes Snaipe#595

@MrAnno MrAnno left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@MrAnno
MrAnno merged commit 0586ad3 into Snaipe:bleeding Jul 17, 2026
9 checks passed
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.

Any Assertion only works if the last value in the __VA_ARGS__ is true

2 participants