HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_test_registry.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // shuffle()
4#include <chrono>
5#include <iomanip>
6#include <iostream>
7#include <random>
8#include <sstream>
9#include <string>
10#include <unordered_set>
11#include <vector>
12
17
18namespace hart
19{
20
21/// @brief Determines whether the task is a test or a generator
22/// @private
23enum class TaskCategory
24{
25 test,
26 generate
27};
28
29/// @brief Runs the test cases
30/// @details For internal use by HART. You're not supposed to interact with it directly, only through the macros
31/// such as @ref HART_RUN_ALL_TESTS(), @ref HART_TEST(), @ref HART_TEST_WITH_TAGS(), @ref HART_GENERATE(), @ref HART_GENERATE_WITH_TAGS()
32/// @ingroup TestRunner
34public:
35
36 /// @brief Gets the singleton instance
38 {
39 static TestRegistry reg;
40 return reg;
41 }
42
43 /// @brief Adds a task (test or generator)
44 /// @details Gets called when a test case is declared with a macro like @ref HART_TEST()
45 /// @private
46 void add (const std::string& name, const std::string& tags, const std::string& file, int line, TaskCategory testCategory, void (*func)())
47 {
48 std::unordered_set<std::string>& registeredNamesContainer =
49 testCategory == TaskCategory::test
50 ? registeredTestNames
51 : registeredGeneratorNames;
52
53 const auto insertResult = registeredNamesContainer.insert (name);
54 const bool isDuplicate = ! insertResult.second;
55
56 if (isDuplicate)
57 HART_THROW_OR_RETURN_VOID (hart::ValueError, std::string ("Duplicate test case name found: ") + name);
58
59 std::vector<TaskInfo>& tasks =
60 testCategory == TaskCategory::test
61 ? tests
62 : generators;
63
64 tasks.emplace_back (TaskInfo {name, tags, file, line, func});
65 }
66
67 /// @brief Runs all tests or generators
68 int runAll()
69 {
70 // TODO: Make data root dir if set, but doesn't exist
71 // TODO: Add support for runnings tasks in a thread pool
72
73 std::cout << hartAsciiArt << std::endl;
74
75 std::vector<TaskInfo>& taskPool =
77 ? generators
78 : tests;
79 std::vector<TaskInfo> tasks;
80 const std::string& requestedTagsUnparsed = CLIConfig::getInstance().getTags();
81 std::unordered_set<std::string> requestedTags;
82
83 if (requestedTagsUnparsed.empty())
84 {
85 tasks = std::move (taskPool);
86 }
87 else
88 {
89 requestedTags = parseTags (requestedTagsUnparsed);
90
91 for (TaskInfo& task : taskPool)
92 {
93 if (task.tags.empty())
94 continue;
95
96 std::unordered_set<std::string> taskTags = parseTags (task.tags);
97
98 if (tagsMatch (requestedTags, taskTags))
99 tasks.emplace_back (std::move (task));
100 }
101 }
102
103 if (tasks.size() == 0)
104 {
105 std::cout << "Nothing to run!" << std::endl;
106 return 0;
107 }
108
110 shuffleTasks (tasks);
111
112 for (const TaskInfo& task : tasks)
113 runTask (task);
114
115 std::cout << std::endl;
116 std::cout << "[ PASSED ] " << tasksPassed << '/' << tasks.size() << std::endl;
117
118 if (tasksFailed > 0)
119 std::cout << "[ FAILED ] " << tasksFailed << '/' << tasks.size() << std::endl;
120
121 const char* resultAsciiArt = tasksFailed > 0 ? failAsciiArt : passAsciiArt;
122 std::cout << std::endl << resultAsciiArt << std::endl;
123 return (int) (tasksFailed != 0);
124 }
125
126private:
127 struct TaskInfo
128 {
129 std::string name;
130 std::string tags;
131 std::string file;
132 int line;
133 void (*func)();
134 };
135
136 TestRegistry() = default; // Private ctor for singleton
137 std::vector<TaskInfo> tests;
138 std::vector<TaskInfo> generators;
139 std::unordered_set<std::string> registeredTestNames;
140 std::unordered_set<std::string> registeredGeneratorNames;
141
142 size_t tasksPassed = 0;
143 size_t tasksFailed = 0;
144
145 void runTask (const TaskInfo& task)
146 {
147 std::cout << "[ ... ] Running " << task.name;
148 bool assertionFailed = false;
149 std::string assertionFailMessage;
150 ExpectationFailureMessages::clear();
151
152 const auto timestampStart = std::chrono::high_resolution_clock::now();
153
154 try
155 {
156 task.func();
157 }
158 catch (const hart::TestAssertException& e)
159 {
160 assertionFailMessage = e.what();
161 assertionFailed = true;
162 }
163 catch (const hart::ConfigurationError& e)
164 {
165 assertionFailMessage = e.what();
166 assertionFailed = true;
167 }
168
169 const auto timestampFinish = std::chrono::high_resolution_clock::now();
170 const auto testDuration = timestampFinish - timestampStart;
171
172 std::cout << '\r';
173 const bool expectationsFailed = ExpectationFailureMessages::get().size() > 0;
174 const std::string testDurationLabel = formatDuration (testDuration);
175
176 if (assertionFailed || expectationsFailed)
177 {
178 // TODO: It would be nice to escape the characters in task.name that need to be escaped for taskSignature... but it's not very important.
179 constexpr char separator[] = "-------------------------------------------";
180 const bool isGenerateTask = CLIConfig::getInstance().shouldRunGenerators();
181 const std::string taskSignature =
182 isGenerateTask
183 ? (task.tags.empty() ? "HART_GENERATE (\"" + task.name + "\")" : "HART_GENERATE_WITH_TAGS (\"" + task.name + "\", " + task.tags + "\")")
184 : (task.tags.empty() ? "HART_TEST (\"" + task.name + "\")" : "HART_TEST_WITH_TAGS (\"" + task.name + "\", " + task.tags + "\")");
185
186 std::cout
187 << "[ </3 ] " << testDurationLabel << task.name << " - failed" << std::endl
188 << separator << std::endl
189 << task.file << ':' << task.line << std::endl
190 << taskSignature << std::endl;
191
192 if (assertionFailed)
193 {
194 std::cout << separator << std::endl << assertionFailMessage << std::endl;
195 }
196
197 for (const std::string& expectationFailureMessage : ExpectationFailureMessages::get())
198 {
199 std::cout << separator << std::endl << expectationFailureMessage << std::endl;
200 }
201
202 std::cout << separator << std::endl;
203 ++tasksFailed;
204 }
205 else
206 {
207 std::cout << "[ <3 ] " << testDurationLabel << task.name << " - passed" << std::endl;
208 ++tasksPassed;
209 }
210 }
211
212 static void shuffleTasks (std::vector<TaskInfo>& tasks)
213 {
215 std::shuffle (tasks.begin(), tasks.end(), rng);
216 }
217
218 static std::string formatDuration (std::chrono::high_resolution_clock::duration duration)
219 {
220 using std::chrono::duration_cast;
221 using std::chrono::microseconds;
222 const long long int durationUs = duration_cast<microseconds> (duration).count();
223 constexpr int targetWidth = 7;
224 std::ostringstream oss;
225 oss << '[';
226
227 if (durationUs >= 1000000)
228 {
229 const double durationSeconds = durationUs / 1.0e6;
230 oss << std::setw (targetWidth - 2) << std::right
231 << std::fixed << std::setprecision (2)
232 << durationSeconds << " s] ";
233 }
234 else if (durationUs >= 1000)
235 {
236 const long long int durationMs = durationUs / 1000;
237 oss << std::setw (targetWidth - 3) << std::right
238 << durationMs << " ms] ";
239 }
240 else
241 {
242 oss << std::setw (targetWidth - 3) << std::right
243 << durationUs << " us] ";
244 }
245
246 return oss.str();
247 }
248
249 std::unordered_set<std::string> parseTags (const std::string& tagString)
250 {
251 std::unordered_set<std::string> tags;
252 size_t start = 0;
253 size_t end = 0;
254
255 while ((start = tagString.find ('[', end)) != std::string::npos)
256 {
257 end = tagString.find (']', start + 1);
258
259 if (end != std::string::npos)
260 tags.insert (tagString.substr (start + 1, end - start - 1));
261 }
262
263 return tags;
264 }
265
266 bool tagsMatch (const std::unordered_set<std::string>& requestedTags, const std::unordered_set<std::string>& testTags)
267 {
268 for (const std::string& tag : testTags)
269 if (requestedTags.find (tag) != requestedTags.end())
270 return true;
271
272 return false;
273 }
274};
275
276} // namespace hart
Thrown when the test runner is misconfigured.
Thrown by test asserts like HART_ASSERT_TRUE() and AudioTestBuilder::assertFalse()
Runs the test cases.
static TestRegistry & getInstance()
Gets the singleton instance.
int runAll()
Runs all tests or generators.
Thrown when an inappropriate value is encountered.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns otherwise.
static const char * hartAsciiArt
static const char * passAsciiArt
static const char * failAsciiArt
Holds values set by the user via CLI interface.
uint_fast32_t getRandomSeed()
Gets random seed set by a "`--seed`/`-s`" argument.
std::string & getTags()
static CLIConfig & getInstance()
Get the singleton instance.