CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
UnitTestFramework.cpp
Go to the documentation of this file.
1
7// ============================================================================
8
9#include "UnitTestFramework.h"
10#include "PsyTime.h"
11#include "jsmn.h"
12
13#include <stdio.h>
14#include <stdarg.h>
15#include <stdlib.h>
16#include <string.h>
17#include <math.h>
18
19#ifdef WINDOWS
20 #include <io.h>
21 #define ISATTY(fd) _isatty(fd)
22 #define FILENO(f) _fileno(f)
23#else
24 #include <unistd.h>
25 #include <dirent.h>
26 #include <sys/wait.h>
27 #include <signal.h>
28 #include <execinfo.h>
29 #include <fcntl.h>
30 #define ISATTY(fd) isatty(fd)
31 #define FILENO(f) fileno(f)
32 #define UT_HAVE_FORK 1
33#endif
34
35// Hard per-test wall-clock cap when running each test in its own process. A
36// test that exceeds this is killed and reported as a timeout (rather than
37// hanging the whole suite).
38#define UT_TEST_TIMEOUT_US (30ULL * 1000000ULL)
39
40namespace cmlabs {
41
42// Best-effort removal of stale CMSDK shared-memory segments left behind by a
43// previous test process that was killed (kill -9) mid-operation. Such a process
44// can leave a PROCESS_SHARED pthread mutex locked inside a persisted segment,
45// which would deadlock the next run that re-opens it. Tests run under system id
46// 0, so clearing these test artifacts before a run makes the suite self-healing
47// and safely repeatable (e.g. for an agent looping to chase a perf target).
48// This assumes no live Psyclone node is using system id 0 on this machine while
49// tests run (which is already required by the "another node running" guard).
50static void ut_clearSegmentsIn(const char* dir, const char* prefix) {
51#ifndef WINDOWS
52 DIR* d = opendir(dir);
53 if (!d) return;
54 size_t plen = strlen(prefix);
55 struct dirent* e;
56 while ((e = readdir(d)) != NULL) {
57 if (strncmp(e->d_name, prefix, plen) == 0) {
58 std::string path = std::string(dir) + "/" + e->d_name;
59 unlink(path.c_str());
60 }
61 }
62 closedir(d);
63#endif
64}
65
67#ifndef WINDOWS
68 // macOS file-backed segments: /tmp/Psyclone_Shmem_<name>
69 ut_clearSegmentsIn("/tmp", "Psyclone_Shmem_");
70 // Linux / Cygwin POSIX shm: shm_open("/Shmem_<name>") -> /dev/shm/Shmem_<name>
71 ut_clearSegmentsIn("/dev/shm", "Shmem_");
72#endif
73}
74
75#ifdef UT_HAVE_FORK
76// ---- Timeout diagnostics for forked tests --------------------------------
77// When a test hangs, the parent's only signal is the wall-clock timeout, and
78// the child's buffered output is lost when it is killed. To make a hang
79// diagnosable we (a) have the child record its current phase to a file on every
80// progress() call (read back by the parent on timeout) and (b) on timeout ask
81// the child, via SIGUSR1, to dump a native stack trace of the stuck thread to a
82// file before we kill it. backtrace_symbols_fd() is async-signal-safe, so the
83// handler is safe to run from a signal.
84static int ut_traceFd = -1;
85static void ut_timeoutTraceHandler(int) {
86 if (ut_traceFd >= 0) {
87 void* frames[128];
88 int n = backtrace(frames, 128);
89 backtrace_symbols_fd(frames, n, ut_traceFd);
90 }
91 _exit(3);
92}
93static void ut_installTimeoutTraceHandler(const std::string& tracePath) {
94 ut_traceFd = open(tracePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
95 signal(SIGUSR1, ut_timeoutTraceHandler);
96}
97
98// Read back the child's last-recorded phase ("percent\taction") as a short
99// human-readable note for the status line.
100static std::string ut_readProgressNote(const std::string& path) {
101 FILE* f = fopen(path.c_str(), "r");
102 if (!f) return "";
103 char buf[512];
104 size_t r = fread(buf, 1, sizeof(buf) - 1, f);
105 fclose(f);
106 buf[r] = 0;
107 std::string s(buf);
108 std::string::size_type tab = s.find('\t');
109 if (tab == std::string::npos) return s;
110 return s.substr(0, tab) + "% \"" + s.substr(tab + 1) + "\"";
111}
112
113// Print the captured stack trace (if any) as indented diagnostic lines.
114static void ut_printTraceFile(const std::string& path) {
115 FILE* f = fopen(path.c_str(), "r");
116 if (!f) return;
117 char line[1024];
118 bool header = false;
119 while (fgets(line, sizeof(line), f)) {
120 if (!header) { printf(" stack trace where the test hung:\n"); header = true; }
121 printf(" %s", line);
122 }
123 if (header) { printf("\n"); fflush(stdout); }
124 fclose(f);
125}
126#endif
127
128// ----------------------------------------------------------------------------
129// Layout constants for consistent output
130// ----------------------------------------------------------------------------
131#define UT_NAME_WIDTH 28 // width of the test-name column on the status line
132#define UT_INDENT " " // 11 spaces, lines up after "[ PASS ] "
133#define UT_METRIC_WIDTH 26 // width of the metric-name column on a subline
134
135// ----------------------------------------------------------------------------
136// Singleton
137// ----------------------------------------------------------------------------
138UnitTestRunner& UnitTestRunner::instance() {
139 static UnitTestRunner theInstance;
140 return theInstance;
141}
142
143UnitTestRunner::UnitTestRunner()
144 : current(NULL), verboseFlag(false), writeJSONFlag(true),
145 forkPerTest(true), isTTY(false), lastProgressLen(0), outputDir("."),
146 compareLoaded(false) {
147 isTTY = ISATTY(FILENO(stdout)) ? true : false;
148}
149
150// ----------------------------------------------------------------------------
151// Registration & configuration
152// ----------------------------------------------------------------------------
153void UnitTestRunner::registerTest(const char* name, UnitTestFunc func,
154 const char* description, const char* category,
155 bool inDefaultRun) {
156 if (!name || !func) return;
157 // Replace if a test with this name was already registered.
158 for (size_t i = 0; i < tests.size(); i++) {
159 if (tests[i].name == name) {
160 tests[i].func = func;
161 tests[i].description = description ? description : "";
162 tests[i].category = category ? category : "";
163 tests[i].inDefaultRun = inDefaultRun;
164 return;
165 }
166 }
167 UnitTestRecord rec;
168 rec.name = name;
169 rec.func = func;
170 rec.description = description ? description : "";
171 rec.category = category ? category : "";
172 rec.inDefaultRun = inDefaultRun;
173 tests.push_back(rec);
174}
175
176void UnitTestRunner::setVerbose(bool v) { verboseFlag = v; }
177void UnitTestRunner::setCompareFile(const char* p) { if (p) compareFile = p; }
178void UnitTestRunner::setOutputFile(const char* p) { if (p) outputFile = p; }
179void UnitTestRunner::setOutputDir(const char* d) { if (d) outputDir = d; }
180void UnitTestRunner::setWriteJSON(bool v) { writeJSONFlag = v; }
181void UnitTestRunner::setForkPerTest(bool v) { forkPerTest = v; }
182
183bool UnitTestRunner::hasTest(const char* name) const {
184 if (!name) return false;
185 for (size_t i = 0; i < tests.size(); i++)
186 if (stricmp(tests[i].name.c_str(), name) == 0) return true;
187 return false;
188}
189
190std::vector<std::string> UnitTestRunner::testNames() const {
191 std::vector<std::string> names;
192 for (size_t i = 0; i < tests.size(); i++) names.push_back(tests[i].name);
193 return names;
194}
195
197 printf("\nAvailable CMSDK unit tests (%u):\n\n", (uint32)tests.size());
198 std::string lastCat = "\x01";
199 for (size_t i = 0; i < tests.size(); i++) {
200 if (tests[i].category != lastCat) {
201 lastCat = tests[i].category;
202 printf(" [%s]\n", lastCat.empty() ? "general" : lastCat.c_str());
203 }
204 printf(" %-22s %s\n", tests[i].name.c_str(), tests[i].description.c_str());
205 }
206 printf("\nRun all with: test=cmsdk Run one with: test=<name>\n\n");
207}
208
209// ----------------------------------------------------------------------------
210// Hooks used by the unittest:: API for the currently running test
211// ----------------------------------------------------------------------------
212bool UnitTestRunner::hookVerbose() const { return verboseFlag; }
213
214void UnitTestRunner::hookProgress(int percent, const char* action) {
215 if (!current) return;
216 if (percent < 0) percent = 0;
217 if (percent > 100) percent = 100;
218
219 // Record the current phase to a file (forked child only) so that, if the test
220 // hangs and is killed by the timeout, the parent can still report which phase
221 // it was in. progress() is called only a handful of times per test, so this is
222 // cheap, and it happens regardless of whether stdout is a terminal.
223 if (!progressPath.empty()) {
224 FILE* pf = fopen(progressPath.c_str(), "w");
225 if (pf) { fprintf(pf, "%d\t%s", percent, action ? action : ""); fclose(pf); }
226 }
227
228 if (!isTTY) return; // keep piped/redirected logs clean
229
230 char buf[512];
231 int n = snprintf(buf, sizeof(buf), " [%3d%%] %-*.*s %s",
233 current->name.c_str(), action ? action : "");
234 if (n < 0) return;
235
236 // Pad to clear any longer text drawn previously.
237 int pad = lastProgressLen - n;
238 fputc('\r', stdout);
239 fputs(buf, stdout);
240 for (int i = 0; i < pad; i++) fputc(' ', stdout);
241 fputc('\r', stdout);
242 fflush(stdout);
243 lastProgressLen = (n > lastProgressLen) ? n : lastProgressLen;
244 // (we keep the max so the next pad clears the longest line)
245 lastProgressLen = n;
246}
247
248void UnitTestRunner::clearProgressLine() {
249 if (!isTTY || lastProgressLen <= 0) return;
250 fputc('\r', stdout);
251 for (int i = 0; i < lastProgressLen; i++) fputc(' ', stdout);
252 fputc('\r', stdout);
253 fflush(stdout);
254 lastProgressLen = 0;
255}
256
257void UnitTestRunner::hookMetric(const char* name, double value, const char* unit, bool higherIsBetter) {
258 if (!current || !name) return;
259 lock.enter();
261 m.name = name;
262 m.value = value;
263 m.unit = unit ? unit : "";
264 m.higherIsBetter = higherIsBetter;
265 current->metrics.push_back(m);
266 lock.leave();
267}
268
269void UnitTestRunner::hookDetail(const char* text) {
270 if (!verboseFlag || !current) return;
271 clearProgressLine();
272 printf(UT_INDENT "%s\n", text ? text : "");
273 fflush(stdout);
274}
275
276void UnitTestRunner::hookFail(const char* text) {
277 if (!current) return;
278 std::string reason = text ? text : "";
279 // Trim trailing whitespace/newlines so the reason renders on one line.
280 while (!reason.empty() &&
281 (reason[reason.size() - 1] == '\n' || reason[reason.size() - 1] == '\r' ||
282 reason[reason.size() - 1] == ' ' || reason[reason.size() - 1] == '\t'))
283 reason.erase(reason.size() - 1);
284 current->failReason = reason;
285
286 // Print the reason RIGHT NOW (always, regardless of verbose). The status
287 // line is printed later by the parent process, and if the test goes on to
288 // hang or crash before then, the per-test timeout/crash note would be all
289 // that survives - so emit the reason immediately while we still can.
290 if (!reason.empty()) {
291 clearProgressLine();
292 printf(UT_INDENT "reason: %s\n", reason.c_str());
293 fflush(stdout);
294 }
295}
296
297// ----------------------------------------------------------------------------
298// Output of results
299// ----------------------------------------------------------------------------
300void UnitTestRunner::printStatusLine(const UnitTestRecord* rec) {
301 const char* tag = rec->passed ? "PASS" : "FAIL";
302 double ms = rec->durationUs / 1000.0;
303 // The test's own fail reason was already printed immediately by hookFail();
304 // here we only append the framework outcome note (timed out / crashed / no
305 // test function) so a hung or crashed test still shows why on the status line.
306 if (rec->passed || rec->endNote.empty())
307 printf("[ %s ] %-*s %10.2f ms\n", tag, UT_NAME_WIDTH, rec->name.c_str(), ms);
308 else
309 printf("[ %s ] %-*s %10.2f ms %s\n", tag, UT_NAME_WIDTH, rec->name.c_str(), ms,
310 rec->endNote.c_str());
311 fflush(stdout);
312}
313
314void UnitTestRunner::printMetricLines(const UnitTestRecord* rec) {
315 for (size_t i = 0; i < rec->metrics.size(); i++) {
316 const UnitTestMetric& m = rec->metrics[i];
317 char line[512];
318 snprintf(line, sizeof(line), UT_INDENT "%-*s %14.3f %-8s",
319 UT_METRIC_WIDTH, m.name.c_str(), m.value, m.unit.c_str());
320 printf("%s", line);
321
322 // Comparison against a previous run
323 double prev = 0.0;
324 std::string key = rec->name + "/" + m.name;
325 if (compareLoaded && comparisonValue(key, prev)) {
326 if (prev != 0.0) {
327 double pct = (m.value - prev) / prev * 100.0;
328 const char* dir;
329 bool improved = m.higherIsBetter ? (m.value > prev) : (m.value < prev);
330 bool worse = m.higherIsBetter ? (m.value < prev) : (m.value > prev);
331 dir = improved ? "better" : (worse ? "worse" : "same");
332 printf(" prev %.3f %+.1f%% (%s)", prev, pct, dir);
333 } else {
334 printf(" prev %.3f", prev);
335 }
336 }
337 printf("\n");
338 }
339 fflush(stdout);
340}
341
342// ----------------------------------------------------------------------------
343// Running
344// ----------------------------------------------------------------------------
345void UnitTestRunner::beginTest(UnitTestRecord* rec) {
346 current = rec;
347 lastProgressLen = 0;
348 rec->ran = true;
349 rec->passed = false;
350 rec->metrics.clear();
351 rec->failReason.clear();
352 rec->endNote.clear();
353}
354
355void UnitTestRunner::finishTest(UnitTestRecord* rec) {
356 clearProgressLine();
357 printStatusLine(rec);
358 printMetricLines(rec);
359 current = NULL;
360}
361
362// ---- child-result serialisation (used by the forked-execution path) --------
363// The child runs one test and records its metrics / fail reason into its own
364// copy of the record; it then serialises them to a small file the parent reads
365// back (fork copies memory, so the parent cannot see the child's heap directly).
366static std::string ut_escape_field(const std::string& s) {
367 std::string out;
368 for (size_t i = 0; i < s.size(); i++) {
369 char c = s[i];
370 if (c == '\n' || c == '\r' || c == '\t') out += ' ';
371 else out += c;
372 }
373 return out;
374}
375
376static void ut_writeChildResult(const std::string& path, const UnitTestRecord* rec) {
377 std::string s;
378 s += "fail\t" + ut_escape_field(rec->failReason) + "\n";
379 for (size_t i = 0; i < rec->metrics.size(); i++) {
380 const UnitTestMetric& m = rec->metrics[i];
381 char num[64];
382 snprintf(num, sizeof(num), "%.10g", m.value);
383 s += std::string("metric\t") + ut_escape_field(m.name) + "\t" + num + "\t" +
384 ut_escape_field(m.unit) + "\t" + (m.higherIsBetter ? "1" : "0") + "\n";
385 }
386 utils::WriteAFile(path.c_str(), s.c_str(), (uint32)s.size());
387}
388
389static void ut_readChildResult(const std::string& path, UnitTestRecord* rec) {
390 std::string data = utils::ReadAFileString(path);
391 if (data.empty()) return;
392 size_t pos = 0;
393 while (pos < data.size()) {
394 size_t eol = data.find('\n', pos);
395 if (eol == std::string::npos) eol = data.size();
396 std::string line = data.substr(pos, eol - pos);
397 pos = eol + 1;
398 if (line.empty()) continue;
399 // split on tabs
400 std::vector<std::string> f;
401 size_t p = 0;
402 while (true) {
403 size_t t = line.find('\t', p);
404 if (t == std::string::npos) { f.push_back(line.substr(p)); break; }
405 f.push_back(line.substr(p, t - p));
406 p = t + 1;
407 }
408 if (f[0] == "fail" && f.size() >= 2) {
409 rec->failReason = f[1];
410 } else if (f[0] == "metric" && f.size() >= 5) {
412 m.name = f[1];
413 m.value = utils::Ascii2Float64(f[2].c_str());
414 m.unit = f[3];
415 m.higherIsBetter = (f[4] == "1");
416 rec->metrics.push_back(m);
417 }
418 }
419}
420
421// Run a single test in its own process so a crash or hang in the test cannot
422// take down the whole suite, and so global/shared state (thread manager,
423// shared memory, singletons) never leaks between tests. Returns true on pass.
424// On non-fork platforms falls back to running inline.
425bool UnitTestRunner::executeRecord(UnitTestRecord* rec) {
426 // Clean any stale shared-memory segments left by a previously crashed/killed
427 // test before starting this one, so it always begins from a clean slate.
429
430#ifdef UT_HAVE_FORK
431 if (forkPerTest) {
432 std::string dir = outputDir.size() ? outputDir : ".";
433 std::string resultPath = dir + "/.cmsdk_result_" + rec->name;
434 std::string progPath = resultPath + ".progress";
435 std::string tracePath = resultPath + ".trace";
436 unlink(resultPath.c_str());
437 unlink(progPath.c_str());
438 unlink(tracePath.c_str());
439
440 fflush(stdout);
441 fflush(stderr);
442 uint64 t0 = GetTimeNow();
443 pid_t pid = fork();
444 if (pid < 0) {
445 // fork failed - run inline as a fallback
446 bool ok = rec->func ? rec->func() : false;
447 rec->durationUs = GetTimeNow() - t0;
448 return ok;
449 }
450 if (pid == 0) {
451 // ---- child ----
452 current = rec;
453 progressPath = progPath; // record current phase for timeout diagnostics
454 ut_installTimeoutTraceHandler(tracePath); // SIGUSR1 -> dump stack trace, then exit
455 lastProgressLen = 0;
456 if (isTTY) hookProgress(0, "starting...");
457 bool ok = false;
458 if (rec->func) ok = rec->func();
459 else rec->failReason = "no test function registered";
460 clearProgressLine(); // erase our progress line so the parent's status line is clean
461 ut_writeChildResult(resultPath, rec);
462 fflush(stdout);
463 _exit(ok ? 0 : 1);
464 }
465
466 // ---- parent ----
467 int status = 0;
468 bool timedOut = false;
469 while (true) {
470 pid_t r = waitpid(pid, &status, WNOHANG);
471 if (r == pid) break;
472 if (r < 0) break;
473 if (GetTimeNow() - t0 > UT_TEST_TIMEOUT_US) {
474 // Ask the stuck child to dump a stack trace of where it hung, give
475 // it a brief grace period to do so, then force-kill if necessary.
476 kill(pid, SIGUSR1);
477 bool reaped = false;
478 uint64 graceStart = GetTimeNow();
479 while (GetTimeNow() - graceStart < 2000000ULL) { // up to 2s
480 pid_t rr = waitpid(pid, &status, WNOHANG);
481 if (rr == pid || rr < 0) { reaped = true; break; }
482 utils::Sleep(15);
483 }
484 if (!reaped) {
485 kill(pid, SIGKILL);
486 waitpid(pid, &status, 0);
487 }
488 timedOut = true;
489 break;
490 }
491 utils::Sleep(15);
492 }
493 rec->durationUs = GetTimeNow() - t0;
494
495 bool ok = false;
496 if (timedOut) {
497 // Report which phase the test was in when it hung, and print the stack
498 // trace the child captured (if any) so the hang is diagnosable.
499 std::string note = utils::StringFormat("timed out after %llus", UT_TEST_TIMEOUT_US / 1000000ULL);
500 std::string lastPhase = ut_readProgressNote(progPath);
501 if (!lastPhase.empty()) note += " (last phase: " + lastPhase + ")";
502 rec->endNote = note;
503 ut_readChildResult(resultPath, rec); // recover any reason/metrics gathered pre-hang
504 ut_printTraceFile(tracePath);
505 } else if (WIFSIGNALED(status)) {
506 rec->endNote = utils::StringFormat("crashed (signal %d)", WTERMSIG(status));
507 ut_readChildResult(resultPath, rec); // recover the reason/metrics gathered pre-crash
508 } else {
509 ut_readChildResult(resultPath, rec);
510 ok = (WIFEXITED(status) && WEXITSTATUS(status) == 0);
511 }
512 unlink(resultPath.c_str());
513 unlink(progPath.c_str());
514 unlink(tracePath.c_str());
515 return ok;
516 }
517#endif
518
519 // Inline fallback (no fork available, or fork disabled).
520 current = rec;
521 lastProgressLen = 0;
522 if (isTTY) hookProgress(0, "starting...");
523 uint64 t0 = GetTimeNow();
524 bool ok = rec->func ? rec->func() : false;
525 rec->durationUs = GetTimeNow() - t0;
526 if (!rec->func) rec->endNote = "no test function registered";
527 return ok;
528}
529
530bool UnitTestRunner::runRecords(std::vector<UnitTestRecord*>& records) {
531 // Start from a clean shared-memory slate so a previously killed run can't
532 // deadlock this one on a stale process-shared mutex.
534
535 if (compareFile.size() && !compareLoaded) loadComparison();
536
537 // In non-verbose mode, silence the logging system's console output so the
538 // only thing on stdout is the consistent status/metric lines. Tests still
539 // report failures via unittest::fail(). Verbose mode leaves logging on.
540 bool savedLogToStdOut = true;
541 if (!verboseFlag) {
542 LogSystem::SetLogReceiver(NULL); // ensures the singleton exists
544 savedLogToStdOut = LogSystem::LogSingleton->printToStdOut;
546 }
547 }
548
549 uint64 startAll = GetTimeNow();
550 int passed = 0, failed = 0;
551
552 for (size_t i = 0; i < records.size(); i++) {
553 UnitTestRecord* rec = records[i];
554 beginTest(rec);
555 bool ok = executeRecord(rec);
556 rec->passed = ok;
557 finishTest(rec);
558 if (ok) passed++; else failed++;
559 }
560
561 uint64 totalUs = GetTimeNow() - startAll;
562
563 printf("\n----------------------------------------------------------------\n");
564 printf("CMSDK tests: %d passed, %d failed, %d total in %.2f ms\n",
565 passed, failed, (int)records.size(), totalUs / 1000.0);
566
567 if (writeJSONFlag) writeResults(records, totalUs, passed, failed);
568 printf("\n");
569
570 if (!verboseFlag && LogSystem::LogSingleton)
571 LogSystem::LogSingleton->printToStdOut = savedLogToStdOut;
572
573 return failed == 0;
574}
575
577 std::vector<UnitTestRecord*> records;
578 int excluded = 0;
579 for (size_t i = 0; i < tests.size(); i++) {
580 if (tests[i].inDefaultRun) records.push_back(&tests[i]);
581 else excluded++;
582 }
583 if (records.empty()) {
584 printf("No CMSDK unit tests are registered.\n");
585 return false;
586 }
587 printf("\nRunning %u CMSDK unit tests", (uint32)records.size());
588 if (excluded)
589 printf(" (%d excluded from the default run; run them by name)", excluded);
590 printf("...\n\n");
591 return runRecords(records);
592}
593
594bool UnitTestRunner::runOne(const char* name) {
595 if (!name) return false;
596 UnitTestRecord* found = NULL;
597 for (size_t i = 0; i < tests.size(); i++) {
598 if (stricmp(tests[i].name.c_str(), name) == 0) { found = &tests[i]; break; }
599 }
600 if (!found) {
601 printf("No CMSDK unit test named '%s'.\n", name);
602 listTests();
603 return false;
604 }
605 std::vector<UnitTestRecord*> records;
606 records.push_back(found);
607 printf("\nRunning CMSDK unit test '%s'...\n\n", found->name.c_str());
608 return runRecords(records);
609}
610
611// ----------------------------------------------------------------------------
612// Comparison file loading
613// ----------------------------------------------------------------------------
614bool UnitTestRunner::comparisonValue(const std::string& key, double& out) const {
615 std::map<std::string, double>::const_iterator it = compareMetrics.find(key);
616 if (it == compareMetrics.end()) return false;
617 out = it->second;
618 return true;
619}
620
621void UnitTestRunner::loadComparison() {
622 compareLoaded = false;
623 compareMetrics.clear();
624
625 std::string json = utils::ReadAFileString(compareFile);
626 if (json.empty()) {
627 printf("Warning: could not read comparison file '%s'\n", compareFile.c_str());
628 return;
629 }
630
631 // Count tokens first, then parse.
632 jsmn_parser parser;
633 jsmn_init(&parser);
634 int needed = jsmn_parse(&parser, json.c_str(), json.size(), NULL, 0);
635 if (needed <= 0) {
636 printf("Warning: comparison file '%s' is not valid JSON\n", compareFile.c_str());
637 return;
638 }
639 jsmntok_t* tokens = new jsmntok_t[needed + 1];
640 jsmn_init(&parser);
641 int count = jsmn_parse(&parser, json.c_str(), json.size(), tokens, needed + 1);
642 if (count <= 0) {
643 delete[] tokens;
644 printf("Warning: failed to parse comparison file '%s'\n", compareFile.c_str());
645 return;
646 }
647
648 // Pull every "test/metric" -> number pair from the flat section. Flat keys
649 // are the only keys containing '/', so they are unambiguous.
650 for (int n = 0; n < count - 1; n++) {
651 if (tokens[n].type != JSMN_STRING) continue;
652 int klen = tokens[n].end - tokens[n].start;
653 if (klen <= 0) continue;
654 const char* kstart = json.c_str() + tokens[n].start;
655 bool hasSlash = false;
656 for (int c = 0; c < klen; c++) { if (kstart[c] == '/') { hasSlash = true; break; } }
657 if (!hasSlash) continue;
658 if (tokens[n + 1].type != JSMN_PRIMITIVE) continue;
659 std::string key(kstart, klen);
660 std::string val(json.c_str() + tokens[n + 1].start, tokens[n + 1].end - tokens[n + 1].start);
661 compareMetrics[key] = utils::Ascii2Float64(val.c_str());
662 }
663
664 delete[] tokens;
665 compareLoaded = true;
666 printf("Comparing against '%s' (%u recorded metrics)\n\n",
667 compareFile.c_str(), (uint32)compareMetrics.size());
668}
669
670// ----------------------------------------------------------------------------
671// Results JSON writing
672// ----------------------------------------------------------------------------
673static std::string jsonEscape(const std::string& s) {
674 std::string out;
675 for (size_t i = 0; i < s.size(); i++) {
676 char c = s[i];
677 switch (c) {
678 case '\"': out += "\\\""; break;
679 case '\\': out += "\\\\"; break;
680 case '\n': out += "\\n"; break;
681 case '\r': out += "\\r"; break;
682 case '\t': out += "\\t"; break;
683 default: out += c; break;
684 }
685 }
686 return out;
687}
688
689std::string UnitTestRunner::timestampForFilename(uint64 t) {
690 struct PsyDateAndTime d = GetDateAndTime(t);
691 char buf[32];
692 snprintf(buf, sizeof(buf), "%04u%02u%02u-%02u%02u%02u",
693 d.year, d.mon, d.day, d.hour, d.min, d.sec);
694 return std::string(buf);
695}
696
697void UnitTestRunner::writeResults(const std::vector<UnitTestRecord*>& records,
698 uint64 totalUs, int passed, int failed) {
699 uint64 now = GetTimeNow();
700 struct PsyDateAndTime d = GetDateAndTime(now);
701 char dt[32];
702 snprintf(dt, sizeof(dt), "%04u-%02u-%02u %02u:%02u:%02u",
703 d.year, d.mon, d.day, d.hour, d.min, d.sec);
704
705 const char* platform =
706#if defined(WINDOWS)
707 "win"
708#elif defined(DARWIN) || defined(OSX) || defined(__APPLE__)
709 "macosx"
710#elif defined(LINUX) || defined(__linux)
711 "linux"
712#else
713 "unknown"
714#endif
715#if defined(ARCH_64)
716 "64"
717#else
718 "32"
719#endif
720 ;
721
722 std::string json;
723 json += "{\n";
724 json += " \"meta\": {\n";
725 json += utils::StringFormat(" \"version\": 1,\n");
726 json += utils::StringFormat(" \"timestamp_us\": %llu,\n", (unsigned long long)now);
727 json += utils::StringFormat(" \"datetime\": \"%s\",\n", dt);
728 json += utils::StringFormat(" \"platform\": \"%s\",\n", platform);
729 json += utils::StringFormat(" \"total\": %d,\n", (int)records.size());
730 json += utils::StringFormat(" \"passed\": %d,\n", passed);
731 json += utils::StringFormat(" \"failed\": %d,\n", failed);
732 json += utils::StringFormat(" \"duration_us\": %llu\n", (unsigned long long)totalUs);
733 json += " },\n";
734
735 // Human-readable structured section
736 json += " \"tests\": {\n";
737 for (size_t i = 0; i < records.size(); i++) {
738 const UnitTestRecord* rec = records[i];
739 json += utils::StringFormat(" \"%s\": {\n", jsonEscape(rec->name).c_str());
740 json += utils::StringFormat(" \"status\": \"%s\",\n", rec->passed ? "pass" : "fail");
741 json += utils::StringFormat(" \"duration_us\": %llu,\n", (unsigned long long)rec->durationUs);
742 json += " \"metrics\": {\n";
743 for (size_t j = 0; j < rec->metrics.size(); j++) {
744 const UnitTestMetric& m = rec->metrics[j];
745 json += utils::StringFormat(
746 " \"%s\": { \"value\": %.6f, \"unit\": \"%s\", \"higher_is_better\": %s }%s\n",
747 jsonEscape(m.name).c_str(), m.value, jsonEscape(m.unit).c_str(),
748 m.higherIsBetter ? "true" : "false",
749 (j + 1 < rec->metrics.size()) ? "," : "");
750 }
751 json += " }\n";
752 json += utils::StringFormat(" }%s\n", (i + 1 < records.size()) ? "," : "");
753 }
754 json += " },\n";
755
756 // Flat machine-comparison section ("test/metric" -> number)
757 json += " \"flat\": {\n";
758 std::vector<std::string> flat;
759 for (size_t i = 0; i < records.size(); i++) {
760 const UnitTestRecord* rec = records[i];
761 flat.push_back(utils::StringFormat(" \"%s/__duration_us\": %llu",
762 jsonEscape(rec->name).c_str(), (unsigned long long)rec->durationUs));
763 for (size_t j = 0; j < rec->metrics.size(); j++) {
764 const UnitTestMetric& m = rec->metrics[j];
765 flat.push_back(utils::StringFormat(" \"%s/%s\": %.6f",
766 jsonEscape(rec->name).c_str(), jsonEscape(m.name).c_str(), m.value));
767 }
768 }
769 for (size_t i = 0; i < flat.size(); i++)
770 json += flat[i] + ((i + 1 < flat.size()) ? ",\n" : "\n");
771 json += " }\n";
772 json += "}\n";
773
774 // Decide output paths.
775 std::vector<std::string> paths;
776 if (outputFile.size()) {
777 paths.push_back(outputFile);
778 } else {
779 std::string dir = outputDir.size() ? outputDir : ".";
780 paths.push_back(dir + "/cmsdk_perf_" + timestampForFilename(now) + ".json");
781 paths.push_back(dir + "/cmsdk_perf_latest.json");
782 }
783
784 for (size_t i = 0; i < paths.size(); i++) {
785 if (utils::WriteAFile(paths[i].c_str(), json.c_str(), (uint32)json.size()))
786 printf("Performance data written to %s\n", paths[i].c_str());
787 else
788 printf("Warning: could not write performance data to %s\n", paths[i].c_str());
789 }
790}
791
792// ----------------------------------------------------------------------------
793// unittest:: free function API
794// ----------------------------------------------------------------------------
795namespace unittest {
796
797// Format a va_list into a std::string using CMSDK's portable formatter
798// (handles the snprintf/_vsnprintf_s differences across MSVC/Cygwin/POSIX).
799static std::string ut_vformat(const char* fmt, va_list args) {
800 uint32 sz = 0;
801 char* s = utils::StringFormatVA(sz, fmt, args);
802 std::string out = s ? s : "";
803 if (s) free(s);
804 return out;
805}
806
807void progress(int percent, const char* action) {
808 UnitTestRunner::instance().hookProgress(percent, action);
809}
810
811void progressf(int percent, const char* fmt, ...) {
812 va_list args;
813 va_start(args, fmt);
814 std::string s = ut_vformat(fmt, args);
815 va_end(args);
816 UnitTestRunner::instance().hookProgress(percent, s.c_str());
817}
818
819void metric(const char* name, double value, const char* unit, bool higherIsBetter) {
820 UnitTestRunner::instance().hookMetric(name, value, unit, higherIsBetter);
821}
822
823void detail(const char* fmt, ...) {
824 if (!UnitTestRunner::instance().hookVerbose()) return;
825 va_list args;
826 va_start(args, fmt);
827 std::string s = ut_vformat(fmt, args);
828 va_end(args);
830}
831
832void fail(const char* fmt, ...) {
833 va_list args;
834 va_start(args, fmt);
835 std::string s = ut_vformat(fmt, args);
836 va_end(args);
838}
839
840bool verbose() {
842}
843
844} // namespace unittest
845
846} // namespace cmlabs
CMSDK time: µs-resolution 64-bit timestamps and the Time Mapping Constant (TMC).
#define FILENO(f)
#define UT_NAME_WIDTH
#define UT_TEST_TIMEOUT_US
#define UT_INDENT
#define UT_METRIC_WIDTH
#define ISATTY(fd)
Small, dependency-free unit test harness used by all CMSDK object tests.
#define stricmp
Definition Utils.h:132
static bool SetLogReceiver(LogReceiver *rec)
Register a receiver that gets every accepted LogEntry.
Definition Utils.cpp:169
bool printToStdOut
Definition Utils.h:310
static LogSystem * LogSingleton
Lazily-created global instance used by all static functions.
Definition Utils.h:278
std::vector< std::string > testNames() const
void hookDetail(const char *text)
Hook behind unittest::detail() for the current test.
void setForkPerTest(bool v)
Run each test in its own process (default: on where supported) so crashes/hangs are isolated.
void setWriteJSON(bool v)
Enable/disable writing the perf JSON file at all.
void listTests() const
Print the registered tests (name, category, description) to stdout.
void hookProgress(int percent, const char *action)
Hook behind unittest::progress()/progressf() for the current test.
void setOutputFile(const char *path)
Set an explicit perf JSON output path (overrides the timestamped default).
bool hasTest(const char *name) const
void hookMetric(const char *name, double value, const char *unit, bool higherIsBetter)
Hook behind unittest::metric() for the current test.
static UnitTestRunner & instance()
Access the singleton (created on first use).
void setVerbose(bool v)
Enable/disable verbose diagnostics (unittest::detail output).
bool hookVerbose() const
Hook behind unittest::verbose().
void setCompareFile(const char *path)
Set a previous perf JSON file to compare metrics against.
bool runAll()
Run every test marked inDefaultRun.
void registerTest(const char *name, UnitTestFunc func, const char *description="", const char *category="", bool inDefaultRun=true)
Register a test with the runner.
bool runOne(const char *name)
Run a single named test in isolation.
void setOutputDir(const char *dir)
Set the directory for the default (timestamped + latest) output files.
void hookFail(const char *text)
Hook behind unittest::fail() for the current test.
uint64 GetTimeNow()
Return the current absolute time (µs since year 0) according to the TMC.
Definition PsyTime.cpp:69
struct PsyDateAndTime GetDateAndTime(uint64 t, bool local=true)
Break a timestamp into calendar fields.
Definition PsyTime.cpp:324
bool(* UnitTestFunc)()
Signature every unit test uses.
std::string ReadAFileString(std::string filename)
Read an entire file into a std::string.
Definition Utils.cpp:6797
char * StringFormatVA(uint32 &size, const char *format, va_list args)
va_list core used by the other StringFormat overloads.
Definition Utils.cpp:6566
bool Sleep(uint32 ms)
Suspend the calling thread.
Definition Utils.cpp:2802
bool WriteAFile(const char *filename, const char *data, uint32 length, bool binary=false)
Write (create/overwrite) a file.
Definition Utils.cpp:6877
std::string StringFormat(const char *format,...)
printf into a std::string.
Definition Utils.cpp:6626
float64 Ascii2Float64(const char *ascii, uint32 start=0, uint32 end=0)
Parse a 64-bit float from a substring (decimal point, not locale dependent).
Definition Utils.cpp:7546
Third-party (vendored): jsmn minimalistic JSON tokenizer by Serge Zaitsev (MIT licence),...
@ JSMN_PRIMITIVE
Definition jsmn.h:33
@ JSMN_STRING
Definition jsmn.h:32
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, unsigned int num_tokens)
Run JSON parser.
Definition jsmn.cpp:156
void jsmn_init(jsmn_parser *parser)
Create JSON parser over an array of tokens.
Definition jsmn.cpp:311
API used by the body of a unit test.
void fail(const char *fmt,...)
Set an explanatory reason shown on the FAIL line.
void progressf(int percent, const char *fmt,...)
printf-style variant of progress().
void metric(const char *name, double value, const char *unit="", bool higherIsBetter=true)
Record a performance metric.
void detail(const char *fmt,...)
Verbose-only indented diagnostic line (shown only when verbose=1).
static std::string ut_vformat(const char *fmt, va_list args)
void progress(int percent, const char *action)
Report progress with a short description of the current action.
static void ClearStaleTestSegments()
struct dirent * readdir(DIR *)
static void ut_writeChildResult(const std::string &path, const UnitTestRecord *rec)
static std::string jsonEscape(const std::string &s)
DIR * opendir(const char *)
Definition direntwin.cpp:53
static void ut_printTraceFile(const std::string &path)
static void ut_timeoutTraceHandler(int)
static void ut_clearSegmentsIn(const char *dir, const char *prefix)
static std::string ut_escape_field(const std::string &s)
int closedir(DIR *)
Definition direntwin.cpp:95
static std::string ut_readProgressNote(const std::string &path)
static int ut_traceFd
static void ut_readChildResult(const std::string &path, UnitTestRecord *rec)
static void ut_installTimeoutTraceHandler(const std::string &tracePath)
One recorded performance metric of a test run.
bool higherIsBetter
Delta-direction hint for compare mode.
std::string unit
Unit label (e.g.
double value
Measured value.
std::string name
Metric name (unique within the test).
Registration and result record for a single unit test.
UnitTestFunc func
The test function itself.
bool passed
Outcome (only meaningful when ran).
std::vector< UnitTestMetric > metrics
Metrics recorded via unittest::metric().
std::string failReason
Reason set by the test via unittest::fail() (printed immediately).
bool inDefaultRun
Included when running the whole suite (test=cmsdk).
std::string description
One-line human description.
std::string name
Canonical name, used by test=<name>.
std::string endNote
Framework outcome note (timed out / crashed / no test function).
std::string category
Grouping label (e.g.
uint64 durationUs
Wall-clock duration in microseconds.
char * d_name
Definition direntwin.h:35
int start
Definition jsmn.h:53
int end
Definition jsmn.h:54