Added TestCaseInfoHasher and tests.

This PR tries to fix #2304 "Test case name hashing should consider tags and class name too".
The goal is to "allow test cases with same name, but different tags, or same names and tags but different class name".

See the following code example that describes what is wanted:

```cpp
Catch::StringRef className1{std::string("C1")};
Catch::NameAndTags nameAndTag1{std::string("N1"), std::string("[T1]")};

Catch::StringRef className2{std::string("C2")};
Catch::NameAndTags nameAndTag2{std::string("N2"), std::string("[T2]")};
```

this set of test cases satisfies the wanted unique test case invariant as soon as:

```
if (N1 == N2 || C1 == C2) then [T1] != [T2]
AND
if (C1 == C2 || [T1] == [T2]) then N1 != N2
```

Before this PR, the code behaves as follows:
When `TestRunOrder::Randomized` is set, `getAllTestsSorted()` does the following:
1) Use `TestHasher` hash the class name of a `TestCaseInfo` (using the FNV-1a algorithm) and push them to a vector.
2) Sort the vector by the hashes, on equality, compare name, if equal compare class name, if also equal compare tags.

After this PR, the code behaves as follows:
When `TestRunOrder::Randomized` is set, `getAllTestsSorted()` does the following:
1) Use `TestCaseInfoHasher` to generate hashes that considers class name, name and tag and push them to a vector.
2) Sorting the vector by the hashes. When the unique test case invariant described above is satisfied, the hashes will be unique.

This is my first contribution to Catch2, hope i went in the right direction and would be happy about feedback, comments, etc.

**Describe the bug**
Recently I changed the "unique test case" criteria to allow test cases with same name, but different tags, or same names and tags but different class name (when a test case hangs off a type).

However, we also hash test cases when ordering them randomly (to get subset-stable ordering), and the hashing currently only considers test case name, which means that hash collisions are now much more realistic (in fact the collision is guaranteed if two test cases have same name, which is a desired property by some users).

_Note that this doesn't break the subset invariant, because we use the ordering of test case infos to break ties, which handles tags and class name properly_

**Expected behavior**
To fix this, the hasher should also consider tags and class name.

**Additional context**
This change also makes the hasher complex enough that it should be promoted to a header and receive direct unit tests.

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 78d95956dd..16efa3fe18 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -157,6 +157,7 @@ set(INTERNAL_HEADERS
     ${SOURCES_DIR}/internal/catch_wildcard_pattern.hpp
     ${SOURCES_DIR}/internal/catch_windows_h_proxy.hpp
     ${SOURCES_DIR}/internal/catch_xmlwriter.hpp
+    ${SOURCES_DIR}/internal/catch_test_case_info_hasher.hpp
 )
 set(IMPL_SOURCES
     ${SOURCES_DIR}/catch_approx.cpp
@@ -213,6 +214,7 @@ set(IMPL_SOURCES
     ${SOURCES_DIR}/catch_version.cpp
     ${SOURCES_DIR}/internal/catch_wildcard_pattern.cpp
     ${SOURCES_DIR}/internal/catch_xmlwriter.cpp
+    ${SOURCES_DIR}/internal/catch_test_case_info_hasher.cpp
 )
 set(INTERNAL_FILES ${IMPL_SOURCES} ${INTERNAL_HEADERS})

diff --git a/src/catch2/catch_all.hpp b/src/catch2/catch_all.hpp
index 92cdc205da..656417f485 100644
--- a/src/catch2/catch_all.hpp
+++ b/src/catch2/catch_all.hpp
@@ -95,6 +95,7 @@
 #include <catch2/internal/catch_stringref.hpp>
 #include <catch2/internal/catch_tag_alias_registry.hpp>
 #include <catch2/internal/catch_template_test_registry.hpp>
+#include <catch2/internal/catch_test_case_info_hasher.hpp>
 #include <catch2/internal/catch_test_case_registry_impl.hpp>
 #include <catch2/internal/catch_test_case_tracker.hpp>
 #include <catch2/internal/catch_test_failure_exception.hpp>

diff --git a/src/catch2/internal/catch_test_case_info_hasher.cpp b/src/catch2/internal/catch_test_case_info_hasher.cpp
new file mode 100644
index 0000000000..75acc978bc
--- /dev/null
+++ b/src/catch2/internal/catch_test_case_info_hasher.cpp
@@ -0,0 +1,31 @@
+#include <catch2/catch_test_case_info.hpp>
+#include <catch2/internal/catch_test_case_info_hasher.hpp>
+
+namespace Catch {
+    TestCaseInfoHasher::TestCaseInfoHasher( hash_t seed ): m_seed( seed ) {}
+
+    uint32_t TestCaseInfoHasher::operator()( TestCaseInfo const& t ) const {
+        // FNV-1a hash algorithm that is designed for uniqueness:
+        const hash_t prime = 1099511628211u;
+        hash_t hash = 14695981039346656037u;
+        for ( const char c : t.name ) {
+            hash ^= c;
+            hash *= prime;
+        }
+        for ( const char c : t.className ) {
+            hash ^= c;
+            hash *= prime;
+        }
+        for ( const Tag& tag : t.tags ) {
+            for ( const char c : tag.original ) {
+                hash ^= c;
+                hash *= prime;
+            }
+        }
+        hash ^= m_seed;
+        hash *= prime;
+        const uint32_t low{ static_cast<uint32_t>( hash ) };
+        const uint32_t high{ static_cast<uint32_t>( hash >> 32 ) };
+        return low * high;
+    }
+} // namespace Catch
diff --git a/src/catch2/internal/catch_test_case_info_hasher.hpp b/src/catch2/internal/catch_test_case_info_hasher.hpp
new file mode 100644
index 0000000000..954bdcdebd
--- /dev/null
+++ b/src/catch2/internal/catch_test_case_info_hasher.hpp
@@ -0,0 +1,22 @@
+#ifndef CATCH_TEST_CASE_INFO_HASHER_HPP_INCLUDED
+#define CATCH_TEST_CASE_INFO_HASHER_HPP_INCLUDED
+
+#include <cstdint>
+
+namespace Catch {
+
+    struct TestCaseInfo;
+
+    class TestCaseInfoHasher {
+    public:
+        using hash_t = std::uint64_t;
+        TestCaseInfoHasher( hash_t seed );
+        uint32_t operator()( TestCaseInfo const& t ) const;
+
+    private:
+        hash_t m_seed;
+    };
+
+} // namespace Catch
+
+#endif /* CATCH_TEST_CASE_INFO_HASHER_HPP_INCLUDED */
diff --git a/src/catch2/internal/catch_test_case_registry_impl.cpp b/src/catch2/internal/catch_test_case_registry_impl.cpp
index 3c1ab54b2a..6c491a959f 100644
--- a/src/catch2/internal/catch_test_case_registry_impl.cpp
+++ b/src/catch2/internal/catch_test_case_registry_impl.cpp
@@ -16,38 +16,13 @@
 #include <catch2/catch_test_case_info.hpp>
 #include <catch2/catch_test_spec.hpp>
 #include <catch2/internal/catch_move_and_forward.hpp>
+#include <catch2/internal/catch_test_case_info_hasher.hpp>

 #include <algorithm>
 #include <set>

 namespace Catch {

-namespace {
-    struct TestHasher {
-        using hash_t = uint64_t;
-
-        explicit TestHasher( hash_t hashSuffix ):
-            m_hashSuffix( hashSuffix ) {}
-
-        uint64_t m_hashSuffix;
-
-        uint32_t operator()( TestCaseInfo const& t ) const {
-            // FNV-1a hash with multiplication fold.
-            const hash_t prime = 1099511628211u;
-            hash_t hash = 14695981039346656037u;
-            for (const char c : t.name) {
-                hash ^= c;
-                hash *= prime;
-            }
-            hash ^= m_hashSuffix;
-            hash *= prime;
-            const uint32_t low{ static_cast<uint32_t>(hash) };
-            const uint32_t high{ static_cast<uint32_t>(hash >> 32) };
-            return low * high;
-        }
-    };
-} // end anonymous namespace
-
     std::vector<TestCaseHandle> sortTests( IConfig const& config, std::vector<TestCaseHandle> const& unsortedTestCases ) {
         switch (config.runOrder()) {
         case TestRunOrder::Declared:
@@ -66,9 +41,9 @@ namespace {
         }
         case TestRunOrder::Randomized: {
             seedRng(config);
-            using TestWithHash = std::pair<TestHasher::hash_t, TestCaseHandle>;
+            using TestWithHash = std::pair<TestCaseInfoHasher::hash_t, TestCaseHandle>;

-            TestHasher h{ config.rngSeed() };
+            TestCaseInfoHasher h{ config.rngSeed() };
             std::vector<TestWithHash> indexed_tests;
             indexed_tests.reserve(unsortedTestCases.size());

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ef1b13eae9..1c52012038 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -88,6 +88,7 @@ set(TEST_SOURCES
         ${SELF_TEST_DIR}/IntrospectiveTests/RandomNumberGeneration.tests.cpp
         ${SELF_TEST_DIR}/IntrospectiveTests/Reporters.tests.cpp
         ${SELF_TEST_DIR}/IntrospectiveTests/Tag.tests.cpp
+        ${SELF_TEST_DIR}/IntrospectiveTests/TestCaseInfoHasher.tests.cpp
         ${SELF_TEST_DIR}/IntrospectiveTests/TestSpecParser.tests.cpp
         ${SELF_TEST_DIR}/IntrospectiveTests/TextFlow.tests.cpp
         ${SELF_TEST_DIR}/IntrospectiveTests/Sharding.tests.cpp
diff --git a/tests/SelfTest/Baselines/automake.sw.approved.txt b/tests/SelfTest/Baselines/automake.sw.approved.txt
index fadb274770..9df8be0df1 100644
--- a/tests/SelfTest/Baselines/automake.sw.approved.txt
+++ b/tests/SelfTest/Baselines/automake.sw.approved.txt
@@ -255,6 +255,8 @@ Message from section two
 :test-result: PASS Test case with one argument
 :test-result: PASS Test enum bit values
 :test-result: PASS Test with special, characters "in name
+:test-result: PASS TestCaseInfoHasher produces different hashes.
+:test-result: PASS TestCaseInfoHasher produces equal hashes.
 :test-result: PASS Testing checked-if
 :test-result: XFAIL Testing checked-if 2
 :test-result: XFAIL Testing checked-if 3
diff --git a/tests/SelfTest/Baselines/automake.sw.multi.approved.txt b/tests/SelfTest/Baselines/automake.sw.multi.approved.txt
index f05de0ab8c..10db54cd22 100644
--- a/tests/SelfTest/Baselines/automake.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/automake.sw.multi.approved.txt
@@ -248,6 +248,8 @@
 :test-result: PASS Test case with one argument
 :test-result: PASS Test enum bit values
 :test-result: PASS Test with special, characters "in name
+:test-result: PASS TestCaseInfoHasher produces different hashes.
+:test-result: PASS TestCaseInfoHasher produces equal hashes.
 :test-result: PASS Testing checked-if
 :test-result: XFAIL Testing checked-if 2
 :test-result: XFAIL Testing checked-if 3
diff --git a/tests/SelfTest/Baselines/compact.sw.approved.txt b/tests/SelfTest/Baselines/compact.sw.approved.txt
index 4688a2289f..8b404c58d5 100644
--- a/tests/SelfTest/Baselines/compact.sw.approved.txt
+++ b/tests/SelfTest/Baselines/compact.sw.approved.txt
@@ -1868,6 +1868,21 @@ Tag.tests.cpp:<line number>: passed: testCase.tags[0] == Tag( "tag1" ) for: {?}
 VariadicMacros.tests.cpp:<line number>: passed: with 1 message: 'no assertions'
 Tricky.tests.cpp:<line number>: passed: 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 CmdLine.tests.cpp:<line number>: passed:
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>)
+!=
+3472848544 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 869111496 (0x<hex digits>)
+!=
+2870097333 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 1172537240 (0x<hex digits>)
+!=
+1403724645 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: h1(testCase1) != h2(testCase2) for: 1836497244 (0x<hex digits>)
+!=
+430288597 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>)
+==
+764519552 (0x<hex digits>)
 Misc.tests.cpp:<line number>: passed: true
 Misc.tests.cpp:<line number>: passed:
 Misc.tests.cpp:<line number>: failed - but was ok: false
diff --git a/tests/SelfTest/Baselines/compact.sw.multi.approved.txt b/tests/SelfTest/Baselines/compact.sw.multi.approved.txt
index c571b0c9c8..3b3e1eef40 100644
--- a/tests/SelfTest/Baselines/compact.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/compact.sw.multi.approved.txt
@@ -1861,6 +1861,21 @@ Tag.tests.cpp:<line number>: passed: testCase.tags[0] == Tag( "tag1" ) for: {?}
 VariadicMacros.tests.cpp:<line number>: passed: with 1 message: 'no assertions'
 Tricky.tests.cpp:<line number>: passed: 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 CmdLine.tests.cpp:<line number>: passed:
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>)
+!=
+3472848544 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 869111496 (0x<hex digits>)
+!=
+2870097333 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 1172537240 (0x<hex digits>)
+!=
+1403724645 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: h1(testCase1) != h2(testCase2) for: 1836497244 (0x<hex digits>)
+!=
+430288597 (0x<hex digits>)
+TestCaseInfoHasher.tests.cpp:<line number>: passed: hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>)
+==
+764519552 (0x<hex digits>)
 Misc.tests.cpp:<line number>: passed: true
 Misc.tests.cpp:<line number>: passed:
 Misc.tests.cpp:<line number>: failed - but was ok: false
diff --git a/tests/SelfTest/Baselines/console.std.approved.txt b/tests/SelfTest/Baselines/console.std.approved.txt
index ca0a77e599..61b1a11230 100644
--- a/tests/SelfTest/Baselines/console.std.approved.txt
+++ b/tests/SelfTest/Baselines/console.std.approved.txt
@@ -1395,6 +1395,6 @@ due to unexpected exception with message:
   Why would you throw a std::string?

 ===============================================================================
-test cases:  386 |  310 passed |  69 failed |  7 failed as expected
-assertions: 2219 | 2064 passed | 128 failed | 27 failed as expected
+test cases:  388 |  312 passed |  69 failed |  7 failed as expected
+assertions: 2224 | 2069 passed | 128 failed | 27 failed as expected

diff --git a/tests/SelfTest/Baselines/console.sw.approved.txt b/tests/SelfTest/Baselines/console.sw.approved.txt
index 5655f50ab7..148a3b5388 100644
--- a/tests/SelfTest/Baselines/console.sw.approved.txt
+++ b/tests/SelfTest/Baselines/console.sw.approved.txt
@@ -13286,6 +13286,76 @@ CmdLine.tests.cpp:<line number>

 CmdLine.tests.cpp:<line number>: PASSED:

+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names are equal, names are equal but tags are different.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  764519552 (0x<hex digits>)
+  !=
+  3472848544 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names are equal, tags are equal but names are different
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  869111496 (0x<hex digits>)
+  !=
+  2870097333 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  names are equal, tags are equal but class names are different
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  1172537240 (0x<hex digits>)
+  !=
+  1403724645 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names and names and tags are equal but hashers are seeded differently.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( h1(testCase1) != h2(testCase2) )
+with expansion:
+  1836497244 (0x<hex digits>)
+  !=
+  430288597 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces equal hashes.
+  class names and names and tags are equal.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) )
+with expansion:
+  764519552 (0x<hex digits>)
+  ==
+  764519552 (0x<hex digits>)
+
 -------------------------------------------------------------------------------
 Testing checked-if
 -------------------------------------------------------------------------------
