Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/TestCaseInfo.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-10-25 10:31:36 +0300
committerGitHub2026-10-25 10:31:36 +0300
commitd2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (patch)
tree0bce4462b2e190c39b3e1c5f697e3e4abd1a5e2a /src/Progmasoft/Catch3/TestCaseInfo.cpp
parent13f81a480a93807c14dddb5abdb97355ced4d4c5 (diff)
parent126dc333564cb72b5115d9a9242d66694694b6d0 (diff)
downloadcatch3-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.cpp307
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