diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e1e31b7..fd05dbdd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,6 +88,7 @@ set(IMPL_HEADERS ${SOURCES_DIR}/internal/catch_floating_point_helpers.hpp ${SOURCES_DIR}/internal/catch_getenv.hpp ${SOURCES_DIR}/internal/catch_istream.hpp + ${SOURCES_DIR}/internal/catch_is_permutation.hpp ${SOURCES_DIR}/internal/catch_lazy_expr.hpp ${SOURCES_DIR}/internal/catch_leak_detector.hpp ${SOURCES_DIR}/internal/catch_list.hpp diff --git a/src/catch2/catch_all.hpp b/src/catch2/catch_all.hpp index a0bf7605..be146421 100644 --- a/src/catch2/catch_all.hpp +++ b/src/catch2/catch_all.hpp @@ -70,6 +70,7 @@ #include #include #include +#include #include #include #include diff --git a/src/catch2/internal/catch_is_permutation.hpp b/src/catch2/internal/catch_is_permutation.hpp new file mode 100644 index 00000000..708053d3 --- /dev/null +++ b/src/catch2/internal/catch_is_permutation.hpp @@ -0,0 +1,138 @@ + +// Copyright Catch2 Authors +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// SPDX-License-Identifier: BSL-1.0 +#ifndef CATCH_IS_PERMUTATION_HPP_INCLUDED +#define CATCH_IS_PERMUTATION_HPP_INCLUDED + +#include +#include + +namespace Catch { + namespace Detail { + + template + ForwardIter find_sentinel( ForwardIter start, + Sentinel sentinel, + T const& value, + Comparator cmp ) { + while ( start != sentinel ) { + if ( cmp( *start, value ) ) { break; } + ++start; + } + return start; + } + + template + std::ptrdiff_t count_sentinel( ForwardIter start, + Sentinel sentinel, + T const& value, + Comparator cmp ) { + std::ptrdiff_t count = 0; + while ( start != sentinel ) { + if ( cmp( *start, value ) ) { ++count; } + ++start; + } + return count; + } + + template + std::enable_if_t::value, + std::ptrdiff_t> + sentinel_distance( ForwardIter iter, const Sentinel sentinel ) { + std::ptrdiff_t dist = 0; + while ( iter != sentinel ) { + ++iter; + ++dist; + } + return dist; + } + + template + std::ptrdiff_t sentinel_distance( ForwardIter first, + ForwardIter last ) { + return std::distance( first, last ); + } + + template + bool check_element_counts( ForwardIter1 first_1, + const Sentinel1 end_1, + ForwardIter2 first_2, + const Sentinel2 end_2, + Comparator cmp ) { + auto cursor = first_1; + while ( cursor != end_1 ) { + if ( find_sentinel( first_1, cursor, *cursor, cmp ) == + cursor ) { + // we haven't checked this element yet + const auto count_in_range_2 = + count_sentinel( first_2, end_2, *cursor, cmp ); + // Not a single instance in 2nd range, so it cannot be a + // permutation of 1st range + if ( count_in_range_2 == 0 ) { return false; } + + const auto count_in_range_1 = + count_sentinel( cursor, end_1, *cursor, cmp ); + if ( count_in_range_1 != count_in_range_2 ) { + return false; + } + } + + ++cursor; + } + + return true; + } + + template + bool is_permutation( ForwardIter1 first_1, + const Sentinel1 end_1, + ForwardIter2 first_2, + const Sentinel2 end_2, + Comparator cmp ) { + // TODO: no optimization for stronger iterators, because we would also have to constrain on sentinel vs not sentinel types + // TODO: Comparator has to be "both sides", e.g. a == b => b == a + // This skips shared prefix of the two ranges + while (first_1 != end_1 && first_2 != end_2 && cmp(*first_1, *first_2)) { + ++first_1; + ++first_2; + } + + // We need to handle case where at least one of the ranges has no more elements + if (first_1 == end_1 || first_2 == end_2) { + return first_1 == end_1 && first_2 == end_2; + } + + // pair counting is n**2, so we pay linear walk to compare the sizes first + auto dist_1 = sentinel_distance( first_1, end_1 ); + auto dist_2 = sentinel_distance( first_2, end_2 ); + + if (dist_1 != dist_2) { return false; } + + // Since we do not try to handle stronger iterators pair (e.g. + // bidir) optimally, the only thing left to do is to check counts in + // the remaining ranges. + return check_element_counts( first_1, end_1, first_2, end_2, cmp ); + } + + } // namespace Detail +} // namespace Catch + +#endif // CATCH_IS_PERMUTATION_HPP_INCLUDED diff --git a/src/catch2/meson.build b/src/catch2/meson.build index b32f72d2..0e114065 100644 --- a/src/catch2/meson.build +++ b/src/catch2/meson.build @@ -93,6 +93,7 @@ internal_headers = [ 'internal/catch_floating_point_helpers.hpp', 'internal/catch_getenv.hpp', 'internal/catch_istream.hpp', + 'internal/catch_is_permutation.hpp', 'internal/catch_lazy_expr.hpp', 'internal/catch_leak_detector.hpp', 'internal/catch_list.hpp', diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 023f478b..7be57abe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -77,6 +77,7 @@ endif(MSVC) #Temporary workaround # Please keep these ordered alphabetically set(TEST_SOURCES ${SELF_TEST_DIR}/TestRegistrations.cpp + ${SELF_TEST_DIR}/IntrospectiveTests/Algorithms.tests.cpp ${SELF_TEST_DIR}/IntrospectiveTests/Clara.tests.cpp ${SELF_TEST_DIR}/IntrospectiveTests/CmdLine.tests.cpp ${SELF_TEST_DIR}/IntrospectiveTests/CmdLineHelpers.tests.cpp @@ -134,6 +135,7 @@ set(TEST_SOURCES set(TEST_HEADERS ${SELF_TEST_DIR}/helpers/parse_test_spec.hpp + ${SELF_TEST_DIR}/helpers/range_test_helpers.hpp ${SELF_TEST_DIR}/helpers/type_with_lit_0_comparisons.hpp ) diff --git a/tests/SelfTest/IntrospectiveTests/Algorithms.tests.cpp b/tests/SelfTest/IntrospectiveTests/Algorithms.tests.cpp new file mode 100644 index 00000000..fa17cf82 --- /dev/null +++ b/tests/SelfTest/IntrospectiveTests/Algorithms.tests.cpp @@ -0,0 +1,94 @@ + +// Copyright Catch2 Authors +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// SPDX-License-Identifier: BSL-1.0 + +#include +#include + +#include + +#include + +namespace { + template + static bool is_permutation(Range1 const& r1, Range2 const& r2) { + using std::begin; using std::end; + return Catch::Detail::is_permutation( + begin( r1 ), end( r1 ), begin( r2 ), end( r2 ), std::equal_to<>{} ); + } +} + +TEST_CASE("is_permutation", "[algorithms][approvals]") { + SECTION( "Handle empty ranges" ) { + std::array empty; + std::array non_empty{ { 2, 3 } }; + REQUIRE( is_permutation( empty, empty ) ); + REQUIRE_FALSE( is_permutation( empty, non_empty ) ); + REQUIRE_FALSE( is_permutation( non_empty, empty ) ); + } + SECTION( "Different length ranges" ) { + std::array arr1{ { 1, 3, 5, 7, 8, 9 } }; + // arr2 is prefix of arr1 + std::array arr2{ { 1, 3, 5, 7 } }; + // arr3 shares prefix with arr1 and arr2, but is not a permutation + std::array arr3{ { 1, 3, 5, 9, 8 } }; + REQUIRE_FALSE( is_permutation( arr1, arr2 ) ); + REQUIRE_FALSE( is_permutation( arr1, arr3 ) ); + REQUIRE_FALSE( is_permutation( arr2, arr3 ) ); + } + SECTION( "Same length ranges" ) { + SECTION( "Shared elements, but different counts" ) { + const std::array + arr1{ { 1, 1, 1, 1, 2, 2 } }, + arr2{ { 1, 1, 2, 2, 2, 2 } }; + REQUIRE_FALSE( is_permutation( arr1, arr2 ) ); + } + SECTION( "Identical ranges" ) { + const std::array + arr1{ { 1, 1, 1, 1, 2, 2 } }, + arr2{ { 1, 1, 2, 2, 2, 2 } }; + REQUIRE( is_permutation( arr1, arr1 ) ); + REQUIRE( is_permutation( arr2, arr2 ) ); + } + SECTION( "Completely distinct elements" ) { + // Completely distinct elements + const std::array + arr1{ { 1, 2, 3, 4 } }, + arr2{ { 10, 20, 30, 40 } }; + REQUIRE_FALSE( is_permutation( arr1, arr2 ) ); + } + SECTION( "Reverse ranges" ) { + const std::array + arr1{ { 1, 2, 3, 4, 5 } }, + arr2{ { 5, 4, 3, 2, 1 } }; + REQUIRE( is_permutation( arr1, arr2 ) ); + } + SECTION( "Shared prefix & permuted elements" ) { + const std::array + arr1{ { 1, 1, 2, 3, 4 } }, + arr2{ { 1, 1, 4, 2, 3 } }; + REQUIRE( is_permutation( arr1, arr2 ) ); + } + SECTION( "Permutations with element count > 1" ) { + const std::array + arr1{ { 2, 2, 3, 3, 3, 1, 1 } }, + arr2{ { 3, 2, 1, 3, 2, 1, 3 } }; + REQUIRE( is_permutation( arr1, arr2 ) ); + } + } +} + +TEST_CASE("is_permutation supports iterator + sentinel pairs", + "[algorithms][is-permutation][approvals]") { + const has_different_begin_end_types + range_1{ 1, 2, 3, 4 }, + range_2{ 4, 3, 2, 1 }; + REQUIRE( is_permutation( range_1, range_2 ) ); + + const has_different_begin_end_types range_3{ 3, 3, 2, 1 }; + REQUIRE_FALSE( is_permutation( range_1, range_3 ) ); +} diff --git a/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp b/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp index 4ca1ead8..651e84e6 100644 --- a/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp +++ b/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp @@ -15,195 +15,15 @@ #include #include +#include + #include -#include #include #include #include #include #include -namespace { - -namespace unrelated { - template - class needs_ADL_begin { - std::vector m_elements; - public: - using iterator = typename std::vector::iterator; - using const_iterator = typename std::vector::const_iterator; - - needs_ADL_begin(std::initializer_list init) : m_elements(init) {} - - const_iterator Begin() const { return m_elements.begin(); } - const_iterator End() const { return m_elements.end(); } - - friend const_iterator begin(needs_ADL_begin const& lhs) { - return lhs.Begin(); - } - friend const_iterator end(needs_ADL_begin const& rhs) { - return rhs.End(); - } - }; -} // end unrelated namespace - -#if defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wunused-function" -#endif - -template -class has_different_begin_end_types { - // Using std::vector leads to annoying issues when T is bool - // so we just use list because the perf is not critical and ugh. - std::list m_elements; - - // Different type for the "end" iterator - struct iterator_end {}; - // Fake-ish forward iterator that only compares to a different type - class iterator { - using underlying_iter = typename std::list::const_iterator; - underlying_iter m_start; - underlying_iter m_end; - - public: - iterator( underlying_iter start, underlying_iter end ): - m_start( start ), m_end( end ) {} - - using iterator_category = std::forward_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = T; - using const_reference = T const&; - using pointer = T const*; - - - friend bool operator==( iterator iter, iterator_end ) { - return iter.m_start == iter.m_end; - } - friend bool operator!=( iterator iter, iterator_end ) { - return iter.m_start != iter.m_end; - } - iterator& operator++() { - ++m_start; - return *this; - } - iterator operator++(int) { - auto tmp(*this); - ++m_start; - return tmp; - } - const_reference operator*() const { - return *m_start; - } - pointer operator->() const { - return m_start; - } - }; - - -public: - explicit has_different_begin_end_types( std::initializer_list init ): - m_elements( init ) {} - - iterator begin() const { - return { m_elements.begin(), m_elements.end() }; - } - - iterator_end end() const { - return {}; - } -}; - -#if defined(__clang__) -# pragma clang diagnostic pop -#endif - -template struct with_mocked_iterator_access { - std::vector m_elements; - - // use plain arrays to have nicer printouts with CHECK(...) - mutable std::unique_ptr m_derefed; - - // We want to check which elements were dereferenced when iterating, so - // we can check whether iterator-using code traverses range correctly - template class basic_iterator { - template - using constify_t = std::conditional_t, U>; - - constify_t* m_origin; - size_t m_origin_idx; - - public: - using iterator_category = std::forward_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = constify_t; - using const_reference = typename std::vector::const_reference; - using reference = typename std::vector::reference; - using pointer = typename std::vector::pointer; - - basic_iterator( constify_t* origin, - std::size_t origin_idx ): - m_origin{ origin }, m_origin_idx{ origin_idx } {} - - friend bool operator==( basic_iterator lhs, basic_iterator rhs ) { - return lhs.m_origin == rhs.m_origin && - lhs.m_origin_idx == rhs.m_origin_idx; - } - friend bool operator!=( basic_iterator lhs, basic_iterator rhs ) { - return !( lhs == rhs ); - } - basic_iterator& operator++() { - ++m_origin_idx; - return *this; - } - basic_iterator operator++( int ) { - auto tmp( *this ); - ++( *this ); - return tmp; - } - const_reference operator*() const { - assert( m_origin_idx < m_origin->m_elements.size() && "Attempted to deref invalid position" ); - m_origin->m_derefed[m_origin_idx] = true; - return m_origin->m_elements[m_origin_idx]; - } - pointer operator->() const { - assert( m_origin_idx < m_origin->m_elements.size() && "Attempted to deref invalid position" ); - return &m_origin->m_elements[m_origin_idx]; - } - }; - - using iterator = basic_iterator; - using const_iterator = basic_iterator; - - with_mocked_iterator_access( std::initializer_list init ): - m_elements( init ), - m_derefed( std::make_unique( m_elements.size() ) ) {} - - const_iterator begin() const { return { this, 0 }; } - const_iterator end() const { return { this, m_elements.size() }; } - iterator begin() { return { this, 0 }; } - iterator end() { return { this, m_elements.size() }; } -}; - -} // end anon namespace - -namespace Catch { - // make sure with_mocked_iterator_access is not considered a range by Catch, - // so that below StringMaker is used instead of the default one for ranges - template - struct is_range> : std::false_type {}; - - template - struct StringMaker> { - static std::string - convert( with_mocked_iterator_access const& access ) { - // We have to avoid the type's iterators, because we check - // their use in tests - return ::Catch::Detail::stringify( access.m_elements ); - } - }; -} // namespace Catch - struct MoveOnlyTestElement { int num = 0; MoveOnlyTestElement(int n) :num(n) {} @@ -287,16 +107,6 @@ namespace { bool empty() const { return false; } }; -namespace unrelated { - struct ADL_empty { - bool Empty() const { return true; } - - friend bool empty(ADL_empty e) { - return e.Empty(); - } - }; - -} // end namespace unrelated } // end unnamed namespace TEST_CASE("Basic use of the Empty range matcher", "[matchers][templated][empty]") { @@ -346,17 +156,6 @@ namespace { return LessThanMatcher{ sz }; } - namespace unrelated { - struct ADL_size { - size_t sz() const { - return 12; - } - friend size_t size(ADL_size s) { - return s.sz(); - } - }; - } // end namespace unrelated - struct has_size { size_t size() const { return 13; diff --git a/tests/SelfTest/helpers/range_test_helpers.hpp b/tests/SelfTest/helpers/range_test_helpers.hpp new file mode 100644 index 00000000..22c2c3cd --- /dev/null +++ b/tests/SelfTest/helpers/range_test_helpers.hpp @@ -0,0 +1,210 @@ + +// Copyright Catch2 Authors +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// SPDX-License-Identifier: BSL-1.0 + +#ifndef CATCH_TEST_HELPERS_RANGE_TEST_HELPERS_HPP_INCLUDED +#define CATCH_TEST_HELPERS_RANGE_TEST_HELPERS_HPP_INCLUDED + +#include + +#include +#include +#include +#include + +namespace unrelated { + template + class needs_ADL_begin { + std::vector m_elements; + + public: + using iterator = typename std::vector::iterator; + using const_iterator = typename std::vector::const_iterator; + + needs_ADL_begin( std::initializer_list init ): m_elements( init ) {} + + const_iterator Begin() const { return m_elements.begin(); } + const_iterator End() const { return m_elements.end(); } + + friend const_iterator begin( needs_ADL_begin const& lhs ) { + return lhs.Begin(); + } + friend const_iterator end( needs_ADL_begin const& rhs ) { + return rhs.End(); + } + }; + + struct ADL_empty { + bool Empty() const { return true; } + + friend bool empty( ADL_empty e ) { return e.Empty(); } + }; + + struct ADL_size { + size_t sz() const { return 12; } + friend size_t size( ADL_size s ) { return s.sz(); } + }; + +} // namespace unrelated + +#if defined( __clang__ ) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +template +class has_different_begin_end_types { + // Using std::vector leads to annoying issues when T is bool + // so we just use list because the perf is not critical and ugh. + std::list m_elements; + + // Different type for the "end" iterator + struct iterator_end {}; + // Fake-ish forward iterator that only compares to a different type + class iterator { + using underlying_iter = typename std::list::const_iterator; + underlying_iter m_start; + underlying_iter m_end; + + public: + iterator( underlying_iter start, underlying_iter end ): + m_start( start ), m_end( end ) {} + + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = T; + using const_reference = T const&; + using pointer = T const*; + + friend bool operator==( iterator iter, iterator_end ) { + return iter.m_start == iter.m_end; + } + friend bool operator==(iterator lhs, iterator rhs) { + return lhs.m_start == rhs.m_start && lhs.m_end == rhs.m_end; + } + friend bool operator!=( iterator iter, iterator_end ) { + return iter.m_start != iter.m_end; + } + friend bool operator!=( iterator lhs, iterator rhs ) { + return !( lhs == rhs ); + } + iterator& operator++() { + ++m_start; + return *this; + } + iterator operator++( int ) { + auto tmp( *this ); + ++m_start; + return tmp; + } + const_reference operator*() const { return *m_start; } + pointer operator->() const { return m_start; } + }; + +public: + explicit has_different_begin_end_types( std::initializer_list init ): + m_elements( init ) {} + + iterator begin() const { return { m_elements.begin(), m_elements.end() }; } + + iterator_end end() const { return {}; } +}; + +#if defined( __clang__ ) +# pragma clang diagnostic pop +#endif + +template +struct with_mocked_iterator_access { + std::vector m_elements; + + // use plain arrays to have nicer printouts with CHECK(...) + mutable std::unique_ptr m_derefed; + + // We want to check which elements were dereferenced when iterating, so + // we can check whether iterator-using code traverses range correctly + template + class basic_iterator { + template + using constify_t = std::conditional_t, U>; + + constify_t* m_origin; + size_t m_origin_idx; + + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = constify_t; + using const_reference = typename std::vector::const_reference; + using reference = typename std::vector::reference; + using pointer = typename std::vector::pointer; + + basic_iterator( constify_t* origin, + std::size_t origin_idx ): + m_origin{ origin }, m_origin_idx{ origin_idx } {} + + friend bool operator==( basic_iterator lhs, basic_iterator rhs ) { + return lhs.m_origin == rhs.m_origin && + lhs.m_origin_idx == rhs.m_origin_idx; + } + friend bool operator!=( basic_iterator lhs, basic_iterator rhs ) { + return !( lhs == rhs ); + } + basic_iterator& operator++() { + ++m_origin_idx; + return *this; + } + basic_iterator operator++( int ) { + auto tmp( *this ); + ++( *this ); + return tmp; + } + const_reference operator*() const { + assert( m_origin_idx < m_origin->m_elements.size() && + "Attempted to deref invalid position" ); + m_origin->m_derefed[m_origin_idx] = true; + return m_origin->m_elements[m_origin_idx]; + } + pointer operator->() const { + assert( m_origin_idx < m_origin->m_elements.size() && + "Attempted to deref invalid position" ); + return &m_origin->m_elements[m_origin_idx]; + } + }; + + using iterator = basic_iterator; + using const_iterator = basic_iterator; + + with_mocked_iterator_access( std::initializer_list init ): + m_elements( init ), + m_derefed( std::make_unique( m_elements.size() ) ) {} + + const_iterator begin() const { return { this, 0 }; } + const_iterator end() const { return { this, m_elements.size() }; } + iterator begin() { return { this, 0 }; } + iterator end() { return { this, m_elements.size() }; } +}; + + +namespace Catch { + // make sure with_mocked_iterator_access is not considered a range by Catch, + // so that below StringMaker is used instead of the default one for ranges + template + struct is_range> : std::false_type {}; + + template + struct StringMaker> { + static std::string + convert( with_mocked_iterator_access const& access ) { + // We have to avoid the type's iterators, because we check + // their use in tests + return ::Catch::Detail::stringify( access.m_elements ); + } + }; +} // namespace Catch + +#endif // CATCH_TEST_HELPERS_RANGE_TEST_HELPERS_HPP_INCLUDED diff --git a/tests/meson.build b/tests/meson.build index c8bdfb4b..f525f041 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -9,6 +9,7 @@ # Please keep these ordered alphabetically self_test_sources = files( 'SelfTest/helpers/parse_test_spec.cpp', + 'SelfTest/IntrospectiveTests/Algorithms.tests.cpp', 'SelfTest/IntrospectiveTests/Clara.tests.cpp', 'SelfTest/IntrospectiveTests/CmdLine.tests.cpp', 'SelfTest/IntrospectiveTests/CmdLineHelpers.tests.cpp',