@@ -17871,6 +17941,6 @@ Misc.tests.cpp:<line number>
 Misc.tests.cpp:<line number>: PASSED:

 ===============================================================================
-test cases:  386 |  296 passed |  83 failed |  7 failed as expected
-assertions: 2234 | 2064 passed | 143 failed | 27 failed as expected
+test cases:  388 |  298 passed |  83 failed |  7 failed as expected
+assertions: 2239 | 2069 passed | 143 failed | 27 failed as expected

diff --git a/tests/SelfTest/Baselines/console.sw.multi.approved.txt b/tests/SelfTest/Baselines/console.sw.multi.approved.txt
index 32fa068957..412303da9b 100644
--- a/tests/SelfTest/Baselines/console.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/console.sw.multi.approved.txt
@@ -13279,6 +13279,76 @@ CmdLine.tests.cpp:<line number>

 CmdLine.tests.cpp:<line number>: PASSED:

+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names are equal, names are equal but tags are different.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  764519552 (0x<hex digits>)
+  !=
+  3472848544 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names are equal, tags are equal but names are different
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  869111496 (0x<hex digits>)
+  !=
+  2870097333 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  names are equal, tags are equal but class names are different
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) )
+with expansion:
+  1172537240 (0x<hex digits>)
+  !=
+  1403724645 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces different hashes.
+  class names and names and tags are equal but hashers are seeded differently.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( h1(testCase1) != h2(testCase2) )
+with expansion:
+  1836497244 (0x<hex digits>)
+  !=
+  430288597 (0x<hex digits>)
+
+-------------------------------------------------------------------------------
+TestCaseInfoHasher produces equal hashes.
+  class names and names and tags are equal.
+-------------------------------------------------------------------------------
+TestCaseInfoHasher.tests.cpp:<line number>
+...............................................................................
+
+TestCaseInfoHasher.tests.cpp:<line number>: PASSED:
+  CHECK( hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) )
+with expansion:
+  764519552 (0x<hex digits>)
+  ==
+  764519552 (0x<hex digits>)
+
 -------------------------------------------------------------------------------
 Testing checked-if
 -------------------------------------------------------------------------------
@@ -17863,6 +17933,6 @@ Misc.tests.cpp:<line number>
 Misc.tests.cpp:<line number>: PASSED:

 ===============================================================================
-test cases:  386 |  296 passed |  83 failed |  7 failed as expected
-assertions: 2234 | 2064 passed | 143 failed | 27 failed as expected
+test cases:  388 |  298 passed |  83 failed |  7 failed as expected
+assertions: 2239 | 2069 passed | 143 failed | 27 failed as expected

diff --git a/tests/SelfTest/Baselines/junit.sw.approved.txt b/tests/SelfTest/Baselines/junit.sw.approved.txt
index 3a099b4f84..7c82e5b3a9 100644
--- a/tests/SelfTest/Baselines/junit.sw.approved.txt
+++ b/tests/SelfTest/Baselines/junit.sw.approved.txt
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <testsuitesloose text artifact
 >
-  <testsuite name="<exe-name>" errors="17" failures="126" tests="2234" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
+  <testsuite name="<exe-name>" errors="17" failures="126" tests="2239" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
     <properties>
       <property name="random-seed" value="1"/>
       <property name="filters" value="~[!nonportable]~[!benchmark]~[approvals] *"/>
@@ -1355,6 +1355,11 @@ Misc.tests.cpp:<line number>
     <testcase classname="<exe-name>.global" name="Test case with one argument" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test enum bit values" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test with special, characters "in name" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names are equal, names are equal but tags are different." time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names are equal, tags are equal but names are different" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./names are equal, tags are equal but class names are different" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names and names and tags are equal but hashers are seeded differently." time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces equal hashes./class names and names and tags are equal." time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Testing checked-if" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Testing checked-if 2" time="{duration}" status="run">
       <skipped message="TEST_CASE tagged with !mayfail"/>
