broken.cpp (8081B)
1 // Broken - Lightweight unit testing for C++ 2 // 3 // This is free and unencumbered software released into the public domain. 4 // 5 // Anyone is free to copy, modify, publish, use, compile, sell, or 6 // distribute this software, either in source code form or as a compiled 7 // binary, for any purpose, commercial or non-commercial, and by any 8 // means. 9 // 10 // In jurisdictions that recognize copyright laws, the author or authors 11 // of this software dedicate any and all copyright interest in the 12 // software to the public domain. We make this dedication for the benefit 13 // of the public at large and to the detriment of our heirs and 14 // successors. We intend this dedication to be an overt act of 15 // relinquishment in perpetuity of all present and future rights to this 16 // software under copyright law. 17 // 18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 22 // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 // OTHER DEALINGS IN THE SOFTWARE. 25 // 26 // For more information, please refer to <http://unlicense.org> 27 28 #include "broken.h" 29 #include <stdarg.h> 30 31 // Broken - Globals 32 // ================ 33 34 // Zero initialized globals. 35 struct BrokenGlobal { 36 // Application arguments. 37 int _argc; 38 const char** _argv; 39 40 // Output file. 41 FILE* _file; 42 43 // Unit tests. 44 BrokenAPI::Unit* _unitList; 45 BrokenAPI::Unit* _unitRunning; 46 47 bool has_arg(const char* a) const noexcept { 48 for (int i = 1; i < _argc; i++) 49 if (strcmp(_argv[i], a) == 0) 50 return true; 51 return false; 52 } 53 54 inline FILE* file() const noexcept { return _file ? _file : stdout; } 55 }; 56 static BrokenGlobal _brokenGlobal; 57 58 // Broken - API 59 // ============ 60 61 // Get whether the string `a` starts with string `b`. 62 static bool BrokenAPI_startsWith(const char* a, const char* b) noexcept { 63 for (size_t i = 0; ; i++) { 64 if (b[i] == '\0') return true; 65 if (a[i] != b[i]) return false; 66 } 67 } 68 69 //! Compares names and priority of two unit tests. 70 static int BrokenAPI_compareUnits(const BrokenAPI::Unit* a, const BrokenAPI::Unit* b) noexcept { 71 if (a->priority == b->priority) 72 return strcmp(a->name, b->name); 73 else 74 return a->priority > b->priority ? 1 : -1; 75 } 76 77 // Get whether the strings `a` and `b` are equal, ignoring case and treating 78 // `-` as `_`. 79 static bool BrokenAPI_matchesFilter(const char* a, const char* b) noexcept { 80 for (size_t i = 0; ; i++) { 81 int ca = (unsigned char)a[i]; 82 int cb = (unsigned char)b[i]; 83 84 // If filter is defined as wildcard the rest automatically matches. 85 if (cb == '*') 86 return true; 87 88 if (ca == '-') ca = '_'; 89 if (cb == '-') cb = '_'; 90 91 if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A'; 92 if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A'; 93 94 if (ca != cb) 95 return false; 96 97 if (ca == '\0') 98 return true; 99 } 100 } 101 102 static bool BrokenAPI_canRun(BrokenAPI::Unit* unit) noexcept { 103 BrokenGlobal& global = _brokenGlobal; 104 105 int i, argc = global._argc; 106 const char** argv = global._argv; 107 108 const char* unitName = unit->name; 109 bool has_filter = false; 110 111 for (i = 1; i < argc; i++) { 112 const char* arg = argv[i]; 113 114 if (BrokenAPI_startsWith(arg, "--run-") && strcmp(arg, "--run-all") != 0) { 115 has_filter = true; 116 117 if (BrokenAPI_matchesFilter(unitName, arg + 6)) 118 return true; 119 } 120 } 121 122 // If no filter has been specified the default is to run. 123 return !has_filter; 124 } 125 126 static void BrokenAPI_runUnit(BrokenAPI::Unit* unit) noexcept { 127 BrokenAPI::info("Running %s", unit->name); 128 129 _brokenGlobal._unitRunning = unit; 130 unit->entry(); 131 _brokenGlobal._unitRunning = NULL; 132 } 133 134 static void BrokenAPI_runAll() noexcept { 135 BrokenAPI::Unit* unit = _brokenGlobal._unitList; 136 137 bool has_units = unit != NULL; 138 size_t count = 0; 139 int currentPriority = 0; 140 141 while (unit != NULL) { 142 if (BrokenAPI_canRun(unit)) { 143 if (currentPriority != unit->priority) { 144 if (count) 145 INFO(""); 146 INFO("[[Priority=%d]]", unit->priority); 147 } 148 149 currentPriority = unit->priority; 150 BrokenAPI_runUnit(unit); 151 count++; 152 } 153 unit = unit->next; 154 } 155 156 if (count) { 157 INFO("\nSuccess:"); 158 INFO(" All tests passed!"); 159 } 160 else { 161 INFO("\nWarning:"); 162 INFO(" No units %s!", has_units ? "matched the filter" : "defined"); 163 } 164 } 165 166 static void BrokenAPI_listAll() noexcept { 167 BrokenAPI::Unit* unit = _brokenGlobal._unitList; 168 169 if (unit != NULL) { 170 INFO("Units:"); 171 do { 172 INFO(" %s [priority=%d]", unit->name, unit->priority); 173 unit = unit->next; 174 } while (unit != NULL); 175 } 176 else { 177 INFO("Warning:"); 178 INFO(" No units defined!"); 179 } 180 } 181 182 bool BrokenAPI::has_arg(const char* name) noexcept { 183 return _brokenGlobal.has_arg(name); 184 } 185 186 void BrokenAPI::addUnit(Unit* unit) noexcept { 187 Unit** pPrev = &_brokenGlobal._unitList; 188 Unit* current = *pPrev; 189 190 // C++ static initialization doesn't guarantee anything. We sort all units by 191 // name so the execution will always happen in deterministic order. 192 while (current != NULL) { 193 if (BrokenAPI_compareUnits(current, unit) >= 0) 194 break; 195 196 pPrev = ¤t->next; 197 current = *pPrev; 198 } 199 200 *pPrev = unit; 201 unit->next = current; 202 } 203 204 void BrokenAPI::setOutputFile(FILE* file) noexcept { 205 BrokenGlobal& global = _brokenGlobal; 206 207 global._file = file; 208 } 209 210 int BrokenAPI::run(int argc, const char* argv[], Entry on_before_run, Entry onAfterRun) { 211 BrokenGlobal& global = _brokenGlobal; 212 213 global._argc = argc; 214 global._argv = argv; 215 216 if (global.has_arg("--help")) { 217 INFO("Options:"); 218 INFO(" --help - print this usage"); 219 INFO(" --list - list all tests"); 220 INFO(" --run-... - run a test(s), trailing wildcards supported"); 221 INFO(" --run-all - run all tests (default)"); 222 return 0; 223 } 224 225 if (global.has_arg("--list")) { 226 BrokenAPI_listAll(); 227 return 0; 228 } 229 230 if (on_before_run) 231 on_before_run(); 232 233 // We don't care about filters here, it's implemented by `runAll`. 234 BrokenAPI_runAll(); 235 236 if (onAfterRun) 237 onAfterRun(); 238 239 return 0; 240 } 241 242 static void BrokenAPI_printMessage(const char* prefix, const char* fmt, va_list ap) noexcept { 243 BrokenGlobal& global = _brokenGlobal; 244 FILE* dst = global.file(); 245 246 if (!fmt || fmt[0] == '\0') { 247 fprintf(dst, "\n"); 248 } 249 else { 250 // This looks scary, but we really want to use only a single call to vfprintf() 251 // in multithreaded code. So we change the format a bit if necessary. 252 enum : unsigned { kBufferSize = 512 }; 253 char staticBuffer[512]; 254 255 size_t fmtSize = strlen(fmt); 256 size_t prefix_size = strlen(prefix); 257 258 char* fmtBuf = staticBuffer; 259 if (fmtSize > kBufferSize - 2 - prefix_size) 260 fmtBuf = static_cast<char*>(malloc(fmtSize + prefix_size + 2)); 261 262 if (!fmtBuf) { 263 fprintf(dst, "%sCannot allocate buffer for vfprintf()\n", prefix); 264 } 265 else { 266 memcpy(fmtBuf, prefix, prefix_size); 267 memcpy(fmtBuf + prefix_size, fmt, fmtSize); 268 269 fmtSize += prefix_size; 270 if (fmtBuf[fmtSize - 1] != '\n') 271 fmtBuf[fmtSize++] = '\n'; 272 fmtBuf[fmtSize] = '\0'; 273 274 vfprintf(dst, fmtBuf, ap); 275 276 if (fmtBuf != staticBuffer) 277 free(fmtBuf); 278 } 279 } 280 281 fflush(dst); 282 } 283 284 void BrokenAPI::info(const char* fmt, ...) noexcept { 285 BrokenGlobal& global = _brokenGlobal; 286 287 va_list ap; 288 va_start(ap, fmt); 289 BrokenAPI_printMessage(global._unitRunning ? " " : "", fmt, ap); 290 va_end(ap); 291 } 292 293 void BrokenAPI::fail(const char* file, int line, const char* expression, const char* fmt, ...) noexcept { 294 BrokenGlobal& global = _brokenGlobal; 295 FILE* dst = global.file(); 296 297 fprintf(dst, " FAILED: %s\n", expression); 298 299 if (fmt) { 300 va_list ap; 301 va_start(ap, fmt); 302 BrokenAPI_printMessage(" REASON: ", fmt, ap); 303 va_end(ap); 304 } 305 306 fprintf(dst, " SOURCE: %s (Line: %d)\n", file, line); 307 fflush(dst); 308 309 abort(); 310 }