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 += "&quot;";
258 				break;
259 			case '&':
260 				output += "&amp;";
261 				break;
262 			case '<':
263 				output += "&lt;";
264 				break;
265 			case '>':
266 				output += "&gt;";
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