diff --git a/tests/SelfTest/Baselines/junit.sw.multi.approved.txt b/tests/SelfTest/Baselines/junit.sw.multi.approved.txt
index 527f7ba0d6..d850f16a42 100644
--- a/tests/SelfTest/Baselines/junit.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/junit.sw.multi.approved.txt
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <testsuites>
-  <testsuite name="<exe-name>" errors="17" failures="126" tests="2234" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
+  <testsuite name="<exe-name>" errors="17" failures="126" tests="2239" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
     <properties>
       <property name="random-seed" value="1"/>
       <property name="filters" value="~[!nonportable]~[!benchmark]~[approvals] *"/>
@@ -1354,6 +1354,11 @@ Misc.tests.cpp:<line number>
     <testcase classname="<exe-name>.global" name="Test case with one argument" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test enum bit values" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test with special, characters "in name" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names are equal, names are equal but tags are different." time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names are equal, tags are equal but names are different" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./names are equal, tags are equal but class names are different" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces different hashes./class names and names and tags are equal but hashers are seeded differently." time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="TestCaseInfoHasher produces equal hashes./class names and names and tags are equal." time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Testing checked-if" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Testing checked-if 2" time="{duration}" status="run">
       <skipped message="TEST_CASE tagged with !mayfail"/>
diff --git a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
index 92db65df57..1a9d215df4 100644
--- a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
+++ b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
@@ -273,6 +273,13 @@
     <testCase name="shortened hide tags are split apart" duration="{duration}"/>
     <testCase name="tags with dots in later positions are not parsed as hidden" duration="{duration}"/>
   </file>
+  <file path="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp">
+    <testCase name="TestCaseInfoHasher produces different hashes./class names are equal, names are equal but tags are different." duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./class names are equal, tags are equal but names are different" duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./names are equal, tags are equal but class names are different" duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./class names and names and tags are equal but hashers are seeded differently." duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces equal hashes./class names and names and tags are equal." duration="{duration}"/>
+  </file>
   <file path="tests/<exe-name>/IntrospectiveTests/TestSpecParser.tests.cpp">
     <testCase name="Parsed tags are matched case insensitive" duration="{duration}"/>
     <testCase name="Parsing tags with non-alphabetical characters is pass-through" duration="{duration}"/>
diff --git a/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt
index c9074bf03b..cbefb06363 100644
--- a/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/sonarqube.sw.multi.approved.txt
@@ -272,6 +272,13 @@
     <testCase name="shortened hide tags are split apart" duration="{duration}"/>
     <testCase name="tags with dots in later positions are not parsed as hidden" duration="{duration}"/>
   </file>
+  <file path="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp">
+    <testCase name="TestCaseInfoHasher produces different hashes./class names are equal, names are equal but tags are different." duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./class names are equal, tags are equal but names are different" duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./names are equal, tags are equal but class names are different" duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces different hashes./class names and names and tags are equal but hashers are seeded differently." duration="{duration}"/>
+    <testCase name="TestCaseInfoHasher produces equal hashes./class names and names and tags are equal." duration="{duration}"/>
+  </file>
   <file path="tests/<exe-name>/IntrospectiveTests/TestSpecParser.tests.cpp">
     <testCase name="Parsed tags are matched case insensitive" duration="{duration}"/>
     <testCase name="Parsing tags with non-alphabetical characters is pass-through" duration="{duration}"/>
diff --git a/tests/SelfTest/Baselines/tap.sw.approved.txt b/tests/SelfTest/Baselines/tap.sw.approved.txt
index 456e80eedf..67ceddfcd9 100644
--- a/tests/SelfTest/Baselines/tap.sw.approved.txt
+++ b/tests/SelfTest/Baselines/tap.sw.approved.txt
@@ -3297,6 +3297,16 @@ ok {test-number} - with 1 message: 'no assertions'
 ok {test-number} - 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 # Test with special, characters "in name
 ok {test-number} -
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>) != 3472848544 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 869111496 (0x<hex digits>) != 2870097333 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 1172537240 (0x<hex digits>) != 1403724645 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - h1(testCase1) != h2(testCase2) for: 1836497244 (0x<hex digits>) != 430288597 (0x<hex digits>)
+# TestCaseInfoHasher produces equal hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>) == 764519552 (0x<hex digits>)
 # Testing checked-if
 ok {test-number} - true
 # Testing checked-if
@@ -4470,5 +4480,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0
 ok {test-number} -
 # xmlentitycheck
 ok {test-number} -
-1..2234
+1..2239

diff --git a/tests/SelfTest/Baselines/tap.sw.multi.approved.txt b/tests/SelfTest/Baselines/tap.sw.multi.approved.txt
index 534005dffc..d369326e51 100644
--- a/tests/SelfTest/Baselines/tap.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/tap.sw.multi.approved.txt
@@ -3290,6 +3290,16 @@ ok {test-number} - with 1 message: 'no assertions'
 ok {test-number} - 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 # Test with special, characters "in name
 ok {test-number} -
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>) != 3472848544 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 869111496 (0x<hex digits>) != 2870097333 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2) for: 1172537240 (0x<hex digits>) != 1403724645 (0x<hex digits>)
+# TestCaseInfoHasher produces different hashes.
+ok {test-number} - h1(testCase1) != h2(testCase2) for: 1836497244 (0x<hex digits>) != 430288597 (0x<hex digits>)
+# TestCaseInfoHasher produces equal hashes.
+ok {test-number} - hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2) for: 764519552 (0x<hex digits>) == 764519552 (0x<hex digits>)
 # Testing checked-if
 ok {test-number} - true
 # Testing checked-if
@@ -4462,5 +4472,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0
 ok {test-number} -
 # xmlentitycheck
 ok {test-number} -
-1..2234
+1..2239

diff --git a/tests/SelfTest/Baselines/teamcity.sw.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.approved.txt
index 43324e060f..d9b98f709b 100644
--- a/tests/SelfTest/Baselines/teamcity.sw.approved.txt
+++ b/tests/SelfTest/Baselines/teamcity.sw.approved.txt
@@ -615,6 +615,10 @@ Misc.tests.cpp:<line number>|nexpression failed|n  CHECK( s1 == s2 )|nwith expan
 ##teamcity[testFinished name='Test enum bit values' duration="{duration}"]
 ##teamcity[testStarted name='Test with special, characters "in name']
 ##teamcity[testFinished name='Test with special, characters "in name' duration="{duration}"]
+##teamcity[testStarted name='TestCaseInfoHasher produces different hashes.']
+##teamcity[testFinished name='TestCaseInfoHasher produces different hashes.' duration="{duration}"]
+##teamcity[testStarted name='TestCaseInfoHasher produces equal hashes.']
+##teamcity[testFinished name='TestCaseInfoHasher produces equal hashes.' duration="{duration}"]
 ##teamcity[testStarted name='Testing checked-if']
 ##teamcity[testFinished name='Testing checked-if' duration="{duration}"]
 ##teamcity[testStarted name='Testing checked-if 2']
diff --git a/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt
index 3bc028a404..ffdae89ceb 100644
--- a/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/teamcity.sw.multi.approved.txt
@@ -615,6 +615,10 @@ Misc.tests.cpp:<line number>|nexpression failed|n  CHECK( s1 == s2 )|nwith expan
 ##teamcity[testFinished name='Test enum bit values' duration="{duration}"]
 ##teamcity[testStarted name='Test with special, characters "in name']
 ##teamcity[testFinished name='Test with special, characters "in name' duration="{duration}"]
+##teamcity[testStarted name='TestCaseInfoHasher produces different hashes.']
+##teamcity[testFinished name='TestCaseInfoHasher produces different hashes.' duration="{duration}"]
+##teamcity[testStarted name='TestCaseInfoHasher produces equal hashes.']
+##teamcity[testFinished name='TestCaseInfoHasher produces equal hashes.' duration="{duration}"]
 ##teamcity[testStarted name='Testing checked-if']
 ##teamcity[testFinished name='Testing checked-if' duration="{duration}"]
 ##teamcity[testStarted name='Testing checked-if 2']
diff --git a/tests/SelfTest/Baselines/xml.sw.approved.txt b/tests/SelfTest/Baselines/xml.sw.approved.txt
index f9518c3d0c..4facaec2a9 100644
--- a/tests/SelfTest/Baselines/xml.sw.approved.txt
+++ b/tests/SelfTest/Baselines/xml.sw.approved.txt
@@ -15579,6 +15579,77 @@ Message from section two
   <TestCase name="Test with special, characters "in name" tags="[cli][regression]" filename="tests/<exe-name>/IntrospectiveTests/CmdLine.tests.cpp" >
     <OverallResult success="true"/>
   </TestCase>
+  <TestCase name="TestCaseInfoHasher produces different hashes." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+    <Section name="class names are equal, names are equal but tags are different." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          764519552 (0x<hex digits>)
+!=
+3472848544 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="class names are equal, tags are equal but names are different" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          869111496 (0x<hex digits>)
+!=
+2870097333 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="names are equal, tags are equal but class names are different" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          1172537240 (0x<hex digits>)
+!=
+1403724645 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="class names and names and tags are equal but hashers are seeded differently." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          h1(testCase1) != h2(testCase2)
+        </Original>
+        <Expanded>
+          1836497244 (0x<hex digits>)
+!=
+430288597 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <OverallResult success="true"/>
+  </TestCase>
+  <TestCase name="TestCaseInfoHasher produces equal hashes." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+    <Section name="class names and names and tags are equal." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          764519552 (0x<hex digits>)
+==
+764519552 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <OverallResult success="true"/>
+  </TestCase>
   <TestCase name="Testing checked-if" tags="[checked-if]" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
     <Expression success="true" type="CHECKED_IF" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
       <Original>
