From f2f5b0450d4b7269bafdfb1bd1788303f6d8ce7b Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Mon, 26 Oct 2026 19:05:22 +0100 Subject: Add poll/epoll/kqueue wrapper to wait for events --- Makefile | 8 +- ot_event.c | 332 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ot_event.h | 39 +++++++ tests/runner.c | 1 + tests/t_event.c | 222 +++++++++++++++++++++++++++++++++++++ tests/test.h | 1 + 6 files changed, 599 insertions(+), 4 deletions(-) create mode 100644 ot_event.c create mode 100644 ot_event.h create mode 100644 tests/t_event.c diff --git a/Makefile b/Makefile index d9b8690..a373633 100644 --- a/Makefile +++ b/Makefile @@ -68,8 +68,8 @@ CFLAGS+=-I$(LIBOWFAT_HEADERS) -DGIT_VERSION=$(GIT_VERSION) -Wall -pipe -pthread LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread BINARY =opentracker -HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_fdstate.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h -SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_fdstate.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c +HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_event.h ot_fdstate.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h +SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_event.c ot_fdstate.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c SOURCES_proxy=proxy.c ot_batch.c ot_fdstate.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c OBJECTS = $(SOURCES:%.c=%.o) @@ -119,8 +119,8 @@ $(OBJECTS_proxy_debug): $(HEADERS) # has to. tests/stubs.c supplies the few symbols those modules reference but # the tests never reach. TESTBIN=tests/runner -TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c -TEST_UNITS=ot_urlencoded.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c +TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c tests/t_event.c +TEST_UNITS=ot_urlencoded.c ot_event.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c TEST_HEADERS=tests/test.h $(HEADERS) # Tests reach code that sits behind feature flags, so the units under test are # built with those features enabled. A test binary is not a production binary. diff --git a/ot_event.c b/ot_event.c new file mode 100644 index 0000000..d24227d --- /dev/null +++ b/ot_event.c @@ -0,0 +1,332 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include +#include +#include + +/* Opentracker */ +#include "ot_event.h" + +/* One caller waits, so the scratch the mechanism needs is kept here rather + than allocated per call. */ + +/* poll works everywhere and is what the BSDs get until kqueue is written. + Defining OT_EVENT_FORCE_POLL picks it on a platform that would otherwise + use something better, which is how both are exercised by the tests. */ +#if defined(OT_EVENT_FORCE_POLL) +#define OT_EVENT_POLL +#elif defined(OT_EVENT_FORCE_KQUEUE) +#define OT_EVENT_KQUEUE +#elif defined(__linux__) +#define OT_EVENT_EPOLL +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) || defined(__APPLE__) +#define OT_EVENT_KQUEUE +#else +#define OT_EVENT_POLL +#endif + +#ifdef OT_EVENT_EPOLL +/* ------------------------------------------------------------- epoll */ + +#include + +static int g_epoll = -1; +static struct epoll_event *g_ready; +static int g_ready_space; + +const char *event_mechanism(void) { return "epoll"; } + +int event_init(void) { + g_ready = NULL; + g_ready_space = 0; + return (g_epoll = epoll_create1(EPOLL_CLOEXEC)) == -1 ? -1 : 0; +} + +void event_deinit(void) { + if (g_epoll != -1) + close(g_epoll); + free(g_ready); + g_epoll = -1; + g_ready = NULL; + g_ready_space = 0; +} + +static int event_ctl(int op, int fd, int what, uint32_t generation) { + struct epoll_event ev; + + memset(&ev, 0, sizeof(ev)); + ev.events = (what & EVENT_READ ? (uint32_t)EPOLLIN : 0) | (what & EVENT_WRITE ? (uint32_t)EPOLLOUT : 0); + /* The generation rides along inside the kernel, so an event says which + connection it belongs to without anybody looking it up */ + ev.data.u64 = ((uint64_t)generation << 32) | (uint32_t)fd; + return epoll_ctl(g_epoll, op, fd, &ev); +} + +int event_add(int fd, int what, uint32_t generation) { return event_ctl(EPOLL_CTL_ADD, fd, what, generation); } +int event_modify(int fd, int what, uint32_t generation) { return event_ctl(EPOLL_CTL_MOD, fd, what, generation); } +int event_del(int fd) { return epoll_ctl(g_epoll, EPOLL_CTL_DEL, fd, NULL); } + +int event_wait(ot_event *out, int max, int timeout_ms) { + int count, i; + + if (max > g_ready_space) { + struct epoll_event *grown = realloc(g_ready, (size_t)max * sizeof(struct epoll_event)); + + if (!grown) + return -1; + g_ready = grown; + g_ready_space = max; + } + + while ((count = epoll_wait(g_epoll, g_ready, max, timeout_ms)) == -1 && errno == EINTR) + ; + for (i = 0; i < count; ++i) { + uint32_t events = g_ready[i].events; + + out[i].fd = (int)(uint32_t)g_ready[i].data.u64; + out[i].generation = (uint32_t)(g_ready[i].data.u64 >> 32); + /* A hangup or an error is reported as both, so whichever side the caller + was waiting on gets to notice */ + out[i].what = (events & (EPOLLIN | EPOLLHUP | EPOLLERR) ? EVENT_READ : 0) | + (events & (EPOLLOUT | EPOLLHUP | EPOLLERR) ? EVENT_WRITE : 0); + } + return count; +} + +#endif /* OT_EVENT_EPOLL */ + +#ifdef OT_EVENT_KQUEUE +/* ------------------------------------------------------------ kqueue */ + +#include +#include +#include + +static int g_kqueue = -1; +static struct kevent *g_ready; +static int g_ready_space; + +const char *event_mechanism(void) { return "kqueue"; } + +int event_init(void) { + g_ready = NULL; + g_ready_space = 0; + return (g_kqueue = kqueue()) == -1 ? -1 : 0; +} + +void event_deinit(void) { + if (g_kqueue != -1) + close(g_kqueue); + free(g_ready); + g_kqueue = -1; + g_ready = NULL; + g_ready_space = 0; +} + +/* kqueue keeps one registration per filter, so reading and writing are two + entries. A change of interest is an add of the one wanted and a delete + of the one not. The generation travels in udata, which the kernel passes + back untouched. */ +static int event_change(int fd, int what, uint32_t generation, int deleting) { + struct kevent change[2], result[2]; + void *udata = (void *)(uintptr_t)(((uint64_t)generation << 32) | (uint32_t)fd); + int succeeded = 0, count, i; + + EV_SET(&change[0], (uintptr_t)fd, EVFILT_READ, + EV_RECEIPT | ((deleting || !(what & EVENT_READ)) ? EV_DELETE : EV_ADD), 0, 0, udata); + EV_SET(&change[1], (uintptr_t)fd, EVFILT_WRITE, + EV_RECEIPT | ((deleting || !(what & EVENT_WRITE)) ? EV_DELETE : EV_ADD), 0, 0, udata); + + if ((count = kevent(g_kqueue, change, 2, result, 2, NULL)) == -1) + return -1; + + for (i = 0; i < count && i < 2; ++i) { + if (!result[i].data) { + ++succeeded; + continue; + } + /* Dropping interest we never registered is not a failure */ + if ((change[i].flags & EV_DELETE) && result[i].data == ENOENT) + continue; + errno = (int)result[i].data; + return -1; + } + + /* Nothing at all was registered or removed, so the descriptor is unknown */ + if (!succeeded) { + errno = ENOENT; + return -1; + } + return 0; +} + +int event_add(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } +int event_modify(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } +int event_del(int fd) { return event_change(fd, 0, 0, 1); } + +int event_wait(ot_event *out, int max, int timeout_ms) { + struct timespec timeout; + struct timespec *deadline = NULL; + int count, i; + + if (max > g_ready_space) { + struct kevent *grown = realloc(g_ready, (size_t)max * sizeof(struct kevent)); + + if (!grown) + return -1; + g_ready = grown; + g_ready_space = max; + } + + if (timeout_ms >= 0) { + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_nsec = (long)(timeout_ms % 1000) * 1000000; + deadline = &timeout; + } + + while ((count = kevent(g_kqueue, NULL, 0, g_ready, max, deadline)) == -1 && errno == EINTR) + ; + for (i = 0; i < count; ++i) { + uint64_t udata = (uint64_t)(uintptr_t)g_ready[i].udata; + int eof = (g_ready[i].flags & (EV_EOF | EV_ERROR)) != 0; + + out[i].fd = (int)(uint32_t)udata; + out[i].generation = (uint32_t)(udata >> 32); + out[i].what = ((g_ready[i].filter == EVFILT_READ || eof) ? EVENT_READ : 0) | + ((g_ready[i].filter == EVFILT_WRITE || eof) ? EVENT_WRITE : 0); + } + return count; +} + +#endif /* OT_EVENT_KQUEUE */ + +#ifdef OT_EVENT_POLL +/* -------------------------------------------------------------- poll */ + +#include + +/* poll wants its descriptors packed, so the registered ones live in one + array and a second maps a descriptor number to its place in it. */ +static struct pollfd *g_pfds; +static uint32_t *g_generation; +static int *g_where; +static int g_count, g_space, g_where_space; + +const char *event_mechanism(void) { return "poll"; } + +int event_init(void) { + g_pfds = NULL, g_generation = NULL, g_where = NULL; + g_count = g_space = g_where_space = 0; + return 0; +} + +void event_deinit(void) { + free(g_pfds), free(g_generation), free(g_where); + g_pfds = NULL, g_generation = NULL, g_where = NULL; + g_count = g_space = g_where_space = 0; +} + +static short event_to_poll(int what) { + return (short)((what & EVENT_READ ? POLLIN : 0) | (what & EVENT_WRITE ? POLLOUT : 0)); +} + +static int event_room_for(int fd) { + if (fd >= g_where_space) { + int space = g_where_space ? g_where_space : 64; + int *grown; + + while (space <= fd) + space *= 2; + if (!(grown = realloc(g_where, (size_t)space * sizeof(int)))) + return -1; + while (g_where_space < space) + grown[g_where_space++] = -1; + g_where = grown; + } + + if (g_count == g_space) { + int space = g_space ? g_space * 2 : 64; + struct pollfd *pfds = realloc(g_pfds, (size_t)space * sizeof(struct pollfd)); + uint32_t *gens; + + if (!pfds) + return -1; + g_pfds = pfds; + if (!(gens = realloc(g_generation, (size_t)space * sizeof(uint32_t)))) + return -1; + g_generation = gens; + g_space = space; + } + return 0; +} + +int event_add(int fd, int what, uint32_t generation) { + if (fd < 0 || event_room_for(fd) == -1) + return -1; + if (g_where[fd] != -1) { + errno = EEXIST; + return -1; + } + + g_pfds[g_count].fd = fd; + g_pfds[g_count].events = event_to_poll(what); + g_pfds[g_count].revents = 0; + g_generation[g_count] = generation; + g_where[fd] = g_count++; + return 0; +} + +int event_modify(int fd, int what, uint32_t generation) { + if (fd < 0 || fd >= g_where_space || g_where[fd] == -1) { + errno = ENOENT; + return -1; + } + g_pfds[g_where[fd]].events = event_to_poll(what); + g_generation[g_where[fd]] = generation; + return 0; +} + +int event_del(int fd) { + int at; + + if (fd < 0 || fd >= g_where_space || (at = g_where[fd]) == -1) { + errno = ENOENT; + return -1; + } + + /* The last one moves into the gap, so the array stays packed */ + g_pfds[at] = g_pfds[--g_count]; + g_generation[at] = g_generation[g_count]; + g_where[g_pfds[at].fd] = at; + g_where[fd] = -1; + return 0; +} + +int event_wait(ot_event *out, int max, int timeout_ms) { + int ready, found = 0, i; + + while ((ready = poll(g_pfds, (nfds_t)g_count, timeout_ms)) == -1 && errno == EINTR) + ; + if (ready <= 0) + return ready; + + for (i = 0; i < g_count && found < max; ++i) { + short revents = g_pfds[i].revents; + + if (!revents) + continue; + out[found].fd = g_pfds[i].fd; + out[found].generation = g_generation[i]; + out[found].what = (revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL) ? EVENT_READ : 0) | + (revents & (POLLOUT | POLLHUP | POLLERR | POLLNVAL) ? EVENT_WRITE : 0); + ++found; + } + return found; +} + +#endif /* OT_EVENT_POLL */ diff --git a/ot_event.h b/ot_event.h new file mode 100644 index 0000000..c1627ab --- /dev/null +++ b/ot_event.h @@ -0,0 +1,39 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +#ifndef OT_EVENT_H__ +#define OT_EVENT_H__ + +#include + +/* Which descriptors have become readable or writable, asked of whichever + mechanism the kernel offers. The generation a descriptor was registered + with is handed back with every event, so a caller can tell an event for + the connection it is holding from one for a connection that has since been + closed and had its number reused. */ + +enum { EVENT_READ = 1, EVENT_WRITE = 2 }; + +typedef struct { + int fd; + uint32_t generation; + int what; +} ot_event; + +int event_init(void); +void event_deinit(void); + +int event_add(int fd, int what, uint32_t generation); +int event_modify(int fd, int what, uint32_t generation); +int event_del(int fd); + +/* Waits at most timeout_ms, or indefinitely when that is negative, and fills + out with up to max ready descriptors. Returns how many, or -1. */ +int event_wait(ot_event *out, int max, int timeout_ms); + +/* Which mechanism was compiled in, for the version string */ +const char *event_mechanism(void); + +#endif diff --git a/tests/runner.c b/tests/runner.c index 9e5c99f..c2350c3 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -26,6 +26,7 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded}, {"ot_mutex", test_mutex}, {"ot_batch", test_batch}, {"ot_fdstate", test_fdstate}, + {"ot_event", test_event}, {"ot_accesslist", test_accesslist}, {NULL, NULL}}; diff --git a/tests/t_event.c b/tests/t_event.c new file mode 100644 index 0000000..306f8cd --- /dev/null +++ b/tests/t_event.c @@ -0,0 +1,222 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include +#include +#include + +/* Opentracker */ +#include "ot_event.h" +#include "test.h" + +/* Against real socket pairs: only the kernel can say whether a descriptor is + ready, so there is nothing useful to mock here. */ + +#define EVENTS 16 + +static int sv[2]; +static ot_event ready[EVENTS]; + +static void pair_open(void) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) + sv[0] = sv[1] = -1; +} + +static void pair_close(void) { + if (sv[0] != -1) + close(sv[0]); + if (sv[1] != -1) + close(sv[1]); + sv[0] = sv[1] = -1; +} + +static int wait_for(int fd, int what, int timeout_ms, uint32_t *generation) { + int count = event_wait(ready, EVENTS, timeout_ms), i; + + for (i = 0; i < count; ++i) + if (ready[i].fd == fd && (ready[i].what & what)) { + if (generation) + *generation = ready[i].generation; + return 1; + } + return 0; +} + +static unsigned long elapsed_ms(struct timeval *since) { + struct timeval now; + + gettimeofday(&now, NULL); + return (unsigned long)((now.tv_sec - since->tv_sec) * 1000 + (now.tv_usec - since->tv_usec) / 1000); +} + +static void case_readable(void) { + uint32_t generation = 0; + + test_case("a descriptor with something to read is reported, with its generation"); + pair_open(); + CHECK(sv[0] != -1); + CHECK_INT(event_add(sv[0], EVENT_READ, 4711), 0); + CHECK_INT(write(sv[1], "x", 1), 1); + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 4711); + + test_case("and keeps being reported until it is read"); + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, NULL), 1); + { + char scratch[4]; + CHECK_INT(read(sv[0], scratch, sizeof(scratch)), 1); + } + CHECK_INT(wait_for(sv[0], EVENT_READ, 50, NULL), 0); + + CHECK_INT(event_del(sv[0]), 0); + pair_close(); +} + +static void case_writable(void) { + test_case("a descriptor with room to write is reported"); + pair_open(); + CHECK(sv[0] != -1); + CHECK_INT(event_add(sv[0], EVENT_WRITE, 1), 0); + CHECK_INT(wait_for(sv[0], EVENT_WRITE, 1000, NULL), 1); + + test_case("and is not, once we say we no longer care"); + CHECK_INT(event_modify(sv[0], EVENT_READ, 1), 0); + CHECK_INT(wait_for(sv[0], EVENT_WRITE, 50, NULL), 0); + + CHECK_INT(event_del(sv[0]), 0); + pair_close(); +} + +static void case_interest_is_separate(void) { + test_case("asking only about writing does not report reading"); + pair_open(); + CHECK(sv[0] != -1); + CHECK_INT(event_add(sv[0], EVENT_WRITE, 1), 0); + CHECK_INT(write(sv[1], "x", 1), 1); + CHECK_INT(wait_for(sv[0], EVENT_READ, 50, NULL), 0); + + test_case("until we ask about it"); + CHECK_INT(event_modify(sv[0], EVENT_READ | EVENT_WRITE, 1), 0); + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, NULL), 1); + + CHECK_INT(event_del(sv[0]), 0); + pair_close(); +} + +static void case_removed_is_silent(void) { + test_case("a removed descriptor is not reported again"); + pair_open(); + CHECK(sv[0] != -1); + CHECK_INT(event_add(sv[0], EVENT_READ, 1), 0); + CHECK_INT(write(sv[1], "x", 1), 1); + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, NULL), 1); + CHECK_INT(event_del(sv[0]), 0); + CHECK_INT(wait_for(sv[0], EVENT_READ, 50, NULL), 0); + pair_close(); +} + +static void case_generations_do_not_mix(void) { + int other[2]; + uint32_t generation = 0; + + test_case("two descriptors carry their own generations"); + pair_open(); + CHECK(sv[0] != -1); + CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, other), 0); + CHECK_INT(event_add(sv[0], EVENT_READ, 100), 0); + CHECK_INT(event_add(other[0], EVENT_READ, 200), 0); + CHECK_INT(write(sv[1], "x", 1), 1); + CHECK_INT(write(other[1], "y", 1), 1); + + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 100); + CHECK_INT(wait_for(other[0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 200); + + test_case("and re-registering one changes only its own"); + CHECK_INT(event_modify(sv[0], EVENT_READ, 101), 0); + CHECK_INT(wait_for(sv[0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 101); + CHECK_INT(wait_for(other[0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 200); + + event_del(sv[0]); + event_del(other[0]); + close(other[0]); + close(other[1]); + pair_close(); +} + +/* Removing one of several has to leave the rest addressable: a backend that + keeps its descriptors packed moves the last one into the gap, and has to + remember where it went. */ +static void case_delete_from_the_middle(void) { + int pair[3][2]; + uint32_t generation = 0; + int i; + + test_case("deleting one of three leaves the others reported correctly"); + for (i = 0; i < 3; ++i) { + CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, pair[i]), 0); + CHECK_INT(event_add(pair[i][0], EVENT_READ, (uint32_t)(10 + i)), 0); + } + CHECK_INT(event_del(pair[1][0]), 0); + + for (i = 0; i < 3; i += 2) { + CHECK_INT(write(pair[i][1], "x", 1), 1); + CHECK_INT(wait_for(pair[i][0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 10 + i); + } + + test_case("and the survivors can still be changed and removed"); + CHECK_INT(event_modify(pair[2][0], EVENT_READ, 99), 0); + CHECK_INT(wait_for(pair[2][0], EVENT_READ, 1000, &generation), 1); + CHECK_INT(generation, 99); + CHECK_INT(event_del(pair[0][0]), 0); + CHECK_INT(event_del(pair[2][0]), 0); + CHECK_INT(wait_for(pair[2][0], EVENT_READ, 50, NULL), 0); + + for (i = 0; i < 3; ++i) + close(pair[i][0]), close(pair[i][1]); +} + +static void case_timeout(void) { + struct timeval started; + + test_case("waiting on nothing comes back empty when the time is up"); + gettimeofday(&started, NULL); + CHECK_INT(event_wait(ready, EVENTS, 200), 0); + { + unsigned long took = elapsed_ms(&started); + /* Scheduling makes the upper bound generous. The point is that it waited + rather than returning at once, and that it came back at all */ + CHECK(took >= 150); + CHECK(took < 5000); + } +} + +static void case_unknown_descriptor(void) { + test_case("changing or removing one that was never added is refused"); + CHECK_INT(event_modify(4096, EVENT_READ, 1), -1); + CHECK_INT(event_del(4096), -1); +} + +void test_event(void) { + CHECK_INT(event_init(), 0); + CHECK(event_mechanism() != NULL); + + case_readable(); + case_writable(); + case_interest_is_separate(); + case_removed_is_silent(); + case_generations_do_not_mix(); + case_delete_from_the_middle(); + case_timeout(); + case_unknown_descriptor(); + + event_deinit(); +} diff --git a/tests/test.h b/tests/test.h index cebd08a..aa4eb00 100644 --- a/tests/test.h +++ b/tests/test.h @@ -89,5 +89,6 @@ void test_socket(void); void test_mutex(void); void test_batch(void); void test_fdstate(void); +void test_event(void); #endif -- cgit v1.3