From f3fdcc15f4b315b6b91e789bd2aa5b6c3d8ab037 Mon Sep 17 00:00:00 2001 From: Matthew Zito Date: Sun, 19 Jan 2025 01:44:01 -0800 Subject: [PATCH] tmp --- Makefile | 4 +- TODO.md | 1 + clib.json | 12 +- deps/libtap/clib.json | 16 ++ deps/libtap/libtap.h | 106 ++++++++++++ deps/libtap/tap.c | 337 +++++++++++++++++++++++++++++++++++++ deps/tap.c/package.json | 23 --- deps/tap.c/tap.c | 321 ----------------------------------- deps/tap.c/tap.h | 114 ------------- include/api/commands.h | 5 + include/cronentry.h | 12 +- include/job.h | 8 + scripts/entrypoint.sh | 3 +- src/api/commands.c | 53 +++--- src/api/ipc.c | 17 +- src/cronentry.c | 6 +- src/crontab.c | 10 +- src/job.c | 8 +- src/utils/json.c | 85 +++++++--- t/integ/ipc_shpec.bash | 9 +- t/integ/utils/run.bash | 10 +- t/unit/crontab_test.c | 34 ++-- t/unit/ipc_commands_test.c | 140 +++++++++++++++ t/unit/main.c | 12 +- t/unit/parser_test.c | 9 +- t/unit/regexpr_test.c | 3 +- t/unit/test_utils.c | 6 +- t/unit/tests.h | 5 + t/unit/utils_test.c | 7 +- x.js | 9 + x.json | 92 ++++++++++ 31 files changed, 890 insertions(+), 587 deletions(-) create mode 100644 deps/libtap/clib.json create mode 100644 deps/libtap/libtap.h create mode 100644 deps/libtap/tap.c delete mode 100644 deps/tap.c/package.json delete mode 100644 deps/tap.c/tap.c delete mode 100644 deps/tap.c/tap.h create mode 100644 t/unit/ipc_commands_test.c create mode 100644 x.js create mode 100644 x.json diff --git a/Makefile b/Makefile index 369217a..a357512 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ TESTDIR := t SRC := $(shell find $(SRCDIR) -name "*.c") TESTS := $(shell find $(TESTDIR) -name "*.c") SRC_NOMAIN := $(filter-out $(SRCDIR)/main.c, $(SRC)) -TEST_DEPS := $(wildcard $(DEPSDIR)/tap.c/*.c) -DEPS := $(filter-out $(wildcard $(DEPSDIR)/tap.c/*), $(wildcard $(DEPSDIR)/*/*.c)) +TEST_DEPS := $(wildcard $(DEPSDIR)/libtap/*.c) +DEPS := $(filter-out $(wildcard $(DEPSDIR)/libtap/*), $(wildcard $(DEPSDIR)/*/*.c)) UNIT_TESTS := $(wildcard $(TESTDIR)/unit/*.c) INCLUDES := -I$(DEPSDIR) -I$(INCDIR) diff --git a/TODO.md b/TODO.md index c6d01d9..3985fb9 100644 --- a/TODO.md +++ b/TODO.md @@ -42,6 +42,7 @@ - [ ] Make all APIs consistent (e.g. return new string or always accept the dest as input) - [ ] Document all behaviors and usage patterns; complete manpage - [ ] Perf improvements +- [ ] Update Makefile template w/manpage and install updates We make tradeoffs between runtime op/main loop work and startup work (loading the crontabs). diff --git a/clib.json b/clib.json index 079868c..4075002 100644 --- a/clib.json +++ b/clib.json @@ -5,13 +5,7 @@ "repo": "exbotanical/chronic", "license": "MIT", "description": "A cron daemon for modern UNIX systems", - "keywords": [ - "cron", - "daemon", - "scheduler", - "job", - "task runner" - ], + "keywords": ["cron", "daemon", "scheduler", "job", "task runner"], "install": "echo TODO", "uninstall": "echo TODO", "dependencies": { @@ -20,7 +14,7 @@ "clibs/commander": "1.3.2" }, "development": { - "thlorenz/tap.c": "*", + "exbotanical/libtap": "*", "entr": "5.4" } -} \ No newline at end of file +} diff --git a/deps/libtap/clib.json b/deps/libtap/clib.json new file mode 100644 index 0000000..73093d2 --- /dev/null +++ b/deps/libtap/clib.json @@ -0,0 +1,16 @@ +{ + "name": "libtap", + "version": "0.0.9", + "author": "Matthew Zito", + "repo": "exbotanical/libtap", + "license": "MIT", + "description": "An implementation of TAP testing", + "keywords": ["test anything protocol", "testing", "unit test"], + "src": ["include/libtap.h", "src/tap.c"], + "development": { + "exbotanical/print-assert": "0.0.1" + }, + "dependencies": { + "strdup": "0.1.5" + } +} diff --git a/deps/libtap/libtap.h b/deps/libtap/libtap.h new file mode 100644 index 0000000..3abe708 --- /dev/null +++ b/deps/libtap/libtap.h @@ -0,0 +1,106 @@ +#ifndef LIBTAP_H +#define LIBTAP_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// TODO: parser for gh actions, etc +unsigned int __tap_ok(unsigned int ok, const char* fn_name, const char* file, const unsigned int line, const char* fmt, ...); +void __tap_skip(unsigned int num_skips, const char* msg); +int __tap_write_shared_mem(int status); + +#define __tap_lives_or_dies(wants_death, code, ...) \ + do { \ + /* set shared memory to 1 */ \ + __tap_write_shared_mem(1); \ + \ + pid_t pid = fork(); \ + switch (pid) { \ + case -1: { \ + perror("fork"); \ + exit(EXIT_FAILURE); \ + } \ + case 0: { \ + close(STDOUT_FILENO); \ + close(STDERR_FILENO); \ + /* execute test code, then set shared memory to zero */ \ + code __tap_write_shared_mem(0); \ + exit(EXIT_SUCCESS); \ + } \ + } \ + \ + if (waitpid(pid, NULL, 0) < 0) { \ + perror("waitpid"); \ + exit(EXIT_FAILURE); \ + } \ + /* grab prev value (and reset) - if 0, code succeeded */ \ + int test_died = __tap_write_shared_mem(0); \ + if (!test_died) { \ + code \ + } \ + __tap_ok(wants_death ? test_died : !test_died, __func__, __FILE__, __LINE__, __VA_ARGS__); \ + } while (0) + +/* Begin public APIs */ + +void todo_start(const char* fmt, ...); +void todo_end(void); +void diag(const char* fmt, ...); +void plan(unsigned int num_tests); +unsigned int exit_status(void); +unsigned int bail_out(const char* fmt, ...); + +#define done_testing() return exit_status() +// clang-format off +#define ok(test, ...) __tap_ok(test ? 1 : 0, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define eq_num(a, b, ...) __tap_ok(a == b ? 1 : 0, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define neq_num(a, b, ...) __tap_ok(a != b ? 1 : 0, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define eq_str(a, b, ...) __tap_ok(strcmp(a, b) == 0 ? 1 : 0, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define neq_str(a, b, ...) __tap_ok(strcmp(a, b) == 0 ? 0 : 1, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define eq_null(a, ...) __tap_ok(a == NULL, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define neq_null(a, ...) __tap_ok(a != NULL, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define lives(...) __tap_lives_or_dies(0, __VA_ARGS__) +#define dies(...) __tap_lives_or_dies(1, __VA_ARGS__) +#define pass(...) ok(1, __VA_ARGS__) +#define fail(...) ok(0, __VA_ARGS__) + +#define skip_start(cond, num_skips, msg) do { if (cond) { __tap_skip(num_skips, msg); break; } +#define skip_end() } while (0) +#define skip(test, msg) __tap_skip(1, msg); + +#ifdef TAP_WANT_PCRE + +unsigned int +__tap_match ( + const char* string, + const char* pattern, + bool want_match, + const char* fn_name, + const char* file, + const unsigned int line, + const char* fmt, + ... +); + +#define match_str(string, pattern, ...) __tap_match(string, pattern, true, __func__, __FILE__, __LINE__, __VA_ARGS__) +#define nomatch_str(string, pattern, ...) __tap_match(string, pattern, false, __func__, __FILE__, __LINE__, __VA_ARGS__) + +#endif /* TAP_WANT_PCRE */ + +// clang-format on +#ifdef __cplusplus +} +#endif + +#endif /* LIBTAP_H */ diff --git a/deps/libtap/tap.c b/deps/libtap/tap.c new file mode 100644 index 0000000..1a56e63 --- /dev/null +++ b/deps/libtap/tap.c @@ -0,0 +1,337 @@ +#define _DEFAULT_SOURCE 1 // For vasprintf +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libtap.h" + +#if defined __APPLE__ || defined BSD +# define MAP_ANONYMOUS MAP_ANON +#endif + +#ifdef TAP_WANT_PTHREAD +# include +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +# define lock() pthread_mutex_lock(&mutex) +# define unlock() pthread_mutex_unlock(&mutex) +#else +# define lock() +# define unlock() +#endif + +#define with_lock(code) \ + lock(); \ + code unlock(); + +#define TAP_FAILURE_EXIT_STATUS 255 + +static unsigned int has_plan = 0; +static pid_t test_runner_pid = 0; +static unsigned int test_died = 0; +static unsigned int num_planned_tests = 0; +static unsigned int num_ran_tests = 0; +static unsigned int num_failed_tests = 0; +static unsigned int is_todo_block = 0; +static char* todo_msg = NULL; + +static inline char* +vstrdupf (const char* fmt, va_list args) { + va_list args_cp; + va_copy(args_cp, args); + if (!fmt) { + fmt = ""; + } + + // Pass length of zero first to determine number of bytes needed + size_t size = vsnprintf(NULL, 0, fmt, args_cp) + 2; + char* buf = (char*)malloc(size); + if (!buf) { + return NULL; + } + + vsnprintf(buf, size, fmt, args); + va_end(args_cp); + + return buf; +} + +noreturn void +tap_panic (const unsigned int errcode, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + + exit(errcode); +} + +static void +diagv (const char* fmt, va_list ap) { + fprintf(stdout, "# "); + vfprintf(stdout, fmt, ap); + fprintf(stdout, "\n"); +} + +static void +cleanup (void) { + // Fast exit if we've forked + if (getpid() != test_runner_pid) { + return; + } + + lock(); + + if (!has_plan) { + diag("Test died before any output could be written"); + + unlock(); + + return; + } + + if (test_died) { + diag("Test died after test %d", num_ran_tests); + + unlock(); + + return; + } + + if (has_plan) { + if (num_planned_tests != num_ran_tests) { + diag("Planned %d %s but ran %d", num_planned_tests, num_planned_tests == 1 ? "test" : "tests", num_ran_tests); + } + } + + if (num_failed_tests) { + diag("Failed %d tests of %d", num_failed_tests, num_ran_tests); + } + + unlock(); +} + +static unsigned int +__tap_vok (unsigned int ok, const char* fn_name, const char* file, const unsigned int line, const char* fmt, va_list args) { + char* msg = vstrdupf(fmt, args); + lock(); + + num_ran_tests++; + + if (!ok) { + num_failed_tests++; + fprintf(stdout, "not "); + } + + fprintf(stdout, "ok %d - ", num_ran_tests); + + // See "Escaping" - https://testanything.org/tap-version-14-specification.html + char* c; + flockfile(stdout); + for (c = msg; *c != '\0'; c++) { + if (*c == '#') { + fputc('\\', stdout); + } + fputc((int)*c, stdout); + } + funlockfile(stdout); + + if (is_todo_block) { + fprintf(stdout, " # TODO %s", todo_msg); + } + fprintf(stdout, "\n"); + free(msg); + + if (!ok) { + diag("\tFailed %stest (%s:%s at line %d)", is_todo_block ? "(TODO)" : "", file, fn_name, line); + } + + unlock(); + + return ok ? 1 : 0; +} + +unsigned int +__tap_ok (unsigned int ok, const char* fn_name, const char* file, const unsigned int line, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + unsigned int ret = __tap_vok(ok, fn_name, file, line, fmt, args); + va_end(args); + return ret; +} + +void +__tap_skip (unsigned int num_skips, const char* msg) { + with_lock({ + while (num_skips--) { + fprintf(stdout, "ok %d # SKIP %s", ++num_ran_tests, msg); + fprintf(stdout, "\n"); + } + }); +} + +int +__tap_write_shared_mem (int status) { + static int* test_died = NULL; + int prev; + + if (!test_died) { + test_died = mmap(0, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + *test_died = 0; + } + + prev = *test_died; + *test_died = status; + return prev; +} + +void +todo_start (const char* fmt, ...) { + va_list ap; + + with_lock({ + va_start(ap, fmt); + if (vasprintf(&todo_msg, fmt, ap) < 0) { + todo_msg = NULL; + } + va_end(ap); + + is_todo_block = 1; + }); +} + +void +todo_end (void) { + with_lock({ + is_todo_block = 0; + free(todo_msg); + todo_msg = NULL; + }); +} + +void +diag (const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + diagv(fmt, ap); + va_end(ap); +} + +void +plan (unsigned int num_ran_tests) { + lock(); + + static unsigned int singleton = 0; + if (!singleton) { + test_runner_pid = getpid(); + atexit(cleanup); + } + + singleton = 1; + + if (has_plan) { + test_died = 1; + + unlock(); + + tap_panic(TAP_FAILURE_EXIT_STATUS, "plan was called twice\n"); + } + + if (num_ran_tests == 0) { + test_died = 1; + + unlock(); + + tap_panic(TAP_FAILURE_EXIT_STATUS, "no tests planned\n"); + } + + has_plan = 1; + + num_planned_tests = num_ran_tests; + fprintf(stdout, "1..%d\n", num_planned_tests); + + unlock(); +} + +unsigned int +exit_status (void) { + unsigned int retval; + + with_lock({ + if (num_planned_tests < num_ran_tests) { + retval = num_ran_tests - num_planned_tests; + } else { + retval = num_failed_tests + (num_planned_tests - num_ran_tests); + } + + if (retval > TAP_FAILURE_EXIT_STATUS) { + retval = TAP_FAILURE_EXIT_STATUS; + } + }); + + return retval; +} + +unsigned int +bail_out (const char* fmt, ...) { + va_list args; + + va_start(args, fmt); + fprintf(stdout, "Bail out! "); + vprintf(fmt, args); + printf("\n"); + va_end(args); + + exit(TAP_FAILURE_EXIT_STATUS); + + return EXIT_SUCCESS; +} + +/* Extra features */ + +// #ifdef TAP_WANT_PCRE TODO: FIX! +#include + +// This should be proportional to the number of anticipated capture groups. +// Each capture group needs three slots (start and end offsets plus internal-use +// slot). We also must account for the main capture group. Thus: (1 + n) * 3, +// where n is the number of desired capture groups. +#define NCAPTGRPS 1 +#define OVECSIZE (1 + NCAPTGRPS) * 3 + +static int ovector[OVECSIZE]; + +// clang-format off +unsigned int +__tap_match ( + const char* string, + const char* pattern, + bool want_match, + const char* fn_name, + const char* file, + const unsigned int line, + const char* fmt, + ... +) { + // clang-format on + const char* error; + int erroffset; + pcre* re = pcre_compile(pattern, 0, &error, &erroffset, NULL); + if (!re) { + tap_panic(TAP_FAILURE_EXIT_STATUS, "Failed to compile regex pattern: %s (reason: %s)\n", error, pattern); + } + + int rc = pcre_exec(re, NULL, string, strlen(string), 0, 0, ovector, OVECSIZE); + + va_list args; + va_start(args, fmt); + unsigned int ret = __tap_vok(want_match ? rc >= 0 : rc < 0, fn_name, file, line, fmt, args); + va_end(args); + return ret; +} + +// #endif /* TAP_WANT_PCRE */ diff --git a/deps/tap.c/package.json b/deps/tap.c/package.json deleted file mode 100644 index f84498b..0000000 --- a/deps/tap.c/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name" : "tap.c", - "version" : "0.0.1", - "description" : "Write tap tests in C.", - "src": [ "tap.c", "tap.h" ], - "repository" : { - "type" : "git", - "url" : "git://github.com/thlorenz/tap.c.git" - }, - "homepage" : "https://github.com/thlorenz/tap.c", - "dependencies" : { - }, - "keywords": ["tap", "test", "check", "tap.c" ] , - "author" : { - "name" : "Thorsten Lorenz", - "email" : "thlorenz@gmx.de", - "url" : "http://thlorenz.com" - }, - "license" : { - "type": "GPLv2", - "url": "https://github.com/thlorenz/tap.c/blob/master/COPYING" - } -} diff --git a/deps/tap.c/tap.c b/deps/tap.c/tap.c deleted file mode 100644 index ea5549a..0000000 --- a/deps/tap.c/tap.c +++ /dev/null @@ -1,321 +0,0 @@ -/* -libtap - Write tests in C -Copyright 2012 Jake Gelbman -This file is licensed under the GPLv2 -*/ - -#define _DEFAULT_SOURCE 1 - -#include "tap.h" - -#include -#include -#include -#include - -static int expected_tests = NO_PLAN; -static int failed_tests; -static int current_test; -static char *todo_mesg; - -static char * -vstrdupf (const char *fmt, va_list args) { - char *str; - int size; - va_list args2; - va_copy(args2, args); - if (!fmt) { - fmt = ""; - } - size = vsnprintf(NULL, 0, fmt, args2) + 2; - str = malloc(size); - if (!str) { - perror("malloc error"); - exit(1); - } - vsprintf(str, fmt, args); - va_end(args2); - return str; -} - -void -tap_plan (int tests, const char *fmt, ...) { - expected_tests = tests; - if (tests == SKIP_ALL) { - char *why; - va_list args; - va_start(args, fmt); - why = vstrdupf(fmt, args); - va_end(args); - printf("1..0 "); - note("SKIP %s\n", why); - exit(0); - } - if (tests != NO_PLAN) { - printf("1..%d\n", tests); - } -} - -int -vok_at_loc (const char *file, int line, int test, const char *fmt, va_list args) { - char *name = vstrdupf(fmt, args); - printf("%sok %d", test ? "" : "not ", ++current_test); - if (*name) { - printf(" - %s", name); - } - if (todo_mesg) { - printf(" # TODO"); - if (*todo_mesg) { - printf(" %s", todo_mesg); - } - } - printf("\n"); - if (!test) { - if (*name) { - diag(" Failed%s test '%s'\n at %s line %d.", todo_mesg ? " (TODO)" : "", name, file, line); - } else { - diag(" Failed%s test at %s line %d.", todo_mesg ? " (TODO)" : "", file, line); - } - if (!todo_mesg) { - failed_tests++; - } - } - free(name); - return test; -} - -int -ok_at_loc (const char *file, int line, int test, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - vok_at_loc(file, line, test, fmt, args); - va_end(args); - return test; -} - -static int -mystrcmp (const char *a, const char *b) { - return a == b ? 0 : !a ? -1 : !b ? 1 : strcmp(a, b); -} - -#define eq(a, b) (!mystrcmp(a, b)) -#define ne(a, b) (mystrcmp(a, b)) - -int -is_at_loc (const char *file, int line, const char *got, const char *expected, const char *fmt, ...) { - int test = eq(got, expected); - va_list args; - va_start(args, fmt); - vok_at_loc(file, line, test, fmt, args); - va_end(args); - if (!test) { - diag(" got: '%s'", got); - diag(" expected: '%s'", expected); - } - return test; -} - -int -isnt_at_loc (const char *file, int line, const char *got, const char *expected, const char *fmt, ...) { - int test = ne(got, expected); - va_list args; - va_start(args, fmt); - vok_at_loc(file, line, test, fmt, args); - va_end(args); - if (!test) { - diag(" got: '%s'", got); - diag(" expected: anything else"); - } - return test; -} - -int -cmp_ok_at_loc (const char *file, int line, int a, const char *op, int b, const char *fmt, ...) { - int test = eq(op, "||") ? a || b - : eq(op, "&&") ? a && b - : eq(op, "|") ? a | b - : eq(op, "^") ? a ^ b - : eq(op, "&") ? a & b - : eq(op, "==") ? a == b - : eq(op, "!=") ? a != b - : eq(op, "<") ? a < b - : eq(op, ">") ? a > b - : eq(op, "<=") ? a <= b - : eq(op, ">=") ? a >= b - : eq(op, "<<") ? a << b - : eq(op, ">>") ? a >> b - : eq(op, "+") ? a + b - : eq(op, "-") ? a - b - : eq(op, "*") ? a * b - : eq(op, "/") ? a / b - : eq(op, "%") ? a % b - : diag("unrecognized operator '%s'", op); - va_list args; - va_start(args, fmt); - vok_at_loc(file, line, test, fmt, args); - va_end(args); - if (!test) { - diag(" %d", a); - diag(" %s", op); - diag(" %d", b); - } - return test; -} - -static void -vdiag_to_fh (FILE *fh, const char *fmt, va_list args) { - char *mesg, *line; - int i; - if (!fmt) { - return; - } - mesg = vstrdupf(fmt, args); - line = mesg; - for (i = 0; *line; i++) { - char c = mesg[i]; - if (!c || c == '\n') { - mesg[i] = '\0'; - fprintf(fh, "# %s\n", line); - if (!c) { - break; - } - mesg[i] = c; - line = mesg + i + 1; - } - } - free(mesg); - return; -} - -int -diag (const char *fmt, ...) { - va_list args; - va_start(args, fmt); - vdiag_to_fh(stderr, fmt, args); - va_end(args); - return 0; -} - -int -note (const char *fmt, ...) { - va_list args; - va_start(args, fmt); - vdiag_to_fh(stdout, fmt, args); - va_end(args); - return 0; -} - -int -exit_status () { - int retval = 0; - if (expected_tests == NO_PLAN) { - printf("1..%d\n", current_test); - } else if (current_test != expected_tests) { - diag("Looks like you planned %d test%s but ran %d.", expected_tests, expected_tests > 1 ? "s" : "", current_test); - retval = 255; - } - if (failed_tests) { - diag("Looks like you failed %d test%s of %d run.", failed_tests, failed_tests > 1 ? "s" : "", current_test); - if (expected_tests == NO_PLAN) { - retval = failed_tests; - } else { - retval = expected_tests - current_test + failed_tests; - } - } - return retval; -} - -int -bail_out (int ignore, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - printf("Bail out! "); - vprintf(fmt, args); - printf("\n"); - va_end(args); - exit(255); - return 0; -} - -void -tap_skip (int n, const char *fmt, ...) { - char *why; - va_list args; - va_start(args, fmt); - why = vstrdupf(fmt, args); - va_end(args); - while (n-- > 0) { - printf("ok %d ", ++current_test); - note("skip %s\n", why); - } - free(why); -} - -void -tap_todo (int ignore, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - todo_mesg = vstrdupf(fmt, args); - va_end(args); -} - -void -tap_end_todo () { - free(todo_mesg); - todo_mesg = NULL; -} - -#ifndef _WIN32 -# include -# include -# include - -# if defined __APPLE__ || defined BSD -# define MAP_ANONYMOUS MAP_ANON -# endif - -/* Create a shared memory int to keep track of whether a piece of code executed -dies. to be used in the dies_ok and lives_ok macros. */ -int -tap_test_died (int status) { - static int *test_died = NULL; - int prev; - if (!test_died) { - test_died = mmap(0, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); - *test_died = 0; - } - prev = *test_died; - *test_died = status; - return prev; -} - -int -like_at_loc (int for_match, const char *file, int line, const char *got, const char *expected, const char *fmt, ...) { - int test; - regex_t re; - va_list args; - int err = regcomp(&re, expected, REG_EXTENDED); - if (err) { - char errbuf[256]; - regerror(err, &re, errbuf, sizeof errbuf); - fprintf(stderr, "Unable to compile regex '%s': %s at %s line %d\n", expected, errbuf, file, line); - exit(255); - } - err = regexec(&re, got, 0, NULL, 0); - regfree(&re); - test = for_match ? !err : err; - va_start(args, fmt); - vok_at_loc(file, line, test, fmt, args); - va_end(args); - if (!test) { - if (for_match) { - diag(" '%s'", got); - diag(" doesn't match: '%s'", expected); - } else { - diag(" '%s'", got); - diag(" matches: '%s'", expected); - } - } - return test; -} -#endif diff --git a/deps/tap.c/tap.h b/deps/tap.c/tap.h deleted file mode 100644 index 3586dc3..0000000 --- a/deps/tap.c/tap.h +++ /dev/null @@ -1,114 +0,0 @@ -/* -libtap - Write tests in C -Copyright 2012 Jake Gelbman -This file is licensed under the GPLv2 -*/ - -#ifndef __TAP_H__ -#define __TAP_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef va_copy -#ifdef __va_copy -#define va_copy __va_copy -#else -#define va_copy(d, s) ((d) = (s)) -#endif -#endif - -#include -#include -#include - -int vok_at_loc (const char *file, int line, int test, const char *fmt, - va_list args); -int ok_at_loc (const char *file, int line, int test, const char *fmt, - ...); -int is_at_loc (const char *file, int line, const char *got, - const char *expected, const char *fmt, ...); -int isnt_at_loc (const char *file, int line, const char *got, - const char *expected, const char *fmt, ...); -int cmp_ok_at_loc (const char *file, int line, int a, const char *op, - int b, const char *fmt, ...); -int bail_out (int ignore, const char *fmt, ...); -void tap_plan (int tests, const char *fmt, ...); -int diag (const char *fmt, ...); -int note (const char *fmt, ...); -int exit_status (void); -void tap_skip (int n, const char *fmt, ...); -void tap_todo (int ignore, const char *fmt, ...); -void tap_end_todo (void); - -#define NO_PLAN -1 -#define SKIP_ALL -2 -#define ok(...) ok_at_loc(__FILE__, __LINE__, __VA_ARGS__, NULL) -#define is(...) is_at_loc(__FILE__, __LINE__, __VA_ARGS__, NULL) -#define isnt(...) isnt_at_loc(__FILE__, __LINE__, __VA_ARGS__, NULL) -#define cmp_ok(...) cmp_ok_at_loc(__FILE__, __LINE__, __VA_ARGS__, NULL) -#define plan(...) tap_plan(__VA_ARGS__, NULL) -#define done_testing() return exit_status() -#define BAIL_OUT(...) bail_out(0, "" __VA_ARGS__, NULL) -#define pass(...) ok(1, "" __VA_ARGS__) -#define fail(...) ok(0, "" __VA_ARGS__) - -#define skip(test, ...) do {if (test) {tap_skip(__VA_ARGS__, NULL); break;} -#define end_skip } while (0) - -#define todo(...) tap_todo(0, "" __VA_ARGS__, NULL) -#define end_todo tap_end_todo() - -#define dies_ok(...) dies_ok_common(1, __VA_ARGS__) -#define lives_ok(...) dies_ok_common(0, __VA_ARGS__) - -#ifdef _WIN32 -#define like(...) tap_skip(1, "like is not implemented on Windows") -#define unlike tap_skip(1, "unlike is not implemented on Windows") -#define dies_ok_common(...) \ - tap_skip(1, "Death detection is not supported on Windows") -#else -#define like(...) like_at_loc(1, __FILE__, __LINE__, __VA_ARGS__, NULL) -#define unlike(...) like_at_loc(0, __FILE__, __LINE__, __VA_ARGS__, NULL) -int like_at_loc (int for_match, const char *file, int line, - const char *got, const char *expected, - const char *fmt, ...); -#include -#include -#include -int tap_test_died (int status); -#define dies_ok_common(for_death, code, ...) \ - do { \ - int cpid; \ - int it_died; \ - tap_test_died(1); \ - cpid = fork(); \ - switch (cpid) { \ - case -1: \ - perror("fork error"); \ - exit(1); \ - case 0: \ - close(1); \ - close(2); \ - code \ - tap_test_died(0); \ - exit(0); \ - } \ - if (waitpid(cpid, NULL, 0) < 0) { \ - perror("waitpid error"); \ - exit(1); \ - } \ - it_died = tap_test_died(0); \ - if (!it_died) \ - {code} \ - ok(for_death ? it_died : !it_died, "" __VA_ARGS__); \ - } while (0) -#endif - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/include/api/commands.h b/include/api/commands.h index 54f11af..6260796 100644 --- a/include/api/commands.h +++ b/include/api/commands.h @@ -2,6 +2,7 @@ #define COMMANDS_H #include "libhash/libhash.h" +#include "libutil/libutil.h" /** * Retrieves the command handlers map singleton. @@ -11,4 +12,8 @@ */ hash_table* get_command_handlers_map(void); +void write_jobs_info(buffer_t* buf); +void write_crontabs_info(buffer_t* buf); +void write_program_info(buffer_t* buf); + #endif /* COMMANDS_H */ diff --git a/include/cronentry.h b/include/cronentry.h index d1286fa..81f630b 100644 --- a/include/cronentry.h +++ b/include/cronentry.h @@ -21,28 +21,28 @@ typedef struct { /** * The pre-parsed cron expression. */ - cron_expr *expr; + cron_expr *expr; /** * The original cron schedule specification in the entry. */ - char schedule[MAX_SCHEDULE_LENGTH]; + char schedule[MAX_SCHEDULE_LENGTH]; /** * The command specified by the entry. */ - char cmd[MAX_COMMAND_LENGTH]; + char cmd[MAX_COMMAND_LENGTH]; /** * The next execution time, relative to the current crond iteration. */ - time_t next; + time_t next; /** * A unique identifier for the entry. Used for logging and non-critical * functions. */ - unsigned int id; + const char *ident; /** * A pointer to the entry's parent crontab. */ - crontab_t *parent; + crontab_t *parent; } cron_entry; /** diff --git a/include/job.h b/include/job.h index 946ca85..7e79f4c 100644 --- a/include/job.h +++ b/include/job.h @@ -104,6 +104,14 @@ void signal_reap_routine(void); */ void try_run_jobs(hash_table *db, time_t ts); +/** + * Creates a new job of type CRON. + * + * @param entry The entry which this job will represent. + * @return job_t* + */ +job_t *new_cronjob(cron_entry *entry); + /** * Deallocates a job with type CRON. */ diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index df4b624..65fe773 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -8,5 +8,6 @@ if [ "$(echo "$ret" | grep 'not ok')" != "" ]; then exit 1 fi -make -s integ_test 2>/dev/null +make -s integ_test + exit $? diff --git a/src/api/commands.c b/src/api/commands.c index a8b0a37..5174dc9 100644 --- a/src/api/commands.c +++ b/src/api/commands.c @@ -14,26 +14,22 @@ typedef struct { const char* command; - void (*handler)(int); + void (*handler)(buffer_t*); } command_handler_entry; -static void write_jobs_command(int client_fd); -static void write_crontabs_command(int client_fd); -static void write_program_info(int client_fd); - static command_handler_entry command_handler_map_index[] = { - {.command = "IPC_LIST_JOBS", .handler = write_jobs_command }, - {.command = "IPC_LIST_CRONTABS", .handler = write_crontabs_command}, - {.command = "IPC_SHOW_INFO", .handler = write_program_info } + {.command = "IPC_LIST_JOBS", .handler = write_jobs_info }, + {.command = "IPC_LIST_CRONTABS", .handler = write_crontabs_info}, + {.command = "IPC_SHOW_INFO", .handler = write_program_info } }; static pthread_once_t command_handlers_map_init_once = PTHREAD_ONCE_INIT; static hash_table* command_handlers_map; -// TODO: just ret str -static void -write_jobs_command (int client_fd) { - write(client_fd, "[", 1); +void +write_jobs_info (buffer_t* buf) { + buffer_append(buf, "["); + unsigned int len = array_size(job_queue); foreach (job_queue, i) { job_t* job = (job_t*)array_get_or_panic(job_queue, i); @@ -55,18 +51,18 @@ write_jobs_command (int client_fd) { i != len - 1 ? "," : "" ); - write(client_fd, s, strlen(s)); + buffer_append(buf, s); free(cmd_esc); free(s); free(ts); } - write(client_fd, "]", 1); + buffer_append(buf, "]"); } -static void -write_crontabs_command (int client_fd) { - write(client_fd, "[", 1); +void +write_crontabs_info (buffer_t* buf) { + buffer_append(buf, "["); unsigned int entries = db->count; HT_ITER_START(db) @@ -75,25 +71,32 @@ write_crontabs_command (int client_fd) { unsigned int len = array_size(ct->entries); foreach (ct->entries, i) { + len--; cron_entry* ce = array_get_or_panic(ct->entries, i); char* ts = to_time_str_secs(ce->next); + char* se = s_concat_arr(ct->envp, ", "); char* cmd_esc = escape_json_string(ce->cmd); char* s = s_fmt( - "{\"id\":\"%d\",\"cmd\":\"%s\",\"schedule\":\"%s\",\"owner\":\"%s\"," + "{\"id\":\"%s\",\"filepath\":\"%s\",\"cmd\":\"%s\",\"schedule\":\"%s\"," + "\"owner\":\"%s\"," "\"envp\":\"%s\",\"next\":\"%s\"}%s", - ce->id, + ce->ident, + entry->key, cmd_esc, ce->schedule, ct->uname, se, ts, - entries != 0 || i != len - 1 ? "," : "" + entries != 0 || len != 0 ? "," : "" ); + log_debug(">>> %s\n", s); + + log_debug(">>> entries != 0 || len != 0 ? %s - entries=%d, len=%d \n", entries != 0 || len != 0 ? "yup" : "nope", entries, len); - write(client_fd, s, strlen(s)); + buffer_append(buf, s); free(cmd_esc); free(s); @@ -102,11 +105,11 @@ write_crontabs_command (int client_fd) { } HT_ITER_END - write(client_fd, "]", 1); + buffer_append(buf, "]"); } -static void -write_program_info (int client_fd) { +void +write_program_info (buffer_t* buf) { char* st = to_time_str_millis(proginfo.start); time_t now = time(NULL); @@ -121,7 +124,7 @@ write_program_info (int client_fd) { uptime, proginfo.version ); - write(client_fd, s, strlen(s)); + buffer_append(buf, s); free(s); free(st); diff --git a/src/api/ipc.c b/src/api/ipc.c index 66cca7b..6cbc27a 100644 --- a/src/api/ipc.c +++ b/src/api/ipc.c @@ -57,7 +57,7 @@ ipc_write_err (int client_fd, const char* msg, ...) { static void* ipc_routine (void* arg __attribute__((unused))) { int client_fd; - char buffer[RECV_BUFFER_SIZE]; + char readbuf[RECV_BUFFER_SIZE]; log_debug("%s\n", "in ipc routine"); while (true) { @@ -66,11 +66,11 @@ ipc_routine (void* arg __attribute__((unused))) { continue; } - read(client_fd, buffer, sizeof(buffer)); - log_debug("API req: '%s'\n", buffer); + read(client_fd, readbuf, sizeof(readbuf)); + log_debug("API req: '%s'\n", readbuf); hash_table* pairs = ht_init(11, free); - if (parse_json(buffer, pairs) != OK) { + if (parse_json(readbuf, pairs) != OK) { ipc_write_err(client_fd, "invalid format"); goto client_done; } @@ -82,17 +82,20 @@ ipc_routine (void* arg __attribute__((unused))) { } log_debug("received command '%s'\n", command); - void (*handler)(int) = ht_get(get_command_handlers_map(), command); + void (*handler)(buffer_t*) = ht_get(get_command_handlers_map(), command); if (!handler) { ipc_write_err(client_fd, "unknown command '%s'", command); goto client_done; } - handler(client_fd); + buffer_t* writebuf = buffer_init(NULL); + handler(writebuf); + write(client_fd, buffer_state(writebuf), buffer_size(writebuf)); + buffer_free(writebuf); client_done: - memset(buffer, 0, RECV_BUFFER_SIZE); + memset(readbuf, 0, RECV_BUFFER_SIZE); ht_delete_table(pairs); close(client_fd); } diff --git a/src/cronentry.c b/src/cronentry.c index f9ef90a..3cfc506 100644 --- a/src/cronentry.c +++ b/src/cronentry.c @@ -6,6 +6,7 @@ #include "logger.h" #include "parser.h" #include "utils/retval.h" +#include "utils/string.h" #include "utils/xmalloc.h" #include "utils/xpanic.h" @@ -72,14 +73,14 @@ new_cron_entry (char* raw, time_t curr, crontab_t* ct, cadence_t cadence) { entry->parent = ct; entry->next = cron_next(entry->expr, curr); - entry->id = ++id_counter; + entry->ident = create_uuid(); return entry; } void renew_cron_entry (cron_entry* entry, time_t curr) { - log_debug("Updating time for entry %d\n", entry->id); + log_debug("Updating time for entry %s\n", entry->ident); entry->next = cron_next(entry->expr, curr); } @@ -87,5 +88,6 @@ renew_cron_entry (cron_entry* entry, time_t curr) { void free_cron_entry (cron_entry* entry) { free(entry->expr); + free(entry->ident); free(entry); } diff --git a/src/crontab.c b/src/crontab.c index 6cb75d0..b979c74 100644 --- a/src/crontab.c +++ b/src/crontab.c @@ -32,10 +32,10 @@ */ static void complete_env (crontab_t* ct) { - hash_table* vars = ct->vars; + hash_table* vars = ct->vars; -// We don't want to have to create users for unit tests. -#ifndef UNIT_TEST + // We don't want to have to create users for unit tests. + // #ifndef UNIT_TEST struct passwd* pw = getpwnam(ct->uname); if (pw) { if (!ht_search(vars, HOMEDIR_ENVVAR)) { @@ -60,7 +60,7 @@ complete_env (crontab_t* ct) { ct->uname ); } -#endif + // #endif // Fill out the envp using the vars map. We're going to need this later and // forevermore, so we might as well get it out of the way upfront. if (vars->count > 0) { @@ -241,7 +241,7 @@ new_crontab (int crontab_fd, bool is_root, time_t curr_time, time_t mtime, char* continue; } - log_debug("New entry (%d) for crontab %s\n", entry->id, uname); + log_debug("New entry (%s) for crontab %s\n", entry->ident, uname); array_push_or_panic(ct->entries, entry); max_entries--; } diff --git a/src/job.c b/src/job.c index 1f80d3c..08b31fd 100644 --- a/src/job.c +++ b/src/job.c @@ -24,13 +24,7 @@ const char* job_state_names[] = {X(PENDING), X(RUNNING), X(EXITED)}; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; -/** - * Creates a new job of type CRON. - * - * @param entry The entry which this job will represent. - * @return job_t* - */ -static job_t* +job_t* new_cronjob (cron_entry* entry) { job_t* job = xmalloc(sizeof(job_t)); job->ident = create_uuid(); diff --git a/src/utils/json.c b/src/utils/json.c index f3106a5..905216a 100644 --- a/src/utils/json.c +++ b/src/utils/json.c @@ -1,5 +1,6 @@ #include "utils/json.h" +#include #include #include @@ -22,44 +23,90 @@ parse_json (const char* json, hash_table* pairs) { goto done; } - // Replace closing brace and skip next opening one + // Replace closing brace and skip the opening one *end = '\0'; start++; - char* pair_str = strtok(start, ","); - while (pair_str) { - char* colon = strchr(pair_str, ':'); - if (!colon) { - log_warn("Invalid key-value pair: %s\n", pair_str); + while (*start) { + // Skip whitespace + while (*start && isspace((unsigned char)*start)) { + start++; + } + + // Parse key + if (*start != '"') { + log_warn("%s\n", "Expected key to start with a double quote"); ret = ERR; goto done; } + char* key_start = ++start; + while (*start && (*start != '"' || *(start - 1) == '\\')) { + start++; + } - *colon = '\0'; - char* key = trim_whitespace(pair_str); - char* value = trim_whitespace(colon + 1); + if (*start != '"') { + log_warn("%s\n", "Malformed JSON: Missing closing quote for key"); + ret = ERR; + goto done; + } + *start = '\0'; // Null-terminate the key + char* key = key_start; - if (*key == '"') { - key++; + // Skip to the colon + start++; + while (*start && isspace((unsigned char)*start)) { + start++; + } + if (*start != ':') { + log_warn("%s\n", "Expected colon after key"); + ret = ERR; + goto done; } - if (*value == '"') { - value++; + start++; + + // Parse value + while (*start && isspace((unsigned char)*start)) { + start++; } - if (key[strlen(key) - 1] == '"') { - key[strlen(key) - 1] = '\0'; + char* value_start; + if (*start == '"') { + value_start = ++start; + while (*start && (*start != '"' || *(start - 1) == '\\')) { + start++; + } + if (*start != '"') { + log_warn("%s\n", "Malformed JSON: Missing closing quote for value"); + ret = ERR; + goto done; + } + } else { + value_start = start; + while (*start && *start != ',' && !isspace((unsigned char)*start)) { + start; + } } - if (value[strlen(value) - 1] == '"') { - value[strlen(value) - 1] = '\0'; + char* value = value_start; + if (*start) { + *start = '\0'; // Null-terminate the value + start++; } + // Trim and store the key-value pair + key = trim_whitespace(key); + value = trim_whitespace(value); ht_insert(pairs, s_copy(key), s_copy(value)); - pair_str = strtok(NULL, ","); + // Skip trailing commas and whitespace + while (*start && isspace((unsigned char)*start)) { + start++; + } + if (*start == ',') { + start++; + } } done: free(mutable_json); - return ret; } diff --git a/t/integ/ipc_shpec.bash b/t/integ/ipc_shpec.bash index 10ae757..91de16a 100644 --- a/t/integ/ipc_shpec.bash +++ b/t/integ/ipc_shpec.bash @@ -149,11 +149,14 @@ describe 'ipc API IPC_SHOW_INFO command' end_describe describe 'ipc API IPC_LIST_CRONTABS command' - n_syscrontabs="$(find /etc/cron.{hourly,daily,weekly,monthly} -type f 2>/dev/null | wc -l)" + n_syscrontabs="$(find /etc/cron.{hourly,daily,weekly,monthly} -type f 2>/dev/null | wc -l)" start_chronic sleep 5 - out="$(sock_call '{ "command" : "IPC_LIST_CRONTABS"}' 2>/dev/null | tr -d '\0')" + out="$(sock_call '{ "command" : "IPC_LIST_CRONTABS"}')" + echo ">>> $out <<<" + + cat .log it 'lists all crontabs' assert equal $(jq 'length' <<< "$out") "$((n_syscrontabs + 3))" @@ -177,7 +180,7 @@ describe 'ipc API IPC_LIST_JOBS command' echo 'sleeping for 60 seconds...' sleep 60 - out="$(sock_call '{ "command" : "IPC_LIST_JOBS"}' 2>/dev/null | tr -d '\0')" + out="$(sock_call '{ "command" : "IPC_LIST_JOBS"}')" it 'lists all jobs' assert gt "$(jq 'length' <<< "$out")" 0 diff --git a/t/integ/utils/run.bash b/t/integ/utils/run.bash index d7303d1..ae7a349 100755 --- a/t/integ/utils/run.bash +++ b/t/integ/utils/run.bash @@ -5,12 +5,12 @@ TESTING_DIR=t/integ UTILS_F=run_utils.bash declare -a SKIP_FILES=( - # 'daemon_shpec.bash' + 'daemon_shpec.bash' # 'ipc_shpec.bash' - # 'peripherals_shpec.bash' - # 'root_shpec.bash' - # 'sig_shpec.bash' - # 'user_shpec.bash' + 'peripherals_shpec.bash' + 'root_shpec.bash' + 'sig_shpec.bash' + 'user_shpec.bash' ) run_test () { diff --git a/t/unit/crontab_test.c b/t/unit/crontab_test.c index 616bad4..13bcb73 100644 --- a/t/unit/crontab_test.c +++ b/t/unit/crontab_test.c @@ -7,15 +7,14 @@ #include #include "cronentry.h" -#include "tap.c/tap.h" #include "tests.h" #include "utils/retval.h" static void validate_entry (cron_entry* entry, cron_entry* expected) { - is(expected->cmd, entry->cmd, "Expect cmd '%s'", expected->cmd); - is(expected->schedule, entry->schedule, "Expect schedule '%s'", expected->schedule); - is( + eq_str(expected->cmd, entry->cmd, "Expect cmd '%s'", expected->cmd); + eq_str(expected->schedule, entry->schedule, "Expect schedule '%s'", expected->schedule); + eq_str( expected->parent->uname, entry->parent->uname, "Expect parent->uname '%s'", @@ -30,7 +29,7 @@ new_crontab_test (void) { int crontab_fd = OK - 1; if ((crontab_fd = get_fd(test_fpath)) < OK) { - fail(); + fail("get_fd failed\n"); } time_t now = time(NULL); @@ -60,9 +59,9 @@ new_crontab_test (void) { } ok(ct->vars->count == 3, "has 3 environment variables"); - is(ht_get(ct->vars, "PATH"), "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "has the PATH environment variable"); - is(ht_get(ct->vars, "SHELL"), "/bin/echo", "has the SHELL environment variable"); - is(ht_get(ct->vars, "KEY"), "VALUE", "has the KEY environment variable"); + eq_str(ht_get(ct->vars, "PATH"), "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "has the PATH environment variable"); + eq_str(ht_get(ct->vars, "SHELL"), "/bin/echo", "has the SHELL environment variable"); + eq_str(ht_get(ct->vars, "KEY"), "VALUE", "has the KEY environment variable"); array_t* expected_envp_entries = array_init(); @@ -72,7 +71,7 @@ new_crontab_test (void) { foreach (expected_envp_entries, i) { char* expected = array_get(expected_envp_entries, i); - is(ct->envp[i], expected, "expect to find %s", expected); + eq_str(ct->envp[i], expected, "expect to find %s", expected); } array_free(expected_envp_entries, free); @@ -86,7 +85,7 @@ new_crontab_test_2 (void) { int crontab_fd = OK - 1; if ((crontab_fd = get_fd(test_fpath)) < OK) { - fail(); + fail("get_fd failed\n"); } time_t now = time(NULL); @@ -128,8 +127,8 @@ new_crontab_test_2 (void) { } ok(ct->vars->count == 2, "has 2 environment variables"); - is(ht_get(ct->vars, "PATH"), "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin", "has the PATH environment variable"); - is(ht_get(ct->vars, "SHELL"), "/bin/sh", "has the SHELL environment variable"); + eq_str(ht_get(ct->vars, "PATH"), "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin", "has the PATH environment variable"); + eq_str(ht_get(ct->vars, "SHELL"), "/bin/sh", "has the SHELL environment variable"); array_t* expected_envp_entries = array_init(); @@ -139,7 +138,7 @@ new_crontab_test_2 (void) { foreach (expected_envp_entries, i) { char* expected = array_get(expected_envp_entries, i); - is(ct->envp[i], expected, "expect to find %s", expected); + eq_str(ct->envp[i], expected, "expect to find %s", expected); } array_free(expected_envp_entries, free); @@ -268,7 +267,6 @@ update_db_test (void) { ok(array_size(ct1->entries) == 2, "user1's crontab has 2 entries"); ok(array_size(ct4->entries) == 2, "the root2 crontab has 2 entries"); - // Cleanup cleanup_test_file(usr_dirname, "user1"); cleanup_test_file(sys_dirname, "root2"); cleanup_test_directory(usr_dirname); @@ -303,11 +301,11 @@ run_virtual_crontabs_tests (void) { cron_entry* ce1 = array_get(ct1->entries, 0); cron_entry* ce2 = array_get(ct2->entries, 0); - is(ce1->cmd, fpath1, "uses executable name as cmd"); - is(ce2->cmd, fpath2, "uses executable name as cmd"); + eq_str(ce1->cmd, fpath1, "uses executable name as cmd"); + eq_str(ce2->cmd, fpath2, "uses executable name as cmd"); - is(ce1->schedule, HOURLY_EXPR, "has a once hourly schedule"); - is(ce2->schedule, HOURLY_EXPR, "has a once hourly schedule"); + eq_str(ce1->schedule, HOURLY_EXPR, "has a once hourly schedule"); + eq_str(ce2->schedule, HOURLY_EXPR, "has a once hourly schedule"); cleanup_test_file(dirname, "1"); diff --git a/t/unit/ipc_commands_test.c b/t/unit/ipc_commands_test.c new file mode 100644 index 0000000..cd016dc --- /dev/null +++ b/t/unit/ipc_commands_test.c @@ -0,0 +1,140 @@ +#include + +#include "api/commands.h" +#include "crontab.h" +#include "globals.h" +#include "job.h" +#include "proginfo.h" +#include "tests.h" +#include "utils/json.h" +#include "utils/time.h" + +#define TIMESTAMP_REGEX \ + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z" + +#define UUID_REGEX \ + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + +hash_table* test_db; +char* usr_dirname; +char* sys_dirname; + +static inline void +setup_test_data (void) { + db = NULL; + + char* usr_dirname = setup_test_directory(); + setup_test_file(usr_dirname, "user1", "* * * * * echo 'test1'\n"); + setup_test_file(usr_dirname, "user2", "* * * * * echo 'test2'\n"); + dir_config usr_dir = {.is_root = false, .path = usr_dirname}; + + char* sys_dirname = setup_test_directory(); + setup_test_file(sys_dirname, "root1", "* * * * * echo 'test1'\n"); + setup_test_file(sys_dirname, "root2", "* * * * * echo 'test2'\n"); + dir_config sys_dir = {.is_root = true, .path = sys_dirname}; + + hash_table* tmp_db = ht_init(0, (free_fn*)free_crontab); + time_t now = time(NULL); + + test_db = update_db(tmp_db, now, &usr_dir, &sys_dir, NULL); +} + +static inline void +teardown_test_data (void) { + cleanup_test_file(usr_dirname, "user1"); + cleanup_test_file(usr_dirname, "user2"); + cleanup_test_file(sys_dirname, "root1"); + cleanup_test_file(sys_dirname, "root2"); + cleanup_test_directory(usr_dirname); + cleanup_test_directory(sys_dirname); + ht_delete_table(test_db); +} + +static void +test_write_jobs_info (void) { + job_queue = array_init(); + setup_test_data(); + db = test_db; + + HT_ITER_START(db) + crontab_t* ct = entry->value; + foreach (ct->entries, i) { + cron_entry* ce = array_get(ct->entries, i); + array_push(job_queue, (void*)new_cronjob(ce)); + } + HT_ITER_END + + buffer_t* buf = buffer_init(NULL); + write_jobs_info(buf); + + // Shitty approx tests until we setup a proper JSON parser + // TODO: Proper parser + const char* ret = buffer_state(buf); + + ok(strlen(ret) > 32, "approx len looks OK"); + match_str(ret, "\"id\":\"" UUID_REGEX "\"", "has valid job id"); + match_str(ret, "\"mailto\":\"root\"", "has mailto for root"); + match_str(ret, "\"mailto\":\"user1\"", "has mailto for user1"); + match_str(ret, "\"mailto\":\"user2\"", "has mailto for user2"); + match_str(ret, "\"state\":\"PENDING\"", "has state"); + match_str(ret, "\"next\":\"" TIMESTAMP_REGEX "\"", "has valid next timestamps"); + + buffer_free(buf); + array_free(job_queue, (free_fn*)free_cronjob); + teardown_test_data(); +} + +static void +test_write_crontabs_info (void) { + setup_test_data(); + db = test_db; + buffer_t* buf = buffer_init(NULL); + write_crontabs_info(buf); + + const char* ret = buffer_state(buf); + + ok(strlen(ret) > 32, "approx len looks OK"); + match_str(ret, "\"schedule\":\"* * * * *\"", "has expected schedule"); + match_str(ret, "\"filepath\":\"\\/tmp\\/.*\\/root1\"", "has expected schedule"); + match_str(ret, "\"filepath\":\"\\/tmp\\/.*\\/root2\"", "has expected schedule"); + match_str(ret, "\"filepath\":\"\\/tmp\\/.*\\/user1\"", "has expected schedule"); + match_str(ret, "\"filepath\":\"\\/tmp\\/.*\\/user2\"", "has expected schedule"); + match_str(ret, "\"owner\":\"root\"", "has expected owner"); + match_str(ret, "\"owner\":\"user1\"", "has expected owner"); + match_str(ret, "\"owner\":\"user2\"", "has expected owner"); + match_str(ret, "\"id\":\"" UUID_REGEX "\"", "has valid entry id"); + match_str(ret, "\"next\":\"" TIMESTAMP_REGEX "\"", "has valid next timestamps"); + + buffer_free(buf); + teardown_test_data(); +} + +static void +test_write_program_info (void) { + proginfo.pid = 123; + struct timespec ts; + get_time(&ts); + proginfo.start = &ts; + memcpy(proginfo.version, "777", 4); + + buffer_t* buf = buffer_init(NULL); + write_program_info(buf); + + hash_table* ht = ht_init(HT_DEFAULT_CAPACITY, free); + + ok(parse_json(buffer_state(buf), ht) == OK, "is valid JSON"); + eq_str(ht_get(ht, "pid"), "123", "has correct pid"); + eq_str(ht_get(ht, "uptime"), "0 days, 0 hours, 0 minutes, 0 seconds", "has correct uptime"); + eq_str(ht_get(ht, "version"), "777", "has correct version"); + match_str(ht_get(ht, "started_at"), "^" TIMESTAMP_REGEX "$", "is valid timestamp (%s)\n", ht_get(ht, "started_at")); + + buffer_free(buf); + ht_delete_table(ht); +} + +void +run_ipc_commands_test (void) { + test_write_jobs_info(); + test_write_crontabs_info(); + test_write_program_info(); +} diff --git a/t/unit/main.c b/t/unit/main.c index a56c2b1..1bc4c62 100644 --- a/t/unit/main.c +++ b/t/unit/main.c @@ -2,7 +2,6 @@ #include "globals.h" #include "libutil/libutil.h" #include "proginfo.h" -#include "tap.c/tap.h" #include "tests.h" // Externs initialized in main @@ -21,12 +20,13 @@ main (int _argc, char** _argv) { usr.uname = "root"; usr.root = true; - plan(198); + plan(221); - run_parser_tests(); - run_regexpr_tests(); - run_crontab_tests(); - run_utils_tests(); + // run_parser_tests(); + // run_regexpr_tests(); + // run_crontab_tests(); + // run_utils_tests(); + run_ipc_commands_test(); done_testing(); } diff --git a/t/unit/parser_test.c b/t/unit/parser_test.c index 2721af5..5718c81 100644 --- a/t/unit/parser_test.c +++ b/t/unit/parser_test.c @@ -4,7 +4,6 @@ #include "constants.h" #include "cronentry.h" -#include "tap.c/tap.h" #include "tests.h" void @@ -30,7 +29,7 @@ strip_comment_test (void) { char* s = s_copy(tc.input); strip_comment(s); - is(s, tc.expect, "Expect '%s'", tc.expect); + eq_str(s, tc.expect, "Expect '%s'", tc.expect); free(s); } @@ -104,7 +103,7 @@ parse_schedule_test (void) { ok(rv == ERR, "returns ERR (reason: %s)", tc.err_msg); } else { ok(rv == OK, "returns OK"); - is(ret, tc.expect, "Expect '%s'", tc.expect ? tc.expect : "NULL"); + eq_str(ret, tc.expect, "Expect '%s'", tc.expect ? tc.expect : "NULL"); } } } @@ -167,7 +166,7 @@ parse_cmd_test (void) { ok(rv == ERR, "returns ERR (reason: %s)", tc.err_msg); } else { ok(rv == OK, "returns OK"); - is(ret, tc.expect, "Expect '%s'", tc.expect ? tc.expect : "NULL"); + eq_str(ret, tc.expect, "Expect '%s'", tc.expect ? tc.expect : "NULL"); } } } @@ -236,7 +235,7 @@ parse_entry_test (void) { ok(ret == ERR, "Expect result to be ERR (%d)", ERR); } else { ok(ret == OK, "Expect result to be OK (%d)", OK); - is(entry.cmd, tc.expect, "Expect '%s'", tc.expect); + eq_str(entry.cmd, tc.expect, "Expect '%s'", tc.expect); } } } diff --git a/t/unit/regexpr_test.c b/t/unit/regexpr_test.c index 18df4b9..4ee3e0c 100644 --- a/t/unit/regexpr_test.c +++ b/t/unit/regexpr_test.c @@ -1,5 +1,4 @@ #include "libutil/libutil.h" -#include "tap.c/tap.h" #include "tests.h" #include "utils/regex.h" @@ -49,7 +48,7 @@ match_variable_test (void) { match_variable(input, ht); - is(ht_get(ht, tc.key), tc.value, "parses the variable and updates the hash table"); + eq_str(ht_get(ht, tc.key), tc.value, "parses the variable and updates the hash table"); } } } diff --git a/t/unit/test_utils.c b/t/unit/test_utils.c index bcc0bc2..3e4ac12 100644 --- a/t/unit/test_utils.c +++ b/t/unit/test_utils.c @@ -1,9 +1,9 @@ +#include #include #include #include #include "libutil/libutil.h" -#include "tap.c/tap.h" #include "tests.h" char* @@ -13,8 +13,7 @@ setup_test_directory (void) { char* dirname = s_copy(mkdtemp(template)); if (dirname == NULL) { - perror("mkdtemp failed"); - fail(); + fail("mkdtemp failed (reason: %s)\n", strerror(errno)); } return dirname; @@ -57,6 +56,7 @@ modify_test_file (const char* dirname, const char* filename, const char* content void cleanup_test_directory (char* dirname) { rmdir(dirname); + free(dirname); } void diff --git a/t/unit/tests.h b/t/unit/tests.h index ed26a8c..ae09f0d 100644 --- a/t/unit/tests.h +++ b/t/unit/tests.h @@ -1,6 +1,10 @@ #ifndef TESTS_H #define TESTS_H +#define TAP_WANT_PCRE 1 +#define TAP_WANT_PTHREAD 1 +#include "libtap/libtap.h" + #define ITER_CASES_TEST(tests, type) \ for (unsigned int i = 0; i < sizeof(tests) / sizeof(type); i++) @@ -16,5 +20,6 @@ void run_parser_tests(void); void run_crontab_tests(void); void run_utils_tests(void); void run_regexpr_tests(void); +void run_ipc_commands_test(void); #endif /* TESTS_H */ diff --git a/t/unit/utils_test.c b/t/unit/utils_test.c index bc8bb61..bffa57d 100644 --- a/t/unit/utils_test.c +++ b/t/unit/utils_test.c @@ -1,6 +1,5 @@ #include "libhash/libhash.h" #include "libutil/libutil.h" -#include "tap.c/tap.h" #include "tests.h" #include "utils/file.h" #include "utils/json.h" @@ -119,8 +118,8 @@ json_parser_test (void) { ok(parse_json(s, pairs) == OK, "returns OK"); ok(pairs->count == 2, "2 pairs"); - is(ht_get(pairs, "id"), "c0687273-4d7b-4873-96f1-0156d988ebc3", "selects the expected json field"); - is(ht_get(pairs, "command"), "list_tabs", "selects the expected json field"); + eq_str(ht_get(pairs, "id"), "c0687273-4d7b-4873-96f1-0156d988ebc3", "selects the expected json field"); + eq_str(ht_get(pairs, "command"), "list_tabs", "selects the expected json field"); } static void @@ -146,7 +145,7 @@ pretty_print_seconds_test (void) { TestCase tc = tests[i]; char* ret = pretty_print_seconds(tc.seconds); - is(tc.expect, ret, "expect %s (got %s)", tc.expect, ret); + eq_str(tc.expect, ret, "expect %s (got %s)", tc.expect, ret); free(ret); } } diff --git a/x.js b/x.js new file mode 100644 index 0000000..c9a50b4 --- /dev/null +++ b/x.js @@ -0,0 +1,9 @@ +const entries = 4 +const items = [2, 0, 3, 8] + +for (let i = 0; i < entries; i++) { + for (let j = 0; j < items[i]; j++) { + const x = i != 0 || j != 0 ? ',' : '' + console.log({ i, j, x }) + } +} diff --git a/x.json b/x.json new file mode 100644 index 0000000..5630ff7 --- /dev/null +++ b/x.json @@ -0,0 +1,92 @@ +[ + { + "id": "3d79f106-70b6-401c-9130-782d7a116302", + "filepath": "/etc/cron.daily/logrotate", + "cmd": "/etc/cron.daily/logrotate", + "schedule": "0 0 * * *", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-24T00:00:00.000Z" + }, + { + "id": "a9b7b972-fb7d-4c21-ac87-4165602cd923", + "filepath": "/etc/cron.daily/apt-compat", + "cmd": "/etc/cron.daily/apt-compat", + "schedule": "0 0 * * *", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-24T00:00:00.000Z" + }, + { + "id": "64219aaf-7e46-4d5d-9544-a02ddeb899d2", + "filepath": "/etc/cron.daily/dpkg", + "cmd": "/etc/cron.daily/dpkg", + "schedule": "0 0 * * *", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-24T00:00:00.000Z" + }, + { + "id": "91e401c5-7224-43df-888e-072f5ea5e5ff", + "filepath": "/etc/crontab", + "cmd": "root cd / && run-parts --report /etc/cron.hourly", + "schedule": "17 * * * *", + "owner": "root", + "envp": "USER=root, HOME=/root, PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin, SHELL=/bin/sh", + "next": "2025-01-23T08:17:00.000Z" + }, + { + "id": "94629c26-17d0-4c80-a475-77d8c1039bf5", + "filepath": "/etc/crontab", + "cmd": "root\u0009test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )", + "schedule": "25 6 * * *", + "owner": "root", + "envp": "USER=root, HOME=/root, PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin, SHELL=/bin/sh", + "next": "2025-01-24T06:25:00.000Z" + }, + { + "id": "c02f879a-b075-41f3-b211-4a726eecdbbb", + "filepath": "/etc/crontab", + "cmd": "root\u0009test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )", + "schedule": "47 6 * * 7", + "owner": "root", + "envp": "USER=root, HOME=/root, PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin, SHELL=/bin/sh", + "next": "2025-01-26T06:47:00.000Z" + }, + { + "id": "11346add-3fb5-493c-9813-6fd28ba8f046", + "filepath": "/etc/crontab", + "cmd": "root\u0009test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )", + "schedule": "52 6 1 * *", + "owner": "root", + "envp": "USER=root, HOME=/root, PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin, SHELL=/bin/sh", + "next": "2025-02-01T06:52:00.000Z" + }, + { + "id": "70807a62-8f88-46f4-b048-171dd532891c", + "filepath": "/etc/cron.d/root", + "cmd": "echo $(date -u +'%Y-%m-%dT%H:%M:%S') >> /tmp/root.1min", + "schedule": "*/1 * * * *", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-23T07:43:00.000Z" + }, + { + "id": "f5af8cd7-107f-4365-adae-0cc490f39a33", + "filepath": "/etc/cron.d/e2scrub_all", + "cmd": "root test -e /run/systemd/system || SERVICE_MODE=1 /usr/lib/x86_64-linux-gnu/e2fsprogs/e2scrub_all_cron", + "schedule": "30 3 * * 0", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-26T03:30:00.000Z" + }, + { + "id": "98ed6c31-bfad-4b45-bead-431e37c46098", + "filepath": "/etc/cron.d/e2scrub_all", + "cmd": "root test -e /run/systemd/system || SERVICE_MODE=1 /sbin/e2scrub_all -A -r", + "schedule": "10 3 * * *", + "owner": "root", + "envp": "PATH=/usr/bin:/bin:/usr/sbin:/sbin, USER=root, SHELL=/bin/bash, HOME=/root", + "next": "2025-01-24T03:10:00.000Z" + } +]