21 #define ISATTY(fd) _isatty(fd)
22 #define FILENO(f) _fileno(f)
30 #define ISATTY(fd) isatty(fd)
31 #define FILENO(f) fileno(f)
32 #define UT_HAVE_FORK 1
38#define UT_TEST_TIMEOUT_US (30ULL * 1000000ULL)
54 size_t plen = strlen(prefix);
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;
88 int n = backtrace(frames, 128);
94 ut_traceFd = open(tracePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
101 FILE* f = fopen(path.c_str(),
"r");
104 size_t r = fread(buf, 1,
sizeof(buf) - 1, f);
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) +
"\"";
115 FILE* f = fopen(path.c_str(),
"r");
119 while (fgets(line,
sizeof(line), f)) {
120 if (!header) { printf(
" stack trace where the test hung:\n"); header =
true; }
123 if (header) { printf(
"\n"); fflush(stdout); }
131#define UT_NAME_WIDTH 28
133#define UT_METRIC_WIDTH 26
139 static UnitTestRunner theInstance;
143UnitTestRunner::UnitTestRunner()
144 : current(NULL), verboseFlag(false), writeJSONFlag(true),
145 forkPerTest(true), isTTY(false), lastProgressLen(0), outputDir(
"."),
146 compareLoaded(false) {
154 const char* description,
const char* category,
156 if (!name || !func)
return;
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;
171 rec.
category = category ? category :
"";
173 tests.push_back(rec);
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;
191 std::vector<std::string> names;
192 for (
size_t i = 0; i < tests.size(); i++) names.push_back(tests[i].name);
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());
204 printf(
" %-22s %s\n", tests[i].name.c_str(), tests[i].description.c_str());
206 printf(
"\nRun all with: test=cmsdk Run one with: test=<name>\n\n");
215 if (!current)
return;
216 if (percent < 0) percent = 0;
217 if (percent > 100) percent = 100;
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); }
231 int n = snprintf(buf,
sizeof(buf),
" [%3d%%] %-*.*s %s",
233 current->name.c_str(), action ? action :
"");
237 int pad = lastProgressLen - n;
240 for (
int i = 0; i < pad; i++) fputc(
' ', stdout);
243 lastProgressLen = (n > lastProgressLen) ? n : lastProgressLen;
248void UnitTestRunner::clearProgressLine() {
249 if (!isTTY || lastProgressLen <= 0)
return;
251 for (
int i = 0; i < lastProgressLen; i++) fputc(
' ', stdout);
258 if (!current || !name)
return;
263 m.
unit = unit ? unit :
"";
265 current->metrics.push_back(m);
270 if (!verboseFlag || !current)
return;
272 printf(
UT_INDENT "%s\n", text ? text :
"");
277 if (!current)
return;
278 std::string reason = text ? text :
"";
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;
290 if (!reason.empty()) {
292 printf(
UT_INDENT "reason: %s\n", reason.c_str());
301 const char* tag = rec->
passed ?
"PASS" :
"FAIL";
314void UnitTestRunner::printMetricLines(
const UnitTestRecord* rec) {
315 for (
size_t i = 0; i < rec->metrics.size(); i++) {
316 const UnitTestMetric& m = rec->metrics[i];
318 snprintf(line,
sizeof(line),
UT_INDENT "%-*s %14.3f %-8s",
324 std::string key = rec->name +
"/" + m.name;
325 if (compareLoaded && comparisonValue(key, prev)) {
327 double pct = (m.value - prev) / prev * 100.0;
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);
334 printf(
" prev %.3f", prev);
350 rec->metrics.clear();
351 rec->failReason.clear();
352 rec->endNote.clear();
357 printStatusLine(rec);
358 printMetricLines(rec);
368 for (
size_t i = 0; i < s.size(); i++) {
370 if (c ==
'\n' || c ==
'\r' || c ==
'\t') out +=
' ';
379 for (
size_t i = 0; i < rec->
metrics.size(); i++) {
382 snprintf(num,
sizeof(num),
"%.10g", m.
value);
391 if (data.empty())
return;
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);
398 if (line.empty())
continue;
400 std::vector<std::string> f;
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));
408 if (f[0] ==
"fail" && f.size() >= 2) {
410 }
else if (f[0] ==
"metric" && f.size() >= 5) {
425bool UnitTestRunner::executeRecord(UnitTestRecord* rec) {
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());
446 bool ok = rec->func ? rec->func() :
false;
453 progressPath = progPath;
458 if (rec->func) ok = rec->func();
459 else rec->failReason =
"no test function registered";
468 bool timedOut =
false;
470 pid_t r = waitpid(pid, &status, WNOHANG);
479 while (
GetTimeNow() - graceStart < 2000000ULL) {
480 pid_t rr = waitpid(pid, &status, WNOHANG);
481 if (rr == pid || rr < 0) { reaped =
true;
break; }
486 waitpid(pid, &status, 0);
501 if (!lastPhase.empty()) note +=
" (last phase: " + lastPhase +
")";
505 }
else if (WIFSIGNALED(status)) {
510 ok = (WIFEXITED(status) && WEXITSTATUS(status) == 0);
512 unlink(resultPath.c_str());
513 unlink(progPath.c_str());
514 unlink(tracePath.c_str());
524 bool ok = rec->func ? rec->func() :
false;
526 if (!rec->func) rec->endNote =
"no test function registered";
530bool UnitTestRunner::runRecords(std::vector<UnitTestRecord*>& records) {
535 if (compareFile.size() && !compareLoaded) loadComparison();
540 bool savedLogToStdOut =
true;
550 int passed = 0, failed = 0;
552 for (
size_t i = 0; i < records.size(); i++) {
553 UnitTestRecord* rec = records[i];
555 bool ok = executeRecord(rec);
558 if (ok) passed++;
else failed++;
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);
567 if (writeJSONFlag) writeResults(records, totalUs, passed, failed);
577 std::vector<UnitTestRecord*> records;
579 for (
size_t i = 0; i < tests.size(); i++) {
580 if (tests[i].inDefaultRun) records.push_back(&tests[i]);
583 if (records.empty()) {
584 printf(
"No CMSDK unit tests are registered.\n");
587 printf(
"\nRunning %u CMSDK unit tests", (uint32)records.size());
589 printf(
" (%d excluded from the default run; run them by name)", excluded);
591 return runRecords(records);
595 if (!name)
return false;
597 for (
size_t i = 0; i < tests.size(); i++) {
598 if (
stricmp(tests[i].name.c_str(), name) == 0) { found = &tests[i];
break; }
601 printf(
"No CMSDK unit test named '%s'.\n", name);
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);
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;
621void UnitTestRunner::loadComparison() {
622 compareLoaded =
false;
623 compareMetrics.clear();
627 printf(
"Warning: could not read comparison file '%s'\n", compareFile.c_str());
634 int needed =
jsmn_parse(&parser, json.c_str(), json.size(), NULL, 0);
636 printf(
"Warning: comparison file '%s' is not valid JSON\n", compareFile.c_str());
639 jsmntok_t* tokens =
new jsmntok_t[needed + 1];
641 int count =
jsmn_parse(&parser, json.c_str(), json.size(), tokens, needed + 1);
644 printf(
"Warning: failed to parse comparison file '%s'\n", compareFile.c_str());
650 for (
int n = 0; n < count - 1; n++) {
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;
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);
665 compareLoaded =
true;
666 printf(
"Comparing against '%s' (%u recorded metrics)\n\n",
667 compareFile.c_str(), (uint32)compareMetrics.size());
675 for (
size_t i = 0; i < s.size(); i++) {
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;
689std::string UnitTestRunner::timestampForFilename(uint64 t) {
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);
697void UnitTestRunner::writeResults(
const std::vector<UnitTestRecord*>& records,
698 uint64 totalUs,
int passed,
int failed) {
702 snprintf(dt,
sizeof(dt),
"%04u-%02u-%02u %02u:%02u:%02u",
703 d.year, d.mon, d.day, d.hour, d.min, d.sec);
705 const char* platform =
708#elif defined(DARWIN) || defined(OSX) || defined(__APPLE__)
710#elif defined(LINUX) || defined(__linux)
724 json +=
" \"meta\": {\n";
736 json +=
" \"tests\": {\n";
737 for (
size_t i = 0; i < records.size(); i++) {
738 const UnitTestRecord* rec = records[i];
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];
746 " \"%s\": { \"value\": %.6f, \"unit\": \"%s\", \"higher_is_better\": %s }%s\n",
748 m.higherIsBetter ?
"true" :
"false",
749 (j + 1 < rec->metrics.size()) ?
"," :
"");
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];
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];
769 for (
size_t i = 0; i < flat.size(); i++)
770 json += flat[i] + ((i + 1 < flat.size()) ?
",\n" :
"\n");
775 std::vector<std::string> paths;
776 if (outputFile.size()) {
777 paths.push_back(outputFile);
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");
784 for (
size_t i = 0; i < paths.size(); i++) {
786 printf(
"Performance data written to %s\n", paths[i].c_str());
788 printf(
"Warning: could not write performance data to %s\n", paths[i].c_str());
799static std::string
ut_vformat(
const char* fmt, va_list args) {
802 std::string out = s ? s :
"";
819void metric(
const char* name,
double value,
const char* unit,
bool higherIsBetter) {
832void fail(
const char* fmt, ...) {
CMSDK time: µs-resolution 64-bit timestamps and the Time Mapping Constant (TMC).
#define UT_TEST_TIMEOUT_US
Small, dependency-free unit test harness used by all CMSDK object tests.
static bool SetLogReceiver(LogReceiver *rec)
Register a receiver that gets every accepted LogEntry.
static LogSystem * LogSingleton
Lazily-created global instance used by all static functions.
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.
struct PsyDateAndTime GetDateAndTime(uint64 t, bool local=true)
Break a timestamp into calendar fields.
bool(* UnitTestFunc)()
Signature every unit test uses.
std::string ReadAFileString(std::string filename)
Read an entire file into a std::string.
char * StringFormatVA(uint32 &size, const char *format, va_list args)
va_list core used by the other StringFormat overloads.
bool Sleep(uint32 ms)
Suspend the calling thread.
bool WriteAFile(const char *filename, const char *data, uint32 length, bool binary=false)
Write (create/overwrite) a file.
std::string StringFormat(const char *format,...)
printf into a std::string.
float64 Ascii2Float64(const char *ascii, uint32 start=0, uint32 end=0)
Parse a 64-bit float from a substring (decimal point, not locale dependent).
Third-party (vendored): jsmn minimalistic JSON tokenizer by Serge Zaitsev (MIT licence),...
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, unsigned int num_tokens)
Run JSON parser.
void jsmn_init(jsmn_parser *parser)
Create JSON parser over an array of tokens.
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 *)
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)
static std::string ut_readProgressNote(const std::string &path)
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.