forked from catchorg/Catch2
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.
250 lines
9.6 KiB
C++
250 lines
9.6 KiB
C++
|
|
// Copyright Catch2 Authors
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE_1_0.txt or copy at
|
|
// https://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
// SPDX-License-Identifier: BSL-1.0
|
|
#include <catch2/catch_test_case_info.hpp>
|
|
#include <catch2/internal/catch_enforce.hpp>
|
|
#include <catch2/catch_test_spec.hpp>
|
|
#include <catch2/interfaces/catch_interfaces_testcase.hpp>
|
|
#include <catch2/internal/catch_string_manip.hpp>
|
|
#include <catch2/internal/catch_case_insensitive_comparisons.hpp>
|
|
|
|
#include <cassert>
|
|
#include <cctype>
|
|
#include <algorithm>
|
|
|
|
namespace Catch {
|
|
|
|
namespace {
|
|
using TCP_underlying_type = uint8_t;
|
|
static_assert(sizeof(TestCaseProperties) == sizeof(TCP_underlying_type),
|
|
"The size of the TestCaseProperties is different from the assumed size");
|
|
|
|
TestCaseProperties operator|(TestCaseProperties lhs, TestCaseProperties rhs) {
|
|
return static_cast<TestCaseProperties>(
|
|
static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs)
|
|
);
|
|
}
|
|
|
|
TestCaseProperties& operator|=(TestCaseProperties& lhs, TestCaseProperties rhs) {
|
|
lhs = static_cast<TestCaseProperties>(
|
|
static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs)
|
|
);
|
|
return lhs;
|
|
}
|
|
|
|
TestCaseProperties operator&(TestCaseProperties lhs, TestCaseProperties rhs) {
|
|
return static_cast<TestCaseProperties>(
|
|
static_cast<TCP_underlying_type>(lhs) & static_cast<TCP_underlying_type>(rhs)
|
|
);
|
|
}
|
|
|
|
bool applies(TestCaseProperties tcp) {
|
|
static_assert(static_cast<TCP_underlying_type>(TestCaseProperties::None) == 0,
|
|
"TestCaseProperties::None must be equal to 0");
|
|
return tcp != TestCaseProperties::None;
|
|
}
|
|
|
|
TestCaseProperties parseSpecialTag( StringRef tag ) {
|
|
if( !tag.empty() && tag[0] == '.' )
|
|
return TestCaseProperties::IsHidden;
|
|
else if( tag == "!throws"_sr )
|
|
return TestCaseProperties::Throws;
|
|
else if( tag == "!shouldfail"_sr )
|
|
return TestCaseProperties::ShouldFail;
|
|
else if( tag == "!mayfail"_sr )
|
|
return TestCaseProperties::MayFail;
|
|
else if( tag == "!nonportable"_sr )
|
|
return TestCaseProperties::NonPortable;
|
|
else if( tag == "!benchmark"_sr )
|
|
return static_cast<TestCaseProperties>(TestCaseProperties::Benchmark | TestCaseProperties::IsHidden );
|
|
else
|
|
return TestCaseProperties::None;
|
|
}
|
|
bool isReservedTag( StringRef tag ) {
|
|
return parseSpecialTag( tag ) == TestCaseProperties::None
|
|
&& tag.size() > 0
|
|
&& !std::isalnum( static_cast<unsigned char>(tag[0]) );
|
|
}
|
|
void enforceNotReservedTag( StringRef tag, SourceLineInfo const& _lineInfo ) {
|
|
CATCH_ENFORCE( !isReservedTag(tag),
|
|
"Tag name: [" << tag << "] is not allowed.\n"
|
|
<< "Tag names starting with non alphanumeric characters are reserved\n"
|
|
<< _lineInfo );
|
|
}
|
|
|
|
std::string makeDefaultName() {
|
|
static size_t counter = 0;
|
|
return "Anonymous test case " + std::to_string(++counter);
|
|
}
|
|
|
|
StringRef extractFilenamePart(StringRef filename) {
|
|
size_t lastDot = filename.size();
|
|
while (lastDot > 0 && filename[lastDot - 1] != '.') {
|
|
--lastDot;
|
|
}
|
|
--lastDot;
|
|
|
|
size_t nameStart = lastDot;
|
|
while (nameStart > 0 && filename[nameStart - 1] != '/' && filename[nameStart - 1] != '\\') {
|
|
--nameStart;
|
|
}
|
|
|
|
return filename.substr(nameStart, lastDot - nameStart);
|
|
}
|
|
|
|
// Returns the upper bound on size of extra tags ([#file]+[.])
|
|
size_t sizeOfExtraTags(StringRef filepath) {
|
|
// [.] is 3, [#] is another 3
|
|
const size_t extras = 3 + 3;
|
|
return extractFilenamePart(filepath).size() + extras;
|
|
}
|
|
} // end unnamed namespace
|
|
|
|
bool operator<( Tag const& lhs, Tag const& rhs ) {
|
|
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>
|
|
makeTestCaseInfo(StringRef _className,
|
|
NameAndTags const& nameAndTags,
|
|
SourceLineInfo const& _lineInfo ) {
|
|
return Detail::make_unique<TestCaseInfo>(_className, nameAndTags, _lineInfo);
|
|
}
|
|
|
|
TestCaseInfo::TestCaseInfo(StringRef _className,
|
|
NameAndTags const& _nameAndTags,
|
|
SourceLineInfo const& _lineInfo):
|
|
name( _nameAndTags.name.empty() ? makeDefaultName() : _nameAndTags.name ),
|
|
className( _className ),
|
|
lineInfo( _lineInfo )
|
|
{
|
|
StringRef originalTags = _nameAndTags.tags;
|
|
// We need to reserve enough space to store all of the tags
|
|
// (including optional hidden tag and filename tag)
|
|
auto requiredSize = originalTags.size() + sizeOfExtraTags(_lineInfo.file);
|
|
backingTags.reserve(requiredSize);
|
|
|
|
// We cannot copy the tags directly, as we need to normalize
|
|
// some tags, so that [.foo] is copied as [.][foo].
|
|
size_t tagStart = 0;
|
|
size_t tagEnd = 0;
|
|
bool inTag = false;
|
|
for (size_t idx = 0; idx < originalTags.size(); ++idx) {
|
|
auto c = originalTags[idx];
|
|
if (c == '[') {
|
|
assert(!inTag);
|
|
inTag = true;
|
|
tagStart = idx;
|
|
}
|
|
if (c == ']') {
|
|
assert(inTag);
|
|
inTag = false;
|
|
tagEnd = idx;
|
|
assert(tagStart < tagEnd);
|
|
|
|
// We need to check the tag for special meanings, copy
|
|
// it over to backing storage and actually reference the
|
|
// backing storage in the saved tags
|
|
StringRef tagStr = originalTags.substr(tagStart+1, tagEnd - tagStart - 1);
|
|
CATCH_ENFORCE(!tagStr.empty(), "Empty tags are not allowed");
|
|
enforceNotReservedTag(tagStr, lineInfo);
|
|
properties |= parseSpecialTag(tagStr);
|
|
// When copying a tag to the backing storage, we need to
|
|
// check if it is a merged hide tag, such as [.foo], and
|
|
// if it is, we need to handle it as if it was [foo].
|
|
if (tagStr.size() > 1 && tagStr[0] == '.') {
|
|
tagStr = tagStr.substr(1, tagStr.size() - 1);
|
|
}
|
|
// We skip over dealing with the [.] tag, as we will add
|
|
// it later unconditionally and then sort and unique all
|
|
// the tags.
|
|
internalAppendTag(tagStr);
|
|
}
|
|
(void)inTag; // Silence "set-but-unused" warning in release mode.
|
|
}
|
|
// Add [.] if relevant
|
|
if (isHidden()) {
|
|
internalAppendTag("."_sr);
|
|
}
|
|
|
|
// Sort and prepare tags
|
|
std::sort(begin(tags), end(tags));
|
|
tags.erase(std::unique(begin(tags), end(tags)),
|
|
end(tags));
|
|
}
|
|
|
|
bool TestCaseInfo::isHidden() const {
|
|
return applies( properties & TestCaseProperties::IsHidden );
|
|
}
|
|
bool TestCaseInfo::throws() const {
|
|
return applies( properties & TestCaseProperties::Throws );
|
|
}
|
|
bool TestCaseInfo::okToFail() const {
|
|
return applies( properties & (TestCaseProperties::ShouldFail | TestCaseProperties::MayFail ) );
|
|
}
|
|
bool TestCaseInfo::expectedToFail() const {
|
|
return applies( properties & (TestCaseProperties::ShouldFail) );
|
|
}
|
|
|
|
void TestCaseInfo::addFilenameTag() {
|
|
std::string combined("#");
|
|
combined += extractFilenamePart(lineInfo.file);
|
|
internalAppendTag(combined);
|
|
}
|
|
|
|
std::string TestCaseInfo::tagsAsString() const {
|
|
std::string ret;
|
|
// '[' and ']' per tag
|
|
std::size_t full_size = 2 * tags.size();
|
|
for (const auto& tag : tags) {
|
|
full_size += tag.original.size();
|
|
}
|
|
ret.reserve(full_size);
|
|
for (const auto& tag : tags) {
|
|
ret.push_back('[');
|
|
ret += tag.original;
|
|
ret.push_back(']');
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void TestCaseInfo::internalAppendTag(StringRef tagStr) {
|
|
backingTags += '[';
|
|
const auto backingStart = backingTags.size();
|
|
backingTags += tagStr;
|
|
const auto backingEnd = backingTags.size();
|
|
backingTags += ']';
|
|
tags.emplace_back(StringRef(backingTags.c_str() + backingStart, backingEnd - backingStart));
|
|
}
|
|
|
|
bool operator<( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) {
|
|
// We want to avoid redoing the string comparisons multiple times,
|
|
// so we store the result of a three-way comparison before using
|
|
// it in the actual comparison logic.
|
|
const auto cmpName = lhs.name.compare( rhs.name );
|
|
if ( cmpName != 0 ) {
|
|
return cmpName < 0;
|
|
}
|
|
const auto cmpClassName = lhs.className.compare( rhs.className );
|
|
if ( cmpClassName != 0 ) {
|
|
return cmpClassName < 0;
|
|
}
|
|
return lhs.tags < rhs.tags;
|
|
}
|
|
|
|
TestCaseInfo const& TestCaseHandle::getTestCaseInfo() const {
|
|
return *m_info;
|
|
}
|
|
|
|
} // end namespace Catch
|