changeset 216:1314ac3aafd4

Meld patches
author Louis Opter <kalessin@kalessin.fr>
date Sun, 02 Aug 2015 19:02:55 -0700
parents 7b980a75598d
children d320d75c9ca1
files add_command_pipe.patch daemon_module.patch series testing_command_pipe.patch
diffstat 4 files changed, 1466 insertions(+), 1501 deletions(-) [+]
line wrap: on
line diff
--- a/add_command_pipe.patch	Sun Aug 02 18:59:24 2015 -0700
+++ b/add_command_pipe.patch	Sun Aug 02 19:02:55 2015 -0700
@@ -1,9 +1,21 @@
 # HG changeset patch
-# Parent  0ded0e4a2bc8e4eb6d24e3f7f86ee6adf8b4c7e1
+# Parent  710a684b58e97eddc7d7a12d85b17b823d087a82
 Add a command pipe to easily do actions from a shell
 
 NOTE: the command pipe is write only, responses aren't returned.
 
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -43,6 +43,8 @@
+     "-D_BSD_SOURCE=1"
+     "-D_DEFAULT_SOURCE=1"
+ 
++    "-D_DARWIN_C_SOURCE=1"
++
+     "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}"
+     "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}"
+ 
 diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
 --- a/core/CMakeLists.txt
 +++ b/core/CMakeLists.txt
@@ -259,7 +271,7 @@
 new file mode 100644
 --- /dev/null
 +++ b/core/pipe.c
-@@ -0,0 +1,233 @@
+@@ -0,0 +1,247 @@
 +// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
 +//
 +// This file is part of lighstd.
@@ -410,6 +422,14 @@
 +    jsmn_init(&pipe->client.jsmn_ctx);
 +}
 +
