From f0002cb9e4f6f403a25ad50252c06694439900f0 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 10 Jan 2011 14:42:38 +1030 Subject: [PATCH] failtest: new module. A module designed to help test "never fails" functions like malloc. --- ccan/failtest/LICENSE | 1 + ccan/failtest/_info | 69 ++++ ccan/failtest/failtest.c | 547 ++++++++++++++++++++++++++++++ ccan/failtest/failtest.h | 170 ++++++++++ ccan/failtest/failtest_override.h | 47 +++ ccan/failtest/failtest_proto.h | 20 ++ ccan/failtest/test/run-failpath.c | 38 +++ ccan/failtest/test/run-history.c | 166 +++++++++ ccan/failtest/test/run-malloc.c | 109 ++++++ ccan/failtest/test/run-write.c | 42 +++ 10 files changed, 1209 insertions(+) create mode 120000 ccan/failtest/LICENSE create mode 100644 ccan/failtest/_info create mode 100644 ccan/failtest/failtest.c create mode 100644 ccan/failtest/failtest.h create mode 100644 ccan/failtest/failtest_override.h create mode 100644 ccan/failtest/failtest_proto.h create mode 100644 ccan/failtest/test/run-failpath.c create mode 100644 ccan/failtest/test/run-history.c create mode 100644 ccan/failtest/test/run-malloc.c create mode 100644 ccan/failtest/test/run-write.c diff --git a/ccan/failtest/LICENSE b/ccan/failtest/LICENSE new file mode 120000 index 00000000..74550445 --- /dev/null +++ b/ccan/failtest/LICENSE @@ -0,0 +1 @@ +../../licenses/LGPL-3 \ No newline at end of file diff --git a/ccan/failtest/_info b/ccan/failtest/_info new file mode 100644 index 00000000..73aa6a2d --- /dev/null +++ b/ccan/failtest/_info @@ -0,0 +1,69 @@ +#include +#include +#include "config.h" + +/** + * failtest - unit test helpers for testing malloc and other failures. + * + * The failtest module overrides various standard functions, and forks + * your unit test at those points to test failure paths. The failing + * child are expected to fail (eg. when malloc fails), but should not + * leak memory or crash. + * + * The unit test is a normal CCAN tap-style test, except it should + * start by calling failtest_init() and end by calling + * failtest_exit(). + * + * You can control what functions fail: see failtest_hook. + * + * Example: + * #include + * #include + * #include + * #include + * #include + * #include + * + * int main(int argc, char *argv[]) + * { + * void *a, *b; + * + * failtest_init(argc, argv); + * plan_tests(3); + * + * // Simple malloc test. + * a = malloc(100); + * if (ok1(a)) { + * // Fill the memory. + * memset(a, 'x', 100); + * b = realloc(a, 200); + * if (ok1(b)) { + * // Fill the rest of the memory. + * memset(b + 100, 'y', 100); + * // Check it got a copy of a as expected. + * ok1(strspn(b, "x") == 100); + * free(b); + * } else { + * // Easy to miss: free a on realloc failure! + * free(a); + * } + * } + * failtest_exit(exit_status()); + * } + * + * License: LGPL + * Author: Rusty Russell + */ +int main(int argc, char *argv[]) +{ + if (argc != 2) + return 1; + + if (strcmp(argv[1], "depends") == 0) { + printf("ccan/compiler\n"); + printf("ccan/read_write_all\n"); + return 0; + } + + return 1; +} diff --git a/ccan/failtest/failtest.c b/ccan/failtest/failtest.c new file mode 100644 index 00000000..092d2091 --- /dev/null +++ b/ccan/failtest/failtest.c @@ -0,0 +1,547 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool (*failtest_hook)(struct failtest_call *history, unsigned num) += failtest_default_hook; + +unsigned int failtest_timeout_ms = 20000; + +const char *failpath; + +enum info_type { + WRITE, + FAILURE, + SUCCESS, + UNEXPECTED +}; + +struct write_info_hdr { + size_t len; + off_t offset; + int fd; +}; + +struct fd_orig { + int fd; + off_t offset; + size_t size; + bool dupped; +}; + +struct write_info { + struct write_info_hdr hdr; + char *data; + size_t oldlen; + char *olddata; +}; + +bool (*failtest_exit_check)(struct failtest_call *history, unsigned num); + +static struct failtest_call *history = NULL; +static unsigned int history_num = 0; +static int control_fd = -1; + +static struct write_info *writes = NULL; +static unsigned int writes_num = 0; + +static struct write_info *child_writes = NULL; +static unsigned int child_writes_num = 0; + +static struct fd_orig *fd_orig = NULL; +static unsigned int fd_orig_num = 0; + +static const char info_to_arg[] = "mceoprw"; + +static struct failtest_call *add_history_(enum failtest_call_type type, + const char *file, + unsigned int line, + const void *elem, + size_t elem_size) +{ + history = realloc(history, (history_num + 1) * sizeof(*history)); + history[history_num].type = type; + history[history_num].file = file; + history[history_num].line = line; + memcpy(&history[history_num].u, elem, elem_size); + return &history[history_num++]; +} + +#define add_history(type, file, line, elem) \ + add_history_((type), (file), (line), (elem), sizeof(*(elem))) + +static void save_fd_orig(int fd) +{ + unsigned int i; + + for (i = 0; i < fd_orig_num; i++) + if (fd_orig[i].fd == fd) + return; + + fd_orig = realloc(fd_orig, (fd_orig_num + 1) * sizeof(*fd_orig)); + fd_orig[fd_orig_num].fd = fd; + fd_orig[fd_orig_num].dupped = false; + fd_orig[fd_orig_num].offset = lseek(fd, 0, SEEK_CUR); + fd_orig[fd_orig_num].size = lseek(fd, 0, SEEK_END); + lseek(fd, fd_orig[fd_orig_num].offset, SEEK_SET); + fd_orig_num++; +} + +bool failtest_default_hook(struct failtest_call *history, unsigned num) +{ + return true; +} + +static bool read_write_info(int fd) +{ + struct write_info_hdr hdr; + + if (!read_all(fd, &hdr, sizeof(hdr))) + return false; + + child_writes = realloc(child_writes, + (child_writes_num+1) * sizeof(child_writes[0])); + child_writes[child_writes_num].hdr = hdr; + child_writes[child_writes_num].data = malloc(hdr.len); + if (!read_all(fd, child_writes[child_writes_num].data, hdr.len)) + return false; + + child_writes_num++; + return true; +} + +static void print_reproduce(void) +{ + unsigned int i; + + printf("To reproduce: --failpath="); + for (i = 0; i < history_num; i++) { + if (history[i].fail) + printf("%c", toupper(info_to_arg[history[i].type])); + else + printf("%c", info_to_arg[history[i].type]); + } + printf("\n"); +} + +static void tell_parent(enum info_type type) +{ + if (control_fd != -1) + write_all(control_fd, &type, sizeof(type)); +} + +static void child_fail(const char *out, size_t outlen, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "%.*s", (int)outlen, out); + print_reproduce(); + tell_parent(FAILURE); + exit(1); +} + +static pid_t child; + +static void hand_down(int signal) +{ + kill(child, signal); +} + +static bool should_fail(struct failtest_call *call) +{ + int status; + int control[2], output[2]; + enum info_type type = UNEXPECTED; + char *out = NULL; + size_t outlen = 0; + + if (failpath) { + if (tolower(*failpath) != info_to_arg[call->type]) + errx(1, "Failpath expected '%c' got '%c'\n", + info_to_arg[call->type], *failpath); + return isupper(*(failpath++)); + } + + if (!failtest_hook(history, history_num)) { + call->fail = false; + return false; + } + + /* We're going to fail in the child. */ + call->fail = true; + if (pipe(control) != 0 || pipe(output) != 0) + err(1, "opening pipe"); + + /* Prevent double-printing (in child and parent) */ + fflush(stdout); + child = fork(); + if (child == -1) + err(1, "forking failed"); + + if (child == 0) { + close(control[0]); + close(output[0]); + dup2(output[1], STDOUT_FILENO); + dup2(output[1], STDERR_FILENO); + if (output[1] != STDOUT_FILENO && output[1] != STDERR_FILENO) + close(output[1]); + control_fd = control[1]; + return true; + } + + signal(SIGUSR1, hand_down); + + close(control[1]); + close(output[1]); + + /* We grab output so we can display it; we grab writes so we + * can compare. */ + do { + struct pollfd pfd[2]; + int ret; + + pfd[0].fd = output[0]; + pfd[0].events = POLLIN|POLLHUP; + pfd[1].fd = control[0]; + pfd[1].events = POLLIN|POLLHUP; + + if (type == SUCCESS) + ret = poll(pfd, 1, failtest_timeout_ms); + else + ret = poll(pfd, 2, failtest_timeout_ms); + + if (ret <= 0) + hand_down(SIGUSR1); + + if (pfd[0].revents & POLLIN) { + ssize_t len; + + out = realloc(out, outlen + 8192); + len = read(output[0], out + outlen, 8192); + outlen += len; + } else if (type != SUCCESS && (pfd[1].revents & POLLIN)) { + if (read_all(control[0], &type, sizeof(type))) { + if (type == WRITE) { + if (!read_write_info(control[0])) + break; + } + } + } else if (pfd[0].revents & POLLHUP) { + break; + } + } while (type != FAILURE); + + close(output[0]); + close(control[0]); + waitpid(child, &status, 0); + if (!WIFEXITED(status)) + child_fail(out, outlen, "Killed by signal %u: ", + WTERMSIG(status)); + /* Child printed failure already, just pass up exit code. */ + if (type == FAILURE) { + fprintf(stderr, "%.*s", (int)outlen, out); + tell_parent(type); + exit(WEXITSTATUS(status) ? WEXITSTATUS(status) : 1); + } + if (WEXITSTATUS(status) != 0) + child_fail(out, outlen, "Exited with status %i: ", + WEXITSTATUS(status)); + + free(out); + signal(SIGUSR1, SIG_DFL); + + /* We continue onwards without failing. */ + call->fail = false; + return false; +} + +void *failtest_calloc(size_t nmemb, size_t size, + const char *file, unsigned line) +{ + struct failtest_call *p; + struct calloc_call call; + call.nmemb = nmemb; + call.size = size; + p = add_history(FAILTEST_CALLOC, file, line, &call); + + if (should_fail(p)) { + p->u.calloc.ret = NULL; + p->error = ENOMEM; + } else { + p->u.calloc.ret = calloc(nmemb, size); + } + errno = p->error; + return p->u.calloc.ret; +} + +void *failtest_malloc(size_t size, const char *file, unsigned line) +{ + struct failtest_call *p; + struct malloc_call call; + call.size = size; + + p = add_history(FAILTEST_MALLOC, file, line, &call); + if (should_fail(p)) { + p->u.calloc.ret = NULL; + p->error = ENOMEM; + } else { + p->u.calloc.ret = malloc(size); + } + errno = p->error; + return p->u.calloc.ret; +} + +void *failtest_realloc(void *ptr, size_t size, const char *file, unsigned line) +{ + struct failtest_call *p; + struct realloc_call call; + call.size = size; + p = add_history(FAILTEST_REALLOC, file, line, &call); + + /* FIXME: Try one child moving allocation, one not. */ + if (should_fail(p)) { + p->u.realloc.ret = NULL; + p->error = ENOMEM; + } else { + p->u.realloc.ret = realloc(ptr, size); + } + errno = p->error; + return p->u.realloc.ret; +} + +int failtest_open(const char *pathname, int flags, + const char *file, unsigned line, ...) +{ + struct failtest_call *p; + struct open_call call; + + call.pathname = strdup(pathname); + call.flags = flags; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, line); + call.mode = va_arg(ap, mode_t); + va_end(ap); + } + p = add_history(FAILTEST_OPEN, file, line, &call); + if (should_fail(p)) { + p->u.open.ret = -1; + /* FIXME: Play with error codes? */ + p->error = EACCES; + } else { + p->u.open.ret = open(pathname, flags, call.mode); + } + errno = p->error; + return p->u.open.ret; +} + +int failtest_pipe(int pipefd[2], const char *file, unsigned line) +{ + struct failtest_call *p; + struct pipe_call call; + + p = add_history(FAILTEST_PIPE, file, line, &call); + if (should_fail(p)) { + p->u.open.ret = -1; + /* FIXME: Play with error codes? */ + p->error = EMFILE; + } else { + p->u.pipe.ret = pipe(p->u.pipe.fds); + } + /* This causes valgrind to notice if they use pipefd[] after failure */ + memcpy(pipefd, p->u.pipe.fds, sizeof(p->u.pipe.fds)); + errno = p->error; + return p->u.pipe.ret; +} + +ssize_t failtest_read(int fd, void *buf, size_t count, + const char *file, unsigned line) +{ + struct failtest_call *p; + struct read_call call; + call.fd = fd; + call.buf = buf; + call.count = count; + p = add_history(FAILTEST_READ, file, line, &call); + + /* This is going to change seek offset, so save it. */ + if (control_fd != -1) + save_fd_orig(fd); + + /* FIXME: Try partial read returns. */ + if (should_fail(p)) { + p->u.read.ret = -1; + p->error = EIO; + } else { + p->u.read.ret = read(fd, buf, count); + } + errno = p->error; + return p->u.read.ret; +} + +static struct write_info *new_write(void) +{ + writes = realloc(writes, (writes_num + 1) * sizeof(*writes)); + return &writes[writes_num++]; +} + +ssize_t failtest_write(int fd, const void *buf, size_t count, + const char *file, unsigned line) +{ + struct failtest_call *p; + struct write_call call; + off_t offset; + + call.fd = fd; + call.buf = buf; + call.count = count; + p = add_history(FAILTEST_WRITE, file, line, &call); + + offset = lseek(fd, 0, SEEK_CUR); + + /* If we're a child, save contents and tell parent about write. */ + if (control_fd != -1) { + struct write_info *winfo = new_write(); + enum info_type type = WRITE; + + save_fd_orig(fd); + + winfo->hdr.len = count; + winfo->hdr.fd = fd; + winfo->data = malloc(count); + memcpy(winfo->data, buf, count); + winfo->hdr.offset = offset; + if (winfo->hdr.offset != (off_t)-1) { + lseek(fd, offset, SEEK_SET); + winfo->olddata = malloc(count); + winfo->oldlen = read(fd, winfo->olddata, count); + if (winfo->oldlen == -1) + winfo->oldlen = 0; + } + write_all(control_fd, &type, sizeof(type)); + write_all(control_fd, &winfo->hdr, sizeof(winfo->hdr)); + write_all(control_fd, winfo->data, count); + } + + /* FIXME: Try partial write returns. */ + if (should_fail(p)) { + p->u.write.ret = -1; + p->error = EIO; + } else { + /* FIXME: We assume same write order in parent and child */ + if (child_writes_num != 0) { + if (child_writes[0].hdr.fd != fd) + errx(1, "Child wrote to fd %u, not %u?", + child_writes[0].hdr.fd, fd); + if (child_writes[0].hdr.offset != offset) + errx(1, "Child wrote to offset %zu, not %zu?", + (size_t)child_writes[0].hdr.offset, + (size_t)offset); + if (child_writes[0].hdr.len != count) + errx(1, "Child wrote length %zu, not %zu?", + child_writes[0].hdr.len, count); + if (memcmp(child_writes[0].data, buf, count)) { + child_fail(NULL, 0, + "Child wrote differently to" + " fd %u than we did!\n", fd); + } + free(child_writes[0].data); + child_writes_num--; + memmove(&child_writes[0], &child_writes[1], + sizeof(child_writes[0]) * child_writes_num); + + /* Is this is a socket or pipe, child wrote it + already. */ + if (offset == (off_t)-1) { + p->u.write.ret = count; + errno = p->error; + return p->u.write.ret; + } + } + p->u.write.ret = write(fd, buf, count); + } + errno = p->error; + return p->u.write.ret; +} + +/* We only trap this so we can dup fds in case we need to restore. */ +int failtest_close(int fd) +{ + unsigned int i; + int newfd = -1; + + for (i = 0; i < fd_orig_num; i++) { + if (fd_orig[i].fd == fd) { + fd_orig[i].fd = newfd = dup(fd); + fd_orig[i].dupped = true; + } + } + + for (i = 0; i < writes_num; i++) { + if (writes[i].hdr.fd == fd) + writes[i].hdr.fd = newfd; + } + return close(fd); +} + +void failtest_init(int argc, char *argv[]) +{ + if (argc == 2 + && strncmp(argv[1], "--failpath=", strlen("--failpath=")) == 0) { + failpath = argv[1] + strlen("--failpath="); + } +} + +void failtest_exit(int status) +{ + unsigned int i; + + if (control_fd == -1) + exit(status); + + if (failtest_exit_check) { + if (!failtest_exit_check(history, history_num)) + child_fail(NULL, 0, "failtest_exit_check failed\n"); + } + + /* Restore any stuff we overwrote. */ + for (i = 0; i < writes_num; i++) { + if (writes[i].hdr.offset == (off_t)-1) + continue; + if (writes[i].oldlen != 0) { + lseek(writes[i].hdr.fd, writes[i].hdr.offset, + SEEK_SET); + write(writes[i].hdr.fd, writes[i].olddata, + writes[i].oldlen); + } + } + + /* Fix up fd offsets, restore sizes. */ + for (i = 0; i < fd_orig_num; i++) { + lseek(fd_orig[i].fd, fd_orig[i].offset, SEEK_SET); + ftruncate(fd_orig[i].fd, fd_orig[i].size); + /* Free up any file descriptors we dup'ed. */ + if (fd_orig[i].dupped) + close(fd_orig[i].fd); + } + + tell_parent(SUCCESS); + exit(0); +} diff --git a/ccan/failtest/failtest.h b/ccan/failtest/failtest.h new file mode 100644 index 00000000..8aae82b3 --- /dev/null +++ b/ccan/failtest/failtest.h @@ -0,0 +1,170 @@ +#ifndef CCAN_FAILTEST_H +#define CCAN_FAILTEST_H +#include +#include +#include + +/** + * failtest_init - initialize the failtest module + * @argc: the number of commandline arguments + * @argv: the commandline argument array + * + * This initializes the module, and in particular if argv[1] is "--failpath=" + * then it ensures that failures follow that pattern. This allows easy + * debugging of complex failure paths. + */ +void failtest_init(int argc, char *argv[]); + +/** + * failtest_exit - clean up and exit the test + * @status: the status (usually exit_status() from ccan/tap). + * + * This cleans up and changes to files made in this child, and exits the test. + * It also calls your failtest_default_hook, if any. + * + * A child which does not exit via failtest_exit() will cause the overall test + * to fail. + */ +void NORETURN failtest_exit(int status); + +/** + * enum failtest_call_type - discriminator for failtest_call.u + */ +enum failtest_call_type { + FAILTEST_MALLOC, + FAILTEST_CALLOC, + FAILTEST_REALLOC, + FAILTEST_OPEN, + FAILTEST_PIPE, + FAILTEST_READ, + FAILTEST_WRITE, +}; + +struct calloc_call { + void *ret; + size_t nmemb; + size_t size; +}; + +struct malloc_call { + void *ret; + size_t size; +}; + +struct realloc_call { + void *ret; + void *ptr; + size_t size; +}; + +struct open_call { + int ret; + const char *pathname; + int flags; + mode_t mode; +}; + +struct pipe_call { + int ret; + int fds[2]; +}; + +struct read_call { + ssize_t ret; + int fd; + void *buf; + size_t count; +}; + +struct write_call { + ssize_t ret; + int fd; + const void *buf; + size_t count; +}; + +/** + * struct failtest_call - description of a call redirected to failtest module + * @type: the call type + * @file: the filename of the caller + * @line: the line number of the caller + * @fail: did this call fail + * @error: the errno (if any) + * @u: the union of call data + * + * This structure is used to represent the ordered history of calls. + * + * See Also: + * failtest_hook, failtest_exit_check + */ +struct failtest_call { + enum failtest_call_type type; + /* Where we were called from. */ + const char *file; + unsigned int line; + /* Did we fail? */ + bool fail; + /* What we set errno to. */ + int error; + /* The actual call data. */ + union { + struct calloc_call calloc; + struct malloc_call malloc; + struct realloc_call realloc; + struct open_call open; + struct pipe_call pipe; + struct read_call read; + struct write_call write; + } u; +}; + +/** + * failtest_hook - whether a certain call should fail or not. + * @history: the ordered history of all failtest calls. + * @num: the number of elements in @history (greater than 0) + * + * The default value of this hook is failtest_default_hook(), which returns + * true (ie. yes, fail the call). + * + * You can override it, and avoid failing certain calls. The parameters + * of the call (but not the return value(s)) will be filled in for the last + * call. + * + * Example: + * static bool dont_fail_allocations(struct failtest_call *history, + * unsigned num) + * { + * return history[num-1].type != FAILTEST_MALLOC + * && history[num-1].type != FAILTEST_CALLOC + * && history[num-1].type != FAILTEST_REALLOC; + * } + * ... + * failtest_hook = dont_fail_allocations; + */ +extern bool (*failtest_hook)(struct failtest_call *history, unsigned num); + +/** + * failtest_exit_check - hook for additional checks on a failed child. + * @history: the ordered history of all failtest calls. + * @num: the number of elements in @history (greater than 0) + * + * Your program might have additional checks to do on failure, such as + * check that a file is not corrupted, or than an error message has been + * logged. + * + * If this returns false, the path to this failure will be printed and the + * overall test will fail. + */ +extern bool (*failtest_exit_check)(struct failtest_call *history, + unsigned num); + +/* This usually fails the call. */ +bool failtest_default_hook(struct failtest_call *history, unsigned num); + +/** + * failtest_timeout_ms - how long to wait before killing child. + * + * Default is 20,000 (20 seconds). + */ +extern unsigned int failtest_timeout_ms; +#endif /* CCAN_FAILTEST_H */ diff --git a/ccan/failtest/failtest_override.h b/ccan/failtest/failtest_override.h new file mode 100644 index 00000000..dd328b50 --- /dev/null +++ b/ccan/failtest/failtest_override.h @@ -0,0 +1,47 @@ +#ifndef CCAN_FAILTEST_OVERRIDE_H +#define CCAN_FAILTEST_OVERRIDE_H +/* This file is included before the source file to test. */ + +/* Replacement of allocators. */ +#include + +#undef calloc +#define calloc(nmemb, size) \ + failtest_calloc((nmemb), (size), __FILE__, __LINE__) + +#undef malloc +#define malloc(size) \ + failtest_malloc((size), __FILE__, __LINE__) + +#undef realloc +#define realloc(ptr, size) \ + failtest_realloc((ptr), (size), __FILE__, __LINE__) + +/* Replacement of I/O. */ +#include +#include +#include +#include + +#undef open +#define open(pathname, flags, ...) \ + failtest_open((pathname), (flags), __FILE__, __LINE__, __VA_ARGS__) + +#undef pipe +#define pipe(pipefd) \ + failtest_pipe((pipefd), __FILE__, __LINE__) + +#undef read +#define read(fd, buf, count) \ + failtest_read((fd), (buf), (count), __FILE__, __LINE__) + +#undef write +#define write(fd, buf, count) \ + failtest_write((fd), (buf), (count), __FILE__, __LINE__) + +#undef close +#define close(fd) failtest_close(fd) + +#include + +#endif /* CCAN_FAILTEST_OVERRIDE_H */ diff --git a/ccan/failtest/failtest_proto.h b/ccan/failtest/failtest_proto.h new file mode 100644 index 00000000..cdb43eda --- /dev/null +++ b/ccan/failtest/failtest_proto.h @@ -0,0 +1,20 @@ +#ifndef CCAN_FAILTEST_PROTO_H +#define CCAN_FAILTEST_PROTO_H +#include + +/* Potentially-failing versions of routines; #defined in failtest.h */ +void *failtest_calloc(size_t nmemb, size_t size, + const char *file, unsigned line); +void *failtest_malloc(size_t size, const char *file, unsigned line); +void *failtest_realloc(void *ptr, size_t size, + const char *file, unsigned line); +int failtest_open(const char *pathname, int flags, + const char *file, unsigned line, ...); +int failtest_pipe(int pipefd[2], const char *file, unsigned line); +ssize_t failtest_read(int fd, void *buf, size_t count, + const char *file, unsigned line); +ssize_t failtest_write(int fd, const void *buf, size_t count, + const char *file, unsigned line); +int failtest_close(int fd); + +#endif /* CCAN_FAILTEST_PROTO_H */ diff --git a/ccan/failtest/test/run-failpath.c b/ccan/failtest/test/run-failpath.c new file mode 100644 index 00000000..c5aca9ef --- /dev/null +++ b/ccan/failtest/test/run-failpath.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include +#include + +int main(void) +{ + int fds[2], fd; + void *p; + + plan_tests(14); + + failpath = "mceopwrMCEOPWR"; + + ok1((p = failtest_malloc(10, "run-failpath.c", 1)) != NULL); + ok1(failtest_calloc(10, 5, "run-failpath.c", 1) != NULL); + ok1((p = failtest_realloc(p, 100, "run-failpath.c", 1)) != NULL); + ok1((fd = failtest_open("failpath-scratch", O_RDWR|O_CREAT, + "run-failpath.c", 1, 0600)) >= 0); + ok1(failtest_pipe(fds, "run-failpath.c", 1) == 0); + ok1(failtest_write(fd, "xxxx", 4, "run-failpath.c", 1) == 4); + lseek(fd, 0, SEEK_SET); + ok1(failtest_read(fd, p, 5, "run-failpath.c", 1) == 4); + + /* Now we're into the failures. */ + ok1(failtest_malloc(10, "run-failpath.c", 1) == NULL); + ok1(failtest_calloc(10, 5, "run-failpath.c", 1) == NULL); + ok1(failtest_realloc(p, 100, "run-failpath.c", 1) == NULL); + ok1(failtest_open("failpath-scratch", O_RDWR|O_CREAT, + "run-failpath.c", 1, 0600) == -1); + ok1(failtest_pipe(fds, "run-failpath.c", 1) == -1); + ok1(failtest_write(fd, "xxxx", 4, "run-failpath.c", 1) == -1); + lseek(fd, 0, SEEK_SET); + ok1(failtest_read(fd, p, 5, "run-failpath.c", 1) == -1); + return exit_status(); +} diff --git a/ccan/failtest/test/run-history.c b/ccan/failtest/test/run-history.c new file mode 100644 index 00000000..3b5220a5 --- /dev/null +++ b/ccan/failtest/test/run-history.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include + +#define printf saved_printf +static int saved_printf(const char *fmt, ...); + +#define fprintf saved_fprintf +static int saved_fprintf(FILE *ignored, const char *fmt, ...); + +/* Include the C files directly. */ +#include + +static char *output = NULL; + +static int saved_vprintf(const char *fmt, va_list ap) +{ + int ret = vsnprintf(NULL, 0, fmt, ap); + int len = 0; + + if (output) + len = strlen(output); + + output = realloc(output, len + ret + 1); + return vsprintf(output + len, fmt, ap); +} + +static int saved_printf(const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = saved_vprintf(fmt, ap); + va_end(ap); + return ret; +} + +static int saved_fprintf(FILE *ignored, const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = saved_vprintf(fmt, ap); + va_end(ap); + return ret; +} + +int main(void) +{ + struct failtest_call *call; + struct calloc_call calloc_call; + struct malloc_call malloc_call; + struct realloc_call realloc_call; + struct open_call open_call; + struct pipe_call pipe_call; + struct read_call read_call; + struct write_call write_call; + char buf[20]; + unsigned int i; + + /* This is how many tests you plan to run */ + plan_tests(47); + + calloc_call.ret = calloc(1, 2); + calloc_call.nmemb = 1; + calloc_call.size = 2; + call = add_history(FAILTEST_CALLOC, "run-history.c", 1, &calloc_call); + ok1(call->type == FAILTEST_CALLOC); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 1); + ok1(call->u.calloc.ret == calloc_call.ret); + ok1(call->u.calloc.nmemb == calloc_call.nmemb); + ok1(call->u.calloc.size == calloc_call.size); + + malloc_call.ret = malloc(2); + malloc_call.size = 2; + call = add_history(FAILTEST_MALLOC, "run-history.c", 2, &malloc_call); + ok1(call->type == FAILTEST_MALLOC); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 2); + ok1(call->u.malloc.ret == malloc_call.ret); + ok1(call->u.malloc.size == malloc_call.size); + + realloc_call.ret = realloc(malloc_call.ret, 3); + realloc_call.ptr = malloc_call.ret; + realloc_call.size = 3; + call = add_history(FAILTEST_REALLOC, "run-history.c", + 3, &realloc_call); + ok1(call->type == FAILTEST_REALLOC); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 3); + ok1(call->u.realloc.ret == realloc_call.ret); + ok1(call->u.realloc.ptr == realloc_call.ptr); + ok1(call->u.realloc.size == realloc_call.size); + + open_call.ret = open("test/run_history.c", O_RDONLY); + open_call.pathname = "test/run_history.c"; + open_call.flags = O_RDONLY; + open_call.mode = 0; + call = add_history(FAILTEST_OPEN, "run-history.c", 4, &open_call); + ok1(call->type == FAILTEST_OPEN); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 4); + ok1(call->u.open.ret == open_call.ret); + ok1(strcmp(call->u.open.pathname, open_call.pathname) == 0); + ok1(call->u.open.flags == open_call.flags); + ok1(call->u.open.mode == open_call.mode); + + pipe_call.ret = pipe(pipe_call.fds); + call = add_history(FAILTEST_PIPE, "run-history.c", 5, &pipe_call); + ok1(call->type == FAILTEST_PIPE); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 5); + ok1(call->u.pipe.ret == pipe_call.ret); + ok1(call->u.pipe.fds[0] == pipe_call.fds[0]); + ok1(call->u.pipe.fds[1] == pipe_call.fds[1]); + + read_call.ret = read(open_call.ret, buf, 20); + read_call.buf = buf; + read_call.fd = open_call.ret; + read_call.count = 20; + call = add_history(FAILTEST_READ, "run-history.c", 6, &read_call); + ok1(call->type == FAILTEST_READ); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 6); + ok1(call->u.read.ret == read_call.ret); + ok1(call->u.read.buf == read_call.buf); + ok1(call->u.read.fd == read_call.fd); + ok1(call->u.read.count == read_call.count); + + write_call.ret = 20; + write_call.buf = buf; + write_call.fd = open_call.ret; + write_call.count = 20; + call = add_history(FAILTEST_WRITE, "run-history.c", 7, &write_call); + ok1(call->type == FAILTEST_WRITE); + ok1(strcmp(call->file, "run-history.c") == 0); + ok1(call->line == 7); + ok1(call->u.write.ret == write_call.ret); + ok1(call->u.write.buf == write_call.buf); + ok1(call->u.write.fd == write_call.fd); + ok1(call->u.write.count == write_call.count); + + ok1(history_num == 7); + + for (i = 0; i < history_num; i++) + history[i].fail = false; + + print_reproduce(); + ok1(strcmp(output, "To reproduce: --failpath=cmeoprw\n") == 0); + free(output); + output = NULL; + + for (i = 0; i < history_num; i++) + history[i].fail = true; + + print_reproduce(); + ok1(strcmp(output, "To reproduce: --failpath=CMEOPRW\n") == 0); + free(output); + output = NULL; + + return exit_status(); +} diff --git a/ccan/failtest/test/run-malloc.c b/ccan/failtest/test/run-malloc.c new file mode 100644 index 00000000..5849ef65 --- /dev/null +++ b/ccan/failtest/test/run-malloc.c @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include + +/* We don't actually want it to exit... */ +static jmp_buf exited; +#define exit(status) longjmp(exited, (status) + 1) + +#define printf saved_printf +static int saved_printf(const char *fmt, ...); + +#define fprintf saved_fprintf +static int saved_fprintf(FILE *ignored, const char *fmt, ...); + +#define vfprintf saved_vfprintf +static int saved_vfprintf(FILE *ignored, const char *fmt, va_list ap); + +/* Hack to avoid a memory leak which valgrind complains about. */ +#define realloc set_realloc +static void *set_realloc(void *ptr, size_t size); + +#define free set_free +static void set_free(void *ptr); + +/* Include the C files directly. */ +#include + +#undef realloc +#undef free + +static char *buffer; +static void *set_realloc(void *ptr, size_t size) +{ + return buffer = realloc(ptr, size); +} + +static void set_free(void *ptr) +{ + if (ptr == buffer) + buffer = NULL; + free(ptr); +} + +static char *output = NULL; + +static int saved_vprintf(const char *fmt, va_list ap) +{ + int ret = vsnprintf(NULL, 0, fmt, ap); + int len = 0; + + if (output) + len = strlen(output); + + output = realloc(output, len + ret + 1); + return vsprintf(output + len, fmt, ap); +} + +static int saved_vfprintf(FILE *ignored, const char *fmt, va_list ap) +{ + return saved_vprintf(fmt, ap); +} + +static int saved_printf(const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = saved_vprintf(fmt, ap); + va_end(ap); + return ret; +} + +static int saved_fprintf(FILE *ignored, const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = saved_vprintf(fmt, ap); + va_end(ap); + return ret; +} + +int main(void) +{ + int status; + + plan_tests(3); + + status = setjmp(exited); + if (status == 0) { + char *p = failtest_malloc(1, "run-malloc.c", 1); + /* If we just segv, valgrind counts that as a failure. + * So kill ourselves creatively. */ + if (!p) + kill(getpid(), SIGSEGV); + fail("Expected child to crash!"); + } else { + ok1(status == 2); + ok1(strstr(output, "Killed by signal")); + ok1(strstr(output, "--failpath=M\n")); + } + free(buffer); + return exit_status(); +} diff --git a/ccan/failtest/test/run-write.c b/ccan/failtest/test/run-write.c new file mode 100644 index 00000000..54255170 --- /dev/null +++ b/ccan/failtest/test/run-write.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include +/* Include the C files directly. */ +#include + +int main(void) +{ + int fd; + char *p; + char buf[] = "Hello world!"; + + plan_tests(5); + + fd = open("run-write-scratchpad", O_RDWR|O_CREAT, 0600); + write(fd, buf, strlen(buf)); + ok1(lseek(fd, 0, SEEK_CUR) == strlen(buf)); + + p = failtest_malloc(100, "run-write.c", 1); + if (!p) { + /* We are the child. Do a heap of writes. */ + unsigned int i; + + for (i = 0; i < strlen(buf)+1; i++) + if (failtest_write(fd, "x", 1, "run-write.c", 1) == 1) + break; + failtest_exit(0); + } + + /* Seek pointer should be left alone! */ + ok1(lseek(fd, 0, SEEK_CUR) == strlen(buf)); + /* Length should be restored. */ + ok1(lseek(fd, 0, SEEK_END) == strlen(buf)); + lseek(fd, 0, SEEK_SET); + ok1(read(fd, buf, strlen(buf)) == strlen("Hello world!")); + ok1(strcmp(buf, "Hello world!") == 0); + + return exit_status(); +} -- 2.39.2