catch_test_case_info.cpp
1 2 // Copyright Catch2 Authors 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE.txt or copy at 5 // https://www.boost.org/LICENSE_1_0.txt) 6 7 // SPDX-License-Identifier: BSL-1.0 8 #include <catch2/catch_test_case_info.hpp> 9 #include <catch2/internal/catch_enforce.hpp> 10 #include <catch2/internal/catch_string_manip.hpp> 11 #include <catch2/internal/catch_case_insensitive_comparisons.hpp> 12 13 #include <cassert> 14 #include <cctype> 15 #include <algorithm> 16 17 namespace Catch { 18 19 namespace { 20 using TCP_underlying_type = uint8_t; 21 static_assert(sizeof(TestCaseProperties) == sizeof(TCP_underlying_type), 22 "The size of the TestCaseProperties is different from the assumed size"); 23 24 TestCaseProperties operator|(TestCaseProperties lhs, TestCaseProperties rhs) { 25 return static_cast<TestCaseProperties>( 26 static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs) 27 ); 28 } 29 30 TestCaseProperties& operator|=(TestCaseProperties& lhs, TestCaseProperties rhs) { 31 lhs = static_cast<TestCaseProperties>( 32 static_cast<TCP_underlying_type>(lhs) | static_cast<TCP_underlying_type>(rhs) 33 ); 34 return lhs; 35 } 36 37 TestCaseProperties operator&(TestCaseProperties lhs, TestCaseProperties rhs) { 38 return static_cast<TestCaseProperties>( 39 static_cast<TCP_underlying_type>(lhs) & static_cast<TCP_underlying_type>(rhs) 40 ); 41 } 42 43 bool applies(TestCaseProperties tcp) { 44 static_assert(static_cast<TCP_underlying_type>(TestCaseProperties::None) == 0, 45 "TestCaseProperties::None must be equal to 0"); 46 return tcp != TestCaseProperties::None; 47 } 48 49 TestCaseProperties parseSpecialTag( StringRef tag ) { 50 if( !tag.empty() && tag[0] == '.' ) 51 return TestCaseProperties::IsHidden; 52 else if( tag == "!throws"_sr ) 53 return TestCaseProperties::Throws; 54 else if( tag == "!shouldfail"_sr ) 55 return TestCaseProperties::ShouldFail; 56 else if( tag == "!mayfail"_sr ) 57 return TestCaseProperties::MayFail; 58 else if( tag == "!nonportable"_sr ) 59 return TestCaseProperties::NonPortable; 60 else if( tag == "!benchmark"_sr ) 61 return TestCaseProperties::Benchmark | TestCaseProperties::IsHidden; 62 else 63 return TestCaseProperties::None; 64 } 65 bool isReservedTag( StringRef tag ) { 66 return parseSpecialTag( tag ) == TestCaseProperties::None 67 && tag.size() > 0 68 && !std::isalnum( static_cast<unsigned char>(tag[0]) ); 69 } 70 void enforceNotReservedTag( StringRef tag, SourceLineInfo const& _lineInfo ) { 71 CATCH_ENFORCE( !isReservedTag(tag), 72 "Tag name: [" << tag << "] is not allowed.\n" 73 << "Tag names starting with non alphanumeric characters are reserved\n" 74 << _lineInfo ); 75 } 76 77 std::string makeDefaultName() { 78 static size_t counter = 0; 79 return "Anonymous test case " + std::to_string(++counter); 80 } 81 82 StringRef extractFilenamePart(StringRef filename) { 83 size_t lastDot = filename.size(); 84 while (lastDot > 0 && filename[lastDot - 1] != '.') { 85 --lastDot; 86 } 87 --lastDot; 88 89 size_t nameStart = lastDot; 90 while (nameStart > 0 && filename[nameStart - 1] != '/' && filename[nameStart - 1] != '\\') { 91 --nameStart; 92 } 93 94 return filename.substr(nameStart, lastDot - nameStart); 95 } 96 97 // Returns the upper bound on size of extra tags ([#file]+[.]) 98 size_t sizeOfExtraTags(StringRef filepath) { 99 // [.] is 3, [#] is another 3 100 const size_t extras = 3 + 3; 101 return extractFilenamePart(filepath).size() + extras; 102 } 103 } // end unnamed namespace 104 105 bool operator<( Tag const& lhs, Tag const& rhs ) { 106 Detail::CaseInsensitiveLess cmp; 107 return cmp( lhs.original, rhs.original ); 108 } 109 bool operator==( Tag const& lhs, Tag const& rhs ) { 110 Detail::CaseInsensitiveEqualTo cmp; 111 return cmp( lhs.original, rhs.original ); 112 } 113 114 Detail::unique_ptr<TestCaseInfo> 115 makeTestCaseInfo(StringRef _className, 116 NameAndTags const& nameAndTags, 117 SourceLineInfo const& _lineInfo ) { 118 return Detail::make_unique<TestCaseInfo>(_className, nameAndTags, _lineInfo); 119 } 120 121 TestCaseInfo::TestCaseInfo(StringRef _className, 122 NameAndTags const& _nameAndTags, 123 SourceLineInfo const& _lineInfo): 124 name( _nameAndTags.name.empty() ? makeDefaultName() : _nameAndTags.name ), 125 className( _className ), 126 lineInfo( _lineInfo ) 127 { 128 StringRef originalTags = _nameAndTags.tags; 129 // We need to reserve enough space to store all of the tags 130 // (including optional hidden tag and filename tag) 131 auto requiredSize = originalTags.size() + sizeOfExtraTags(_lineInfo.file); 132 backingTags.reserve(requiredSize); 133 134 // We cannot copy the tags directly, as we need to normalize 135 // some tags, so that [.foo] is copied as [.][foo]. 136 size_t tagStart = 0; 137 size_t tagEnd = 0; 138 bool inTag = false; 139 for (size_t idx = 0; idx < originalTags.size(); ++idx) { 140 auto c = originalTags[idx]; 141 if (c == '[') { 142 CATCH_ENFORCE( 143 !inTag, 144 "Found '[' inside a tag while registering test case '" 145 << _nameAndTags.name << "' at " << _lineInfo ); 146 147 inTag = true; 148 tagStart = idx; 149 } 150 if (c == ']') { 151 CATCH_ENFORCE( 152 inTag, 153 "Found unmatched ']' while registering test case '" 154 << _nameAndTags.name << "' at " << _lineInfo ); 155 156 inTag = false; 157 tagEnd = idx; 158 assert(tagStart < tagEnd); 159 160 // We need to check the tag for special meanings, copy 161 // it over to backing storage and actually reference the 162 // backing storage in the saved tags 163 StringRef tagStr = originalTags.substr(tagStart+1, tagEnd - tagStart - 1); 164 CATCH_ENFORCE( !tagStr.empty(), 165 "Found an empty tag while registering test case '" 166 << _nameAndTags.name << "' at " 167 << _lineInfo ); 168 169 enforceNotReservedTag(tagStr, lineInfo); 170 properties |= parseSpecialTag(tagStr); 171 // When copying a tag to the backing storage, we need to 172 // check if it is a merged hide tag, such as [.foo], and 173 // if it is, we need to handle it as if it was [foo]. 174 if (tagStr.size() > 1 && tagStr[0] == '.') { 175 tagStr = tagStr.substr(1, tagStr.size() - 1); 176 } 177 // We skip over dealing with the [.] tag, as we will add 178 // it later unconditionally and then sort and unique all 179 // the tags. 180 internalAppendTag(tagStr); 181 } 182 } 183 CATCH_ENFORCE( !inTag, 184 "Found an unclosed tag while registering test case '" 185 << _nameAndTags.name << "' at " << _lineInfo ); 186 187 188 // Add [.] if relevant 189 if (isHidden()) { 190 internalAppendTag("."_sr); 191 } 192 193 // Sort and prepare tags 194 std::sort(begin(tags), end(tags)); 195 tags.erase(std::unique(begin(tags), end(tags)), 196 end(tags)); 197 } 198 199 bool TestCaseInfo::isHidden() const { 200 return applies( properties & TestCaseProperties::IsHidden ); 201 } 202 bool TestCaseInfo::throws() const { 203 return applies( properties & TestCaseProperties::Throws ); 204 } 205 bool TestCaseInfo::okToFail() const { 206 return applies( properties & (TestCaseProperties::ShouldFail | TestCaseProperties::MayFail ) ); 207 } 208 bool TestCaseInfo::expectedToFail() const { 209 return applies( properties & (TestCaseProperties::ShouldFail) ); 210 } 211 212 void TestCaseInfo::addFilenameTag() { 213 std::string combined("#"); 214 combined += extractFilenamePart(lineInfo.file); 215 internalAppendTag(combined); 216 } 217 218 std::string TestCaseInfo::tagsAsString() const { 219 std::string ret; 220 // '[' and ']' per tag 221 std::size_t full_size = 2 * tags.size(); 222 for (const auto& tag : tags) { 223 full_size += tag.original.size(); 224 } 225 ret.reserve(full_size); 226 for (const auto& tag : tags) { 227 ret.push_back('['); 228 ret += tag.original; 229 ret.push_back(']'); 230 } 231 232 return ret; 233 } 234 235 void TestCaseInfo::internalAppendTag(StringRef tagStr) { 236 backingTags += '['; 237 const auto backingStart = backingTags.size(); 238 backingTags += tagStr; 239 const auto backingEnd = backingTags.size(); 240 backingTags += ']'; 241 tags.emplace_back(StringRef(backingTags.c_str() + backingStart, backingEnd - backingStart)); 242 } 243 244 bool operator<( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) { 245 // We want to avoid redoing the string comparisons multiple times, 246 // so we store the result of a three-way comparison before using 247 // it in the actual comparison logic. 248 const auto cmpName = lhs.name.compare( rhs.name ); 249 if ( cmpName != 0 ) { 250 return cmpName < 0; 251 } 252 const auto cmpClassName = lhs.className.compare( rhs.className ); 253 if ( cmpClassName != 0 ) { 254 return cmpClassName < 0; 255 } 256 return lhs.tags < rhs.tags; 257 } 258 259 TestCaseInfo const& TestCaseHandle::getTestCaseInfo() const { 260 return *m_info; 261 } 262 263 } // end namespace Catch