diff options
| author | Leitwolf | 2026-10-25 10:31:36 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-25 10:31:36 +0300 |
| commit | d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (patch) | |
| tree | 0bce4462b2e190c39b3e1c5f697e3e4abd1a5e2a /src/Progmasoft/Catch3/TestCaseInfo.cpp | |
| parent | 13f81a480a93807c14dddb5abdb97355ced4d4c5 (diff) | |
| parent | 126dc333564cb72b5115d9a9242d66694694b6d0 (diff) | |
| download | catch3-d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58.tar.gz | |
Merge pull request #18 from Progmasoft/build/catch3-identity
Give Catch3 its own build names and headers
Diffstat (limited to 'src/Progmasoft/Catch3/TestCaseInfo.cpp')
| -rw-r--r-- | src/Progmasoft/Catch3/TestCaseInfo.cpp | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/src/Progmasoft/Catch3/TestCaseInfo.cpp b/src/Progmasoft/Catch3/TestCaseInfo.cpp new file mode 100644 index 00000000..1f2d7562 --- /dev/null +++ b/src/Progmasoft/Catch3/TestCaseInfo.cpp @@ -0,0 +1,307 @@ + +// 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 <Progmasoft/Catch3/TestCaseInfo.hpp> +#include <Progmasoft/Catch3/Internal/Enforce.hpp> +#include <Progmasoft/Catch3/Internal/StringManip.hpp> +#include <Progmasoft/Catch3/Internal/CaseInsensitiveComparisons.hpp> +#include <Progmasoft/Catch3/Internal/TestRegistry.hpp> + +#include <cassert> +#include <cctype> +#include <algorithm> +#include <utility> + +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"); + + constexpr TestCaseProperties operator|(TestCaseProperties lhs, TestCaseProperties rhs) { + return static_cast<TestCaseProperties>( + static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs) + ); + } + + constexpr TestCaseProperties& operator|=(TestCaseProperties& lhs, TestCaseProperties rhs) { + lhs = static_cast<TestCaseProperties>( + static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs) + ); + return lhs; + } + + constexpr TestCaseProperties operator&(TestCaseProperties lhs, TestCaseProperties rhs) { + return static_cast<TestCaseProperties>( + static_cast<TCP_underlying_type>(lhs) & static_cast<TCP_underlying_type>(rhs) + ); + } + + constexpr 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; + } + + bool isRunnerTagWithValue( StringRef tag ) { + for ( StringRef prefix : { "!resource:"_sr, "!depends:"_sr, + "!timeout:"_sr, "!retries:"_sr } ) { + if ( tag.size() >= prefix.size() && + tag.substr( 0, prefix.size() ) == prefix ) { + return true; + } + } + return false; + } + + TestCaseProperties propertiesFromTag( StringRef tag, SourceLineInfo const& _lineInfo ) { + if ( tag.empty() ) { return TestCaseProperties::None; } + // All tags starting with alphanumeric characters are normal. + if ( std::isalnum( tag[0] ) ) { return TestCaseProperties::None; } + + // Explicitly check known special tags, and error out if it is not one of them + if ( 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 TestCaseProperties::Benchmark | + TestCaseProperties::IsHidden; + } + // Read by the Catch3 runner: scheduling, limits and quarantine. + else if ( tag == "!serial"_sr || tag == "!isolated"_sr || + tag == "!quarantine"_sr ) { + return TestCaseProperties::None; + } else if ( isRunnerTagWithValue( tag ) ) { + CATCH_ENFORCE( tag[tag.size() - 1] != ':', + "Tag name: [" << tag << "] needs a value after ':'\n" + << _lineInfo ); + return TestCaseProperties::None; + } else { + CATCH_RUNTIME_ERROR( + "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); + } + + constexpr StringRef extractFilenamePart(StringRef filename) { + size_t lastDot = filename.size(); + while (lastDot > 0 && filename[lastDot - 1] != '.') { + --lastDot; + } + // In theory we could have filename without any extension in it + if ( lastDot == 0 ) { return StringRef(); } + + --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]+[.]) + constexpr 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); + + // Since there is usually only small number of tags on a test case, + // the geometric growth on vector does not get time to properly + // amortize the reallocations. But we can save a lot of time by + // pre-allocating space for all tags. + if ( !originalTags.empty() ) { + size_t tagCount = 1; // 1 for filename tag + for ( size_t i = 0; i < originalTags.size(); ++i ) { + if ( originalTags[i] == '[' ) { ++tagCount; } + } + tags.reserve( tagCount ); + } + + // 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 == '[') { + CATCH_ENFORCE( + !inTag, + "Found '[' inside a tag while registering test case '" + << _nameAndTags.name << "' at " << _lineInfo ); + + inTag = true; + tagStart = idx; + } + if (c == ']') { + CATCH_ENFORCE( + inTag, + "Found unmatched ']' while registering test case '" + << _nameAndTags.name << "' at " << _lineInfo ); + + 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(), + "Found an empty tag while registering test case '" + << _nameAndTags.name << "' at " + << _lineInfo ); + + properties |= propertiesFromTag(tagStr, _lineInfo); + // 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); + } + } + CATCH_ENFORCE( !inTag, + "Found an unclosed tag while registering test case '" + << _nameAndTags.name << "' at " << _lineInfo ); + + + // Add [.] if relevant + if (isHidden()) { + internalAppendTag("."_sr); + } + + // Tests usually have small number of tags, so we explicitly handle + // the 1 and 2 tags cases for better performance in low-opt builds. + switch ( tags.size() ) { + case 1: + break; + case 2: + if ( tags[0] == tags[1] ) { tags.pop_back(); } + else if ( tags[1] < tags[0] ) { std::swap( tags[0], tags[1] ); } + break; + default: + 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; + } + + bool operator==( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) { + return lhs.name == rhs.name + && lhs.className == rhs.className + && lhs.tags == rhs.tags; + } + +} // end namespace Catch |