/* SPDX-License-Identifier: GPL-2.0-or-later UMKa - User-Mode KolibriOS developer tools runtests - the test runner Copyright (C) 2023 Ivan Baravy */ #include #include #include #include #include #include #include #include #include #include #include "optparse/optparse.h" #ifndef PATH_MAX #define PATH_MAX 0x1000 #endif #define CMPFILE_BUF_LEN 0x100000 #define TIMEOUT_FILENAME "timeout.txt" #define TIMEOUT_STR_MAX_LEN 16 #define ACTION_RUN 0 _Thread_local char bufa[CMPFILE_BUF_LEN]; _Thread_local char bufb[CMPFILE_BUF_LEN]; _Thread_local char timeoutfname[PATH_MAX]; _Thread_local char reffname[PATH_MAX]; _Thread_local char outfname[PATH_MAX]; int coverage = 0; int no_timeout = 0; int silent_success = 1; static int is_valid_test(const char *name) { // Check that name starts with t\d\d\d if (name[0] != 't') { return 0; } for (size_t i = 1; i < 4 && name[i]; i++) { if (name[1] < '0' || name[1] > '9') { return 0; } } // Check that run.us file exists sprintf(reffname, "%s/%s", name, "run.us"); FILE *f = fopen(reffname, "rb"); if (f) { fclose(f); } return f != NULL; } static int cmpfiles(const char *fnamea, const char *fnameb) { int result = 0; FILE *filea = fopen(fnamea, "rb"); if (!filea) { fprintf(stderr, "Can't open file '%s' for reading: %s\n", fnamea, strerror(errno)); return -1; } FILE *fileb = fopen(fnameb, "rb"); if (!fileb) { fprintf(stderr, "Can't open file '%s' for reading: %s\n", fnameb, strerror(errno)); fclose(filea); return -1; } while (1) { size_t reada = fread(bufa, 1, CMPFILE_BUF_LEN, filea); size_t readb = fread(bufb, 1, CMPFILE_BUF_LEN, fileb); if (reada != readb || memcmp(bufa, bufb, reada)) { fprintf(stderr, "[!] files %s and %s don't match\n", fnamea, fnameb); result = -1; break; } if (feof(filea) && feof(fileb)) { break; } } fclose(filea); fclose(fileb); return result; } static int check_test_artefacts(const char *testname) { DIR *tdir = opendir(testname); if (!tdir) { fprintf(stderr, "Can't open directory '%s': %s\n", testname, strerror(errno)); return -1; } struct dirent *dent; while ((dent = readdir(tdir))) { const char *refdot = strstr(dent->d_name, "ref."); if (!refdot) { continue; } size_t ref_off = refdot - dent->d_name; sprintf(reffname, "%s/%s", testname, dent->d_name); strcpy(outfname, reffname); memcpy(outfname + strlen(testname) + 1 + ref_off, "out", 3); if (cmpfiles(reffname, outfname)) { return -1; } } closedir(tdir); return 0; } struct test_wait_arg { int pid; pthread_cond_t *cond; }; static time_t get_test_timeout(const char *testname) { sprintf(timeoutfname, "%s/%s", testname, TIMEOUT_FILENAME); FILE *f = fopen(timeoutfname, "rb"); if (!f) { fprintf(stderr, "[!] Can't open %s\n: %s\n", TIMEOUT_FILENAME, strerror(errno)); return 0; } char str[TIMEOUT_STR_MAX_LEN]; fread(str, 1, TIMEOUT_STR_MAX_LEN, f); fclose(f); unsigned n, sec = 0, min = 0; char *end; n = strtoul(str, &end, 0); switch (*end) { case 's': sec = n; break; case 'm': min = n; sec = strtoul(end+1, &end, 0); break; default: fprintf(stderr, "[!] bad timeout value: %s\n", str); return 0; } return min*60 + sec; } #ifndef _WIN32 #include static void * thread_wait(void *arg) { struct test_wait_arg *wa = arg; int status; waitpid(wa->pid, &status, 0); pthread_cond_signal(wa->cond); return (void *)(intptr_t)status; } static void * run_test(const void *arg) { const char *test_name = arg; void *result = NULL; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex); int child; if (!(child = fork())) { chdir(test_name); if (coverage) { char covfile[64]; sprintf(covfile, "../cov_%s", test_name); execl("/usr/bin/taskset", "taskset", "1", "sudo", "../../umka_shell", "-ri", "run.us", "-o", "out.log", "-c", covfile, NULL); } else { execl("../../umka_shell", "../../umka_shell", "-ri", "run.us", "-o", "out.log", NULL); } fprintf(stderr, "Can't run test command: %s\n", strerror(errno)); return (void *)-1; } pthread_t t; struct test_wait_arg wa = {.pid = child, .cond = &cond}; pthread_create(&t, NULL, thread_wait, &wa); time_t tout = get_test_timeout(test_name); struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); if (no_timeout) { ts.tv_sec += 60*60*24*7; // a week } else { ts.tv_sec += tout; } int status; if ((status = pthread_cond_timedwait(&cond, &mutex, &ts))) { fprintf(stderr, "[!] %s: timeout (%llim%llis)\n", test_name, tout/60, tout % 60); kill(child, SIGKILL); result = (void *)(intptr_t)status; } pthread_join(t, &result); pthread_mutex_unlock(&mutex); return result; } #else #include #include #include #include #include static void * run_test(const void *arg) { const char *test_name = arg; void *result = NULL; STARTUPINFO si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); memset(&pi, 0, sizeof(pi)); unsigned tout = get_test_timeout(test_name); if(!CreateProcessA(NULL, "../umka_shell -ri run.us -o out.log", NULL, NULL, FALSE, 0, NULL, test_name, &si, &pi)) { fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError()); return (void *)-1; } DWORD wait_res = WaitForSingleObject(pi.hProcess, tout*1000); if (wait_res == WAIT_TIMEOUT) { TerminateProcess(pi.hProcess, 1); result = (void *)(intptr_t)-1; } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return result; } #endif pthread_mutex_t readdir_mutex = PTHREAD_MUTEX_INITIALIZER; static void * thread_run_test(void *arg) { DIR *cwd = arg; struct dirent *dent; while (1) { pthread_mutex_lock(&readdir_mutex); dent = readdir(cwd); pthread_mutex_unlock(&readdir_mutex); if (!dent) { break; } const char *testname = dent->d_name; if (!is_valid_test(testname)) { continue; } fprintf(stderr, "running test %s\n", testname); if (run_test(testname) || check_test_artefacts(testname)) { fprintf(stderr, "[!] test %s failed\n", testname); } else if (!silent_success) { fprintf(stderr, "test %s ok\n", testname); } } return NULL; } int main(int argc, char *argv[]) { (void)argc; // int action = ACTION_RUN; size_t nthreads = 1; struct optparse opts; optparse_init(&opts, argv); int opt; while ((opt = optparse(&opts, "cj:st")) != -1) { switch (opt) { case 'c': coverage = 1; break; case 'j': nthreads = strtoul(opts.optarg, NULL, 0); break; case 's': silent_success = 0; break; case 't': no_timeout = 1; break; default: fprintf(stderr, "[!] Unknown option: '%c'\n", opt); exit(1); } } DIR *cwd = opendir("."); pthread_t *threads = malloc(sizeof(pthread_t)*nthreads); for (size_t i = 0; i < nthreads; i++) { pthread_create(threads + i, NULL, thread_run_test, cwd); } for (size_t i = 0; i < nthreads; i++) { void *retval; pthread_join(threads[i], &retval); } free(threads); return 0; }