++static mode_t
++lgtd_command_pipe_get_umask(void)
++{
++    mode_t mask = umask(0);
++    umask(mask);
++    return mask;
++}
++
 +bool
 +lgtd_command_pipe_open(const char *path)
 +{
@@ -432,8 +452,14 @@
 +    pipe->fd = -1;
 +
 +    mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    if (mkfifo(path, mode) && errno != EEXIST) {
-+        goto error;
++    if (mkfifo(path, mode)) {
++        if (errno != EEXIST) {
++            goto error;
++        }
++        mode &= ~lgtd_command_pipe_get_umask();
++        if (chmod(path, mode)) {
++            goto error;
++        }
 +    }
 +
 +    pipe->fd = open(path, O_RDONLY|O_NONBLOCK);
@@ -594,10 +620,1342 @@
 +{
 +    bufferevent_write(client->io, buf, bufsz);
 +}
+diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h
+new file mode 100644
+--- /dev/null
++++ b/tests/core/mock_event2.h
+@@ -0,0 +1,109 @@
++#pragma once
++
++#ifndef MOCKED_EVBUFFER_DRAIN
++int
++evbuffer_drain(struct evbuffer *buf, size_t len)
++{
++    (void)buf;
++    (void)len;
++    return 0;
++}
++#endif
++
++#ifndef MOCKED_EVBUFFER_NEW
++struct evbuffer *
++evbuffer_new(void)
++{
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_EVENT_FREE
++void
++evbuffer_free(struct evbuffer *buf)
++{
++    (void)buf;
++}
++#endif
++
++#ifndef MOCKED_EVBUFFER_GET_LENGTH
++size_t
++evbuffer_get_length(const struct evbuffer *buf)
++{
++    (void)buf;
++    return 0;
++}
++#endif
++
++#ifndef MOCKED_EVBUFFER_PULLUP
++unsigned char *
++evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
++{
++    (void)buf;
++    (void)size;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_EVBUFFER_READ
++int
++evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch)
++{
++    (void)buffer;
++    (void)fd;
++    return howmuch;
++}
++#endif
++
++#ifndef MOCKED_EVENT_ADD
++int
++event_add(struct event *ev, const struct timeval *timeout)
++{
++    (void)ev;
++    (void)timeout;
++    return 0;
++}
++#endif
++
++#ifndef MOCKED_EVENT_DEL
++int
++event_del(struct event *ev)
++{
++    (void)ev;
++    return 0;
++}
++#endif
++
++#ifndef MOCKED_EVENT_FREE
++void
++event_free(struct event *ev)
++{
++    (void)ev;
++}
++#endif
++
++#ifndef MOCKED_EVENT_NEW
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    (void)base;
++    (void)fd;
++    (void)events;
++    (void)cb;
++    (void)ctx;
++    return NULL;
++}
++#endif
++
++#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
++int
++evutil_make_socket_nonblocking(evutil_socket_t fd)
++{
++    (void)fd;
++    return 0;
++}
++#endif
+diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/CMakeLists.txt
+@@ -0,0 +1,25 @@
++INCLUDE_DIRECTORIES(
++    ${CMAKE_CURRENT_SOURCE_DIR}
++    ${CMAKE_CURRENT_BINARY_DIR}
++)
++
++ADD_CORE_LIBRARY(
++    test_core_pipe STATIC
++    ${LIGHTSD_SOURCE_DIR}/core/jsmn.c
++    ${LIGHTSD_SOURCE_DIR}/core/log.c
++    ${LIGHTSD_SOURCE_DIR}/core/stats.c
++    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
++    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
++    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
++    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
++    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
++)
++
++FUNCTION(ADD_PIPE_TEST TEST_SOURCE)
++    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_pipe)
++ENDFUNCTION()
++
++FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
++FOREACH(TEST ${TESTS})
++    ADD_PIPE_TEST(${TEST})
++ENDFOREACH()
+diff --git a/tests/core/pipe/test_pipe_close.c b/tests/core/pipe/test_pipe_close.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_close.c
+@@ -0,0 +1,117 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVENT_DEL
++#define MOCKED_EVBUFFER_FREE
++#define MOCKED_EVENT_FREE
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#include "tests_pipe_utils.h"
++
++char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    (void)base;
++    (void)fd;
++    (void)events;
++    (void)cb;
++    (void)ctx;
++
++    return (void *)1;
++}
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    return (void *)2;
++}
++
++static int event_del_call_count = 0;
++
++int
++event_del(struct event *ev)
++{
++    (void)ev;
++
++    event_del_call_count++;
++
++    return 0;
++}
++
++static int evbuffer_free_call_count = 0;
++
++void
++evbuffer_free(struct evbuffer *buf)
++{
++    (void)buf;
++
++    evbuffer_free_call_count++;
++}
++
++static int event_free_call_count = 0;
++
++void
++event_free(struct event *ev)
++{
++    (void)ev;
++
++    event_free_call_count++;
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    int pipe_fd = SLIST_FIRST(&lgtd_command_pipes)->fd;
++
++    lgtd_command_pipe_close_all();
++
++    if (event_del_call_count != 1) {
++        errx(1, "event_del_call_count = %d", event_del_call_count);
++    }
++    if (evbuffer_free_call_count != 1) {
++        errx(1, "evbuffer_free_call_count = %d", evbuffer_free_call_count);
++    }
++    if (event_free_call_count != 1) {
++        errx(1, "event_free_call_count = %d", event_free_call_count);
++    }
++    struct stat sb;
++    if (fstat(pipe_fd, &sb) != -1 && errno != EBADF) {
++        errx(1, "the pipe file descriptor wasn't closed correctly");
++    }
++    if (stat(path, &sb) != -1 && errno != ENOENT) {
++        errx(1, "the pipe wasn't removed correctly");
++    }
++
++    return 0;
++}
+diff --git a/tests/core/pipe/test_pipe_open.c b/tests/core/pipe/test_pipe_open.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_open.c
+@@ -0,0 +1,171 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVENT_ADD
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#define MOCKED_CLIENT_OPEN_FROM_PIPE
++#include "tests_pipe_utils.h"
++
++char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++static bool make_socket_nonblocking_call_count = 0;
++
++int 
++evutil_make_socket_nonblocking(evutil_socket_t fd)
++{
++    if (fd <= 0) {
++        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
++    }
++
++    make_socket_nonblocking_call_count++;
++
++    return 0;
++}
++
++static int event_new_call_count = 0;
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    if (base != lgtd_ev_base) {
++        errx(
++            1, "unexpected lgtd_ev_base = %p (expected %p)",
++            base, lgtd_ev_base
++        );
++    }
++    if (fd <= 0) {
++        errx(1, "got invalid fd %d in event_new", fd);
++    }
++    if (events != (EV_READ|EV_PERSIST)) {
++        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
++    }
++    if (cb != lgtd_command_pipe_read_callback) {
++        errx(1, "the read callback wasn't set correctly");
++    }
++    if (!ctx) {
++        errx(1, "the callback context wasn't set correctly");
++    }
++
++    event_new_call_count++;
++
++    return (void *)1;
++}
++
++static int evbuffer_new_call_count = 0;
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    evbuffer_new_call_count++;
++
++    return (void *)2;
++}
++
++static int event_add_call_count = 0;
++
++int
++event_add(struct event *ev, const struct timeval *timeout)
++{
++    if (ev != (void *)1) {
++        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
++    }
++
++    if (timeout) {
++        errx(1, "a timeout shouldn't have been passed");
++    }
++
++    event_add_call_count++;
++
++    return 0;
++}
++
++static int client_open_from_pipe_call_count = 0;
++
++void
++lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
++{
++    if (!pipe_client) {
++        errx(1, "missing pipe_client");
++    }
++
++    client_open_from_pipe_call_count++;
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    if (make_socket_nonblocking_call_count != 1) {
++        errx(
++            1, "make_socket_nonblocking_call_count = %d",
++            make_socket_nonblocking_call_count
++        );
++    }
++    if (event_new_call_count != 1) {
++        errx(1, "event_new_call_count = %d", event_new_call_count);
++    }
++    if (evbuffer_new_call_count != 1) {
++        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
++    }
++    if (event_add_call_count != 1) {
++        errx(1, "event_add_call_count = %d", event_add_call_count);
++    }
++    if (SLIST_EMPTY(&lgtd_command_pipes)) {
++        errx(1, "the list of command pipes shouldn't be empty");
++    }
++
++    struct stat sb;
++    if (stat(path, &sb)) {
++        errx(1, "can't stat pipe %s", path);
++    }
++
++    mode_t expected_mode;
++    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
++    expected_mode &= ~lgtd_command_pipe_get_umask();
++    if (sb.st_mode != expected_mode) {
++        errx(
++            1, "unexpected mode %o (expected %o)",
++            sb.st_mode, expected_mode
++        );
++    }
++
++    // make sure it's idempotent:
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++    if (event_new_call_count != 1) {
++        errx(1, "event_new_call_count = %d", event_new_call_count);
++    }
++
++    return 0;
++}
+diff --git a/tests/core/pipe/test_pipe_open_fifo_already_exists.c b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
+@@ -0,0 +1,176 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVENT_ADD
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#define MOCKED_CLIENT_OPEN_FROM_PIPE
++#include "tests_pipe_utils.h"
++
++char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++static bool make_socket_nonblocking_call_count = 0;
++
++int 
++evutil_make_socket_nonblocking(evutil_socket_t fd)
++{
++    if (fd <= 0) {
++        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
++    }
++
++    make_socket_nonblocking_call_count++;
++
++    return 0;
++}
++
++static int event_new_call_count = 0;
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    if (base != lgtd_ev_base) {
++        errx(
++            1, "unexpected lgtd_ev_base = %p (expected %p)",
++            base, lgtd_ev_base
++        );
++    }
++    if (fd <= 0) {
++        errx(1, "got invalid fd %d in event_new", fd);
++    }
++    if (events != (EV_READ|EV_PERSIST)) {
++        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
++    }
++    if (cb != lgtd_command_pipe_read_callback) {
++        errx(1, "the read callback wasn't set correctly");
++    }
++    if (!ctx) {
++        errx(1, "the callback context wasn't set correctly");
++    }
++
++    event_new_call_count++;
++
++    return (void *)1;
++}
++
++static int evbuffer_new_call_count = 0;
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    evbuffer_new_call_count++;
++
++    return (void *)2;
++}
++
++static int event_add_call_count = 0;
++
++int
++event_add(struct event *ev, const struct timeval *timeout)
++{
++    if (ev != (void *)1) {
++        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
++    }
++
++    if (timeout) {
++        errx(1, "a timeout shouldn't have been passed");
++    }
++
++    event_add_call_count++;
++
++    return 0;
++}
++
++static int client_open_from_pipe_call_count = 0;
++
++void
++lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
++{
++    if (!pipe_client) {
++        errx(1, "missing pipe_client");
++    }
++
++    client_open_from_pipe_call_count++;
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++
++    if (mkfifo(path, 0)) {
++        errx(1, "can't open fifo");
++    }
++
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    if (make_socket_nonblocking_call_count != 1) {
++        errx(
++            1, "make_socket_nonblocking_call_count = %d",
++            make_socket_nonblocking_call_count
++        );
++    }
++    if (event_new_call_count != 1) {
++        errx(1, "event_new_call_count = %d", event_new_call_count);
++    }
++    if (evbuffer_new_call_count != 1) {
++        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
++    }
++    if (event_add_call_count != 1) {
++        errx(1, "event_add_call_count = %d", event_add_call_count);
++    }
++    if (SLIST_EMPTY(&lgtd_command_pipes)) {
++        errx(1, "the list of command pipes shouldn't be empty");
++    }
++
++    struct stat sb;
++    if (stat(path, &sb)) {
++        errx(1, "can't stat pipe %s", path);
++    }
++
++    mode_t expected_mode;
++    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
++    expected_mode &= ~lgtd_command_pipe_get_umask();
++    if (sb.st_mode != expected_mode) {
++        errx(
++            1, "unexpected mode %o (expected %o)",
++            sb.st_mode, expected_mode
++        );
++    }
++
++    // make sure it's idempotent:
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++    if (event_new_call_count != 1) {
++        errx(1, "event_new_call_count = %d", event_new_call_count);
++    }
++
++    return 0;
++}
+diff --git a/tests/core/pipe/test_pipe_read_callback.c b/tests/core/pipe/test_pipe_read_callback.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_read_callback.c
+@@ -0,0 +1,192 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVBUFFER_READ
++#define MOCKED_EVBUFFER_PULLUP
++#define MOCKED_EVBUFFER_GET_LENGTH
++#define MOCKED_EVBUFFER_DRAIN
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#define MOCKED_JSONRPC_DISPATCH_REQUEST
++#include "tests_pipe_utils.h"
++
++static unsigned char request[] = ("{"
++    "\"jsonrpc\": \"2.0\","
++    "\"method\": \"get_light_state\","
++    "\"params\": [\"*\"],"
++    "\"id\": 42"
++"}");
++
++static char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++static int jsonrpc_dispatch_request_call_count = 0;
++
++void
++lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
++{
++    (void)client;
++    (void)parsed;
++
++    if (!parsed) {
++        errx(1, "number of parsed json tokens not passed in");
++    }
++
++    if (memcmp(client->json, request, sizeof(request))) {
++        errx(1, "got unexpected json");
++    }
++
++    jsonrpc_dispatch_request_call_count++;
++}
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    (void)base;
++    (void)fd;
++    (void)events;
++    (void)cb;
++    (void)ctx;
++
++    return (void *)1;
++}
++
++static int
++get_nbytes_read(int call_count)
++{
++    switch (call_count) {
++    case 0:
++        return sizeof(request) - 1; // we don't return the '\0'
++    default:
++        return 0;
++    }
++}
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    return (void *)2;
++}
++
++static int evbuffer_drain_call_count = 0;
++
++int
++evbuffer_drain(struct evbuffer *buf, size_t len)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    jsmn_parser jsmn_ctx;
++    jsmn_init(&jsmn_ctx);
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
++        errx(1, "the client json parser context wasn't re-initialized");
++    }
++
++
++    switch (evbuffer_drain_call_count) {
++    case 0:
++        if (len != sizeof(request) - 1) {
++            errx(
++                1, "trying to drain %ju bytes (expected %ju)",
++                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
++            );
++        }
++        break;
++    default:
++        break;
++    }
++    evbuffer_drain_call_count++;
++
++    return 0;
++}
++
++static int evbuffer_pullup_call_count = 0;
++
++unsigned char *
++evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    if (size != -1) {
++        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
++    }
++
++    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
++}
++
++static int evbuffer_get_length_call_count = 0;
++
++size_t
++evbuffer_get_length(const struct evbuffer *buf)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    return get_nbytes_read(evbuffer_get_length_call_count++);
++}
++
++static int evbuffer_read_call_count = 0;
++
++int
++evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (fd != pipe->fd) {
++        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
++    }
++
++    if (howmuch != -1) {
++        errx(
++            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
++        );
++    }
++
++    return get_nbytes_read(evbuffer_read_call_count++);
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++
++    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
++
++    return 0;
++}
+diff --git a/tests/core/pipe/test_pipe_read_callback_extra_data.c b/tests/core/pipe/test_pipe_read_callback_extra_data.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_read_callback_extra_data.c
+@@ -0,0 +1,219 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVBUFFER_READ
++#define MOCKED_EVBUFFER_PULLUP
++#define MOCKED_EVBUFFER_GET_LENGTH
++#define MOCKED_EVBUFFER_DRAIN
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#define MOCKED_JSONRPC_DISPATCH_REQUEST
++#include "tests_pipe_utils.h"
++
++#define REQUEST_1 "{"                   \
++    "\"jsonrpc\": \"2.0\","             \
++    "\"method\": \"get_light_state\","  \
++    "\"params\": [\"*\"],"              \
++    "\"id\": 42"                        \
++"}"
++#define EXTRA_DATA "BLUBLBULBUBUHIFESHFUSsoundsaboutright" 
++
++static unsigned char request[] = (
++    REQUEST_1
++    EXTRA_DATA
++);
++
++static char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++static int jsonrpc_dispatch_request_call_count = 0;
++
++void
++lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
++{
++    (void)client;
++    (void)parsed;
++
++    if (!parsed) {
++        errx(1, "number of parsed json tokens not passed in");
++    }
++
++    if (memcmp(client->json, request, sizeof(request))) {
++        errx(1, "got unexpected json");
++    }
++
++    jsonrpc_dispatch_request_call_count++;
++}
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    (void)base;
++    (void)fd;
++    (void)events;
++    (void)cb;
++    (void)ctx;
++
++    return (void *)1;
++}
++
++static int
++get_nbytes_read(int call_count)
++{
++    switch (call_count) {
++    case 0:
++        return sizeof(request) - 1; // we don't return the '\0'
++    default:
++        return 0;
++    }
++}
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    return (void *)2;
++}
++
++static int evbuffer_drain_call_count = 0;
++
++int
++evbuffer_drain(struct evbuffer *buf, size_t len)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    jsmn_parser jsmn_ctx;
++    jsmn_init(&jsmn_ctx);
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
++        errx(1, "the client json parser context wasn't re-initialized");
++    }
++
++    switch (evbuffer_drain_call_count) {
++    case 0:
++        if (len != sizeof(REQUEST_1) - 1) {
++            errx(
++                1, "trying to drain %ju bytes (expected %ju)",
++                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
++            );
++        }
++        break;
++    case 1:
++        if (len != sizeof(request) - sizeof(REQUEST_1)) {
++            errx(
++                1, "trying to drain %ju bytes (expected %ju)",
++                (uintmax_t)len, sizeof(request) - sizeof(REQUEST_1)
++            );
++        }
++        break;
++    default:
++        break;
++    }
++    evbuffer_drain_call_count++;
++
++    return 0;
++}
++
++static int evbuffer_pullup_call_count = 0;
++
++unsigned char *
++evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    if (size != -1) {
++        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
++    }
++
++    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
++}
++
++static int evbuffer_get_length_call_count = 0;
++
++size_t
++evbuffer_get_length(const struct evbuffer *buf)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    size_t len;
++    switch (evbuffer_get_length_call_count) {
++    case 0:
++        len = sizeof(request) - 1;
++        break;
++    case 1:
++        len = sizeof(request) - sizeof(REQUEST_1);
++        break;
++    default:
++        len = 0;
++        break;
++    }
++    evbuffer_get_length_call_count++;
++
++    return len;
++}
++
++static int evbuffer_read_call_count = 0;
++
++int
++evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (fd != pipe->fd) {
++        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
++    }
++
++    if (howmuch != -1) {
++        errx(
++            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
++        );
++    }
++
++    return get_nbytes_read(evbuffer_read_call_count++);
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++
++    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
++
++    return 0;
++}
+diff --git a/tests/core/pipe/test_pipe_read_callback_multiple_requests.c b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
+@@ -0,0 +1,259 @@
++#include "pipe.c"
++
++#include <sys/tree.h>
++#include <endian.h>
++#include <limits.h>
++
++#include "lifx/wire_proto.h"
++
++#define MOCKED_EVENT_NEW
++#define MOCKED_EVBUFFER_NEW
++#define MOCKED_EVBUFFER_READ
++#define MOCKED_EVBUFFER_PULLUP
++#define MOCKED_EVBUFFER_GET_LENGTH
++#define MOCKED_EVBUFFER_DRAIN
++#include "mock_event2.h"
++#include "mock_gateway.h"
++#include "mock_daemon.h"
++
++#include "tests_utils.h"
++#define MOCKED_JSONRPC_DISPATCH_REQUEST
++#include "tests_pipe_utils.h"
++
++#define REQUEST_1 "{"                   \
++    "\"jsonrpc\": \"2.0\","             \
++    "\"method\": \"get_light_state\","  \
++    "\"params\": [\"*\"],"              \
++    "\"id\": 42"                        \
++"}"
++
++#define REQUEST_2 "{"           \
++    "\"jsonrpc\": \"2.0\","     \
++    "\"method\": \"power_on\"," \
++    "\"params\": [\"*\"],"      \
++    "\"id\": 43"                \
++"}"
++
++static unsigned char request[] = (
++    REQUEST_1
++    REQUEST_2
++);
++
++static char *tmpdir = NULL;
++
++void
++cleanup_tmpdir(void)
++{
++    lgtd_tests_remove_temp_dir(tmpdir);
++}
++
++static int jsonrpc_dispatch_request_call_count = 0;
++
++void
++lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
++{
++    (void)client;
++    (void)parsed;
++
++    if (!parsed) {
++        errx(1, "number of parsed json tokens not passed in");
++    }
++
++    switch (jsonrpc_dispatch_request_call_count) {
++    case 0:
++        if (memcmp(client->json, request, sizeof(request) - 1)) {
++            errx(
++                1, "got unexpected json %s (expected %s)",
++                client->json, request
++            );
++        }
++        break;
++    case 1:
++        if (memcmp(client->json, REQUEST_2, sizeof(REQUEST_2) - 1)) {
++            errx(
++                1, "got unexpected json %s (expected %s)",
++                client->json, REQUEST_2
++            );
++        }
++        break;
++    default:
++        errx(
++            1, "jsonrpc_dispatch_request_call_count = %d",
++            jsonrpc_dispatch_request_call_count
++        );
++        break;
++    }
++
++    jsonrpc_dispatch_request_call_count++;
++}
++
++struct event *
++event_new(struct event_base *base,
++          evutil_socket_t fd,
++          short events,
++          event_callback_fn cb,
++          void *ctx)
++{
++    (void)base;
++    (void)fd;
++    (void)events;
++    (void)cb;
++    (void)ctx;
++
++    return (void *)1;
++}
++
++struct evbuffer *
++evbuffer_new(void)
++{
++    return (void *)2;
++}
++
++static int evbuffer_drain_call_count = 0;
++
++int
++evbuffer_drain(struct evbuffer *buf, size_t len)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    jsmn_parser jsmn_ctx;
++    jsmn_init(&jsmn_ctx);
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
++        errx(1, "the client json parser context wasn't re-initialized");
++    }
++
++    switch (evbuffer_drain_call_count) {
++    case 0:
++        if (len != sizeof(REQUEST_1) - 1) {
++            errx(
++                1, "trying to drain %ju bytes (expected %ju)",
++                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_1) - 1
++            );
++        }
++        break;
++    case 1:
++        if (len != sizeof(REQUEST_2) - 1) {
++            errx(
++                1, "trying to drain %ju bytes (expected %ju)",
++                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_2) - 1
++            );
++        }
++        break;
++    default:
++        errx(1, "evbuffer_drain_call_count = %d", evbuffer_drain_call_count);
++        break;
++    }
++    evbuffer_drain_call_count++;
++
++    return 0;
++}
++
++static int evbuffer_pullup_call_count = 0;
++
++unsigned char *
++evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    if (size != -1) {
++        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
++    }
++
++    int offset;
++    switch (evbuffer_pullup_call_count) {
++    case 0:
++        offset = 0;
++        break;
++    case 1:
++        offset = sizeof(REQUEST_1) - 1;
++        break;
++    default:
++        offset = sizeof(request);
++        break;
++    }
++    evbuffer_pullup_call_count++;
++
++    return &request[offset];
++}
++
++static int evbuffer_get_length_call_count = 0;
++
++size_t
++evbuffer_get_length(const struct evbuffer *buf)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    size_t len;
++    switch (evbuffer_get_length_call_count) {
++    case 0:
++        len = sizeof(request) - 1;
++        break;
++    case 1:
++        len = sizeof(request) - sizeof(REQUEST_1);
++        break;
++    default:
++        len = 0;
++        break;
++    }
++    evbuffer_get_length_call_count++;
++
++    return len;
++}
++
++static int evbuffer_read_call_count = 0;
++
++int
++evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
++{
++    if (buf != (void *)2) {
++        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++    if (fd != pipe->fd) {
++        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
++    }
++
++    if (howmuch != -1) {
++        errx(
++            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
++        );
++    }
++
++    int rv = 0;
++    switch (evbuffer_read_call_count) {
++    case 0:
++        rv = sizeof(request) - 1;
++    default:
++        break;
++    }
++    evbuffer_read_call_count++;
++
++    return rv;
++}
++
++int
++main(void)
++{
++    tmpdir = lgtd_tests_make_temp_dir();
++    atexit(cleanup_tmpdir);
++
++    char path[PATH_MAX] = { 0 };
++    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
++    if (!lgtd_command_pipe_open(path)) {
++        errx(1, "couldn't open pipe");
++    }
++
++    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
++
++    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
++
++    return 0;
++}
+diff --git a/tests/core/pipe/tests_pipe_utils.h b/tests/core/pipe/tests_pipe_utils.h
+new file mode 100644
+--- /dev/null
++++ b/tests/core/pipe/tests_pipe_utils.h
+@@ -0,0 +1,19 @@
++#pragma once
++
++#ifndef MOCKED_CLIENT_OPEN_FROM_PIPE
++void
++lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
++{
++    memset(pipe_client, 0, sizeof(*pipe_client));
++    jsmn_init(&pipe_client->jsmn_ctx);
++}
++#endif
++
++#ifndef MOCKED_JSONRPC_DISPATCH_REQUEST
++void
++lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
++{
++    (void)client;
++    (void)parsed;
++}
++#endif
 diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h
 --- a/tests/core/proto/tests_proto_utils.h
 +++ b/tests/core/proto/tests_proto_utils.h
-@@ -16,7 +16,7 @@
+@@ -18,7 +18,7 @@
  void
  lgtd_client_send_response(struct lgtd_client *client, const char *msg)
  {
@@ -606,3 +1964,93 @@
  }
  #endif
  
+diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c
+--- a/tests/core/tests_shims.c
++++ b/tests/core/tests_shims.c
+@@ -24,7 +24,7 @@
+     .verbosity = LGTD_DEBUG
+ };
+ 
+-struct event_base *lgtd_ev_base = NULL;
++struct event_base *lgtd_ev_base = (void *)0x1234;
+ 
+ const char *lgtd_binds = NULL;
+ 
+diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
+--- a/tests/core/tests_utils.c
++++ b/tests/core/tests_utils.c
+@@ -2,13 +2,18 @@
+ #include <sys/tree.h>
+ #include <sys/socket.h>
+ #include <assert.h>
++#include <dirent.h>
+ #include <endian.h>
++#include <err.h>
++#include <limits.h>
+ #include <netinet/in.h>
+ #include <stdarg.h>
+ #include <stdbool.h>
+ #include <stdlib.h>
+ #include <stdint.h>
++#include <stdio.h>
+ #include <string.h>
++#include <unistd.h>
+ 
+ #include <event2/util.h>
+ 
+@@ -112,3 +117,42 @@
+ 
+     return site;
+ }
++
++char *
++lgtd_tests_make_temp_dir(void)
++{
++    char buf[PATH_MAX] = { 0 };
++    int n = snprintf(buf, sizeof(buf), "%slightsd.test.XXXXXXXX", P_tmpdir);
++    if (n >= (int)sizeof(buf)) {
++        errx(1, "cannot allocate temporary directory");
++    }
++    return strdup(mkdtemp(buf));
++}
++
++void
++lgtd_tests_remove_temp_dir(char *path)
++{
++    DIR *tmpdir = opendir(path);
++    if (!tmpdir) {
++        return;
++    }
++
++    struct dirent db;
++    struct dirent *entry = &db;
++    while (!readdir_r(tmpdir, entry, &entry) && entry) {
++        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
++            continue;
++        }
++        char buf[PATH_MAX] = { 0 };
++        snprintf(buf, sizeof(buf), "%s/%s", path, entry->d_name);
++        unlink(buf);
++    }
++
++    closedir(tmpdir);
++
++    if (rmdir(path)) {
++        warn("couldn't remove tempdir %s", path);
++    }
++
++    free(path);
++}
+diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
+--- a/tests/core/tests_utils.h
++++ b/tests/core/tests_utils.h
+@@ -30,6 +30,9 @@
+     return true;
+ }
+ 
++char *lgtd_tests_make_temp_dir(void);
++void lgtd_tests_remove_temp_dir(char *);
++
+ struct lgtd_lifx_gateway *lgtd_tests_insert_mock_gateway(int);
+ struct lgtd_lifx_bulb *lgtd_tests_insert_mock_bulb(struct lgtd_lifx_gateway *, uint64_t);
+ struct lgtd_proto_target_list *lgtd_tests_build_target_list(const char *, ...);
--- a/daemon_module.patch	Sun Aug 02 18:59:24 2015 -0700
+++ b/daemon_module.patch	Sun Aug 02 19:02:55 2015 -0700
@@ -1,5 +1,5 @@
 # HG changeset patch
