1// [Broken]
2// Lightweight Unit Testing for C++.
3//
4// [License]
5// Public Domain (Unlicense)
6
7#include "./broken.h"
8#include <stdarg.h>
9
10// ============================================================================
11// [Broken - Global]
12// ============================================================================
13
14// Zero initialized globals.
15struct BrokenGlobal {
16 // Application arguments.
17 int _argc;
18 const char** _argv;
19
20 // Output file.
21 FILE* _file;
22
23 // Unit tests.
24 BrokenAPI::Unit* _unitList;
25 BrokenAPI::Unit* _unitRunning;
26
27 bool hasArg(const char* a) const noexcept {
28 for (int i = 1; i < _argc; i++)
29 if (strcmp(_argv[i], a) == 0)
30 return true;
31 return false;
32 }
33
34 inline FILE* file() const noexcept { return _file ? _file : stdout; }
35};
36static BrokenGlobal _brokenGlobal;
37
38// ============================================================================
39// [Broken - API]
40// ============================================================================
41
42// Get whether the string `a` starts with string `b`.
43static bool BrokenAPI_startsWith(const char* a, const char* b) noexcept {
44 for (size_t i = 0; ; i++) {
45 if (b[i] == '\0') return true;
46 if (a[i] != b[i]) return false;
47 }
48}
49
50// Get whether the strings `a` and `b` are equal, ignoring case and treating
51// `-` as `_`.
52static bool BrokenAPI_matchesFilter(const char* a, const char* b) noexcept {
53 for (size_t i = 0; ; i++) {
54 int ca = (unsigned char)a[i];
55 int cb = (unsigned char)b[i];
56
57 // If filter is defined as wildcard the rest automatically matches.
58 if (cb == '*')
59 return true;
60
61 if (ca == '-') ca = '_';
62 if (cb == '-') cb = '_';
63
64 if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A';
65 if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A';
66
67 if (ca != cb)
68 return false;
69
70 if (ca == '\0')
71 return true;
72 }
73}
74
75static bool BrokenAPI_canRun(BrokenAPI::Unit* unit) noexcept {
76 BrokenGlobal& global = _brokenGlobal;
77
78 int i, argc = global._argc;
79 const char** argv = global._argv;
80
81 const char* unitName = unit->name;
82 bool hasFilter = false;
83
84 for (i = 1; i < argc; i++) {
85 const char* arg = argv[i];
86
87 if (BrokenAPI_startsWith(arg, "--run-") && strcmp(arg, "--run-all") != 0) {
88 hasFilter = true;
89
90 if (BrokenAPI_matchesFilter(unitName, arg + 6))
91 return true;
92 }
93 }
94
95 // If no filter has been specified the default is to run.
96 return !hasFilter;
97}
98
99static void BrokenAPI_runUnit(BrokenAPI::Unit* unit) noexcept {
100 BrokenAPI::info("Running %s", unit->name);
101
102 _brokenGlobal._unitRunning = unit;
103 unit->entry();
104 _brokenGlobal._unitRunning = NULL;
105}
106
107static void BrokenAPI_runAll() noexcept {
108 BrokenAPI::Unit* unit = _brokenGlobal._unitList;
109
110 bool hasUnits = unit != NULL;
111 size_t count = 0;
112
113 while (unit != NULL) {
114 if (BrokenAPI_canRun(unit)) {
115 BrokenAPI_runUnit(unit);
116 count++;
117 }
118 unit = unit->next;
119 }
120
121 if (count) {
122 INFO("\nSuccess:");
123 INFO(" All tests passed!");
124 }
125 else {
126 INFO("\nWarning:");
127 INFO(" No units %s!", hasUnits ? "matched the filter" : "defined");
128 }
129}
130
131static void BrokenAPI_listAll() noexcept {
132 BrokenAPI::Unit* unit = _brokenGlobal._unitList;
133
134 if (unit != NULL) {
135 INFO("Units:");
136 do {
137 INFO(" %s", unit->name);
138 unit = unit->next;
139 } while (unit != NULL);
140 }
141 else {
142 INFO("Warning:");
143 INFO(" No units defined!");
144 }
145}
146
147bool BrokenAPI::hasArg(const char* name) noexcept {
148 return _brokenGlobal.hasArg(name);
149}
150
151void BrokenAPI::add(Unit* unit) noexcept {
152 Unit** pPrev = &_brokenGlobal._unitList;
153 Unit* current = *pPrev;
154
155 // C++ static initialization doesn't guarantee anything. We sort all units by
156 // name so the execution will always happen in deterministic order.
157 while (current != NULL) {
158 if (strcmp(current->name, unit->name) >= 0)
159 break;
160
161 pPrev = &current->next;
162 current = *pPrev;
163 }
164
165 *pPrev = unit;
166 unit->next = current;
167}
168
169void BrokenAPI::setOutputFile(FILE* file) noexcept {
170 BrokenGlobal& global = _brokenGlobal;
171
172 global._file = file;
173}
174
175int BrokenAPI::run(int argc, const char* argv[], Entry onBeforeRun, Entry onAfterRun) noexcept {
176 BrokenGlobal& global = _brokenGlobal;
177
178 global._argc = argc;
179 global._argv = argv;
180
181 if (global.hasArg("--help")) {
182 INFO("Options:");
183 INFO(" --help - print this usage");
184 INFO(" --list - list all tests");
185 INFO(" --run-... - run a test(s), trailing wildcards supported");
186 INFO(" --run-all - run all tests");
187 return 0;
188 }
189
190 if (global.hasArg("--list")) {
191 BrokenAPI_listAll();
192 return 0;
193 }
194
195 if (onBeforeRun)
196 onBeforeRun();
197
198 // We don't care about filters here, it's implemented by `runAll`.
199 BrokenAPI_runAll();
200
201 if (onAfterRun)
202 onAfterRun();
203
204 return 0;
205}
206
207static void BrokenAPI_printMessage(const char* prefix, const char* fmt, va_list ap) noexcept {
208 BrokenGlobal& global = _brokenGlobal;
209 FILE* dst = global.file();
210
211 if (!fmt || fmt[0] == '\0') {
212 fprintf(dst, "\n");
213 }
214 else {
215 // This looks scary, but we really want to use only a single call to vfprintf()
216 // in multithreaded code. So we change the format a bit if necessary.
217 enum : unsigned { kBufferSize = 512 };
218 char staticBuffer[512];
219
220 size_t fmtSize = strlen(fmt);
221 size_t prefixSize = strlen(prefix);
222
223 char* fmtBuf = staticBuffer;
224 if (fmtSize > kBufferSize - 2 - prefixSize)
225 fmtBuf = static_cast<char*>(malloc(fmtSize + prefixSize + 2));
226
227 if (!fmtBuf) {
228 fprintf(dst, "%sCannot allocate buffer for vfprintf()\n", prefix);
229 }
230 else {
231 memcpy(fmtBuf, prefix, prefixSize);
232 memcpy(fmtBuf + prefixSize, fmt, fmtSize);
233
234 fmtSize += prefixSize;
235 if (fmtBuf[fmtSize - 1] != '\n')
236 fmtBuf[fmtSize++] = '\n';
237 fmtBuf[fmtSize] = '\0';
238
239 vfprintf(dst, fmtBuf, ap);
240
241 if (fmtBuf != staticBuffer)
242 free(fmtBuf);
243 }
244 }
245
246 fflush(dst);
247}
248
249void BrokenAPI::info(const char* fmt, ...) noexcept {
250 BrokenGlobal& global = _brokenGlobal;
251 va_list ap;
252 va_start(ap, fmt);
253 BrokenAPI_printMessage(global._unitRunning ? " " : "", fmt, ap);
254 va_end(ap);
255}
256
257void BrokenAPI::fail(const char* file, int line, const char* fmt, ...) noexcept {
258 BrokenGlobal& global = _brokenGlobal;
259 FILE* dst = global.file();
260
261 va_list ap;
262 va_start(ap, fmt);
263 BrokenAPI_printMessage(" FAILED!", fmt, ap);
264 va_end(ap);
265
266 fprintf(dst, " File: %s (Line: %d)\n", file, line);
267 fflush(dst);
268
269 exit(1);
270}
271