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
42#define UT_TEST_TIMEOUT_US (30ULL * 1000000ULL * UT_TIME_SCALE)
58 size_t plen = strlen(prefix);
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;
79 const char* attaching = getenv(
"PSY_CONC_CHILD");
80 if (attaching && *attaching)
102 int n = backtrace(frames, 128);
108 ut_traceFd = open(tracePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
115 FILE* f = fopen(path.c_str(),
"r");
118 size_t r = fread(buf, 1,
sizeof(buf) - 1, f);
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) +
"\"";
129 FILE* f = fopen(path.c_str(),
"r");
133 while (fgets(line,
sizeof(line), f)) {
134 if (!header) { printf(
" stack trace where the test hung:\n"); header =
true; }
137 if (header) { printf(
"\n"); fflush(stdout); }
145#define UT_NAME_WIDTH 28
147#define UT_METRIC_WIDTH 26
153 static UnitTestRunner theInstance;
157UnitTestRunner::UnitTestRunner()
158 : current(NULL), verboseFlag(false), writeJSONFlag(true),
159 forkPerTest(true), isTTY(false), lastProgressLen(0), outputDir(
"."),
160 compareLoaded(false) {
168 const char* description,
const char* category,
170 if (!name || !func)
return;
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;
185 rec.
category = category ? category :
"";
187 tests.push_back(rec);
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;
205 std::vector<std::string> names;
206 for (
size_t i = 0; i < tests.size(); i++) names.push_back(tests[i].name);
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());
218 printf(
" %-22s %s\n", tests[i].name.c_str(), tests[i].description.c_str());
220 printf(
"\nRun all with: test=cmsdk Run one with: test=<name>\n\n");
229 if (!current)
return;
230 if (percent < 0) percent = 0;
231 if (percent > 100) percent = 100;
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); }
245 int n = snprintf(buf,
sizeof(buf),
" [%3d%%] %-*.*s %s",
247 current->name.c_str(), action ? action :
"");
251 int pad = lastProgressLen - n;
254 for (
int i = 0; i < pad; i++) fputc(
' ', stdout);
257 lastProgressLen = (n > lastProgressLen) ? n : lastProgressLen;
262void UnitTestRunner::clearProgressLine() {
263 if (!isTTY || lastProgressLen <= 0)
return;
265 for (
int i = 0; i < lastProgressLen; i++) fputc(
' ', stdout);
272 if (!current || !name)
return;
277 m.
unit = unit ? unit :
"";
279 current->metrics.push_back(m);
284 if (!current)
return;
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());
295 if (!verboseFlag)
return;
297 printf(
UT_INDENT "%s\n", text ? text :
"");
302 if (!current)
return;
303 std::string reason = text ? text :
"";
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;
315 if (!reason.empty()) {
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());
327 printf(
UT_INDENT "reason: %s\n", reason.c_str());
336 const char* tag = rec->
passed ?
"PASS" :
"FAIL";
349void UnitTestRunner::printMetricLines(
const UnitTestRecord* rec) {
350 for (
size_t i = 0; i < rec->metrics.size(); i++) {
351 const UnitTestMetric& m = rec->metrics[i];
353 snprintf(line,
sizeof(line),
UT_INDENT "%-*s %14.3f %-8s",
359 std::string key = rec->name +
"/" + m.name;
360 if (compareLoaded && comparisonValue(key, prev)) {
362 double pct = (m.value - prev) / prev * 100.0;
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);
369 printf(
" prev %.3f", prev);
385 rec->metrics.clear();
386 rec->failReason.clear();
387 rec->endNote.clear();
392 printStatusLine(rec);
393 printMetricLines(rec);
403 for (
size_t i = 0; i < s.size(); i++) {
405 if (c ==
'\n' || c ==
'\r' || c ==
'\t') out +=
' ';
414 for (
size_t i = 0; i < rec->
metrics.size(); i++) {
417 snprintf(num,
sizeof(num),
"%.10g", m.
value);
426 if (data.empty())
return;
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);
433 if (line.empty())
continue;
435 std::vector<std::string> f;
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));
443 if (f[0] ==
"fail" && f.size() >= 2) {
445 }
else if (f[0] ==
"metric" && f.size() >= 5) {
460bool UnitTestRunner::executeRecord(UnitTestRecord* rec) {
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());
481 bool ok = rec->func ? rec->func() :
false;
488 progressPath = progPath;
493 if (rec->func) ok = rec->func();
494 else rec->failReason =
"no test function registered";
503 bool timedOut =
false;
505 pid_t r = waitpid(pid, &status, WNOHANG);
514 while (
GetTimeNow() - graceStart < 2000000ULL) {
515 pid_t rr = waitpid(pid, &status, WNOHANG);
516 if (rr == pid || rr < 0) { reaped =
true;
break; }
521 waitpid(pid, &status, 0);
536 if (!lastPhase.empty()) note +=
" (last phase: " + lastPhase +
")";
540 }
else if (WIFSIGNALED(status)) {
545 ok = (WIFEXITED(status) && WEXITSTATUS(status) == 0);
547 unlink(resultPath.c_str());
548 unlink(progPath.c_str());
549 unlink(tracePath.c_str());
559 bool ok = rec->func ? rec->func() :
false;
561 if (!rec->func) rec->endNote =
"no test function registered";
565bool UnitTestRunner::runRecords(std::vector<UnitTestRecord*>& records) {
570 if (compareFile.size() && !compareLoaded) loadComparison();
575 bool savedLogToStdOut =
true;
585 int passed = 0, failed = 0;
587 for (
size_t i = 0; i < records.size(); i++) {
588 UnitTestRecord* rec = records[i];
590 bool ok = executeRecord(rec);
593 if (ok) passed++;
else failed++;
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);
602 if (writeJSONFlag) writeResults(records, totalUs, passed, failed);
612 std::vector<UnitTestRecord*> records;
614 for (
size_t i = 0; i < tests.size(); i++) {
615 if (tests[i].inDefaultRun) records.push_back(&tests[i]);
618 if (records.empty()) {
619 printf(
"No CMSDK unit tests are registered.\n");
622 printf(
"\nRunning %u CMSDK unit tests", (uint32)records.size());
624 printf(
" (%d excluded from the default run; run them by name)", excluded);
626 return runRecords(records);
630 if (!name)
return false;
632 for (
size_t i = 0; i < tests.size(); i++) {
633 if (
stricmp(tests[i].name.c_str(), name) == 0) { found = &tests[i];
break; }
636 printf(
"No CMSDK unit test named '%s'.\n", name);
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);
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;
656void UnitTestRunner::loadComparison() {
657 compareLoaded =
false;
658 compareMetrics.clear();
662 printf(
"Warning: could not read comparison file '%s'\n", compareFile.c_str());
669 int needed =
jsmn_parse(&parser, json.c_str(), json.size(), NULL, 0);
671 printf(
"Warning: comparison file '%s' is not valid JSON\n", compareFile.c_str());
674 jsmntok_t* tokens =
new jsmntok_t[needed + 1];
676 int count =
jsmn_parse(&parser, json.c_str(), json.size(), tokens, needed + 1);
679 printf(
"Warning: failed to parse comparison file '%s'\n", compareFile.c_str());
685 for (
int n = 0; n < count - 1; n++) {
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;
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);
700 compareLoaded =
true;
701 printf(
"Comparing against '%s' (%u recorded metrics)\n\n",
702 compareFile.c_str(), (uint32)compareMetrics.size());
710 for (
size_t i = 0; i < s.size(); i++) {
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;
724std::string UnitTestRunner::timestampForFilename(uint64 t) {
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);
732void UnitTestRunner::writeResults(
const std::vector<UnitTestRecord*>& records,
733 uint64 totalUs,
int passed,
int failed) {
741 snprintf(dt,
sizeof(dt),
"%04u-%02u-%02u %02u:%02u:%02u",
742 d.year, d.mon, d.day, d.hour, d.min, d.sec);
744 const char* platform =
747#elif defined(DARWIN) || defined(OSX) || defined(__APPLE__)
749#elif defined(LINUX) || defined(__linux)
763 json +=
" \"meta\": {\n";
775 json +=
" \"tests\": {\n";
776 for (
size_t i = 0; i < records.size(); i++) {
777 const UnitTestRecord* rec = records[i];
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];
785 " \"%s\": { \"value\": %.6f, \"unit\": \"%s\", \"higher_is_better\": %s }%s\n",
787 m.higherIsBetter ?
"true" :
"false",
788 (j + 1 < rec->metrics.size()) ?
"," :
"");
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];
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];
808 for (
size_t i = 0; i < flat.size(); i++)
809 json += flat[i] + ((i + 1 < flat.size()) ?
",\n" :
"\n");
814 std::vector<std::string> paths;
815 if (outputFile.size()) {
816 paths.push_back(outputFile);
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");
823 for (
size_t i = 0; i < paths.size(); i++) {
825 printf(
"Performance data written to %s\n", paths[i].c_str());
827 printf(
"Warning: could not write performance data to %s\n", paths[i].c_str());
838static std::string
ut_vformat(
const char* fmt, va_list args) {
841 std::string out = s ? s :
"";
858void metric(
const char* name,
double value,
const char* unit,
bool higherIsBetter) {
872void 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.
bool Sleep(uint32 ms)
Suspend the calling thread.
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.
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 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).
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.