pebble/tools/clar/clar.c
2025-01-27 11:38:16 -08:00

735 lines
16 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <math.h>
#include <stdarg.h>
#include <unistd.h>
/* required for sandboxing */
#include <sys/types.h>
#include <sys/stat.h>
#ifdef _WIN32
# include <windows.h>
# include <io.h>
# include <shellapi.h>
# include <direct.h>
# define _MAIN_CC __cdecl
# define stat(path, st) _stat(path, st)
# define mkdir(path, mode) _mkdir(path)
# define chdir(path) _chdir(path)
# define access(path, mode) _access(path, mode)
# define strdup(str) _strdup(str)
# ifndef __MINGW32__
# pragma comment(lib, "shell32")
# define strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)
# define W_OK 02
# define S_ISDIR(x) ((x & _S_IFDIR) != 0)
# define snprint_eq(buf,sz,fmt,a,b) _snprintf_s(buf,sz,_TRUNCATE,fmt,a,b)
# else
# define snprint_eq snprintf
# endif
typedef struct _stat STAT_T;
#else
# ifdef UNITTEST_DEBUG
# include <signal.h>
# endif
# include <sys/wait.h> /* waitpid(2) */
# include <unistd.h>
# define _MAIN_CC
# define snprint_eq snprintf
typedef struct stat STAT_T;
#endif
#include "clar.h"
#define CL_WITHIN(n, min, max) ((n) >= (min) && (n) <= (max))
bool clar_expecting_passert = false;
bool clar_passert_occurred = false;
jmp_buf clar_passert_jmp_buf;
static void fs_rm(const char *_source);
static void fs_copy(const char *_source, const char *dest);
static const char *
fixture_path(const char *base, const char *fixture_name);
struct clar_error {
const char *test;
int test_number;
const char *suite;
const char *file;
int line_number;
const char *error_msg;
char *description;
struct clar_error *next;
};
static struct {
const char *active_test;
const char *active_suite;
int suite_errors;
int total_errors;
int test_count;
int report_errors_only;
int exit_on_error;
struct clar_error *errors;
struct clar_error *last_error;
void (*local_cleanup)(void *);
void *local_cleanup_payload;
jmp_buf trampoline;
int trampoline_enabled;
} _clar;
struct clar_func {
const char *name;
void (*ptr)(void);
};
struct clar_suite {
int index;
const char *name;
struct clar_func initialize;
struct clar_func cleanup;
const char **categories;
const struct clar_func *tests;
size_t test_count;
};
/* From clar_print_*.c */
static void clar_print_init(int test_count, int suite_count, const char *suite_names);
static void clar_print_shutdown(int test_count, int suite_count, int error_count);
static void clar_print_error(int num, const struct clar_error *error);
static void clar_print_ontest(const char *test_name, int test_number, int failed);
static void clar_print_onsuite(const char *suite_name, int suite_index);
static void clar_print_onabort(const char *msg, ...);
/* From clar_sandbox.c */
static void clar_unsandbox(void);
static int clar_sandbox(void);
/* From clar_mock.c */
static void clar_mock_reset(void);
static void clar_mock_cleanup(void);
/* From clar_categorize.c */
static int clar_category_is_suite_enabled(const struct clar_suite *);
static void clar_category_enable(const char *category);
static void clar_category_enable_all(size_t, const struct clar_suite *);
static void clar_category_print_enabled(const char *prefix);
/* Event callback overrides */
${clar_event_overrides}
/* Autogenerated test data by clar */
${clar_callbacks}
${clar_categories}
static const struct clar_suite _clar_suites[] = {
${clar_suites}
};
static size_t _clar_suite_count = ${clar_suite_count};
static size_t _clar_callback_count = ${clar_callback_count};
/* Core test functions */
static void
clar_report_errors(void)
{
int i = 1;
struct clar_error *error, *next;
error = _clar.errors;
while (error != NULL) {
next = error->next;
clar_print_error(i++, error);
free(error->description);
free(error);
error = next;
}
_clar.errors = _clar.last_error = NULL;
}
static void
clar_run_test(
const struct clar_func *test,
const struct clar_func *initialize,
const struct clar_func *cleanup)
{
int error_st = _clar.suite_errors;
clar_on_test();
_clar.trampoline_enabled = 1;
clar_passert_occurred = false;
clar_expecting_passert = false;
if (setjmp(_clar.trampoline) == 0) {
if (initialize->ptr != NULL)
initialize->ptr();
printf("\n#------------------------------------------------------------------------");
printf("\n# Running test: %s...\n", test->name);
test->ptr();
}
_clar.trampoline_enabled = 0;
if (_clar.local_cleanup != NULL)
_clar.local_cleanup(_clar.local_cleanup_payload);
if (cleanup->ptr != NULL)
cleanup->ptr();
_clar.test_count++;
/* remove any local-set cleanup methods */
_clar.local_cleanup = NULL;
_clar.local_cleanup_payload = NULL;
if (_clar.report_errors_only)
clar_report_errors();
else
clar_print_ontest(
test->name,
_clar.test_count,
(_clar.suite_errors > error_st)
);
}
static void
clar_run_suite(const struct clar_suite *suite)
{
const struct clar_func *test = suite->tests;
size_t i;
if (!clar_category_is_suite_enabled(suite))
return;
if (_clar.exit_on_error && _clar.total_errors)
return;
if (!_clar.report_errors_only)
clar_print_onsuite(suite->name, suite->index);
clar_on_suite();
_clar.active_suite = suite->name;
_clar.suite_errors = 0;
for (i = 0; i < suite->test_count; ++i) {
_clar.active_test = test[i].name;
clar_run_test(&test[i], &suite->initialize, &suite->cleanup);
if (_clar.exit_on_error && _clar.total_errors)
return;
}
}
#if 0 /* temporarily disabled */
static void
clar_run_single(const struct clar_func *test,
const struct clar_suite *suite)
{
_clar.suite_errors = 0;
_clar.active_suite = suite->name;
_clar.active_test = test->name;
clar_run_test(test, &suite->initialize, &suite->cleanup);
}
#endif
static void
clar_usage(const char *arg)
{
printf("Usage: %s [options]\n\n", arg);
printf("Options:\n");
printf(" -sXX\t\tRun only the suite number or name XX\n");
printf(" -i<name>\tInclude category <name> tests\n");
printf(" -q \t\tOnly report tests that had an error\n");
printf(" -Q \t\tQuit as soon as a test fails\n");
printf(" -l \t\tPrint suite, category, and test names\n");
printf(" -tXX\t\tRun a specifc test by name\n");
exit(-1);
}
static void
clar_parse_args(int argc, char **argv)
{
int i;
for (i = 1; i < argc; ++i) {
char *argument = argv[i];
if (argument[0] != '-')
clar_usage(argv[0]);
switch (argument[1]) {
case 's': { /* given suite number, name, or prefix */
int num = 0, offset = (argument[2] == '=') ? 3 : 2;
int len = 0, is_num = 1, has_colon = 0, j;
for (argument += offset; *argument; ++argument) {
len++;
if (*argument >= '0' && *argument <= '9')
num = (num * 10) + (*argument - '0');
else {
is_num = 0;
if (*argument == ':')
has_colon = 1;
}
}
argument = argv[i] + offset;
if (!len)
clar_usage(argv[0]);
else if (is_num) {
if ((size_t)num >= _clar_suite_count) {
clar_print_onabort("Suite number %d does not exist.\n", num);
exit(-1);
}
clar_run_suite(&_clar_suites[num]);
}
else if (!has_colon || argument[-1] == ':') {
for (j = 0; j < (int)_clar_suite_count; ++j)
if (strncmp(argument, _clar_suites[j].name, len) == 0)
clar_run_suite(&_clar_suites[j]);
}
else {
for (j = 0; j < (int)_clar_suite_count; ++j)
if (strcmp(argument, _clar_suites[j].name) == 0) {
clar_run_suite(&_clar_suites[j]);
break;
}
}
if (_clar.active_suite == NULL) {
clar_print_onabort("No suite matching '%s' found.\n", argument);
exit(-1);
}
break;
}
// Run a particular test only. This code was added on top of base clar.
case 't': {
int offset = (argument[2] == '=') ? 3 : 2;
char *test_name = argument + offset;
const struct clar_suite *suite = &_clar_suites[0];
const struct clar_func *test_funcs = suite->tests;
const struct clar_func *test = NULL;
for (int i = 0; i < suite->test_count; i++) {
if (!strcmp(test_funcs[i].name, test_name)) {
test = &test_funcs[i];
break;
}
}
if (test == NULL) {
clar_print_onabort("No test named '%s'.\n", argument);
exit(-1);
}
_clar.active_suite = suite->name;
_clar.suite_errors = 0;
_clar.active_test = test->name;
clar_run_test(test, &suite->initialize, &suite->cleanup);
break;
}
case 'q':
_clar.report_errors_only = 1;
break;
case 'Q':
_clar.exit_on_error = 1;
break;
case 'i': {
int offset = (argument[2] == '=') ? 3 : 2;
if (strcasecmp("all", argument + offset) == 0)
clar_category_enable_all(_clar_suite_count, _clar_suites);
else
clar_category_enable(argument + offset);
break;
}
case 'l': {
size_t j;
printf("Test suites (use -s<name> to run just one):\n");
for (j = 0; j < _clar_suite_count; ++j)
printf(" %3d: %s\n", (int)j, _clar_suites[j].name);
printf("\nCategories (use -i<category> to include):\n");
clar_category_enable_all(_clar_suite_count, _clar_suites);
clar_category_print_enabled(" - ");
printf("\nTest names (use -t<name> to run just one):\n");
for (j = 0; j < _clar_suite_count; ++j) {
const struct clar_suite *suite = &_clar_suites[j];
for (int i = 0; i < suite->test_count; i++) {
printf(" %s\n", suite->tests[i].name);
}
}
printf("\n");
exit(0);
}
default:
clar_usage(argv[0]);
}
}
}
static int
clar_test(int argc, char **argv)
{
clar_print_init(
(int)_clar_callback_count,
(int)_clar_suite_count,
""
);
if (clar_sandbox() < 0) {
clar_print_onabort("Failed to sandbox the test runner.\n");
exit(-1);
}
clar_mock_reset();
clar_on_init();
if (argc > 1)
clar_parse_args(argc, argv);
if (_clar.active_suite == NULL) {
size_t i;
for (i = 0; i < _clar_suite_count; ++i)
clar_run_suite(&_clar_suites[i]);
}
clar_print_shutdown(
_clar.test_count,
(int)_clar_suite_count,
_clar.total_errors
);
clar_on_shutdown();
clar_mock_cleanup();
clar_unsandbox();
return _clar.total_errors;
}
char *strdup(const char *s1);
void
clar__assert(
int condition,
const char *file,
int line,
const char *error_msg,
const char *description,
int should_abort)
{
struct clar_error *error;
if (condition)
return;
#ifndef _WIN32
#ifdef UNITTEST_DEBUG
// Break in debugger:
raise(SIGINT);
#endif
#endif
error = calloc(1, sizeof(struct clar_error));
if (_clar.errors == NULL)
_clar.errors = error;
if (_clar.last_error != NULL)
_clar.last_error->next = error;
_clar.last_error = error;
error->test = _clar.active_test;
error->test_number = _clar.test_count;
error->suite = _clar.active_suite;
error->file = file;
error->line_number = line;
error->error_msg = error_msg;
if (description != NULL)
error->description = strdup(description);
_clar.suite_errors++;
_clar.total_errors++;
if (should_abort) {
if (!_clar.trampoline_enabled) {
clar_print_onabort(
"Fatal error: a cleanup method raised an exception.");
clar_report_errors();
exit(-1);
}
longjmp(_clar.trampoline, -1);
}
}
void clar__assert_equal_s(
const char *s1,
const char *s2,
const char *file,
int line,
const char *err,
int should_abort)
{
int match = (s1 == NULL || s2 == NULL) ? (s1 == s2) : (strcmp(s1, s2) == 0);
if (!match) {
char buf[4096];
snprint_eq(buf, 4096, "'%s' != '%s'", s1, s2);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void clar__assert_equal_i(
int i1,
int i2,
const char *file,
int line,
const char *err,
int should_abort)
{
if (i1 != i2) {
char buf[128];
snprint_eq(buf, 128, "%d != %d", i1, i2);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void clar__assert_equal_d(
double d1,
double d2,
const char *file,
int line,
const char *err,
int should_abort)
{
if (d1 != d2) {
char buf[128];
snprint_eq(buf, 128, "%f != %f", d1, d2);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void clar__assert_within(
int n,
int min,
int max,
const char *file,
int line,
const char *err,
int should_abort)
{
if (!CL_WITHIN(n, min, max)) {
char buf[256];
snprint_eq(buf, 256, "%d not within [%d, %d]", n, min, max);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void clar__assert_near(
int i1,
int i2,
int abs_err,
const char *file,
int line,
const char *err,
int should_abort)
{
if (abs(i1 - i2) > abs_err) {
char buf[256];
snprint_eq(buf, 256, "Difference between %d and %d exceeds %d", i1, i2, abs_err);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void clar__assert_cmp_i(
int i1,
int i2,
ClarCmpOp op,
const char *file,
int line,
const char *err,
int should_abort)
{
char *fmt;
bool rv = false;
switch (op) {
case ClarCmpOp_EQ:
fmt = "NOT ==";
rv = (i1 == i2);
break;
case ClarCmpOp_LE:
rv = (i1 <= i2);
fmt = "NOT <=";
break;
case ClarCmpOp_LT:
rv = (i1 < i2);
fmt = "NOT <";
break;
case ClarCmpOp_GE:
rv = (i1 >= i2);
fmt = "NOT >=";
break;
case ClarCmpOp_GT:
rv = (i1 > i2);
fmt = "NOT >";
break;
case ClarCmpOp_NE:
rv = (i1 != i2);
fmt = "NOT !=";
break;
}
if (!rv) {
char buf[256] = {};
snprintf(buf, 256, "%d %s %d", i1, fmt, i2);
clar__assert(0, file, line, err, buf, should_abort);
}
}
// Shamelessly stolen from
// http://stackoverflow.com/questions/7775991/how-to-get-hexdump-of-a-structure-data
void hex_dump(char *desc, void *addr, int len) {
int i;
unsigned char buff[17];
unsigned char *pc = (unsigned char*)addr;
// Output description if given.
if (desc != NULL) {
printf("%s:\n", desc);
}
// Process every byte in the data.
for (i = 0; i < len; i++) {
// Multiple of 16 means new line (with line offset).
if ((i % 16) == 0) {
// Just don't print ASCII for the zeroth line.
if (i != 0) {
printf(" %s\n", buff);
}
// Output the offset.
printf(" %04x ", i);
}
// Now the hex code for the specific character.
printf(" %02x", pc[i]);
// And store a printable ASCII character for later.
if ((pc[i] < 0x20) || (pc[i] > 0x7e)) {
buff[i % 16] = '.';
} else {
buff[i % 16] = pc[i];
}
buff[(i % 16) + 1] = '\0';
}
// Pad out last line if not exactly 16 characters.
while ((i % 16) != 0) {
printf(" ");
i++;
}
// And print the final ASCII bit.
printf(" %s\n", buff);
}
void clar__assert_equal_m(
uint8_t *m1,
uint8_t *m2,
int n,
const char *file,
int line,
const char *err,
int should_abort)
{
if (m1 == m2 || n == 0) {
return;
}
if (m1 == NULL || m2 == NULL) {
char buf[4096];
snprint_eq(buf, 4096, "'%p' != '%p'", m1, m2);
clar__assert(0, file, line, err, buf, should_abort);
}
for (int offset = 0; offset < n; offset++) {
if (m1[offset] == m2[offset]) {
continue;
}
int len = 4096;
int off = 0;
char buf[len];
off += snprint_eq(buf + off, len - off, "Mismatch at offset %d. Look above for diff.\n", offset);
hex_dump("m1:", m1, n);
hex_dump("m2:", m2, n);
fflush(stdout);
clar__assert(0, file, line, err, buf, should_abort);
}
}
void cl_set_cleanup(void (*cleanup)(void *), void *opaque)
{
_clar.local_cleanup = cleanup;
_clar.local_cleanup_payload = opaque;
}
${clar_modules}
int _MAIN_CC main(int argc, char *argv[])
{
return clar_test(argc, argv);
}