#include "ihct.h" #include "vector.h" #include #include #include #include #include #include // handle tests that yield fatal signals. #include #include #include // The point of which to restore to on a fatal signal. jmp_buf restore_environment; // the sigaction used for catching fatal signals. struct sigaction recover_action; // The final summary string printed. char *summary_str; // A list of all units. static ihct_vector *testunits; // An array of all first failed (or last if all successful) assert results in every test. static ihct_test_result **ihct_results; // Object representing a testing unit, containing the units name and its procedure // (implemented test function). typedef struct { char *name; ihct_test_proc procedure; } ihct_unit; // The number of seconds passed until a test is considered timedout. // Default 3. Can be set with -t [time in sec] int test_timeout = 3; pthread_cond_t routine_done = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // These are ISO/IEC 6429 escape sequences for // communicating text attributes to terminal emulators. // Note that some compilers do not understand '\x1b', and therefore \033[0m is // used instead. #define IHCT_RESET "\033[0m" #define IHCT_BOLD "\033[1m" #define IHCT_FG_GRAY "\033[30;1m" #define IHCT_FG_RED "\033[31;1m" #define IHCT_FG_GREEN "\033[32;1m" #define IHCT_FG_YELLOW "\033[33;1m" #define IHCT_FG_BLUE "\033[34;1m" #define IHCT_FG_MAGENTA "\033[35;1m" #define IHCT_FG_CYAN "\033[36;1m" #define IHCT_FG_WHITE "\033[37;1m" #define IHCT_BG_BLACK "\033[40;1m" #define IHCT_BG_RED "\033[41;1m" #define IHCT_BG_GREEN "\033[42;1m" #define IHCT_BG_YELLOW "\033[43;1m" #define IHCT_BG_BLUE "\033[44;1m" #define IHCT_BG_MAGENTA "\033[45;1m" #define IHCT_BG_CYAN "\033[46;1m" #define IHCT_BG_GRAY "\033[47;1m" // Procedure called when a signal is thrown within a test. When a fatal signal is // recieved, we jump back to before the test is ran, giving the signal code. static void ihct_recovery_proc(int sig) { // Restore. longjmp(restore_environment, sig); } // Binds the sigaction to signals, and make it call ihct_recovery_proc. static void ihct_setup_recover_action(void) { recover_action.sa_handler = &ihct_recovery_proc; sigemptyset(&recover_action.sa_mask); recover_action.sa_flags = 0; // binding sigactions. Dont pick up on user interrupts (let them be managed // normally). sigaction(SIGSEGV, &recover_action, NULL); sigaction(SIGTERM, &recover_action, NULL); sigaction(SIGFPE, &recover_action, NULL); sigaction(SIGILL, &recover_action, NULL); sigaction(SIGABRT, &recover_action, NULL); sigaction(SIGBUS, &recover_action, NULL); } void ihct_print_result(ihct_test_result *result) { switch (result->status) { case PASS: fputs(IHCT_BG_GREEN IHCT_BOLD "." IHCT_RESET, stdout); break; case FAIL_FORCE: case FAIL: fputs(IHCT_BG_RED IHCT_BOLD ":" IHCT_RESET, stdout); break; case ERR: fputs(IHCT_BG_RED IHCT_BOLD "!" IHCT_RESET, stdout); break; case TIMEOUT: fputs(IHCT_BG_YELLOW IHCT_BOLD "?" IHCT_RESET, stdout); break; } } // Reallocates and appends string s to summary_str void ihct_add_to_summary(char *s) { char *p = realloc(summary_str, strlen(summary_str) + strlen(s) + 1); if(!p) { printf("Couldn't allocate a str big enough to hold summary. Aborting.\n"); return; } summary_str = p; strcat(summary_str, s); } void ihct_add_error_to_summary(ihct_test_result *res, ihct_unit *unit) { char *msg; char *msg_format; size_t msg_size; switch (res->status) { case PASS: break; case FAIL: msg_format = IHCT_BOLD "%s:%d: " IHCT_RESET "assertion in '" IHCT_BOLD "%s" IHCT_RESET "' " IHCT_FG_RED "failed" IHCT_RESET ":\n\t'" IHCT_FG_YELLOW "%s" IHCT_RESET "'\n"; msg_size = snprintf(NULL, 0, msg_format, res->file, res->line, unit->name, res->code) + 1; msg = calloc(msg_size, sizeof(*msg)); sprintf(msg, msg_format, res->file, res->line, unit->name, res->code); break; case FAIL_FORCE: msg_format = IHCT_BOLD "%s:%d: " IHCT_RESET "'" IHCT_BOLD "%s" IHCT_RESET "' " IHCT_FG_RED "forcefully failed" IHCT_RESET ".\n"; msg_size = snprintf(NULL, 0, msg_format, res->file, res->line, unit->name) + 1; msg = calloc(msg_size, sizeof(*msg)); sprintf(msg, msg_format, res->file, res->line, unit->name); break; case ERR: msg_format = "unit '" IHCT_BOLD "%s" IHCT_RESET "' had to restore because of fatal signal (" IHCT_FG_RED "%s" IHCT_RESET ")\n"; msg_size = snprintf(NULL, 0, msg_format, unit->name, res->code) + 1; msg = calloc(msg_size, sizeof(*msg)); sprintf(msg, msg_format, unit->name, res->code); break; case TIMEOUT: msg_format = "unit '" IHCT_BOLD "%s" IHCT_RESET "' " IHCT_FG_YELLOW "timed out " IHCT_RESET "(took " IHCT_FG_YELLOW "5 " IHCT_RESET "seconds).\n"; msg_size = snprintf(NULL, 0, msg_format, unit->name) + 1; msg = calloc(msg_size, sizeof(*msg)); sprintf(msg, msg_format, unit->name); } ihct_add_to_summary(msg); free(msg); } bool ihct_assert_impl(bool eval, ihct_test_result *result, char *code, char *file, unsigned long line) { result->status = eval ? PASS : FAIL; if(!eval) { result->file = file; result->line = line; result->code = code; return false; } return true; } void ihct_pass_impl(ihct_test_result *result, char *file, unsigned long line) { result->status = PASS; result->file = file; result->line = line; } void ihct_fail_impl(ihct_test_result *result, char *file, unsigned long line) { result->status = FAIL_FORCE; result->file = file; result->line = line; } static ihct_unit *ihct_init_unit(char *name, ihct_test_proc procedure) { ihct_unit *unit = (ihct_unit *)malloc(sizeof(ihct_unit)); char *strmem = malloc(strlen(name) + 1); strcpy(strmem, name); unit->name = strmem; unit->procedure = procedure; return unit; } void ihct_construct_test_impl(char *name, ihct_test_proc procedure) { ihct_unit *unit = ihct_init_unit(name, procedure); ihct_vector_add(testunits, unit); } static void ihct_unit_free(ihct_unit *unit) { free(unit->name); free(unit); } void ihct_init(void) { // atm, only initializes the unit list. Is this neccessary? testunits = ihct_vector_init(); } struct routine_run_unit_data { ihct_test_proc proc; ihct_test_result *result; }; // Routine run in a separate thread to execute the unit. void *routine_run_unit(void *arg) { struct routine_run_unit_data *data = (struct routine_run_unit_data *)arg; int old; // Allow the thread to be canceled. pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old); // Create a jump point, to be able to restore when encountering fatal signal. // When returning here because of a fatal signal, we abort unit with status // ERR. Has to be done inside of thread, refer to man setjmp: // "If, in a multithreaded program, a longjmp() call employs an env // buffer that was initialized by a call to setjmp() in a different // thread, the behavior is undefined.". ihct_setup_recover_action(); int restore_status = setjmp(restore_environment); if(restore_status != 0) { char *p = malloc(strlen(strsignal(restore_status)) + 1); strcpy(p, strsignal(restore_status)); data->result->code = p; data->result->status = ERR; // We still want to emit finished signal pthread_mutex_lock(&lock); pthread_cond_signal(&routine_done); pthread_mutex_unlock(&lock); return NULL; } // Run test, and save it's result. (*data->proc)(data->result); // Emit signal that thread is finished. pthread_mutex_lock(&lock); pthread_cond_signal(&routine_done); pthread_mutex_unlock(&lock); return NULL; } static ihct_test_result *ihct_run_specific(ihct_unit *unit) { // Allocate memory for the tests result, and set it to passed by default. ihct_test_result *result = malloc(sizeof(ihct_test_result)); result->status = PASS; // lock current thread. pthread_mutex_lock(&lock); // Create a separate thread to run the test in. We set a limited time // the process may be run, and abort if it times out. struct timespec timeout; clock_gettime(CLOCK_REALTIME, &timeout); timeout.tv_sec += test_timeout; // Create a temporary data struct to carry data into thread. struct routine_run_unit_data data = {unit->procedure, result}; // Create new thread to run the unit routine. pthread_t tid; pthread_cond_init(&routine_done, NULL); pthread_create(&tid, NULL, routine_run_unit, &data); int err = pthread_cond_timedwait(&routine_done, &lock, &timeout); pthread_mutex_unlock(&lock); // If timed out, force quit thread and return TIMEOUT. // Note that this is not safe memory. There is a high chance that // the thread may not be freed. Looking into solving this. if(err == ETIMEDOUT) { pthread_cancel(tid); result->status = TIMEOUT; return result; } //pthread_join(procedure, NULL); pthread_join(tid, NULL); // Run test, and save it's result into i. //(*unit->procedure)(result); return result; } int ihct_run(int argc, char **argv) { unsigned unit_count = testunits->size; // Allocate results ihct_results = calloc(unit_count, sizeof(ihct_test_result *)); unsigned failed_count = 0; // initialize the summary string summary_str = malloc(sizeof(char)); *summary_str = '\0'; // handle args int c; while((c = getopt(argc, argv, "t:")) != -1) { switch(c) { case 't': test_timeout = atoi(optarg); break; case '?': printf("unknown option '%c'.\n", optopt); } } // start clock struct timespec tbegin, tend; clock_gettime(CLOCK_MONOTONIC, &tbegin); // Iterate over every test for(unsigned i = 0; i < unit_count; i++) { ihct_unit *unit = ihct_vector_get(testunits, i); ihct_results[i] = ihct_run_specific(unit); // ensure 80 width if(i % 80 == 0 && i != 0) putc('\n', stdout); ihct_print_result(ihct_results[i]); fflush(stdout); if(ihct_results[i]->status) { failed_count++; ihct_add_error_to_summary(ihct_results[i], unit); } // Frees both the unit and the result. ihct_unit_free(unit); // Also frees dynamic allocated string if status is err (the signal name). if(ihct_results[i]->status == ERR) free(ihct_results[i]->code); free(ihct_results[i]); } free(ihct_results); ihct_vector_free(testunits); clock_gettime(CLOCK_MONOTONIC, &tend); double elapsed = (tend.tv_sec - tbegin.tv_sec); elapsed += (tend.tv_nsec - tbegin.tv_nsec) / 1000000000.0; // print all messages if(strlen(summary_str) > 4) printf("\n\n%s\n", summary_str); else puts("\n\n"); // Free summary str free(summary_str); printf("tests took %.2f seconds\n", elapsed); if(failed_count) { char *status_format = IHCT_FG_GREEN "%d successful " IHCT_RESET "and " IHCT_FG_RED "%d failed " IHCT_RESET "of " IHCT_FG_YELLOW "%d run" IHCT_RESET "\n"; printf(status_format, unit_count-failed_count, failed_count, unit_count); printf(IHCT_FG_RED "FAILURE\n" IHCT_RESET); return 1; } char *status_format = IHCT_FG_GREEN "%d successful " IHCT_RESET "of " IHCT_FG_YELLOW "%d run" IHCT_RESET "\n"; printf(status_format, unit_count, unit_count); printf(IHCT_FG_GREEN "SUCCESS\n" IHCT_RESET); return 0; } // Lets the program run tests on itself. This is done with the compiler flag // IHCT_TEST_SELF. Still requires an external main entrypoint. #ifdef IHCT_TEST_SELF // Create two internal test procedures, for testing the test creation. static void itest_true(ihct_test_result *result) { IHCT_ASSERT(true); } static void itest_false(ihct_test_result *result) { IHCT_ASSERT(false); } IHCT_TEST(self_unit_create) { ihct_unit *u = ihct_init_unit("internal_true", &itest_true); IHCT_ASSERT_STR(u->name, "internal_true"); IHCT_ASSERT(&itest_true == u->procedure); } IHCT_TEST(self_vector_create) { ihct_vector *v = ihct_init_vector(); IHCT_ASSERT(v != NULL); IHCT_ASSERT(v->data == NULL); IHCT_ASSERT(v->size == 0); ihct_free_vector(v); } IHCT_TEST(self_vector_all) { ihct_vector *v = ihct_init_vector(); for(int i = 0; i < 1000; ++i) { int *t = malloc(sizeof(int)); *t = i * 2; ihct_vector_add(v, t); } for(int i = 0; i < 1000; ++i) { int *t = ihct_vector_get(v, i); IHCT_ASSERT(i * 2 == *t); free(t); } ihct_free_vector(v); } #endif