summaryrefslogtreecommitdiff
path: root/uunit.h
blob: 92feb8d1a37bdb9e60ae71dd4566cb7fdcfb1d79 (plain)
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// -*- c++ -*-
// This is the header-only implementation of the uUnit unit-test framework.
// Copyright 2020 Bent Bisballe Nyeng (deva@aasimon.org)
// This file released under the CC0-1.0 license. See CC0-1.0 file for details.
#pragma once

#include <cstddef>
#include <iostream>
#include <list>
#include <vector>
#include <functional>
#include <string>
#include <sstream>
#include <fstream>
#include <cmath>
#include <exception>
#include <typeinfo>

class uUnit
{
public:
	uUnit()
	{
		if(uUnit::suite_list == nullptr)
		{
			uUnit::suite_list = this;
			return;
		}

		auto unit = uUnit::suite_list;
		while(unit->next_unit)
		{
			unit = unit->next_unit;
		}
		unit->next_unit = this;
	}

	virtual ~uUnit() = default;

	//! Overload to prepare stuff for each of the tests.
	virtual void setup() {}

	//! Overload to tear down stuff for each of the tests.
	virtual void teardown() {}

	struct test_result
	{
		std::string func;
		std::string file;
		std::size_t line;
		std::string msg;
		std::string failure_type;
		std::size_t id;
	};

	//! Run test
	//! \param test_suite the name of a test suite or null for all.
	//! \param test_name the name of a test name inside a test suite. Only valid
	//!  if test_suite is non-null. nullptr for all tests.
	static int runTests(std::ofstream& out)
	{
		std::size_t test_num{0};

		std::list<test_result> failed_tests;
		std::list<test_result> successful_tests;

		for(auto suite = uUnit::suite_list; suite; suite = suite->next_unit)
		{
			for(const auto& test : suite->tests)
			{
				++test_num;
				try
				{
					try
					{
						suite->setup();
						test.func();
					}
					catch(...)
					{
						// call teardown and ignore exceptions
						try { suite->teardown(); } catch(...) {}
						throw; // rethrow setup/test.func exception
					}
					suite->teardown();
				}
				catch(test_result& result)
				{
					status_cb(test.name, test.file, false);
					result.id = test_num;
					result.func = test.name;
					result.failure_type = "Assertion";
					failed_tests.push_back(result);
					continue;
				}
				catch(...)
				{
					status_cb(test.name, test.file, false);
					test_result result;
					result.id = test_num;
					result.func = test.name;
					result.file = test.file;
					result.line = 0;
					try
					{
						throw;
					}
					catch(std::exception& e)
					{
						result.msg = std::string("Uncaught exception: ") + e.what();
					}
					catch(...)
					{
						result.msg = "Uncaught exception without std::exception type";
					}
					result.failure_type = "Exception";
					failed_tests.push_back(result);
					continue;
				}
				status_cb(test.name, test.file, true);
				test_result result{test.name, {}, {}, {}, {}, {}};
				result.id = test_num;
				successful_tests.push_back(result);
			}
		}

		out << "<?xml version=\"1.0\" encoding='ISO-8859-1' standalone='yes' ?>" <<
			std::endl;
		out << "<TestRun>" << std::endl;
		out << "	<FailedTests>" << std::endl;
		for(const auto& test : failed_tests)
		{
			out << "		<FailedTest id=\"" << test.id << "\">" << std::endl; // constexpr newline cross-platform specifik
			out << "			<Name>" << sanitize(test.func) << "</Name>" << std::endl;
			out << "			<FailureType>" << sanitize(test.failure_type) <<
				"</FailureType>" << std::endl;
			out << "			<Location>" << std::endl;
			out << "				<File>" << sanitize(test.file) << "</File>" << std::endl;
			out << "				<Line>" << test.line << "</Line>" << std::endl;
			out << "			</Location>" << std::endl;
			out << "			<Message>" << sanitize(test.msg) << "</Message>" <<
				std::endl;
			out << "		</FailedTest>" << std::endl;
		}
		out << "	</FailedTests>" << std::endl;
		out << "	<SuccessfulTests>" << std::endl;
		for(const auto& test : successful_tests)
		{
			out << "		<Test id=\"" << test.id << "\">" << std::endl;
			out << "			<Name>" << sanitize(test.func) << "</Name>" << std::endl;
			out << "		</Test>" << std::endl;

		}
		out << "	</SuccessfulTests>" << std::endl;
		out << "	<Statistics>" << std::endl;
		out << "		<Tests>" << (successful_tests.size() + failed_tests.size()) <<
			"</Tests>" << std::endl;
		out << "		<FailuresTotal>" << failed_tests.size() << "</FailuresTotal>" <<
			std::endl;
		out << "		<Errors>0</Errors>" << std::endl;
		out << "		<Failures>" << failed_tests.size() << "</Failures>" <<
			std::endl;
		out << "	</Statistics>" << std::endl;
		out << "</TestRun>" << std::endl;

		return failed_tests.size() == 0 ? 0 : 1;
	}

