1 // -*- c++ -*-
2 // This is the header-only implementation of the uUnit unit-test framework.
3 // Copyright 2020 Bent Bisballe Nyeng (deva@aasimon.org)
4 // This file released under the CC0-1.0 license. See CC0-1.0 file for details.
5 #pragma once
6
7 #include <cstddef>
8 #include <iostream>
9 #include <list>
10 #include <vector>
11 #include <functional>
12 #include <string>
13 #include <sstream>
14 #include <fstream>
15 #include <cmath>
16 #include <exception>
17 #include <typeinfo>
18
19 class uUnit
20 {
21 public:
uUnit()22 uUnit()
23 {
24 if(uUnit::suite_list == nullptr)
25 {
26 uUnit::suite_list = this;
27 return;
28 }
29
30 auto unit = uUnit::suite_list;
31 while(unit->next_unit)
32 {
33 unit = unit->next_unit;
34 }
35 unit->next_unit = this;
36 }
37
38 virtual ~uUnit() = default;
39
40 //! Overload to prepare stuff for each of the tests.
setup()41 virtual void setup() {}
42
43 //! Overload to tear down stuff for each of the tests.
teardown()44 virtual void teardown() {}
45
46 struct test_result
47 {
48 std::string func;
49 std::string file;
50 std::size_t line;
51 std::string msg;
52 std::string failure_type;
53 int id;
54 };
55
56 //! Run test
57 //! \param test_suite the name of a test suite or null for all.
58 //! \param test_name the name of a test name inside a test suite. Only valid
59 //! if test_suite is non-null. nullptr for all tests.
runTests(std::ofstream & out)60 static int runTests(std::ofstream& out)
61 {
62 std::size_t test_num{0};
63
64 std::list<test_result> failed_tests;
65 std::list<test_result> successful_tests;
66
67 for(auto suite = uUnit::suite_list; suite; suite = suite->next_unit)
68 {
69 for(auto test : suite->tests)
70 {
71 ++test_num;
72 try
73 {
74 suite->setup();
75 test.func();
76 suite->teardown();
77 }
78 catch(test_result& result)
79 {
80 status_cb(test.name, test.file, false);
81 result.id = test_num;
82 result.func = test.name;
83 result.failure_type = "Assertion";
84 failed_tests.push_back(result);
85 continue;
86 }
87 catch(...)
88 {
89 status_cb(test.name, test.file, false);
90 test_result result;
91 result.id = test_num;
92 result.func = test.name;
93 result.file = test.file;
94 result.line = 0;
95 try
96 {
97 throw;
98 }
99 catch(std::exception& e)
100 {
101 result.msg = std::string("Uncaught exception: ") + e.what();
102 }
103 catch(...)
104 {
105 result.msg = "Uncaught exception without std::exception type";
106 }
107 result.failure_type = "Exception";
108 failed_tests.push_back(result);
109 continue;
110 }
111 status_cb(test.name, test.file, true);
112 test_result result{test.name};
113 result.id = test_num;
114 successful_tests.push_back(result);
115 }
116 }
117
118 out << "<?xml version=\"1.0\" encoding='ISO-8859-1' standalone='yes' ?>" <<
119 std::endl;
120 out << "<TestRun>" << std::endl;
121 out << " <FailedTests>" << std::endl;
122 for(const auto& test : failed_tests)
123 {
124 out << " <FailedTest id=\"" << test.id << "\">" << std::endl; // constexpr newline cross-platform specifik
125 out << " <Name>" << sanitize(test.func) << "</Name>" << std::endl;
126 out << " <FailureType>" << sanitize(test.failure_type) <<
127 "</FailureType>" << std::endl;
128 out << " <Location>" << std::endl;
129 out << " <File>" << sanitize(test.file) << "</File>" << std::endl;
130 out << " <Line>" << test.line << "</Line>" << std::endl;
131 out << " </Location>" << std::endl;
132 out << " <Message>" << sanitize(test.msg) << "</Message>" <<
133 std::endl;
134 out << " </FailedTest>" << std::endl;
135 }
136 out << " </FailedTests>" << std::endl;
137 out << " <SuccessfulTests>" << std::endl;
138 for(const auto& test : successful_tests)
139 {
140 out << " <Test id=\"" << test.id << "\">" << std::endl;
141 out << " <Name>" << sanitize(test.func) << "</Name>" << std::endl;
142 out << " </Test>" << std::endl;
143
144 }
145 out << " </SuccessfulTests>" << std::endl;
146 out << " <Statistics>" << std::endl;
147 out << " <Tests>" << (successful_tests.size() + failed_tests.size()) <<
148 "</Tests>" << std::endl;
149 out << " <FailuresTotal>" << failed_tests.size() << "</FailuresTotal>" <<
150 std::endl;
151 out << " <Errors>0</Errors>" << std::endl;
152 out << " <Failures>" << failed_tests.size() << "</Failures>" <<
153 std::endl;
154 out << " </Statistics>" << std::endl;
155 out << "</TestRun>" << std::endl;
156
157 return failed_tests.size() == 0 ? 0 : 1;
158 }
159
160 static std::function<void(const char*, const char*, bool)> status_cb;
161
162 protected:
163 template<typename O, typename F>
registerTest(O * obj,const F & fn,const char * name,const char * file)164 void registerTest(O* obj, const F& fn, const char* name, const char* file)
165 {
166 tests.push_back({std::bind(fn, obj), name, file});
167 }
168 #define uUNIT_TEST(func) \
169 registerTest(this, &func, #func, __FILE__)
170 #define uTEST(args...) uUNIT_TEST(args)
171
u_assert(bool value,const char * expr,const char * file,std::size_t line)172 void u_assert(bool value, const char* expr,
173 const char* file, std::size_t line)
174 {
175 if(!value)
176 {
177 std::ostringstream ss;
178 ss << "assertion failed" << std::endl <<
179 "- Expression: " << expr << "" << std::endl;
180 throw test_result{"", file, line, ss.str()};
181 }
182 }
183 //! Convenience macro to pass along filename and linenumber
184 #define uUNIT_ASSERT(value) \
185 u_assert(value, #value, __FILE__, __LINE__)
186 #define uASSERT(args...) uUNIT_ASSERT(args)
187
assert_equal(double expected,double value,const char * file,std::size_t line)188 void assert_equal(double expected, double value,
189 const char* file, std::size_t line)
190 {
191 if(std::fabs(expected - value) > 0.0000001)
192 {
193 std::ostringstream ss;
194 ss << "equality assertion failed" << std::endl <<
195 "- Expected: " << expected << "" << std::endl <<
196 "- Actual : " << value << "" << std::endl;
197 throw test_result{"", file, line, ss.str()};
198 }
199 }
200 template<typename T>
assert_equal(T expected,T value,const char * file,std::size_t line)201 void assert_equal(T expected, T value,
202 const char* file, std::size_t line)
203 {
204 if(expected != value)
205 {
206 std::ostringstream ss;
207 ss << "equality assertion failed" << std::endl <<
208 "- Expected: " << expected << "" << std::endl <<
209 "- Actual : " << value << "" << std::endl;
210 throw test_result{"", file, line, ss.str()};
211 }
212 }
213 //! Convenience macro to pass along filename and linenumber
214 #define uUNIT_ASSERT_EQUAL(expected, value) \
215 assert_equal(expected, value, __FILE__, __LINE__)
216 #define uASSERT_EQUAL(args...) uUNIT_ASSERT_EQUAL(args)
217
218 template<typename T>
assert_throws(const char * expected,std::function<void ()> expr,const char * file,std::size_t line)219 void assert_throws(const char* expected, std::function<void()> expr,
220 const char* file, std::size_t line)
221 {
222 try
223 {
224 expr();
225 std::ostringstream ss;
226 ss << "throws assertion failed" << std::endl <<
227 "- Expected: " << expected << " to be thrown" << std::endl <<
228 "- Actual : no exception was thrown" << std::endl;
229 throw test_result{"", file, line, ss.str()};
230 }
231 catch(const T& t)
232 {
233 // T was thrown as expected
234 }
235 catch(...)
236 {
237 std::ostringstream ss;
238 ss << "throws assertion failed" << std::endl <<
239 "- Expected: " << expected << " to be thrown" << std::endl <<
240 "- Actual : unexpected exception was thrown" << std::endl;
241 throw test_result{"", file, line, ss.str()};
242 }
243 }
244 #define uUNIT_ASSERT_THROWS(expected, expr) \
245 assert_throws<expected>(#expected, [&](){ expr; }, __FILE__, __LINE__)
246 #define uASSERT_THROWS(args...) uUNIT_ASSERT_THROWS(args)
247
248 private:
sanitize(const std::string & input)249 static std::string sanitize(const std::string& input)
250 {
251 std::string output;
252 for(auto c : input)
253 {
254 switch(c)
255 {
256 case '"':
257 output += """;
258 break;
259 case '&':
260 output += "&";
261 break;
262 case '<':
263 output += "<";
264 break;
265 case '>':
266 output += ">";
267 break;
268 default:
269 output += c;
270 break;
271 }
272 }
273 return output;
274 }
275
276 static uUnit* suite_list;
277 uUnit* next_unit{nullptr};
278
279 struct test_t
280 {
281 std::function<void()> func;
282 const char* name;
283 const char* file;
284 };
285
286 std::vector<test_t> tests;
287 };
288
289 #ifdef uUNIT_MAIN
290
291 uUnit* uUnit::suite_list{nullptr};
292
293 namespace
294 {
295 //! Default implementation of test result reporter function.
296 //! Overload this with your own implementation by assigning the uUnit::status_cb
297 //! function pointer to a function with the same signature.
report_result(const char * name,const char * file,bool success)298 void report_result(const char* name, const char* file, bool success)
299 {
300 if(success)
301 {
302 std::cout << ".";
303 }
304 else
305 {
306 std::cout << "F";
307 }
308 std::cout << std::flush;
309 }
310 }
311
312 std::function<void(const char*, const char*, bool)> uUnit::status_cb{report_result};
313
314 #endif
315