Refactor implementation of case-insensitivity in tags

By not materializing the lower cased tags ahead of time, we
save allocations at the cost of worsened performance when comparing
two tags.

Since there are rarely many tags, and commonly they are not
compared even if present, this is almost always a win. The new
implementation also improves the robustness of the code
responsible for handling tags in a case-insensitive manner.
This commit is contained in:
Martin Hořeňovský 2021-12-26 22:10:20 +01:00
parent cbb6764fb1
commit 45577a1f4c
No known key found for this signature in database
GPG Key ID: DE48307B8B0D381A
10 changed files with 43 additions and 44 deletions

View File

@ -22,7 +22,7 @@ std::string ws(int const level) {
} }
std::ostream& operator<<(std::ostream& out, Catch::Tag t) { std::ostream& operator<<(std::ostream& out, Catch::Tag t) {
return out << "original: " << t.original << "lower cased: " << t.lowerCased; return out << "original: " << t.original;
} }
template< typename T > template< typename T >

View File

@ -10,6 +10,7 @@
#include <catch2/catch_test_spec.hpp> #include <catch2/catch_test_spec.hpp>
#include <catch2/interfaces/catch_interfaces_testcase.hpp> #include <catch2/interfaces/catch_interfaces_testcase.hpp>
#include <catch2/internal/catch_string_manip.hpp> #include <catch2/internal/catch_string_manip.hpp>
#include <catch2/internal/catch_case_insensitive_comparisons.hpp>
#include <cassert> #include <cassert>
#include <cctype> #include <cctype>
@ -104,7 +105,12 @@ namespace Catch {
} // end unnamed namespace } // end unnamed namespace
bool operator<( Tag const& lhs, Tag const& rhs ) { bool operator<( Tag const& lhs, Tag const& rhs ) {
return lhs.original < rhs.original; Detail::CaseInsensitiveLess cmp;
return cmp( lhs.original, rhs.original );
}
bool operator==( Tag const& lhs, Tag const& rhs ) {
Detail::CaseInsensitiveEqualTo cmp;
return cmp( lhs.original, rhs.original );
} }
Detail::unique_ptr<TestCaseInfo> Detail::unique_ptr<TestCaseInfo>
@ -126,7 +132,6 @@ namespace Catch {
// (including optional hidden tag and filename tag) // (including optional hidden tag and filename tag)
auto requiredSize = originalTags.size() + sizeOfExtraTags(_lineInfo.file); auto requiredSize = originalTags.size() + sizeOfExtraTags(_lineInfo.file);
backingTags.reserve(requiredSize); backingTags.reserve(requiredSize);
backingLCaseTags.reserve(requiredSize);
// We cannot copy the tags directly, as we need to normalize // We cannot copy the tags directly, as we need to normalize
// some tags, so that [.foo] is copied as [.][foo]. // some tags, so that [.foo] is copied as [.][foo].
@ -172,9 +177,8 @@ namespace Catch {
} }
// Sort and prepare tags // Sort and prepare tags
toLowerInPlace(backingLCaseTags); std::sort(begin(tags), end(tags));
std::sort(begin(tags), end(tags), [](Tag lhs, Tag rhs) { return lhs.lowerCased < rhs.lowerCased; }); tags.erase(std::unique(begin(tags), end(tags)),
tags.erase(std::unique(begin(tags), end(tags), [](Tag lhs, Tag rhs) {return lhs.lowerCased == rhs.lowerCased; }),
end(tags)); end(tags));
} }
@ -195,9 +199,6 @@ namespace Catch {
std::string combined("#"); std::string combined("#");
combined += extractFilenamePart(lineInfo.file); combined += extractFilenamePart(lineInfo.file);
internalAppendTag(combined); internalAppendTag(combined);
// TBD: Running this over all tags again is inefficient, but
// simple enough. In practice, the overhead is small enough.
toLowerInPlace(backingLCaseTags);
} }
std::string TestCaseInfo::tagsAsString() const { std::string TestCaseInfo::tagsAsString() const {
@ -223,13 +224,7 @@ namespace Catch {
backingTags += tagStr; backingTags += tagStr;
const auto backingEnd = backingTags.size(); const auto backingEnd = backingTags.size();
backingTags += ']'; backingTags += ']';
backingLCaseTags += '['; tags.emplace_back(StringRef(backingTags.c_str() + backingStart, backingEnd - backingStart));
// We append the tag to the lower-case backing storage as-is,
// because we will perform the lower casing later, in bulk
backingLCaseTags += tagStr;
backingLCaseTags += ']';
tags.emplace_back(StringRef(backingTags.c_str() + backingStart, backingEnd - backingStart),
StringRef(backingLCaseTags.c_str() + backingStart, backingEnd - backingStart));
} }
bool operator<( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) { bool operator<( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) {

View File

@ -25,13 +25,21 @@
namespace Catch { namespace Catch {
/**
* A **view** of a tag string that provides case insensitive comparisons
*
* Note that in Catch2 internals, the square brackets around tags are
* not a part of tag's representation, so e.g. "[cool-tag]" is represented
* as "cool-tag" internally.
*/
struct Tag { struct Tag {
Tag(StringRef original_, StringRef lowerCased_): constexpr Tag(StringRef original_):
original(original_), lowerCased(lowerCased_) original(original_)
{} {}
StringRef original, lowerCased; StringRef original;
friend bool operator<( Tag const& lhs, Tag const& rhs ); friend bool operator< ( Tag const& lhs, Tag const& rhs );
friend bool operator==( Tag const& lhs, Tag const& rhs );
}; };
struct ITestInvoker; struct ITestInvoker;
@ -79,7 +87,7 @@ namespace Catch {
std::string name; std::string name;
StringRef className; StringRef className;
private: private:
std::string backingTags, backingLCaseTags; std::string backingTags;
// Internally we copy tags to the backing storage and then add // Internally we copy tags to the backing storage and then add
// refs to this storage to the tags vector. // refs to this storage to the tags vector.
void internalAppendTag(StringRef tagString); void internalAppendTag(StringRef tagString);

View File

@ -38,15 +38,13 @@ namespace Catch {
TestSpec::TagPattern::TagPattern( std::string const& tag, std::string const& filterString ) TestSpec::TagPattern::TagPattern( std::string const& tag, std::string const& filterString )
: Pattern( filterString ) : Pattern( filterString )
, m_tag( toLower( tag ) ) , m_tag( tag )
{} {}
bool TestSpec::TagPattern::matches( TestCaseInfo const& testCase ) const { bool TestSpec::TagPattern::matches( TestCaseInfo const& testCase ) const {
return std::find_if(begin(testCase.tags), return std::find( begin( testCase.tags ),
end(testCase.tags), end( testCase.tags ),
[&](Tag const& tag) { Tag( m_tag ) ) != end( testCase.tags );
return tag.lowerCased == m_tag;
}) != end(testCase.tags);
} }
bool TestSpec::Filter::matches( TestCaseInfo const& testCase ) const { bool TestSpec::Filter::matches( TestCaseInfo const& testCase ) const {

View File

@ -13,6 +13,7 @@
#include <catch2/interfaces/catch_interfaces_testcase.hpp> #include <catch2/interfaces/catch_interfaces_testcase.hpp>
#include <catch2/interfaces/catch_interfaces_reporter_factory.hpp> #include <catch2/interfaces/catch_interfaces_reporter_factory.hpp>
#include <catch2/internal/catch_move_and_forward.hpp> #include <catch2/internal/catch_move_and_forward.hpp>
#include <catch2/internal/catch_case_insensitive_comparisons.hpp>
#include <catch2/internal/catch_context.hpp> #include <catch2/internal/catch_context.hpp>
#include <catch2/catch_config.hpp> #include <catch2/catch_config.hpp>
@ -32,12 +33,12 @@ namespace Catch {
auto const& testSpec = config.testSpec(); auto const& testSpec = config.testSpec();
std::vector<TestCaseHandle> matchedTestCases = filterTests(getAllTestCasesSorted(config), testSpec, config); std::vector<TestCaseHandle> matchedTestCases = filterTests(getAllTestCasesSorted(config), testSpec, config);
std::map<StringRef, TagInfo> tagCounts; std::map<StringRef, TagInfo, Detail::CaseInsensitiveLess> tagCounts;
for (auto const& testCase : matchedTestCases) { for (auto const& testCase : matchedTestCases) {
for (auto const& tagName : testCase.getTestCaseInfo().tags) { for (auto const& tagName : testCase.getTestCaseInfo().tags) {
auto it = tagCounts.find(tagName.lowerCased); auto it = tagCounts.find(tagName.original);
if (it == tagCounts.end()) if (it == tagCounts.end())
it = tagCounts.insert(std::make_pair(tagName.lowerCased, TagInfo())).first; it = tagCounts.insert(std::make_pair(tagName.original, TagInfo())).first;
it->second.add(tagName.original); it->second.add(tagName.original);
} }
} }

View File

@ -2281,7 +2281,7 @@ InternalBenchmark.tests.cpp:<line number>: passed: Timing.result == Timing.itera
InternalBenchmark.tests.cpp:<line number>: passed: Timing.iterations >= time.count() for: 128 >= 100 InternalBenchmark.tests.cpp:<line number>: passed: Timing.iterations >= time.count() for: 128 >= 100
Misc.tests.cpp:<line number>: failed: false with 1 message: '3' Misc.tests.cpp:<line number>: failed: false with 1 message: '3'
Message.tests.cpp:<line number>: failed: false with 2 messages: 'hi' and 'i := 7' Message.tests.cpp:<line number>: failed: false with 2 messages: 'hi' and 'i := 7'
Tag.tests.cpp:<line number>: passed: tags, VectorContains("magic-tag"_catch_sr) && VectorContains("."_catch_sr) for: { ., magic-tag } ( Contains: magic-tag and Contains: . ) Tag.tests.cpp:<line number>: passed: testcase.tags, VectorContains( Tag( "magic-tag" ) ) && VectorContains( Tag( "."_catch_sr ) ) for: { {?}, {?} } ( Contains: {?} and Contains: {?} )
StringManip.tests.cpp:<line number>: passed: splitStringRef("", ','), Equals(std::vector<StringRef>()) for: { } Equals: { } StringManip.tests.cpp:<line number>: passed: splitStringRef("", ','), Equals(std::vector<StringRef>()) for: { } Equals: { }
StringManip.tests.cpp:<line number>: passed: splitStringRef("abc", ','), Equals(std::vector<StringRef>{"abc"}) for: { abc } Equals: { abc } StringManip.tests.cpp:<line number>: passed: splitStringRef("abc", ','), Equals(std::vector<StringRef>{"abc"}) for: { abc } Equals: { abc }
StringManip.tests.cpp:<line number>: passed: splitStringRef("abc,def", ','), Equals(std::vector<StringRef>{"abc", "def"}) for: { abc, def } Equals: { abc, def } StringManip.tests.cpp:<line number>: passed: splitStringRef("abc,def", ','), Equals(std::vector<StringRef>{"abc", "def"}) for: { abc, def } Equals: { abc, def }

View File

@ -16278,9 +16278,9 @@ Tag.tests.cpp:<line number>
............................................................................... ...............................................................................
Tag.tests.cpp:<line number>: PASSED: Tag.tests.cpp:<line number>: PASSED:
REQUIRE_THAT( tags, VectorContains("magic-tag"_catch_sr) && VectorContains("."_catch_sr) ) REQUIRE_THAT( testcase.tags, VectorContains( Tag( "magic-tag" ) ) && VectorContains( Tag( "."_catch_sr ) ) )
with expansion: with expansion:
{ ., magic-tag } ( Contains: magic-tag and Contains: . ) { {?}, {?} } ( Contains: {?} and Contains: {?} )
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
splitString splitString

View File

@ -4119,7 +4119,7 @@ not ok {test-number} - false with 1 message: '3'
# sends information to INFO # sends information to INFO
not ok {test-number} - false with 2 messages: 'hi' and 'i := 7' not ok {test-number} - false with 2 messages: 'hi' and 'i := 7'
# shortened hide tags are split apart # shortened hide tags are split apart
ok {test-number} - tags, VectorContains("magic-tag"_catch_sr) && VectorContains("."_catch_sr) for: { ., magic-tag } ( Contains: magic-tag and Contains: . ) ok {test-number} - testcase.tags, VectorContains( Tag( "magic-tag" ) ) && VectorContains( Tag( "."_catch_sr ) ) for: { {?}, {?} } ( Contains: {?} and Contains: {?} )
# splitString # splitString
ok {test-number} - splitStringRef("", ','), Equals(std::vector<StringRef>()) for: { } Equals: { } ok {test-number} - splitStringRef("", ','), Equals(std::vector<StringRef>()) for: { } Equals: { }
# splitString # splitString

View File

@ -19158,10 +19158,10 @@ loose text artifact
<TestCase name="shortened hide tags are split apart" tags="[tags]" filename="tests/<exe-name>/IntrospectiveTests/Tag.tests.cpp" > <TestCase name="shortened hide tags are split apart" tags="[tags]" filename="tests/<exe-name>/IntrospectiveTests/Tag.tests.cpp" >
<Expression success="true" type="REQUIRE_THAT" filename="tests/<exe-name>/IntrospectiveTests/Tag.tests.cpp" > <Expression success="true" type="REQUIRE_THAT" filename="tests/<exe-name>/IntrospectiveTests/Tag.tests.cpp" >
<Original> <Original>
tags, VectorContains("magic-tag"_catch_sr) &amp;&amp; VectorContains("."_catch_sr) testcase.tags, VectorContains( Tag( "magic-tag" ) ) &amp;&amp; VectorContains( Tag( "."_catch_sr ) )
</Original> </Original>
<Expanded> <Expanded>
{ ., magic-tag } ( Contains: magic-tag and Contains: . ) { {?}, {?} } ( Contains: {?} and Contains: {?} )
</Expanded> </Expanded>
</Expression> </Expression>
<OverallResult success="true"/> <OverallResult success="true"/>

View File

@ -44,15 +44,12 @@ constexpr Catch::SourceLineInfo dummySourceLineInfo = CATCH_INTERNAL_LINEINFO;
TEST_CASE("shortened hide tags are split apart", "[tags]") { TEST_CASE("shortened hide tags are split apart", "[tags]") {
using Catch::StringRef; using Catch::StringRef;
using Catch::Tag;
using Catch::Matchers::VectorContains; using Catch::Matchers::VectorContains;
Catch::TestCaseInfo testcase("", {"fake test name", "[.magic-tag]"}, dummySourceLineInfo);
// Extract parsed tags into strings Catch::TestCaseInfo testcase("", {"fake test name", "[.magic-tag]"}, dummySourceLineInfo);
std::vector<StringRef> tags; REQUIRE_THAT( testcase.tags, VectorContains( Tag( "magic-tag" ) )
for (auto const& tag : testcase.tags) { && VectorContains( Tag( "."_catch_sr ) ) );
tags.push_back(tag.lowerCased);
}
REQUIRE_THAT(tags, VectorContains("magic-tag"_catch_sr) && VectorContains("."_catch_sr));
} }
TEST_CASE("tags with dots in later positions are not parsed as hidden", "[tags]") { TEST_CASE("tags with dots in later positions are not parsed as hidden", "[tags]") {