@@ -20991,6 +21062,6 @@ loose text artifact
     </Section>
     <OverallResult success="true"/>
   </TestCase>
-  <OverallResults successes="2064" failures="143" expectedFailures="27"/>
-  <OverallResultsCases successes="296" failures="83" expectedFailures="7"/>
+  <OverallResults successes="2069" failures="143" expectedFailures="27"/>
+  <OverallResultsCases successes="298" failures="83" expectedFailures="7"/>
 </Catch2TestRun>
diff --git a/tests/SelfTest/Baselines/xml.sw.multi.approved.txt b/tests/SelfTest/Baselines/xml.sw.multi.approved.txt
index 6617afe324..f6b172f590 100644
--- a/tests/SelfTest/Baselines/xml.sw.multi.approved.txt
+++ b/tests/SelfTest/Baselines/xml.sw.multi.approved.txt
@@ -15579,6 +15579,77 @@ Message from section two
   <TestCase name="Test with special, characters "in name" tags="[cli][regression]" filename="tests/<exe-name>/IntrospectiveTests/CmdLine.tests.cpp" >
     <OverallResult success="true"/>
   </TestCase>
+  <TestCase name="TestCaseInfoHasher produces different hashes." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+    <Section name="class names are equal, names are equal but tags are different." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          764519552 (0x<hex digits>)
+!=
+3472848544 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="class names are equal, tags are equal but names are different" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          869111496 (0x<hex digits>)
+!=
+2870097333 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="names are equal, tags are equal but class names are different" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          1172537240 (0x<hex digits>)
+!=
+1403724645 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <Section name="class names and names and tags are equal but hashers are seeded differently." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          h1(testCase1) != h2(testCase2)
+        </Original>
+        <Expanded>
+          1836497244 (0x<hex digits>)
+!=
+430288597 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <OverallResult success="true"/>
+  </TestCase>
+  <TestCase name="TestCaseInfoHasher produces equal hashes." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+    <Section name="class names and names and tags are equal." filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+      <Expression success="true" type="CHECK" filename="tests/<exe-name>/IntrospectiveTests/TestCaseInfoHasher.tests.cpp" >
+        <Original>
+          hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2)
+        </Original>
+        <Expanded>
+          764519552 (0x<hex digits>)
+==
+764519552 (0x<hex digits>)
+        </Expanded>
+      </Expression>
+      <OverallResults successes="1" failures="0" expectedFailures="0"/>
+    </Section>
+    <OverallResult success="true"/>
+  </TestCase>
   <TestCase name="Testing checked-if" tags="[checked-if]" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
     <Expression success="true" type="CHECKED_IF" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
       <Original>
@@ -20990,6 +21061,6 @@ There is no extra whitespace here
     </Section>
     <OverallResult success="true"/>
   </TestCase>
-  <OverallResults successes="2064" failures="143" expectedFailures="27"/>
-  <OverallResultsCases successes="296" failures="83" expectedFailures="7"/>
+  <OverallResults successes="2069" failures="143" expectedFailures="27"/>
+  <OverallResultsCases successes="298" failures="83" expectedFailures="7"/>
 </Catch2TestRun>
diff --git a/tests/SelfTest/IntrospectiveTests/TestCaseInfoHasher.tests.cpp b/tests/SelfTest/IntrospectiveTests/TestCaseInfoHasher.tests.cpp
new file mode 100644
index 0000000000..450073601c
--- /dev/null
+++ b/tests/SelfTest/IntrospectiveTests/TestCaseInfoHasher.tests.cpp
@@ -0,0 +1,51 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/catch_test_case_info.hpp>
+#include <catch2/internal/catch_test_case_info_hasher.hpp>
+
+static constexpr Catch::SourceLineInfo dummySourceLineInfo = CATCH_INTERNAL_LINEINFO;
+
+TEST_CASE( "TestCaseInfoHasher produces equal hashes." ) {
+    SECTION( "class names and names and tags are equal." ) {
+        Catch::TestCaseInfo testCase1("", {"name", "[.magic-tag1]"}, dummySourceLineInfo);
+        Catch::TestCaseInfo testCase2("", {"name", "[.magic-tag1]"}, dummySourceLineInfo);
+
+        Catch::TestCaseInfoHasher hasherWithCustomSeed(123456789u);
+        CHECK(hasherWithCustomSeed(testCase1) == hasherWithCustomSeed(testCase2));
+    }
+}
+
+TEST_CASE( "TestCaseInfoHasher produces different hashes." ) {
+    SECTION( "class names are equal, names are equal but tags are different." ) {
+        Catch::TestCaseInfo testCase1("", {"name", "[.magic-tag1]"}, dummySourceLineInfo);
+        Catch::TestCaseInfo testCase2("", {"name", "[.magic-tag2]"}, dummySourceLineInfo);
+
+        Catch::TestCaseInfoHasher hasherWithCustomSeed(123456789u);
+        CHECK(hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2));
+    }
+
+    SECTION( "class names are equal, tags are equal but names are different" ) {
+        Catch::TestCaseInfo testCase1("", {"name1", "[.magic-tag]"}, dummySourceLineInfo);
+        Catch::TestCaseInfo testCase2("", {"name2", "[.magic-tag]"}, dummySourceLineInfo);
+
+        Catch::TestCaseInfoHasher hasherWithCustomSeed(123456789u);
+        CHECK(hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2));
+    }
+
+    SECTION( "names are equal, tags are equal but class names are different" ) {
+        Catch::TestCaseInfo testCase1("class1", {"name", "[.magic-tag]"}, dummySourceLineInfo);
+        Catch::TestCaseInfo testCase2("class2", {"name", "[.magic-tag]"}, dummySourceLineInfo);
+
+        Catch::TestCaseInfoHasher hasherWithCustomSeed(123456789u);
+        CHECK(hasherWithCustomSeed(testCase1) != hasherWithCustomSeed(testCase2));
+    }
+
+    SECTION( "class names and names and tags are equal but hashers are seeded differently." ) {
+        Catch::TestCaseInfo testCase1("", {"name", "[.magic-tag1]"}, dummySourceLineInfo);
+        Catch::TestCaseInfo testCase2("", {"name", "[.magic-tag1]"}, dummySourceLineInfo);
+
+        Catch::TestCaseInfoHasher h1(14695981039346656037u);
+        Catch::TestCaseInfoHasher h2(14695981039346656038u);
+
+        CHECK(h1(testCase1) != h2(testCase2));
+    }
+}


Make Approx::operator() const to fix #2273 for 2.x

## Description
Backport @horenmar's fix for 2.x branch

## GitHub Issues
#2273

Approx::operator() not const-correct

**Describe the bug**

