src/test.c
author Tero Marttila <terom@fixme.fi>
Sun, 03 May 2009 17:19:36 +0300
branchnew-transport
changeset 167 0d2d8ca879d8
parent 129 361740b82fe5
permissions -rw-r--r--
update test.c to reflect new transport stuff
/**
 * The main test code entry point
 */
#include "transport_test.h"
#include "line_proto.h"
#include "irc_queue.h"
#include "irc_conn.h"
#include "irc_net.h"
#include "fifo.h"
#include "log.h"
#include "str.h"
#include "error.h"

#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <assert.h>
#include <ctype.h>

#define DUMP_STR_BUF 1024
#define DUMP_STR_COUNT 8
#define DUMP_STR_TAIL 10

/**
 * Global test-running state
 */
struct test_ctx {
    /** The event_base that we have setup */
    struct event_base *ev_base;

} _test_ctx;


/**
 * This re-formats the given string to escape values, and returns a pointer to an internal static buffer.
 *
 * If len is given as >= 0, only the given number of chars will be dumped from str.
 *
 * The buffer cycles a bit, so the returned pointers remain valid across DUMP_STR_COUNT calls.
 *
 * The resulting string is truncated to (DUMP_STR_BUF - DUMP_STR_TAIL) bytes, not including the ending "...'\0".
 *
 * @param str the string to dump, should be NUL-terminated unless len is given
 * @param len if negative, ignored, otherwise, only this many bytes are dumped from str
 * @param return a pointer to a static buffer that remains valid across DUMP_STR_COUNT calls to this function
 */
const char *dump_strn (const char *str, ssize_t len)
{
    static char dump_buf[DUMP_STR_COUNT][DUMP_STR_BUF];
    static size_t dump_idx = 0;
    
    // pick a buffer to use
    char *buf = dump_buf[dump_idx++];
    
    // cycle
    if (dump_idx >= DUMP_STR_COUNT)
        dump_idx = 0;
    
    str_quote(buf, DUMP_STR_BUF, str, len);

    // ok
    return buf;
}

const char *dump_str (const char *str) 
{
    return dump_strn(str, -1);
}

void assert_null (const void *ptr)
{
    if (ptr)
        FATAL("%p != NULL", ptr);
}

void assert_strcmp (const char *is, const char *should_be)
{
    if (!is || strcmp(is, should_be))
        FATAL("%s != %s", dump_str(is), dump_str(should_be));
}

void assert_strncmp (const char *is, const char *should_be, size_t n)
{   
    if (!is || strncmp(is, should_be, n))
        FATAL("%s:%u != %s", dump_strn(is, n), (unsigned) n, dump_strn(should_be, n));
}

void assert_strlen (const char *str, size_t n)
{
    if (!str || strlen(str) != n)
        FATAL("strlen(%s) != %u", dump_str(str), (unsigned) n);
}

void assert_strnull (const char *str)
{
    if (str != NULL)
        FATAL("%s != NULL", dump_str(str));
}

void assert_success (err_t err)
{
    if (err != SUCCESS)
        FATAL("error: %s", error_name(err));
}

void assert_err (err_t err, err_t target)
{
    if (err != target)
        FATAL("error: <%s> != <%s>", error_name(err), error_name(target));
}

void assert_error_info (struct error_info *is, struct error_info *should_be)
{
    if (ERROR_CODE(is) != ERROR_CODE(should_be) || ERROR_EXTRA(is) != ERROR_EXTRA(should_be))
        FATAL("error: <%s> != <%s>", error_msg(is), error_msg(should_be));
}

void assert_transport_read (transport_t *transport, const char *str)
{
    size_t len = strlen(str);
    char buf[len];
    error_t err;

    log_debug("read: %p: %s", transport, dump_str(str));
    
    // read it
    assert(transport_read(transport, buf, len, &err) == (int) len);

    // cmp
    assert_strncmp(buf, str, len);
}

void assert_transport_write (transport_t *transport, const char *str)
{
    size_t len = strlen(str);
    error_t err;

    log_debug("write: %p: %s", transport, dump_str(str));

    // write it
    assert(transport_write(transport, str, len, &err) == (int) len);
}

void assert_transport_eof (transport_t *transport)
{
    char buf;
    error_t err;

    log_debug("eof: %p", transport);

    assert_err(-transport_read(transport, &buf, 1, &err), ERR_EOF);
}

