1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
// 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 <catch2/catch_test_case_info.hpp>
#include <catch2/internal/catch_enforce.hpp>
#include <catch2/internal/catch_string_manip.hpp>
#include <catch2/internal/catch_case_insensitive_comparisons.hpp>
#include <catch2/internal/catch_test_registry.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;
}
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;
} 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
|