The `Approx` type has an overload of `template <typename T, ...SFINAE...> Approx operator()(T const&)` which (correct me if I'm wrong) is meant to be a factory function for instances that have the same epsilon, margin, and scale, but that use the passed value.

AFAICT this should be const on the instance, but it's not.

Minimum failing example:
```C++
#include <catch2/catch.hpp>

TEST_CASE("Approx factory is const-correct") {
  // Set up a template Approx with problem-specific margin, etc.
  Approx const apprx = Approx(0.0).margin(1e-6);
  double value = 1.0;
  // Use template in assertions
  REQUIRE(value == apprx(1.0));
}
```

**Expected behavior**
Above test compiles, runs and passes.

**Reproduction steps**
See above.

**Platform information:**
 - OS: RHEL 8
 - Compiler+version: GCC 8.2.0
 - Catch version: 2.13.6

**Additional context**
Add any other context about the problem here.

diff --git a/include/internal/catch_approx.h b/include/internal/catch_approx.h
index 4522e5ad70..2d12efe406 100644
--- a/include/internal/catch_approx.h
+++ b/include/internal/catch_approx.h
@@ -33,7 +33,7 @@ namespace Detail {
         Approx operator-() const;

         template <typename T, typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
-        Approx operator()( T const& value ) {
+        Approx operator()( T const& value ) const {
             Approx approx( static_cast<double>(value) );
             approx.m_epsilon = m_epsilon;
             approx.m_margin = m_margin;

diff --git a/projects/SelfTest/UsageTests/Approx.tests.cpp b/projects/SelfTest/UsageTests/Approx.tests.cpp
index 4029223a2b..6280599ef7 100644
--- a/projects/SelfTest/UsageTests/Approx.tests.cpp
+++ b/projects/SelfTest/UsageTests/Approx.tests.cpp
@@ -212,4 +212,11 @@ TEST_CASE( "Comparison with explicitly convertible types", "[Approx]" )

 }

+TEST_CASE("Approx::operator() is const correct", "[Approx][.approvals]") {
+  const Approx ap = Approx(0.0).margin(0.01);
+
+  // As long as this compiles, the test should be considered passing
+  REQUIRE(1.0 == ap(1.0));
+}
+
 }} // namespace ApproxTests


Suppress failure of CHECKED_IF and CHECKED_ELSE

Resolves #1390

Make CHECKED_IF and CHECKED_ELSE "ok to fail"

## Description
Both `CHECKED_IF` and `CHECKED_ELSE` are currently fairly obscure macros that simplify using `if`/`else` in tests.

However, entering the `else` branch fails the test in which it occurs, because they are not marked as being ok to fail (tagged with `Catch::ResultDisposition::SuppressFail` to be exact). This behaviour makes them less than useful, but with a change they should be actually usable.

Milestone 3.0, because it is a theoretically breaking change.

diff --git a/docs/deprecations.md b/docs/deprecations.md
index c0e51b46dc..8edf2842ea 100644
--- a/docs/deprecations.md
+++ b/docs/deprecations.md
@@ -12,14 +12,6 @@ at least the next major release.

 ## Planned changes

-### `CHECKED_IF` and `CHECKED_ELSE`
-
-To make the `CHECKED_IF` and `CHECKED_ELSE` macros more useful, they will
-be marked as "OK to fail" (`Catch::ResultDisposition::SuppressFail` flag
-will be added), which means that their failure will not fail the test,
-making the `else` actually useful.
-
-
 ### Console Colour API

 The API for Catch2's console colour will be changed to take an extra
diff --git a/docs/other-macros.md b/docs/other-macros.md
index 50593faee7..f23ee6c242 100644
--- a/docs/other-macros.md
+++ b/docs/other-macros.md
@@ -15,6 +15,8 @@ stringification machinery to the _expr_ and records the result. As with
 evaluates to `true`. `CHECKED_ELSE( expr )` work similarly, but the block
 is entered only if the _expr_ evaluated to `false`.

+> `CHECKED_X` macros were changed to not count as failure in Catch2 X.Y.Z.
+
 Example:
 ```cpp
 int a = ...;
diff --git a/src/catch2/internal/catch_run_context.cpp b/src/catch2/internal/catch_run_context.cpp
index 8379a7aa02..e2719d3127 100644
--- a/src/catch2/internal/catch_run_context.cpp
+++ b/src/catch2/internal/catch_run_context.cpp
@@ -230,9 +230,11 @@ namespace Catch {
         if (result.getResultType() == ResultWas::Ok) {
             m_totals.assertions.passed++;
             m_lastAssertionPassed = true;
-        } else if (!result.isOk()) {
+        } else if (!result.succeeded()) {
             m_lastAssertionPassed = false;
-            if( m_activeTestCase->getTestCaseInfo().okToFail() )
+            if (result.isOk()) {
+            }
+            else if( m_activeTestCase->getTestCaseInfo().okToFail() )
                 m_totals.assertions.failedButOk++;
             else
                 m_totals.assertions.failed++;

diff --git a/src/catch2/catch_test_macros.hpp b/src/catch2/catch_test_macros.hpp
index ae50915026..cd33a65c26 100644
--- a/src/catch2/catch_test_macros.hpp
+++ b/src/catch2/catch_test_macros.hpp
@@ -32,8 +32,8 @@

   #define CATCH_CHECK( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
   #define CATCH_CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ )
-  #define CATCH_CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CATCH_CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
-  #define CATCH_CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CATCH_CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
+  #define CATCH_CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CATCH_CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )
+  #define CATCH_CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CATCH_CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )
   #define CATCH_CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )

   #define CATCH_CHECK_THROWS( ... )  INTERNAL_CATCH_THROWS( "CATCH_CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
@@ -123,8 +123,8 @@

   #define CHECK( ... ) INTERNAL_CATCH_TEST( "CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
   #define CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ )
-  #define CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
-  #define CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
+  #define CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )
+  #define CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )
   #define CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ )

   #define CHECK_THROWS( ... )  INTERNAL_CATCH_THROWS( "CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ )
diff --git a/tests/SelfTest/Baselines/automake.sw.approved.txt b/tests/SelfTest/Baselines/automake.sw.approved.txt
index 983d976484..c4ad5b1e37 100644
--- a/tests/SelfTest/Baselines/automake.sw.approved.txt
+++ b/tests/SelfTest/Baselines/automake.sw.approved.txt
@@ -231,6 +231,9 @@ Message from section two
 :test-result: PASS Test case with one argument
 :test-result: PASS Test enum bit values
 :test-result: PASS Test with special, characters "in name
+:test-result: PASS Testing checked-if
+:test-result: XFAIL Testing checked-if 2
+:test-result: XFAIL Testing checked-if 3
 :test-result: FAIL The NO_FAIL macro reports a failure but does not fail the test
 :test-result: PASS The default listing implementation write to provided stream
 :test-result: FAIL This test 'should' fail but doesn't
diff --git a/tests/SelfTest/Baselines/compact.sw.approved.txt b/tests/SelfTest/Baselines/compact.sw.approved.txt
index e757222db5..92488f5f37 100644
--- a/tests/SelfTest/Baselines/compact.sw.approved.txt
+++ b/tests/SelfTest/Baselines/compact.sw.approved.txt
@@ -1714,6 +1714,16 @@ Misc.tests.cpp:<line number>: passed: v.capacity() >= V for: 15 >= 15
 VariadicMacros.tests.cpp:<line number>: passed: with 1 message: 'no assertions'
 Tricky.tests.cpp:<line number>: passed: 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 CmdLine.tests.cpp:<line number>: passed:
+Misc.tests.cpp:<line number>: passed: true
+Misc.tests.cpp:<line number>: passed:
+Misc.tests.cpp:<line number>: failed - but was ok: false
+Misc.tests.cpp:<line number>: passed: true
+Misc.tests.cpp:<line number>: failed - but was ok: false
+Misc.tests.cpp:<line number>: passed:
+Misc.tests.cpp:<line number>: passed: true
+Misc.tests.cpp:<line number>: failed: explicitly
+Misc.tests.cpp:<line number>: failed - but was ok: false
+Misc.tests.cpp:<line number>: failed: explicitly
 Message.tests.cpp:<line number>: failed - but was ok: 1 == 2
 Reporters.tests.cpp:<line number>: passed: listingString, Contains("[fakeTag]"s) for: "All available tags:
    1  [fakeTag]
@@ -1996,11 +2006,11 @@ InternalBenchmark.tests.cpp:<line number>: passed: called == 1 for: 1 == 1
 Tricky.tests.cpp:<line number>: passed: obj.prop != 0 for: 0x<hex digits> != 0
 Misc.tests.cpp:<line number>: passed: flag for: true
 Misc.tests.cpp:<line number>: passed: testCheckedElse( true ) for: true
-Misc.tests.cpp:<line number>: failed: flag for: false
+Misc.tests.cpp:<line number>: failed - but was ok: flag for: false
 Misc.tests.cpp:<line number>: failed: testCheckedElse( false ) for: false
 Misc.tests.cpp:<line number>: passed: flag for: true
 Misc.tests.cpp:<line number>: passed: testCheckedIf( true ) for: true
-Misc.tests.cpp:<line number>: failed: flag for: false
+Misc.tests.cpp:<line number>: failed - but was ok: flag for: false
 Misc.tests.cpp:<line number>: failed: testCheckedIf( false ) for: false
 InternalBenchmark.tests.cpp:<line number>: passed: o.samples_seen == static_cast<int>(x.size()) for: 6 == 6
 InternalBenchmark.tests.cpp:<line number>: passed: o.low_severe == los for: 0 == 0
@@ -2342,5 +2352,5 @@ InternalBenchmark.tests.cpp:<line number>: passed: med == 18. for: 18.0 == 18.0
 InternalBenchmark.tests.cpp:<line number>: passed: q3 == 23. for: 23.0 == 23.0
 Misc.tests.cpp:<line number>: passed:
 Misc.tests.cpp:<line number>: passed:
-Failed 86 test cases, failed 148 assertions.
+Failed 86 test cases, failed 146 assertions.

diff --git a/tests/SelfTest/Baselines/console.std.approved.txt b/tests/SelfTest/Baselines/console.std.approved.txt
index f91f4b80f3..d9e8f2b8c7 100644
--- a/tests/SelfTest/Baselines/console.std.approved.txt
+++ b/tests/SelfTest/Baselines/console.std.approved.txt
@@ -922,6 +922,22 @@ with expansion:
   }
   "

+-------------------------------------------------------------------------------
+Testing checked-if 2
+-------------------------------------------------------------------------------
+Misc.tests.cpp:<line number>
+...............................................................................
+
+Misc.tests.cpp:<line number>: FAILED:
+
+-------------------------------------------------------------------------------
+Testing checked-if 3
+-------------------------------------------------------------------------------
+Misc.tests.cpp:<line number>
+...............................................................................
+
+Misc.tests.cpp:<line number>: FAILED:
+
 -------------------------------------------------------------------------------
 Thrown string literals are translated
 -------------------------------------------------------------------------------
@@ -1135,11 +1151,6 @@ checkedElse, failing
 Misc.tests.cpp:<line number>
 ...............................................................................

-Misc.tests.cpp:<line number>: FAILED:
-  CHECKED_ELSE( flag )
-with expansion:
-  false
-
 Misc.tests.cpp:<line number>: FAILED:
   REQUIRE( testCheckedElse( false ) )
 with expansion:
@@ -1151,11 +1162,6 @@ checkedIf, failing
 Misc.tests.cpp:<line number>
 ...............................................................................

-Misc.tests.cpp:<line number>: FAILED:
-  CHECKED_IF( flag )
-with expansion:
-  false
-
 Misc.tests.cpp:<line number>: FAILED:
   REQUIRE( testCheckedIf( false ) )
 with expansion:
@@ -1380,6 +1386,6 @@ due to unexpected exception with message:
   Why would you throw a std::string?

 ===============================================================================
-test cases:  356 |  282 passed |  70 failed |  4 failed as expected
-assertions: 2077 | 1925 passed | 131 failed | 21 failed as expected
+test cases:  359 |  283 passed |  70 failed |  6 failed as expected
+assertions: 2082 | 1930 passed | 129 failed | 23 failed as expected

diff --git a/tests/SelfTest/Baselines/console.sw.approved.txt b/tests/SelfTest/Baselines/console.sw.approved.txt
index 5f77647a9d..7e53a539dc 100644
--- a/tests/SelfTest/Baselines/console.sw.approved.txt
+++ b/tests/SelfTest/Baselines/console.sw.approved.txt
@@ -12298,6 +12298,50 @@ CmdLine.tests.cpp:<line number>

 CmdLine.tests.cpp:<line number>: PASSED:

+-------------------------------------------------------------------------------
+Testing checked-if
+-------------------------------------------------------------------------------
+Misc.tests.cpp:<line number>
+...............................................................................
+
+Misc.tests.cpp:<line number>: PASSED:
+  CHECKED_IF( true )
+
+Misc.tests.cpp:<line number>: PASSED:
+
+Misc.tests.cpp:<line number>: FAILED - but was ok:
+  CHECKED_IF( false )
+
+Misc.tests.cpp:<line number>: PASSED:
+  CHECKED_ELSE( true )
+
+Misc.tests.cpp:<line number>: FAILED - but was ok:
+  CHECKED_ELSE( false )
+
+Misc.tests.cpp:<line number>: PASSED:
+
+-------------------------------------------------------------------------------
+Testing checked-if 2
+-------------------------------------------------------------------------------
+Misc.tests.cpp:<line number>
+...............................................................................
+
+Misc.tests.cpp:<line number>: PASSED:
+  CHECKED_IF( true )
+
+Misc.tests.cpp:<line number>: FAILED:
+
+-------------------------------------------------------------------------------
+Testing checked-if 3
+-------------------------------------------------------------------------------
+Misc.tests.cpp:<line number>
+...............................................................................
+
+Misc.tests.cpp:<line number>: FAILED - but was ok:
+  CHECKED_ELSE( false )
+
+Misc.tests.cpp:<line number>: FAILED:
+
 -------------------------------------------------------------------------------
 The NO_FAIL macro reports a failure but does not fail the test
 -------------------------------------------------------------------------------
@@ -14176,7 +14220,7 @@ checkedElse, failing
 Misc.tests.cpp:<line number>
 ...............................................................................

-Misc.tests.cpp:<line number>: FAILED:
+Misc.tests.cpp:<line number>: FAILED - but was ok:
   CHECKED_ELSE( flag )
 with expansion:
   false
@@ -14208,7 +14252,7 @@ checkedIf, failing
 Misc.tests.cpp:<line number>
 ...............................................................................

-Misc.tests.cpp:<line number>: FAILED:
+Misc.tests.cpp:<line number>: FAILED - but was ok:
   CHECKED_IF( flag )
 with expansion:
   false
@@ -16722,6 +16766,6 @@ Misc.tests.cpp:<line number>
 Misc.tests.cpp:<line number>: PASSED:

 ===============================================================================
-test cases:  356 |  266 passed |  86 failed |  4 failed as expected
-assertions: 2094 | 1925 passed | 148 failed | 21 failed as expected
+test cases:  359 |  267 passed |  86 failed |  6 failed as expected
+assertions: 2099 | 1930 passed | 146 failed | 23 failed as expected

diff --git a/tests/SelfTest/Baselines/junit.sw.approved.txt b/tests/SelfTest/Baselines/junit.sw.approved.txt
index 12a1456f8e..d90b164554 100644
--- a/tests/SelfTest/Baselines/junit.sw.approved.txt
+++ b/tests/SelfTest/Baselines/junit.sw.approved.txt
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <testsuitesloose text artifact
 >
-  <testsuite name="<exe-name>" errors="17" failures="132" tests="2095" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
+  <testsuite name="<exe-name>" errors="17" failures="130" tests="2100" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
     <properties>
       <property name="filters" value="~[!nonportable]~[!benchmark]~[approvals] *"/>
       <property name="random-seed" value="1"/>
@@ -1276,6 +1276,19 @@ Misc.tests.cpp:<line number>
     <testcase classname="<exe-name>.global" name="Test case with one argument" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test enum bit values" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="Test with special, characters "in name" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="Testing checked-if" time="{duration}" status="run"/>
+    <testcase classname="<exe-name>.global" name="Testing checked-if 2" time="{duration}" status="run">
+      <failure type="FAIL">
+FAILED:
+Misc.tests.cpp:<line number>
+      </failure>
+    </testcase>
+    <testcase classname="<exe-name>.global" name="Testing checked-if 3" time="{duration}" status="run">
+      <failure type="FAIL">
+FAILED:
+Misc.tests.cpp:<line number>
+      </failure>
+    </testcase>
     <testcase classname="<exe-name>.global" name="The NO_FAIL macro reports a failure but does not fail the test" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="The default listing implementation write to provided stream/Listing tags" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="The default listing implementation write to provided stream/Listing reporters" time="{duration}" status="run"/>
@@ -1505,13 +1518,6 @@ Exception.tests.cpp:<line number>
     <testcase classname="<exe-name>.global" name="boolean member" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="checkedElse" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="checkedElse, failing" time="{duration}" status="run">
-      <failure message="flag" type="CHECKED_ELSE">
-FAILED:
-  CHECKED_ELSE( flag )
-with expansion:
-  false
-Misc.tests.cpp:<line number>
-      </failure>
       <failure message="testCheckedElse( false )" type="REQUIRE">
 FAILED:
   REQUIRE( testCheckedElse( false ) )
@@ -1522,13 +1528,6 @@ Misc.tests.cpp:<line number>
     </testcase>
     <testcase classname="<exe-name>.global" name="checkedIf" time="{duration}" status="run"/>
     <testcase classname="<exe-name>.global" name="checkedIf, failing" time="{duration}" status="run">
-      <failure message="flag" type="CHECKED_IF">
-FAILED:
-  CHECKED_IF( flag )
-with expansion:
-  false
-Misc.tests.cpp:<line number>
-      </failure>
       <failure message="testCheckedIf( false )" type="REQUIRE">
 FAILED:
   REQUIRE( testCheckedIf( false ) )
diff --git a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
index 1f0cc5dad5..b900a55cbb 100644
--- a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
+++ b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt
@@ -1583,17 +1583,23 @@ Misc.tests.cpp:<line number>
     <testCase name="TemplateTestSig: vectors can be sized and resized - std::string,15/resizing smaller changes size but not capacity/We can use the 'swap trick' to reset the capacity" duration="{duration}"/>
     <testCase name="TemplateTestSig: vectors can be sized and resized - std::string,15/reserving bigger changes capacity but not size" duration="{duration}"/>
     <testCase name="TemplateTestSig: vectors can be sized and resized - std::string,15/reserving smaller does not change size or capacity" duration="{duration}"/>
+    <testCase name="Testing checked-if" duration="{duration}"/>
+    <testCase name="Testing checked-if 2" duration="{duration}">
+      <skipped message="FAIL()">
+FAILED:
+Misc.tests.cpp:<line number>
+      </skipped>
+    </testCase>
+    <testCase name="Testing checked-if 3" duration="{duration}">
+      <skipped message="FAIL()">
+FAILED:
+Misc.tests.cpp:<line number>
+      </skipped>
+    </testCase>
     <testCase name="This test 'should' fail but doesn't" duration="{duration}"/>
     <testCase name="atomic if" duration="{duration}"/>
     <testCase name="checkedElse" duration="{duration}"/>
     <testCase name="checkedElse, failing" duration="{duration}">
-      <failure message="CHECKED_ELSE(flag)">
-FAILED:
-	CHECKED_ELSE( flag )
-with expansion:
-	false
-Misc.tests.cpp:<line number>
-      </failure>
       <failure message="REQUIRE(testCheckedElse( false ))">
 FAILED:
 	REQUIRE( testCheckedElse( false ) )
@@ -1604,13 +1610,6 @@ Misc.tests.cpp:<line number>
     </testCase>
     <testCase name="checkedIf" duration="{duration}"/>
     <testCase name="checkedIf, failing" duration="{duration}">
-      <failure message="CHECKED_IF(flag)">
-FAILED:
-	CHECKED_IF( flag )
-with expansion:
-	false
-Misc.tests.cpp:<line number>
-      </failure>
       <failure message="REQUIRE(testCheckedIf( false ))">
 FAILED:
 	REQUIRE( testCheckedIf( false ) )
diff --git a/tests/SelfTest/Baselines/tap.sw.approved.txt b/tests/SelfTest/Baselines/tap.sw.approved.txt
index 8bb648f5eb..a6e63f6da4 100644
--- a/tests/SelfTest/Baselines/tap.sw.approved.txt
+++ b/tests/SelfTest/Baselines/tap.sw.approved.txt
@@ -3073,6 +3073,26 @@ ok {test-number} - with 1 message: 'no assertions'
 ok {test-number} - 0x<hex digits> == bit30and31 for: 3221225472 (0x<hex digits>) == 3221225472
 # Test with special, characters "in name
 ok {test-number} -
+# Testing checked-if
+ok {test-number} - true
+# Testing checked-if
+ok {test-number} -
+# Testing checked-if
+ok {test-number} - false  # TODO
+# Testing checked-if
+ok {test-number} - true
+# Testing checked-if
+ok {test-number} - false  # TODO
+# Testing checked-if
+ok {test-number} -
+# Testing checked-if 2
+ok {test-number} - true
+# Testing checked-if 2
+not ok {test-number} - explicitly
+# Testing checked-if 3
+ok {test-number} - false  # TODO
+# Testing checked-if 3
+not ok {test-number} - explicitly
 # The NO_FAIL macro reports a failure but does not fail the test
 ok {test-number} - 1 == 2  # TODO
 # The default listing implementation write to provided stream
@@ -3570,7 +3590,7 @@ ok {test-number} - flag for: true
 # checkedElse
 ok {test-number} - testCheckedElse( true ) for: true
 # checkedElse, failing
-not ok {test-number} - flag for: false
+ok {test-number} - flag for: false  # TODO
 # checkedElse, failing
 not ok {test-number} - testCheckedElse( false ) for: false
 # checkedIf
@@ -3578,7 +3598,7 @@ ok {test-number} - flag for: true
 # checkedIf
 ok {test-number} - testCheckedIf( true ) for: true
 # checkedIf, failing
-not ok {test-number} - flag for: false
+ok {test-number} - flag for: false  # TODO
 # checkedIf, failing
 not ok {test-number} - testCheckedIf( false ) for: false
 # classify_outliers
@@ -4180,5 +4200,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0
 ok {test-number} -
 # xmlentitycheck
 ok {test-number} -
-1..2094
+1..2099

diff --git a/tests/SelfTest/Baselines/teamcity.sw.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.approved.txt
index e64c984fb1..2c1a8c5c29 100644
--- a/tests/SelfTest/Baselines/teamcity.sw.approved.txt
+++ b/tests/SelfTest/Baselines/teamcity.sw.approved.txt
@@ -563,6 +563,14 @@ Misc.tests.cpp:<line number>|nexpression failed|n  CHECK( s1 == s2 )|nwith expan
 ##teamcity[testFinished name='Test enum bit values' duration="{duration}"]
 ##teamcity[testStarted name='Test with special, characters "in name']
 ##teamcity[testFinished name='Test with special, characters "in name' duration="{duration}"]
+##teamcity[testStarted name='Testing checked-if']
+##teamcity[testFinished name='Testing checked-if' duration="{duration}"]
+##teamcity[testStarted name='Testing checked-if 2']
+Misc.tests.cpp:<line number>|nexplicit failure- failure ignore as test marked as |'ok to fail|'|n']
+##teamcity[testFinished name='Testing checked-if 2' duration="{duration}"]
+##teamcity[testStarted name='Testing checked-if 3']
+Misc.tests.cpp:<line number>|nexplicit failure- failure ignore as test marked as |'ok to fail|'|n']
+##teamcity[testFinished name='Testing checked-if 3' duration="{duration}"]
 ##teamcity[testStarted name='The NO_FAIL macro reports a failure but does not fail the test']
 ##teamcity[testFinished name='The NO_FAIL macro reports a failure but does not fail the test' duration="{duration}"]
 ##teamcity[testStarted name='The default listing implementation write to provided stream']
@@ -661,13 +669,11 @@ Exception.tests.cpp:<line number>|nunexpected exception with message:|n  "unexpe
 ##teamcity[testStarted name='checkedElse']
 ##teamcity[testFinished name='checkedElse' duration="{duration}"]
 ##teamcity[testStarted name='checkedElse, failing']
-Misc.tests.cpp:<line number>|nexpression failed|n  CHECKED_ELSE( flag )|nwith expansion:|n  false|n']
 Misc.tests.cpp:<line number>|nexpression failed|n  REQUIRE( testCheckedElse( false ) )|nwith expansion:|n  false|n']
 ##teamcity[testFinished name='checkedElse, failing' duration="{duration}"]
 ##teamcity[testStarted name='checkedIf']
 ##teamcity[testFinished name='checkedIf' duration="{duration}"]
 ##teamcity[testStarted name='checkedIf, failing']
-Misc.tests.cpp:<line number>|nexpression failed|n  CHECKED_IF( flag )|nwith expansion:|n  false|n']
 Misc.tests.cpp:<line number>|nexpression failed|n  REQUIRE( testCheckedIf( false ) )|nwith expansion:|n  false|n']
 ##teamcity[testFinished name='checkedIf, failing' duration="{duration}"]
 ##teamcity[testStarted name='classify_outliers']
diff --git a/tests/SelfTest/Baselines/xml.sw.approved.txt b/tests/SelfTest/Baselines/xml.sw.approved.txt
index a624d5bdbf..4aecb91e3b 100644
--- a/tests/SelfTest/Baselines/xml.sw.approved.txt
+++ b/tests/SelfTest/Baselines/xml.sw.approved.txt
@@ -14425,6 +14425,65 @@ Message from section two
     <TestCase name="Test with special, characters "in name" tags="[cli][regression]" filename="tests/<exe-name>/IntrospectiveTests/CmdLine.tests.cpp" >
       <OverallResult success="true"/>
     </TestCase>
+    <TestCase name="Testing checked-if" tags="[checked-if]" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+      <Expression success="true" type="CHECKED_IF" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          true
+        </Original>
+        <Expanded>
+          true
+        </Expanded>
+      </Expression>
+      <Expression success="false" type="CHECKED_IF" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          false
+        </Original>
+        <Expanded>
+          false
+        </Expanded>
+      </Expression>
+      <Expression success="true" type="CHECKED_ELSE" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          true
+        </Original>
+        <Expanded>
+          true
+        </Expanded>
+      </Expression>
+      <Expression success="false" type="CHECKED_ELSE" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          false
+        </Original>
+        <Expanded>
+          false
+        </Expanded>
+      </Expression>
+      <OverallResult success="true"/>
+    </TestCase>
+    <TestCase name="Testing checked-if 2" tags="[!shouldfail][checked-if]" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+      <Expression success="true" type="CHECKED_IF" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          true
+        </Original>
+        <Expanded>
+          true
+        </Expanded>
+      </Expression>
+      <Failure filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" />
+      <OverallResult success="true"/>
+    </TestCase>
+    <TestCase name="Testing checked-if 3" tags="[!shouldfail][checked-if]" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+      <Expression success="false" type="CHECKED_ELSE" filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" >
+        <Original>
+          false
+        </Original>
+        <Expanded>
+          false
+        </Expanded>
+      </Expression>
+      <Failure filename="tests/<exe-name>/UsageTests/Misc.tests.cpp" />
+      <OverallResult success="true"/>
+    </TestCase>
     <TestCase name="The NO_FAIL macro reports a failure but does not fail the test" tags="[messages]" filename="tests/<exe-name>/UsageTests/Message.tests.cpp" >
       <Expression success="false" type="CHECK_NOFAIL" filename="tests/<exe-name>/UsageTests/Message.tests.cpp" >
         <Original>
@@ -19667,9 +19726,9 @@ loose text artifact
       </Section>
       <OverallResult success="true"/>
     </TestCase>
-    <OverallResults successes="1925" failures="149" expectedFailures="21"/>
-    <OverallResultsCases successes="266" failures="86" expectedFailures="4"/>
+    <OverallResults successes="1930" failures="147" expectedFailures="23"/>
+    <OverallResultsCases successes="267" failures="86" expectedFailures="6"/>
   </Group>
-  <OverallResults successes="1925" failures="148" expectedFailures="21"/>
-  <OverallResultsCases successes="266" failures="86" expectedFailures="4"/>
+  <OverallResults successes="1930" failures="146" expectedFailures="23"/>
+  <OverallResultsCases successes="267" failures="86" expectedFailures="6"/>
 </Catch>
diff --git a/tests/SelfTest/UsageTests/Misc.tests.cpp b/tests/SelfTest/UsageTests/Misc.tests.cpp
index 277723a8b7..5dfef6b19a 100644
--- a/tests/SelfTest/UsageTests/Misc.tests.cpp
+++ b/tests/SelfTest/UsageTests/Misc.tests.cpp
@@ -182,6 +182,39 @@ TEST_CASE( "checkedElse, failing", "[failing][.]" ) {
     REQUIRE( testCheckedElse( false ) );
 }

+TEST_CASE("Testing checked-if", "[checked-if]") {
+    CHECKED_IF(true) {
+        SUCCEED();
+    }
+    CHECKED_IF(false) {
+        FAIL();
+    }
+    CHECKED_ELSE(true) {
+        FAIL();
+    }
+    CHECKED_ELSE(false) {
+        SUCCEED();
+    }
+}
+
+TEST_CASE("Testing checked-if 2", "[checked-if][!shouldfail]") {
+    CHECKED_IF(true) {
+        FAIL();
+    }
+    // If the checked if is not entered, this passes and the test
+    // fails, because of the [!shouldfail] tag.
+    SUCCEED();
+}
+
+TEST_CASE("Testing checked-if 3", "[checked-if][!shouldfail]") {
+    CHECKED_ELSE(false) {
+        FAIL();
+    }
+    // If the checked false is not entered, this passes and the test
+    // fails, because of the [!shouldfail] tag.
+    SUCCEED();
+}
+
 TEST_CASE( "xmlentitycheck" ) {
     SECTION( "embedded xml: <test>it should be possible to embed xml characters, such as <, \" or &, or even whole <xml>documents</xml> within an attribute</test>" ) {
         SUCCEED(); // We need this here to stop it failing due to no tests


Comparison expression creation fix

## Description
Why.
This PR fixes the issue that user-defined operators can be a better match than the ones used to create Catch2 expression (defined in `Decomposer` and `ExprLhs`).

What.
- Overload with forwarding reference is added for all the comparison operators used in expressions.
- This overload doesn't compile for bit field non-const reference, so another overload that takes all arithmetic types by value is added. It replaces `bool` overload where it existed before.
- Operators are now defined as hidden friends to fix compilation on GCC.

## GitHub Issues
Closes #2121

## Note
I'm sorry for not fully reading the contribution guidelines. I just have a problem and propose a solution. Feel free to edit my code.

Problem with user provided operator == (with proposed fix)

**Describe the bug**
The test doesn't compile when the user provides a more general `operator ==` overload than `ExprLhs`.
`operator ==` in the code sample below is a better match when r-value reference is passed because it accepts forwarding reference (`U&&`) and `ExprLhs` accepts only const reference (`RhsT const& rhs`) https://github.com/catchorg/Catch2/blob/devel/src/catch2/internal/catch_decomposer.hpp#L187
```
        template<typename RhsT>
        auto operator == ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
            return { compareEqual( m_lhs, rhs ), m_lhs, "=="_sr, rhs };
        }