void assert_transport_data (struct transport_test *tp, const char *fmt, ...)
{
    char buf[TRANSPORT_TEST_FMT_MAX];
    va_list vargs;
    size_t len;
    
    va_start(vargs, fmt);

    if ((len = vsnprintf(buf, TRANSPORT_TEST_FMT_MAX, fmt, vargs)) >= TRANSPORT_TEST_FMT_MAX)
        FATAL("input too long: %zu bytes", len);

    va_end(vargs);

    // get the data out
    char *out;
    
    transport_test_pull_buf(tp, &out, &len);
    
    log_debug("pull_buf: %s", dump_strn(out, len));
    
    // should be the same
    assert_strncmp(out, buf, len);
    assert_strlen(buf, len);

    // cleanup
    free(out);
}

/**
 * Setup the global sock_stream state
 */
struct event_base* setup_sock (void)
{
    struct event_base *ev_base;
    struct error_info err;

    assert((ev_base = event_base_new()));
    assert_success(sock_init(ev_base, &err));

    return ev_base;
}

/**
 * Create an empty transport_test
 */
struct transport_test* setup_transport_test ()
{
    struct transport_test *tp;
   
    assert ((tp = transport_test_create(NULL)) != NULL);

    return tp;
}

void assert_str_quote (size_t buf_size, const char *data, ssize_t len, const char *target, size_t out)
{
    char buf[buf_size];
    
    size_t ret = str_quote(buf, buf_size, data, len);

    log_debug("str_quote(%zu, %zd) -> %s:%zu / %s:%zu", buf_size, len, buf, ret, target, out);

    assert_strcmp(buf, target);
    assert(ret == out);
}

void test_str_quote (void)
{
    log_info("testing str_quote()");

    assert_str_quote(5,     NULL,           -1,     "NULL",         4   );
    assert_str_quote(16,    "foo",          -1,     "'foo'",        5   );
    assert_str_quote(16,    "foobar",       3,      "'foo'",        5   );
    assert_str_quote(16,    "\r\n",         -1,     "'\\r\\n'",     6   );
    assert_str_quote(16,    "\x13",         -1,     "'\\x13'",      6   );
    assert_str_quote(16,    "x'y",          -1,     "'x\\'y'",      6   );
    assert_str_quote(7,     "1234567890",   -1,     "'1'...",       12  );
    assert_str_quote(9,     "1234567890",   -1,     "'123'...",     12  );
}

struct str_format_ctx {
    const char *name;

    const char *value;
};

err_t test_str_format_cb (const char *name, const char **value, ssize_t *value_len, void *arg)
{
    struct str_format_ctx *ctx = arg;

    assert_strcmp(name, ctx->name);

    *value = ctx->value;
    *value_len = -1;

    return SUCCESS;
}

void assert_str_format (const char *format, const char *name, const char *value, const char *out)
{
    struct str_format_ctx ctx = { name, value };
    char buf[512];

    assert_success(str_format(buf, sizeof(buf), format, test_str_format_cb, &ctx));
    
    log_debug("str_format(%s), { %s:%s } -> %s / %s", format, name, value, buf, out);

    assert_strcmp(buf, out);
}

void test_str_format (void)
{
    log_info("test str_format()");

    assert_str_format("foo", NULL, NULL, "foo");
    assert_str_format("foo {bar} quux", "bar", "XXX", "foo XXX quux");
}