-# Parent  c4724f39ca4bfe9e0356a7e56e38d7201f614d04
+# Parent  d7a2d37c150198e9b0626332d1cc57c538bd5447
 
 diff --git a/README.rst b/README.rst
 --- a/README.rst
@@ -490,7 +490,7 @@
      SLIST_HEAD_INITIALIZER(&lgtd_command_pipes);
  
  static void
-@@ -166,6 +166,7 @@
+@@ -174,6 +174,7 @@
          return false;
      }
  
@@ -568,8 +568,8 @@
 +    ${LIGHTSD_SOURCE_DIR}/core/log.c
 +    ${LIGHTSD_SOURCE_DIR}/core/stats.c
 +    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
++    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
 +    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
 +    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
 +    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
 +)
@@ -1002,7 +1002,7 @@
 diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
 --- a/tests/core/tests_utils.c
 +++ b/tests/core/tests_utils.c
-@@ -19,6 +19,9 @@
+@@ -24,6 +24,9 @@
  #include "core/jsonrpc.h"
  #include "core/client.h"
  #include "core/proto.h"
@@ -1012,7 +1012,7 @@
  #include "lifx/bulb.h"
  #include "lifx/gateway.h"
  #include "tests_utils.h"
-@@ -26,6 +29,9 @@
+@@ -31,6 +34,9 @@
  struct lgtd_lifx_gateway_list lgtd_lifx_gateways =
      LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways);
  