```

**Expected behavior**
The test should compile.

**Reproduction steps**
```
namespace adl {

struct activate_adl {};

struct equality_expression {
    operator bool() const { return true; }
};

template <class T, class U>
constexpr auto operator == (T&&, U&&) {
    return equality_expression{};
}

}

TEST_CASE("User provided equality operator", "[compilation]") {
    REQUIRE(0 == adl::activate_adl{});
}
```
error: no matching member function for call to 'handleExpr' REQUIRE(0 == adl::activate_adl{});

**Fix**
My first attempt was to change the `operator == ` definition (and similarly all other operators) to
```
        template<typename RhsT>
        auto operator == ( RhsT && rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
            return { compareEqual( m_lhs, rhs ), m_lhs, "=="_sr, rhs };
        }
```
However, this broke a test for bitfields
error: non-const reference cannot bind to bit-field 'v' REQUIRE(0 == y.v);

This can be resolved by two not so clean overloads, maybe you know a better way:
```
        template<typename RhsT>
        auto operator == ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
            return { compareEqual( m_lhs, rhs ), m_lhs, "=="_sr, rhs };
        }
        template<typename RhsT, std::enable_if_t<!std::is_arithmetic<std::decay_t<RhsT>>::value, int> = 0>
        auto operator == ( RhsT && rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
            return { compareEqual( m_lhs, rhs ), m_lhs, "=="_sr, rhs };
        }