void test_dump_str (void)
{
    log_info("dumping example strings on stdout:");

    log_debug("normal: %s", dump_str("Hello World"));
    log_debug("escapes: %s", dump_str("foo\r\nbar\a\001"));
    log_debug("length: %s", dump_strn("<-->**", 4));
    log_debug("overflow: %s", dump_str( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
    log_debug("null: %s", dump_str(NULL));
    log_debug("quote: %s", dump_str("foo\\bar'quux"));
}

void test_transport_test (void)
{
    struct transport_info info = { NULL, NULL, 0 };
    struct transport_test *tp = transport_test_create(&info);
    transport_t *transport = transport_test_cast(tp);

    // put the read data
    log_info("test transport_test_push_*");
    transport_test_push_buf(tp, "foo", 3);
    transport_test_push_str(tp, "barx");
    transport_test_push_fmt(tp, "xx %s xx", "quux");
    transport_test_push_eof(tp);
    
    // read it out
    log_info("test transport_test_read");

    assert_transport_read(transport, "foo");
    assert_transport_read(transport, "ba");
    assert_transport_read(transport, "rx");
    assert_transport_read(transport, "xx quux xx");
    assert_transport_eof(transport);

    // write some data in
    log_info("test transport_test_write");

    assert_transport_write(transport, "test ");
    assert_transport_write(transport, "data");
    
    // check output
    log_info("test transport_test_pull_*");

    assert_transport_data(tp, "test data");
    assert_transport_data(tp, "");

    // cleanup
    transport_test_destroy(tp);
}

void assert_read_line (struct line_proto *lp, const char *line_str)
{
    char *line_buf;
    
    log_debug("expect: %s", dump_str(line_str));

    assert_success(line_proto_recv(lp, &line_buf));

    if (line_str) {
        assert_strcmp(line_buf, line_str);

    } else {
        assert_strnull(line_buf);

    }
}

/**
 * Context info for test_line_proto callbacks
 */
struct _lp_test_ctx {
    /** Expected line */
    const char *line;

    /** Expected error */
    struct error_info err;
};

static void _lp_on_line (char *line, void *arg)
{
    struct _lp_test_ctx *ctx = arg;

    log_debug("%s", dump_str(line));

    assert_strcmp(line, ctx->line);

    ctx->line = NULL;
}

static void _lp_on_error (struct error_info *err, void *arg)
{
    struct _lp_test_ctx *ctx = arg;

    assert_error_info(err, &ctx->err);
}

static struct line_proto_callbacks _lp_callbacks = {
    .on_line        = &_lp_on_line,
    .on_error       = &_lp_on_error,
};

void test_line_proto (void)
{
    struct transport_test *tp = transport_test_create(NULL);
    transport_t *transport = transport_test_cast(tp);
    struct line_proto *lp;
    struct _lp_test_ctx ctx;
    struct error_info err;
    
    // put the read data
    log_debug("transport_test_push_*");
    transport_test_push_str(tp, "hello\r\n");
    transport_test_push_str(tp, "world\n");
    transport_test_push_str(tp, "this ");
    transport_test_push_str(tp, "is a line\r");
    transport_test_push_str(tp, "\nfragment");

    // create the lp
    assert_success(line_proto_create(&lp, transport, 128, &_lp_callbacks, &ctx, &err));
    
    log_info("test line_proto_recv");

    // then read some lines from it
    assert_read_line(lp, "hello");
    assert_read_line(lp, "world");
    assert_read_line(lp, "this is a line");
    assert_read_line(lp, NULL);

    // then add a final bit to trigger on_line
    log_info("test on_line");

    ctx.line = "fragment";
    transport_test_push_str(tp, "\r\n");
    assert_strnull(ctx.line);

    // test writing
    log_info("test line_proto_send");
    assert_success(-line_proto_send(lp, "foobar\r\n"));
    assert_success(-line_proto_send(lp, "quux\r\n"));
    assert_transport_data(tp, "foobar\r\nquux\r\n");

    // XXX: test partial writes

    // cleanup
    line_proto_destroy(lp);
}

void test_irc_queue (void)
{
    struct transport_test *tp = transport_test_create(NULL);
    transport_t *transport = transport_test_cast(tp);
    struct line_proto *lp;
    struct irc_queue *queue;
    struct irc_queue_entry *queue_entry;
    struct error_info err;

    // create the lp
    assert_success(line_proto_create(&lp, transport, 128, &_lp_callbacks, NULL, &err));

    // create the queue
    assert_success(irc_queue_create(&queue, _test_ctx.ev_base, lp, &err));

    struct irc_line line = {
        NULL, "TEST", { "fooX" }
    };

    // then test simple writes, we should be able to push five lines directly
    log_info("test irc_queue_process (irc_queue_send_direct)");
    line.args[0] = "foo0"; assert_success(irc_queue_process(queue, &line));
    line.args[0] = "foo1"; assert_success(irc_queue_process(queue, &line));
    line.args[0] = "foo2"; assert_success(irc_queue_process(queue, &line));
    line.args[0] = "foo3"; assert_success(irc_queue_process(queue, &line));
    line.args[0] = "foo4"; assert_success(irc_queue_process(queue, &line));

    // they should all be output
    assert_transport_data(tp,
            "TEST foo0\r\n"
            "TEST foo1\r\n"
            "TEST foo2\r\n"
            "TEST foo3\r\n"
            "TEST foo4\r\n"
    );

    // then enqueue
    log_info("test irc_queue_process (irc_queue_put)");
    line.args[0] = "foo5"; assert_success(irc_queue_process(queue, &line));

    // ensure it was enqueued
    assert((queue_entry = TAILQ_FIRST(&queue->list)) != NULL);
    assert_strcmp(queue_entry->line_buf, "TEST foo5\r\n");

    // ensure timer is set
    assert(event_pending(queue->ev, EV_TIMEOUT, NULL));

    // run the event loop to let the timer run
    log_info("running the event loop once...");
    assert(event_base_loop(_test_ctx.ev_base, EVLOOP_ONCE) == 0);

    // test to check that the line was now sent
    log_info("checking that the delayed line was sent...");
    assert_transport_data(tp, "TEST foo5\r\n");
    assert(TAILQ_EMPTY(&queue->list));
    assert(!event_pending(queue->ev, EV_TIMEOUT, NULL));

    // cleanup
    irc_queue_destroy(queue);
}

struct test_conn_ctx {
    /** Callback flags */
    bool on_registered, on_TEST, on_error, on_quit;
};

static void _conn_on_registered (struct irc_conn *conn, void *arg)
{
    struct test_conn_ctx *ctx = arg;

    (void) conn;

    if (ctx) ctx->on_registered = true;

    log_debug("registered");
}

static void _conn_on_error (struct irc_conn *conn, struct error_info *err, void *arg)
{
    struct test_conn_ctx *ctx = arg;
    
    (void) conn;
    (void) err;

    if (ctx) ctx->on_error = true;

    log_debug("on_error");
}

static void _conn_on_quit (struct irc_conn *conn, void *arg)
{
    struct test_conn_ctx *ctx = arg;

    (void) conn;

    if (ctx) ctx->on_quit = true;

    log_debug("on_quit");
}

static void _conn_on_TEST (const struct irc_line *line, void *arg)
{
    struct test_conn_ctx *ctx = arg;

    assert(line->source);
    assert(!line->source->nickname && !line->source->username && line->source->hostname);
    assert_strcmp(line->command, "TEST");
    assert_strcmp(line->args[0], "arg0");
    assert_strnull(line->args[1]);

    if (ctx) ctx->on_TEST = true;

    log_debug("on_TEST");
}

static struct irc_conn_callbacks _conn_callbacks = {
    .on_registered      = &_conn_on_registered,
    .on_error           = &_conn_on_error,
    .on_quit            = &_conn_on_quit,
};

static struct irc_cmd_handler _conn_handlers[] = {
    {   "TEST",         &_conn_on_TEST  },
    {   NULL,           NULL            }
};

/**
 * Create and return a new irc_conn with the given ctx (will be initialized to zero).
 */
struct irc_conn* setup_irc_conn (struct transport_test *tp, bool noisy, struct test_conn_ctx *ctx)
{
    struct irc_conn *conn;
    struct error_info err;
    struct irc_conn_register_info register_info = {
        "nick", "user", "realname"
    };

    // init the ctx
    memset(ctx, 0, sizeof(*ctx));

    // create the irc_conn
    assert_success(irc_conn_create(&conn, transport_test_cast(tp), &_conn_callbacks, ctx, &err));

    // test register
    if (noisy) log_info("test irc_conn_register");
    assert_success(irc_conn_register(conn, &register_info));
    assert_transport_data(tp, "NICK nick\r\nUSER user 0 * realname\r\n");
 
    // test on_register callback    
    if (noisy) log_info("test irc_conn_callbacks.on_register");
    transport_test_push_str(tp, "001 mynick :Blaa blaa blaa\r\n");
    if (ctx) assert(ctx->on_registered);
    assert_strcmp(conn->nickname, "mynick");
   
    // ok
    return conn;
}

void test_irc_conn (void)
{
    struct test_conn_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_conn *conn = setup_irc_conn(tp, true, &ctx);

    // add our test handlers
    assert_success(irc_conn_add_cmd_handlers(conn, _conn_handlers, &ctx));

    // test on_TEST handler
    // XXX: come up with a better prefix
    log_info("test irc_conn.handlers");
    transport_test_push_str(tp, ":foobar-prefix TEST arg0\r\n");
    assert(ctx.on_TEST);

    // test PING/PONG
    log_info("test PING/PONG");
    transport_test_push_str(tp, "PING foo\r\n");
    assert_transport_data(tp, "PONG foo\r\n");

    // quit nicely
    log_info("test QUIT");
    assert_success(irc_conn_QUIT(conn, "bye now"));
    assert_transport_data(tp, "QUIT :bye now\r\n");
    assert(conn->quitting);

    transport_test_push_str(tp, "ERROR :Closing Link: Quit\r\n");
    transport_test_push_eof(tp);
    assert(conn->quit && !conn->quitting && !conn->registered);
    assert(ctx.on_quit);
    assert(!ctx.on_error);

    // destroy it
    irc_conn_destroy(conn);
}

void test_irc_conn_self_nick (void)
{
    struct test_conn_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_conn *conn = setup_irc_conn(tp, false, &ctx);
    
    log_info("test irc_conn_on_NICK");
    transport_test_push_fmt(tp, ":mynick!user@somehost NICK mynick2\r\n");
    assert_strcmp(conn->nickname, "mynick2");

    // cleanup
    irc_conn_destroy(conn);
}

struct test_chan_ctx {
    /** The channel name */
    const char *channel;

    /** The channel we're supposed to be testing */
    struct irc_chan *chan;
    
    /** Flags for callbacks called*/
    bool on_chan_self_join, on_chan_self_part, on_chan_join, on_chan_part;

};

void _on_chan_self_join (struct irc_chan *chan, void *arg)
{
    struct test_chan_ctx *ctx = arg;

    assert(chan == ctx->chan);
    
    ctx->on_chan_self_join = true;

    log_debug("on_self_join");
}

void _on_chan_join (struct irc_chan *chan, const struct irc_nm *source, void *arg)
{
    struct test_chan_ctx *ctx = arg;

    assert(chan == ctx->chan);

    // XXX: verify source

    ctx->on_chan_join = true;

    log_debug("on_join");
}

void _on_chan_part (struct irc_chan *chan, const struct irc_nm *source, const char *msg, void *arg)
{
    struct test_chan_ctx *ctx = arg;

    assert(chan == ctx->chan);

    // XXX: verify source
    // XXX: verify msg

    ctx->on_chan_part = true;

    log_debug("on_part");
}


struct irc_chan_callbacks _chan_callbacks = {
    .on_self_join       = &_on_chan_self_join,
    .on_join            = &_on_chan_join,
    .on_part            = &_on_chan_part,
};

/**
 * Setup an irc_net using the given socket, and consume the register request output, but do not push the RPL_WELCOME
 */
struct irc_net* setup_irc_net_unregistered (struct transport_test *tp)
{
    struct irc_net *net;
    struct irc_net_info net_info = {
        .register_info = {
            "nick", "user", "realname"
        },
    };
    struct error_info err;

    // create the irc_net
    net_info.transport = transport_test_cast(tp);
    assert_success(irc_net_create(&net, &net_info, &err));

    // test register output
    assert_transport_data(tp, "NICK nick\r\nUSER user 0 * realname\r\n");
    
    // ok
    return net; 
}

/**
 * Push to RPL_WELCOME reply, and test state
 */
void do_irc_net_welcome (struct transport_test *tp, struct irc_net *net)
{
    // registration reply
    transport_test_push_fmt(tp, "001 mynick :Blaa blaa blaa\r\n");
    assert(net->conn->registered);
    assert_strcmp(net->conn->nickname, "mynick");

}

/**
 * Creates an irc_net and puts it into the registered state
 */
struct irc_net* setup_irc_net (struct transport_test *tp)
{
    struct irc_net *net;   

    net = setup_irc_net_unregistered(tp);
    do_irc_net_welcome(tp, net);
    
    // ok
    return net;
}

/**
 * General test for irc_net to handle startup
 */
void test_irc_net (void)
{
    struct transport_test *tp = setup_transport_test();
    
    // create the network
    log_info("test irc_net_create");
    struct irc_net *net = setup_irc_net_unregistered(tp);

    // send the registration reply
    log_info("test irc_conn_on_RPL_WELCOME");
    do_irc_net_welcome(tp, net);

    // test errors by setting EOF
    log_info("test irc_net_error");
    transport_test_push_eof(tp);
    assert(net->conn == NULL);

    // cleanup
    irc_net_destroy(net);
}

/**
 * Ensure that an irc_chan_user exists/doesn't exist for the given channel/nickname, and return it
 */
struct irc_chan_user* check_chan_user (struct irc_chan *chan, const char *nickname, bool exists)
{
    struct irc_chan_user *chan_user = irc_chan_get_user(chan, nickname);
    
    if (exists && chan_user == NULL)
        FATAL("user %s not found in channel %s", dump_str(nickname), dump_str(irc_chan_name(chan)));
    
    if (!exists && chan_user)
        FATAL("user %s should not be on channel %s anymore", dump_str(nickname), dump_str(irc_chan_name(chan)));

    log_debug("%s, exists=%d -> %p: user=%p, nickname=%s", 
            nickname, exists, chan_user, chan_user ? chan_user->user : NULL, chan_user ? chan_user->user->nickname : NULL);
    
    if (chan_user)
        assert_strcmp(chan_user->user->nickname, nickname);

    return chan_user;
}

/**
 * Creates an irc_chan on the given irc_net, but does not check any output (useful for testing offline add).
 *
 * You must pass a test_chan_ctx for use with later operations, this will be initialized for you.
 */
struct irc_chan* setup_irc_chan_raw (struct irc_net *net, const char *channel, struct test_chan_ctx *ctx)
{
    struct irc_chan *chan;
    struct irc_chan_info chan_info = {
        .channel        = channel,
    };
    struct error_info err;
    
    // initialize the given ctx
    memset(ctx, 0, sizeof(*ctx));
    ctx->channel = channel;

    // add a channel
    assert_success(irc_net_add_chan(net, &chan, &chan_info, &err));
    assert(!chan->joined);
    assert_success(irc_chan_add_callbacks(chan, &_chan_callbacks, ctx));
    ctx->chan = chan;
    
    // ok
    return chan;
}

/**
 * Checks that the JOIN request for a channel was sent, and sends the basic JOIN reply
 */
void do_irc_chan_join (struct transport_test *tp, struct test_chan_ctx *ctx)
{
    // JOIN request
    assert(ctx->chan->joining);
    assert_transport_data(tp, "JOIN %s\r\n", ctx->channel);

    // JOIN reply
    transport_test_push_fmt(tp, ":mynick!user@host JOIN %s\r\n", ctx->channel);
    assert(!ctx->chan->joining && ctx->chan->joined);
    assert(ctx->on_chan_self_join);
}

/**
 * Sends a short RPL_NAMREPLY/RPL_ENDOFNAMES reply and checks that the users list matches
 */
void do_irc_chan_namreply (struct transport_test *tp, struct test_chan_ctx *ctx)
{
    // RPL_NAMREPLY
    transport_test_push_fmt(tp, "353 mynick = %s :mynick userA +userB @userC\r\n", ctx->channel);
    transport_test_push_fmt(tp, "353 mynick = %s :trailingspace \r\n", ctx->channel);
    transport_test_push_fmt(tp, "366 mynick %s :End of NAMES\r\n", ctx->channel);
    
    // XXX: this should be an exclusive test, i.e. these should be the only ones...
    check_chan_user(ctx->chan, "mynick", true);
    check_chan_user(ctx->chan, "userA", true);
    check_chan_user(ctx->chan, "userB", true);
    check_chan_user(ctx->chan, "userC", true);
}

/**
 * Creates an irc_chan on the given irc_net, and checks up to the JOIN reply
 */
struct irc_chan* setup_irc_chan_join (struct transport_test *tp, struct irc_net *net, const char *channel, struct test_chan_ctx *ctx)
{
    setup_irc_chan_raw(net, channel, ctx);
    do_irc_chan_join(tp, ctx);

    // ok
    return ctx->chan;
}

/**
 * Creates an irc_chan on the given irc_net, sends the JOIN stuff plus RPL_NAMREPLY
 */
struct irc_chan* setup_irc_chan (struct transport_test *tp, struct irc_net *net, const char *channel, struct test_chan_ctx *ctx)
{
    setup_irc_chan_raw(net, channel, ctx);
    do_irc_chan_join(tp, ctx);
    do_irc_chan_namreply(tp, ctx);

    // ok
    return ctx->chan;
}


/**
 * Call irc_net_add_chan while offline, and ensure that we send the JOIN request after RPL_WELCOME, and handle the join
 * reply OK.
 */
void test_irc_chan_add_offline (void)
{
    struct test_chan_ctx ctx;

    struct transport_test *tp = setup_transport_test();

    log_info("test irc_net_create");
    struct irc_net *net = setup_irc_net_unregistered(tp);

    // add an offline channel
    log_info("test offline irc_net_add_chan");
    struct irc_chan *chan = setup_irc_chan_raw(net, "#test", &ctx);
    assert(!chan->joining && !chan->joined);

    // send the registration reply
    log_info("test irc_conn_on_RPL_WELCOME");
    do_irc_net_welcome(tp, net);
    
    // test the join sequence
    log_info("test irc_chan_join/irc_chan_on_JOIN");
    do_irc_chan_join(tp, &ctx);
    
    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_namreply (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    setup_irc_chan_join(tp, net, "#test", &ctx);

    log_info("test irc_chan_on_RPL_NAMREPLY");
    do_irc_chan_namreply(tp, &ctx);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_user_join (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // have a user join
    log_info("test irc_chan_on_JOIN");
    transport_test_push_fmt(tp, ":newuser!someone@somewhere JOIN %s\r\n", "#test");
    assert(ctx.on_chan_join);
    check_chan_user(chan, "newuser", true);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_user_part (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // have a user join
    log_info("test irc_chan_on_PART");
    transport_test_push_fmt(tp, ":userA!someone@somewhere PART %s\r\n", "#test");
    assert(ctx.on_chan_part); ctx.on_chan_part = NULL;
    check_chan_user(chan, "userA", false);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_user_kick (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // kick a user
    log_info("test irc_chan_on_KICK (other)");
    transport_test_push_fmt(tp, ":userA!someone@somewhere KICK %s userB\r\n", "#test");
    check_chan_user(chan, "userA", true);
    check_chan_user(chan, "userB", false);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_self_kick (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // kick a user
    log_info("test irc_chan_on_KICK (self)");
    transport_test_push_fmt(tp, ":userA!someone@somewhere KICK %s mynick foobar\r\n", "#test");
    assert(!chan->joined);
    assert(chan->kicked);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_user_nick (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // rename one of the users
    log_info("test irc_net_on_chanuser");
    transport_test_push_fmt(tp, ":userA!someone@somewhere NICK userA2\r\n");
    check_chan_user(chan, "userA", false);
    check_chan_user(chan, "userB", true);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_user_quit (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // rename one of the users
    log_info("test irc_net_on_chanuser");
    transport_test_push_fmt(tp, ":userA!someone@somewhere QUIT foo\r\n");
    check_chan_user(chan, "userA", false);

    // cleanup
    irc_net_destroy(net);
}

void _test_irc_chan_on_CTCP_ACTION (const struct irc_line *line, void *arg)
{
    bool *flag = arg;

    log_debug("CTCP ACTION");

    *flag = true;
}

static struct irc_cmd_handler _test_irc_chan_handlers[] = {
    {   "CTCP ACTION",      &_test_irc_chan_on_CTCP_ACTION  },
    {   NULL,               NULL                            }
};

void test_irc_chan_CTCP_ACTION (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);
    bool cb_ok = false;

    // add our handler
    assert_success(irc_cmd_add(&chan->handlers, _test_irc_chan_handlers, &cb_ok));

    // rename one of the users
    log_info("test irc_conn_on_CTCP_ACTION");
    transport_test_push_fmt(tp, ":userA!someone@somewhere PRIVMSG #test \001ACTION hello  world\001\r\n");
    assert(cb_ok);

    // cleanup
    irc_net_destroy(net);
}

void test_irc_chan_privmsg (void)
{
    struct test_chan_ctx ctx;
    struct transport_test *tp = setup_transport_test();
    struct irc_net *net = setup_irc_net(tp);
    struct irc_chan *chan = setup_irc_chan(tp, net, "#test", &ctx);

    // rename one of the users
    log_info("test irc_chan_PRIVMSG");
    assert_success(irc_chan_PRIVMSG(chan, "foobar quux"));
    assert_transport_data(tp, "PRIVMSG #test :foobar quux\r\n");

    // cleanup
    irc_net_destroy(net);
}

// XXX: needs to be split off into its own test_fifo.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
struct test_fifo_ctx {
    /** Path to the fifo */
    const char *path;

    /** The write end */
    int fd;

    /** callback invoked? */
    bool on_read;

    /** Still running? */
    bool run;
};

/**
 * Open the FIFO and write the test string to it
 */
static void test_fifo_open_write (struct test_fifo_ctx *ctx)
{
    // ...raw, for writing
    if ((ctx->fd = open(ctx->path, O_WRONLY)) < 0)
        FATAL_PERROR("open");

    // write something into it
    if (write(ctx->fd, "test", 4) != 4)
        FATAL_PERROR("write");
 
}

static void test_fifo_close (struct test_fifo_ctx *ctx)
{
    close(ctx->fd);
    ctx->fd = -1;
}

static void test_fifo_on_read (transport_t *fifo, void *arg)
{
    int ret;
    char buf[16];
    struct test_fifo_ctx *ctx = arg;
    error_t err;

    // read it back out
    log_info("test fifo_read");
    if ((ret = transport_read(fifo, buf, 16, &err)) < 0)
        assert_success(-ret);

    assert(ret == 4);
    assert_strncmp(buf, "test", 4);
 
    if (ctx->on_read) {
        test_fifo_close(ctx);
        ctx->run = false;
        return;
    }
   
    // re-open the fifo
    log_info("test fifo-re-open");
    test_fifo_close(ctx);
    test_fifo_open_write(ctx);

    ctx->on_read = true;
}

static struct transport_callbacks test_fifo_callbacks = {
    .on_read = test_fifo_on_read,
};

void test_fifo (void)
{
    transport_t *fifo;
    struct error_info err;
    struct test_fifo_ctx _ctx, *ctx = &_ctx; memset(ctx, 0, sizeof(*ctx));
    struct transport_info info = { &test_fifo_callbacks, ctx, TRANSPORT_READ };

    // XXX: requires that this be run in a suitable CWD
    ctx->path = "test.fifo";

    // create the fifo
    if ((mkfifo(ctx->path, 0600) < 0) && (errno != EEXIST))
        FATAL_PERROR("mkfifo");

    // open it
    log_info("test fifo_open_read");
    assert_success(fifo_open_read(&info, &fifo, _test_ctx.ev_base, ctx->path, &err));
    
    // put some data into it
    test_fifo_open_write(ctx);
   
    // run the event loop
    log_debug("running the event loop...");
    ctx->run = true;

    while (ctx->run)
        assert(event_base_loop(_test_ctx.ev_base, EVLOOP_ONCE) == 0);

    // check
    assert(ctx->fd < 0);
    
    // cleanup
    transport_destroy(fifo);
}

/**
 * Test definition
 */
struct test {
    /** Test name */
    const char *name;

    /** Test func */
    void (*func) (void);
    
    bool optional;
};

#define DEF_TEST(name) { #name, &test_ ## name, false }
#define DEF_TEST_OPTIONAL(name) { #name, &test_ ## name, true }
#define DEF_TEST_END { NULL, NULL, false }

static struct test _tests[] = {
    DEF_TEST(           str_quote                   ),
    DEF_TEST(           str_format                  ),
    DEF_TEST(           dump_str                    ),
    DEF_TEST(           transport_test              ),
    DEF_TEST(           line_proto                  ),
    DEF_TEST(           irc_queue                   ),
    // XXX: irc_line_parse_invalid_prefix
    DEF_TEST(           irc_conn                    ),
    DEF_TEST(           irc_conn_self_nick          ),
    DEF_TEST(           irc_net                     ),
    DEF_TEST(           irc_chan_add_offline        ),
    DEF_TEST(           irc_chan_namreply           ),
    DEF_TEST(           irc_chan_user_join          ),
    DEF_TEST(           irc_chan_user_part          ),
    DEF_TEST(           irc_chan_user_kick          ),
    DEF_TEST(           irc_chan_self_kick          ),
    DEF_TEST(           irc_chan_user_nick          ),
    DEF_TEST(           irc_chan_user_quit          ),
    DEF_TEST(           irc_chan_CTCP_ACTION        ),
    DEF_TEST(           irc_chan_privmsg            ),
    DEF_TEST_OPTIONAL(  fifo                        ),
    DEF_TEST_END
};

/**
 * Command-line option codes
 */
enum option_code {
    OPT_HELP            = 'h',
    OPT_DEBUG           = 'd',
    OPT_QUIET           = 'q',
    OPT_LIST            = 'l',
    
    /** Options without short names */
    _OPT_EXT_BEGIN      = 0x00ff,
};

/**
 * Command-line option definitions
 */
static struct option options[] = {
    {"help",            0,  NULL,   OPT_HELP        },
    {"debug",           0,  NULL,   OPT_DEBUG       },
    {"quiet",           0,  NULL,   OPT_QUIET       },
    {"list",            0,  NULL,   OPT_LIST        },
    {0,                 0,  0,      0               },
};

/**
 * Display --help output on stdout
 */
static void usage (const char *exe) 
{
    printf("Usage: %s [OPTIONS]\n", exe);
    printf("\n");
    printf(" --help / -h            display this message\n");
    printf(" --debug / -d           display DEBUG log messages\n");
    printf(" --quiet / -q           supress INFO log messages\n");
    printf(" --list / -l            list all tests\n");
}

static void list_tests (struct test *tests)
{
    struct test *test;
    
    printf("Available tests:\n");

    for (test = tests; test->name; test++) {
        printf("\t%s\n", test->name);
    }
}

int main (int argc, char **argv)
{
    struct test *test;
    size_t test_count = 0;
    
    int opt, option_index;
    const char *filter = NULL;

    // parse options
    while ((opt = getopt_long(argc, argv, "hdql", options, &option_index)) != -1) {
        switch (opt) {
            case OPT_HELP:
                usage(argv[0]);
                exit(EXIT_SUCCESS);
            
            case OPT_DEBUG:
                set_log_level(LOG_DEBUG);
                break;

            case OPT_QUIET:
                set_log_level(LOG_WARN);
                break;
           
            case OPT_LIST:
                list_tests(_tests);
                exit(EXIT_SUCCESS);

            case '?':
                usage(argv[0]);
                exit(EXIT_FAILURE);
        }
    }

    if (optind < argc) {
        if (optind == argc - 1) {
            // filter
            filter = argv[optind];
            
            log_info("only running tests: %s", filter);
        } else {
            FATAL("too many arguments");
        }
    }

    // setup the sockets stuff
    _test_ctx.ev_base = setup_sock();

    // run tests
    for (test = _tests; test->name; test++) {
        if ((filter && strcmp(test->name, filter)) || (!filter && test->optional))
            continue;

        log_info("Running test: %s", test->name);
        
        test_count++;
        test->func();
    }

    // no tests run?
    if (test_count == 0)
        FATAL("no tests run");

    log_info("done, ran %zu tests", test_count);
}