@@ -1022,7 +1022,7 @@
  struct lgtd_lifx_gateway *
  lgtd_tests_insert_mock_gateway(int id)
  {
-@@ -36,6 +42,8 @@
+@@ -41,6 +47,8 @@
  
      LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
  
@@ -1031,11 +1031,10 @@
      return gw;
  }
  
-@@ -112,3 +120,14 @@
- 
+@@ -118,6 +126,17 @@
      return site;
  }
-+
+ 
 +struct lgtd_listen *
 +lgtd_tests_insert_mock_listener(const char *addr, const char *port)
 +{
@@ -1046,10 +1045,14 @@
 +
 +    return listener;
 +}
++
+ char *
+ lgtd_tests_make_temp_dir(void)
+ {
 diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
 --- a/tests/core/tests_utils.h
 +++ b/tests/core/tests_utils.h
-@@ -37,3 +37,4 @@
+@@ -40,3 +40,4 @@
  struct lgtd_lifx_site *lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *,
                                                  struct lgtd_lifx_gateway *,
                                                  int);
--- a/series	Sun Aug 02 18:59:24 2015 -0700
+++ b/series	Sun Aug 02 19:02:55 2015 -0700
@@ -4,4 +4,3 @@
 add_command_pipe.patch
 fix_usage_and_version.patch
 daemon_module.patch