	static std::function<void(const char*, const char*, bool)> status_cb;

	static void u_assert(bool value, const char* expr,
	                     const char* file, std::size_t line)
	{
		if(!value)
		{
			std::ostringstream ss;
			ss << "assertion failed" << std::endl <<
				"- Expression: " << expr << "" << std::endl;
			throw test_result{"", file, line, ss.str(), {}, {}};
		}
	}
	//! Convenience macro to pass along filename and linenumber
	#define uUNIT_ASSERT(value)	  \
		uUnit::u_assert(value, #value, __FILE__, __LINE__)
	#define uASSERT(...) uUNIT_ASSERT(__VA_ARGS__)

	static void assert_equal(double expected, double value,
	                         const char* file, std::size_t line)
	{
		if(std::fabs(expected - value) > 0.0000001)
		{
			std::ostringstream ss;
			ss << "equality assertion failed" << std::endl <<
				"- Expected: " << expected << "" << std::endl <<
				"- Actual  : " << value << "" << std::endl;
			throw test_result{"", file, line, ss.str(), {}, {}};
		}
	}

	template<typename T, typename U>
	static void assert_equal(const T& expected, const U& value,
	                         const char* file, std::size_t line)
	{
		if(expected != value)
		{
			std::ostringstream ss;
			ss << "equality assertion failed" << std::endl <<
				"- Expected: " << expected << "" << std::endl <<
				"- Actual  : " << value << "" << std::endl;
			throw test_result{"", file, line, ss.str(), {}, {}};
		}
	}
	//! Convenience macro to pass along filename and linenumber
	#define uUNIT_ASSERT_EQUAL(expected, value) \
		uUnit::assert_equal(expected, value, __FILE__, __LINE__)
	#define uASSERT_EQUAL(...) uUNIT_ASSERT_EQUAL(__VA_ARGS__)

	template<typename T>
	static void assert_throws(const char* expected, std::function<void()> expr,
	                          const char* file, std::size_t line)
	{
		try
		{
			expr();
			std::ostringstream ss;
			ss << "throws assertion failed" << std::endl <<
				"- Expected: " << expected << " to be thrown" << std::endl <<
				"- Actual  : no exception was thrown" << std::endl;
			throw test_result{"", file, line, ss.str(), {}, {}};
		}
		catch(const T& t)
		{
			// T was thrown as expected
		}
		catch(...)
		{
			std::ostringstream ss;
			ss << "throws assertion failed" << std::endl <<
				"- Expected: " << expected << " to be thrown" << std::endl <<
				"- Actual  : unexpected exception was thrown" << std::endl;
			throw test_result{"", file, line, ss.str(), {}, {}};
		}
	}
	#define uUNIT_ASSERT_THROWS(expected, expr) \
		uUnit::assert_throws<expected>(#expected, [&](){expr;}, __FILE__, __LINE__)
	#define uASSERT_THROWS(...) uUNIT_ASSERT_THROWS(__VA_ARGS__)

protected:
	template<typename O, typename F>
	void registerTest(O* obj, const F& fn, const char* name, const char* file)
	{
		tests.push_back({std::bind(fn, obj), name, file});
	}
	#define uUNIT_TEST(func)	  \
		registerTest(this, &func, #func, __FILE__)
	#define uTEST(...) uUNIT_TEST(__VA_ARGS__)

private:
	static std::string sanitize(const std::string& input)
	{
		std::string output;
		for(auto c : input)
		{
			switch(c)
			{
			case '"':
				output += "&quot;";
				break;
			case '&':
				output += "&amp;";
				break;
			case '<':
				output += "&lt;";
				break;
			case '>':
				output += "&gt;";
				break;
			default:
				output += c;
				break;
			}
		}
		return output;
	}

	static uUnit* suite_list;
	uUnit* next_unit{nullptr};

	struct test_t
	{
		std::function<void()> func;
		const char* name;
		const char* file;
	};

	std::vector<test_t> tests;
};

#ifdef uUNIT_MAIN

uUnit* uUnit::suite_list{nullptr};

namespace
{
//! Default implementation of test result reporter function.
//! Overload this with your own implementation by assigning the uUnit::status_cb
//! function pointer to a function with the same signature.
void report_result(const char* name, const char* file, bool success)
{
	(void)name;
	(void)file;
	if(success)
	{
		std::cout << ".";
	}
	else
	{
		std::cout << "F";
	}
	std::cout << std::flush;
}
}

std::function<void(const char*, const char*, bool)> uUnit::status_cb{report_result};

#endif