```

**Unrelated note**
I don't think const reference here prolongs the lifetime of rhs, because it's not local but stored in a class: `BinaryExpr<LhsT, RhsT const&>`. Not sure if it's a problem.

diff --git a/src/catch2/internal/catch_decomposer.hpp b/src/catch2/internal/catch_decomposer.hpp
index 9af5c19f70..a747c34cd6 100644
--- a/src/catch2/internal/catch_decomposer.hpp
+++ b/src/catch2/internal/catch_decomposer.hpp
@@ -183,60 +183,53 @@ namespace Catch {
     public:
         explicit ExprLhs( LhsT lhs ) : m_lhs( lhs ) {}

-        template<typename RhsT>
-        auto operator == ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { compareEqual( m_lhs, rhs ), m_lhs, "=="_sr, rhs };
+        template<typename RhsT, std::enable_if_t<!std::is_arithmetic<std::remove_reference_t<RhsT>>::value, int> = 0>
+        friend auto operator == ( ExprLhs && lhs, RhsT && rhs ) -> BinaryExpr<LhsT, RhsT const&> {
+            return { compareEqual( lhs.m_lhs, rhs ), lhs.m_lhs, "=="_sr, rhs };
         }
-        auto operator == ( bool rhs ) -> BinaryExpr<LhsT, bool> const {
-            return { m_lhs == rhs, m_lhs, "=="_sr, rhs };
+        template<typename RhsT, std::enable_if_t<std::is_arithmetic<RhsT>::value, int> = 0>
+        friend auto operator == ( ExprLhs && lhs, RhsT rhs ) -> BinaryExpr<LhsT, RhsT> {
+            return { compareEqual( lhs.m_lhs, rhs ), lhs.m_lhs, "=="_sr, rhs };
         }

-        template<typename RhsT>
-        auto operator != ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { compareNotEqual( m_lhs, rhs ), m_lhs, "!="_sr, rhs };
+        template<typename RhsT, std::enable_if_t<!std::is_arithmetic<std::remove_reference_t<RhsT>>::value, int> = 0>
+        friend auto operator != ( ExprLhs && lhs, RhsT && rhs ) -> BinaryExpr<LhsT, RhsT const&> {
+            return { compareNotEqual( lhs.m_lhs, rhs ), lhs.m_lhs, "!="_sr, rhs };
         }
-        auto operator != ( bool rhs ) -> BinaryExpr<LhsT, bool> const {
-            return { m_lhs != rhs, m_lhs, "!="_sr, rhs };
+        template<typename RhsT, std::enable_if_t<std::is_arithmetic<RhsT>::value, int> = 0>
+        friend auto operator != ( ExprLhs && lhs, RhsT rhs ) -> BinaryExpr<LhsT, RhsT> {
+            return { compareNotEqual( lhs.m_lhs, rhs ), lhs.m_lhs, "!="_sr, rhs };
         }

-        template<typename RhsT>
-        auto operator > ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs > rhs), m_lhs, ">"_sr, rhs };
-        }
-        template<typename RhsT>
-        auto operator < ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs < rhs), m_lhs, "<"_sr, rhs };
-        }
-        template<typename RhsT>
-        auto operator >= ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs >= rhs), m_lhs, ">="_sr, rhs };
-        }
-        template<typename RhsT>
-        auto operator <= ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs <= rhs), m_lhs, "<="_sr, rhs };
-        }
-        template <typename RhsT>
-        auto operator | (RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs | rhs), m_lhs, "|"_sr, rhs };
-        }
-        template <typename RhsT>
-        auto operator & (RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs & rhs), m_lhs, "&"_sr, rhs };
-        }
-        template <typename RhsT>
-        auto operator ^ (RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
-            return { static_cast<bool>(m_lhs ^ rhs), m_lhs, "^"_sr, rhs };
+    #define CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(op) \
+        template<typename RhsT, std::enable_if_t<!std::is_arithmetic<std::remove_reference_t<RhsT>>::value, int> = 0> \
+        friend auto operator op ( ExprLhs && lhs, RhsT && rhs ) -> BinaryExpr<LhsT, RhsT const&> { \
+            return { static_cast<bool>(lhs.m_lhs op rhs), lhs.m_lhs, #op##_sr, rhs }; \
+        } \
+        template<typename RhsT, std::enable_if_t<std::is_arithmetic<RhsT>::value, int> = 0> \
+        friend auto operator op ( ExprLhs && lhs, RhsT rhs ) -> BinaryExpr<LhsT, RhsT> { \
+            return { static_cast<bool>(lhs.m_lhs op rhs), lhs.m_lhs, #op##_sr, rhs }; \
         }

+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(<)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(>)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(<=)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(>=)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(|)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(&)
+        CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR(^)
+
+    #undef CATCH_INTERNAL_DEFINE_EXPRESSION_OPERATOR
+
         template<typename RhsT>
-        auto operator && ( RhsT const& ) -> BinaryExpr<LhsT, RhsT const&> const {
+        friend auto operator && ( ExprLhs &&, RhsT && ) -> BinaryExpr<LhsT, RhsT const&> {
             static_assert(always_false<RhsT>::value,
             "operator&& is not supported inside assertions, "
             "wrap the expression inside parentheses, or decompose it");
         }

         template<typename RhsT>
-        auto operator || ( RhsT const& ) -> BinaryExpr<LhsT, RhsT const&> const {
+        friend auto operator || ( ExprLhs &&, RhsT && ) -> BinaryExpr<LhsT, RhsT const&> {
             static_assert(always_false<RhsT>::value,
             "operator|| is not supported inside assertions, "
             "wrap the expression inside parentheses, or decompose it");
@@ -247,21 +240,15 @@ namespace Catch {
         }
     };

-    void handleExpression( ITransientExpression const& expr );
-
-    template<typename T>
-    void handleExpression( ExprLhs<T> const& expr ) {
-        handleExpression( expr.makeUnaryExpr() );
-    }
-
     struct Decomposer {
-        template<typename T>
-        auto operator <= ( T const& lhs ) -> ExprLhs<T const&> {
-            return ExprLhs<T const&>{ lhs };
+        template<typename T, std::enable_if_t<!std::is_arithmetic<std::remove_reference_t<T>>::value, int> = 0>
+        friend auto operator <= ( Decomposer &&, T && lhs ) -> ExprLhs<T const&> {
+            return ExprLhs<const T&>{ lhs };
         }

-        auto operator <=( bool value ) -> ExprLhs<bool> {
-            return ExprLhs<bool>{ value };
+        template<typename T, std::enable_if_t<std::is_arithmetic<T>::value, int> = 0>
+        friend auto operator <= ( Decomposer &&, T value ) -> ExprLhs<T> {
+            return ExprLhs<T>{ value };
         }
     };

diff --git a/tests/SelfTest/UsageTests/Compilation.tests.cpp b/tests/SelfTest/UsageTests/Compilation.tests.cpp
index 5f8c82a38a..cce190f2cb 100644
--- a/tests/SelfTest/UsageTests/Compilation.tests.cpp
+++ b/tests/SelfTest/UsageTests/Compilation.tests.cpp
@@ -277,3 +277,42 @@ namespace {
 TEST_CASE("Immovable types are supported in basic assertions", "[compilation][.approvals]") {
     REQUIRE(ImmovableType{} == ImmovableType{});
 }
+
+namespace adl {
+
+struct always_true {
+    explicit operator bool() const { return true; }
+};
+
+#define COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(op) \
+template <class T, class U> \
+auto operator op (T&&, U&&) { \
+    return always_true{}; \
+}
+
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(==)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(!=)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(<)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(>)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(<=)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(>=)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(|)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(&)
+COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR(^)
+
+#undef COMPILATION_TEST_DEFINE_UNIVERSAL_OPERATOR
+
+}
+
+TEST_CASE("ADL universal operators don't hijack expression deconstruction", "[compilation][.approvals]") {
+    REQUIRE(adl::always_true{});
+    REQUIRE(0 == adl::always_true{});
+    REQUIRE(0 != adl::always_true{});
+    REQUIRE(0 < adl::always_true{});
+    REQUIRE(0 > adl::always_true{});
+    REQUIRE(0 <= adl::always_true{});
+    REQUIRE(0 >= adl::always_true{});
+    REQUIRE(0 | adl::always_true{});
+    REQUIRE(0 & adl::always_true{});
+    REQUIRE(0 ^ adl::always_true{});
+}