-testing_command_pipe.patch
--- a/testing_command_pipe.patch	Sun Aug 02 18:59:24 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1485 +0,0 @@
-# HG changeset patch
-# Parent  2781c3dbb72d815271a446c90a08da3aef0936d6
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -43,6 +43,8 @@
-     "-D_BSD_SOURCE=1"
-     "-D_DEFAULT_SOURCE=1"
- 
-+    "-D_DARWIN_C_SOURCE=1"
-+
-     "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}"
-     "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}"
- 
-diff --git a/core/pipe.c b/core/pipe.c
---- a/core/pipe.c
-+++ b/core/pipe.c
-@@ -148,6 +148,14 @@
-     jsmn_init(&pipe->client.jsmn_ctx);
- }
- 
-+static mode_t
-+lgtd_command_pipe_get_umask(void)
-+{
-+    mode_t mask = umask(0);
-+    umask(mask);
-+    return mask;
-+}
-+
- bool
- lgtd_command_pipe_open(const char *path)
- {
-@@ -171,8 +179,14 @@
-     pipe->fd = -1;
- 
-     mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
--    if (mkfifo(path, mode) && errno != EEXIST) {
--        goto error;
-+    if (mkfifo(path, mode)) {
-+        if (errno != EEXIST) {
-+            goto error;
-+        }
-+        mode &= ~lgtd_command_pipe_get_umask();
-+        if (chmod(path, mode)) {
-+            goto error;
-+        }
-     }
- 
-     pipe->fd = open(path, O_RDONLY|O_NONBLOCK);
-diff --git a/tests/core/daemon/CMakeLists.txt b/tests/core/daemon/CMakeLists.txt
---- a/tests/core/daemon/CMakeLists.txt
-+++ b/tests/core/daemon/CMakeLists.txt
-@@ -8,8 +8,8 @@
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
--    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
-diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/mock_event2.h
-@@ -0,0 +1,109 @@
-+#pragma once
-+
-+#ifndef MOCKED_EVBUFFER_DRAIN
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    (void)buf;
-+    (void)len;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_NEW
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_FREE
-+void
-+evbuffer_free(struct evbuffer *buf)
-+{
-+    (void)buf;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_GET_LENGTH
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    (void)buf;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_PULLUP
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    (void)buf;
-+    (void)size;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_READ
-+int
-+evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch)
-+{
-+    (void)buffer;
-+    (void)fd;
-+    return howmuch;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_ADD
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    (void)ev;
-+    (void)timeout;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_DEL
-+int
-+event_del(struct event *ev)
-+{
-+    (void)ev;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_FREE
-+void
-+event_free(struct event *ev)
-+{
-+    (void)ev;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_NEW
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+int
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    (void)fd;
-+    return 0;
-+}
-+#endif
-diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/CMakeLists.txt
-@@ -0,0 +1,25 @@
-+INCLUDE_DIRECTORIES(
-+    ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}
-+)
-+
-+ADD_CORE_LIBRARY(
-+    test_core_pipe STATIC
-+    ${LIGHTSD_SOURCE_DIR}/core/jsmn.c
-+    ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
-+)
-+
-+FUNCTION(ADD_PIPE_TEST TEST_SOURCE)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_pipe)
-+ENDFUNCTION()
-+
-+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-+FOREACH(TEST ${TESTS})
-+    ADD_PIPE_TEST(${TEST})
-+ENDFOREACH()
-diff --git a/tests/core/pipe/test_pipe_close.c b/tests/core/pipe/test_pipe_close.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_close.c
-@@ -0,0 +1,117 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_DEL
-+#define MOCKED_EVBUFFER_FREE
-+#define MOCKED_EVENT_FREE
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int event_del_call_count = 0;
-+
-+int
-+event_del(struct event *ev)
-+{
-+    (void)ev;
-+
-+    event_del_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_free_call_count = 0;
-+
-+void
-+evbuffer_free(struct evbuffer *buf)
-+{
-+    (void)buf;
-+
-+    evbuffer_free_call_count++;
-+}
-+
-+static int event_free_call_count = 0;
-+
-+void
-+event_free(struct event *ev)
-+{
-+    (void)ev;
-+
-+    event_free_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    int pipe_fd = SLIST_FIRST(&lgtd_command_pipes)->fd;
-+
-+    lgtd_command_pipe_close_all();
-+
-+    if (event_del_call_count != 1) {
-+        errx(1, "event_del_call_count = %d", event_del_call_count);
-+    }
-+    if (evbuffer_free_call_count != 1) {
-+        errx(1, "evbuffer_free_call_count = %d", evbuffer_free_call_count);
-+    }
-+    if (event_free_call_count != 1) {
-+        errx(1, "event_free_call_count = %d", event_free_call_count);
-+    }
-+    struct stat sb;
-+    if (fstat(pipe_fd, &sb) != -1 && errno != EBADF) {
-+        errx(1, "the pipe file descriptor wasn't closed correctly");
-+    }
-+    if (stat(path, &sb) != -1 && errno != ENOENT) {
-+        errx(1, "the pipe wasn't removed correctly");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_open.c b/tests/core/pipe/test_pipe_open.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_open.c
-@@ -0,0 +1,171 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_ADD
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_CLIENT_OPEN_FROM_PIPE
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static bool make_socket_nonblocking_call_count = 0;
-+
-+int 
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
-+    }
-+
-+    make_socket_nonblocking_call_count++;
-+
-+    return 0;
-+}
-+
-+static int event_new_call_count = 0;
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    if (base != lgtd_ev_base) {
-+        errx(
-+            1, "unexpected lgtd_ev_base = %p (expected %p)",
-+            base, lgtd_ev_base
-+        );
-+    }
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in event_new", fd);
-+    }
-+    if (events != (EV_READ|EV_PERSIST)) {
-+        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
-+    }
-+    if (cb != lgtd_command_pipe_read_callback) {
-+        errx(1, "the read callback wasn't set correctly");
-+    }
-+    if (!ctx) {
-+        errx(1, "the callback context wasn't set correctly");
-+    }
-+
-+    event_new_call_count++;
-+
-+    return (void *)1;
-+}
-+
-+static int evbuffer_new_call_count = 0;
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    evbuffer_new_call_count++;
-+
-+    return (void *)2;
-+}
-+
-+static int event_add_call_count = 0;
-+
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    if (ev != (void *)1) {
-+        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
-+    }
-+
-+    if (timeout) {
-+        errx(1, "a timeout shouldn't have been passed");
-+    }
-+
-+    event_add_call_count++;
-+
-+    return 0;
-+}
-+
-+static int client_open_from_pipe_call_count = 0;
-+
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    if (!pipe_client) {
-+        errx(1, "missing pipe_client");
-+    }
-+
-+    client_open_from_pipe_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    if (make_socket_nonblocking_call_count != 1) {
-+        errx(
-+            1, "make_socket_nonblocking_call_count = %d",
-+            make_socket_nonblocking_call_count
-+        );
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+    if (evbuffer_new_call_count != 1) {
-+        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
-+    }
-+    if (event_add_call_count != 1) {
-+        errx(1, "event_add_call_count = %d", event_add_call_count);
-+    }
-+    if (SLIST_EMPTY(&lgtd_command_pipes)) {
-+        errx(1, "the list of command pipes shouldn't be empty");
-+    }
-+
-+    struct stat sb;
-+    if (stat(path, &sb)) {
-+        errx(1, "can't stat pipe %s", path);
-+    }
-+
-+    mode_t expected_mode;
-+    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    expected_mode &= ~lgtd_command_pipe_get_umask();
-+    if (sb.st_mode != expected_mode) {
-+        errx(
-+            1, "unexpected mode %o (expected %o)",
-+            sb.st_mode, expected_mode
-+        );
-+    }
-+
-+    // make sure it's idempotent:
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_open_fifo_already_exists.c b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-@@ -0,0 +1,176 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_ADD
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_CLIENT_OPEN_FROM_PIPE
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static bool make_socket_nonblocking_call_count = 0;
-+
-+int 
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
-+    }
-+
-+    make_socket_nonblocking_call_count++;
-+
-+    return 0;
-+}
-+
-+static int event_new_call_count = 0;
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    if (base != lgtd_ev_base) {
-+        errx(
-+            1, "unexpected lgtd_ev_base = %p (expected %p)",
-+            base, lgtd_ev_base
-+        );
-+    }
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in event_new", fd);
-+    }
-+    if (events != (EV_READ|EV_PERSIST)) {
-+        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
-+    }
-+    if (cb != lgtd_command_pipe_read_callback) {
-+        errx(1, "the read callback wasn't set correctly");
-+    }
-+    if (!ctx) {
-+        errx(1, "the callback context wasn't set correctly");
-+    }
-+
-+    event_new_call_count++;
-+
-+    return (void *)1;
-+}
-+
-+static int evbuffer_new_call_count = 0;
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    evbuffer_new_call_count++;
-+
-+    return (void *)2;
-+}
-+
-+static int event_add_call_count = 0;
-+
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    if (ev != (void *)1) {
-+        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
-+    }
-+
-+    if (timeout) {
-+        errx(1, "a timeout shouldn't have been passed");
-+    }
-+
-+    event_add_call_count++;
-+
-+    return 0;
-+}
-+
-+static int client_open_from_pipe_call_count = 0;
-+
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    if (!pipe_client) {
-+        errx(1, "missing pipe_client");
-+    }
-+
-+    client_open_from_pipe_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+
-+    if (mkfifo(path, 0)) {
-+        errx(1, "can't open fifo");
-+    }
-+
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    if (make_socket_nonblocking_call_count != 1) {
-+        errx(
-+            1, "make_socket_nonblocking_call_count = %d",
-+            make_socket_nonblocking_call_count
-+        );
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+    if (evbuffer_new_call_count != 1) {
-+        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
-+    }
-+    if (event_add_call_count != 1) {
-+        errx(1, "event_add_call_count = %d", event_add_call_count);
-+    }
-+    if (SLIST_EMPTY(&lgtd_command_pipes)) {
-+        errx(1, "the list of command pipes shouldn't be empty");
-+    }
-+
-+    struct stat sb;
-+    if (stat(path, &sb)) {
-+        errx(1, "can't stat pipe %s", path);
-+    }
-+
-+    mode_t expected_mode;
-+    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    expected_mode &= ~lgtd_command_pipe_get_umask();
-+    if (sb.st_mode != expected_mode) {
-+        errx(
-+            1, "unexpected mode %o (expected %o)",
-+            sb.st_mode, expected_mode
-+        );
-+    }
-+
-+    // make sure it's idempotent:
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback.c b/tests/core/pipe/test_pipe_read_callback.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback.c
-@@ -0,0 +1,192 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+static unsigned char request[] = ("{"
-+    "\"jsonrpc\": \"2.0\","
-+    "\"method\": \"get_light_state\","
-+    "\"params\": [\"*\"],"
-+    "\"id\": 42"
-+"}");
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    if (memcmp(client->json, request, sizeof(request))) {
-+        errx(1, "got unexpected json");
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+static int
-+get_nbytes_read(int call_count)
-+{
-+    switch (call_count) {
-+    case 0:
-+        return sizeof(request) - 1; // we don't return the '\0'
-+    default:
-+        return 0;
-+    }
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(request) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
-+            );
-+        }
-+        break;
-+    default:
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    return get_nbytes_read(evbuffer_get_length_call_count++);
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    return get_nbytes_read(evbuffer_read_call_count++);
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback_extra_data.c b/tests/core/pipe/test_pipe_read_callback_extra_data.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback_extra_data.c
-@@ -0,0 +1,219 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+#define REQUEST_1 "{"                   \
-+    "\"jsonrpc\": \"2.0\","             \
-+    "\"method\": \"get_light_state\","  \
-+    "\"params\": [\"*\"],"              \
-+    "\"id\": 42"                        \
-+"}"
-+#define EXTRA_DATA "BLUBLBULBUBUHIFESHFUSsoundsaboutright" 
-+
-+static unsigned char request[] = (
-+    REQUEST_1
-+    EXTRA_DATA
-+);
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    if (memcmp(client->json, request, sizeof(request))) {
-+        errx(1, "got unexpected json");
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+static int
-+get_nbytes_read(int call_count)
-+{
-+    switch (call_count) {
-+    case 0:
-+        return sizeof(request) - 1; // we don't return the '\0'
-+    default:
-+        return 0;
-+    }
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(REQUEST_1) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (len != sizeof(request) - sizeof(REQUEST_1)) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, sizeof(request) - sizeof(REQUEST_1)
-+            );
-+        }
-+        break;
-+    default:
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    size_t len;
-+    switch (evbuffer_get_length_call_count) {
-+    case 0:
-+        len = sizeof(request) - 1;
-+        break;
-+    case 1:
-+        len = sizeof(request) - sizeof(REQUEST_1);
-+        break;
-+    default:
-+        len = 0;
-+        break;
-+    }
-+    evbuffer_get_length_call_count++;
-+
-+    return len;
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    return get_nbytes_read(evbuffer_read_call_count++);
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback_multiple_requests.c b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
-@@ -0,0 +1,259 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_daemon.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+#define REQUEST_1 "{"                   \
-+    "\"jsonrpc\": \"2.0\","             \
-+    "\"method\": \"get_light_state\","  \
-+    "\"params\": [\"*\"],"              \
-+    "\"id\": 42"                        \
-+"}"
-+
-+#define REQUEST_2 "{"           \
-+    "\"jsonrpc\": \"2.0\","     \
-+    "\"method\": \"power_on\"," \
-+    "\"params\": [\"*\"],"      \
-+    "\"id\": 43"                \
-+"}"
-+
-+static unsigned char request[] = (
-+    REQUEST_1
-+    REQUEST_2
-+);
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    switch (jsonrpc_dispatch_request_call_count) {
-+    case 0:
-+        if (memcmp(client->json, request, sizeof(request) - 1)) {
-+            errx(
-+                1, "got unexpected json %s (expected %s)",
-+                client->json, request
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (memcmp(client->json, REQUEST_2, sizeof(REQUEST_2) - 1)) {
-+            errx(
-+                1, "got unexpected json %s (expected %s)",
-+                client->json, REQUEST_2
-+            );
-+        }
-+        break;
-+    default:
-+        errx(
-+            1, "jsonrpc_dispatch_request_call_count = %d",
-+            jsonrpc_dispatch_request_call_count
-+        );
-+        break;
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(REQUEST_1) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_1) - 1
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (len != sizeof(REQUEST_2) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_2) - 1
-+            );
-+        }
-+        break;
-+    default:
-+        errx(1, "evbuffer_drain_call_count = %d", evbuffer_drain_call_count);
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    int offset;
-+    switch (evbuffer_pullup_call_count) {
-+    case 0:
-+        offset = 0;
-+        break;
-+    case 1:
-+        offset = sizeof(REQUEST_1) - 1;
-+        break;
-+    default:
-+        offset = sizeof(request);
-+        break;
-+    }
-+    evbuffer_pullup_call_count++;
-+
-+    return &request[offset];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    size_t len;
-+    switch (evbuffer_get_length_call_count) {
-+    case 0:
-+        len = sizeof(request) - 1;
-+        break;
-+    case 1:
-+        len = sizeof(request) - sizeof(REQUEST_1);
-+        break;
-+    default:
-+        len = 0;
-+        break;
-+    }
-+    evbuffer_get_length_call_count++;
-+
-+    return len;
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    int rv = 0;
-+    switch (evbuffer_read_call_count) {
-+    case 0:
-+        rv = sizeof(request) - 1;
-+    default:
-+        break;
-+    }
-+    evbuffer_read_call_count++;
-+
-+    return rv;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/tests_pipe_utils.h b/tests/core/pipe/tests_pipe_utils.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/tests_pipe_utils.h
-@@ -0,0 +1,19 @@
-+#pragma once
-+
-+#ifndef MOCKED_CLIENT_OPEN_FROM_PIPE
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    memset(pipe_client, 0, sizeof(*pipe_client));
-+    jsmn_init(&pipe_client->jsmn_ctx);
-+}
-+#endif
-+
-+#ifndef MOCKED_JSONRPC_DISPATCH_REQUEST
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+}
-+#endif
-diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c
---- a/tests/core/tests_shims.c
-+++ b/tests/core/tests_shims.c
-@@ -24,7 +24,7 @@
-     .verbosity = LGTD_DEBUG
- };
- 
--struct event_base *lgtd_ev_base = NULL;
-+struct event_base *lgtd_ev_base = (void *)0x1234;
- 
- const char *lgtd_binds = NULL;
- 
-diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
---- a/tests/core/tests_utils.c
-+++ b/tests/core/tests_utils.c
-@@ -2,13 +2,18 @@
- #include <sys/tree.h>
- #include <sys/socket.h>
- #include <assert.h>
-+#include <dirent.h>
- #include <endian.h>
-+#include <err.h>
-+#include <limits.h>
- #include <netinet/in.h>
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdlib.h>
- #include <stdint.h>
-+#include <stdio.h>
- #include <string.h>
-+#include <unistd.h>
- 
- #include <event2/util.h>
- 
-@@ -131,3 +136,42 @@
- 
-     return listener;
- }
-+
-+char *
-+lgtd_tests_make_temp_dir(void)
-+{
-+    char buf[PATH_MAX] = { 0 };
-+    int n = snprintf(buf, sizeof(buf), "%slightsd.test.XXXXXXXX", P_tmpdir);
-+    if (n >= (int)sizeof(buf)) {
-+        errx(1, "cannot allocate temporary directory");
-+    }
-+    return strdup(mkdtemp(buf));
-+}
-+
-+void
-+lgtd_tests_remove_temp_dir(char *path)
-+{
-+    DIR *tmpdir = opendir(path);
-+    if (!tmpdir) {
-+        return;
-+    }
-+
-+    struct dirent db;
-+    struct dirent *entry = &db;
-+    while (!readdir_r(tmpdir, entry, &entry) && entry) {
-+        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
-+            continue;
-+        }
-+        char buf[PATH_MAX] = { 0 };
-+        snprintf(buf, sizeof(buf), "%s/%s", path, entry->d_name);
-+        unlink(buf);
-+    }
-+
-+    closedir(tmpdir);
-+
-+    if (rmdir(path)) {
-+        warn("couldn't remove tempdir %s", path);
-+    }
-+
-+    free(path);
-+}
-diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
---- a/tests/core/tests_utils.h
-+++ b/tests/core/tests_utils.h
-@@ -30,6 +30,9 @@
-     return true;
- }
- 
-+char *lgtd_tests_make_temp_dir(void);
-+void lgtd_tests_remove_temp_dir(char *);
-+
- struct lgtd_lifx_gateway *lgtd_tests_insert_mock_gateway(int);
- struct lgtd_lifx_bulb *lgtd_tests_insert_mock_bulb(struct lgtd_lifx_gateway *, uint64_t);
- struct lgtd_proto_target_list *lgtd_tests_build_target_list(const char *, ...);