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