changeset 620:c79ae2987955

Irccd: create a brand new irccd-test executable, closes #569 @3h
author David Demelier <markand@malikania.fr>
date Thu, 21 Dec 2017 21:55:57 +0100
parents a2ece4ed9f5d
children 1afefb4ffcf8
files CHANGES.md CMakeLists.txt cmake/IrccdOptions.cmake cmake/internal/sysconfig.hpp.in cmake/packages/FindEditline.cmake doc/html/CMakeLists.txt doc/src/irccd-test.md irccd-test/CMakeLists.txt irccd-test/main.cpp libirccd-test/CMakeLists.txt libirccd-test/irccd/test/debug_server.cpp libirccd-test/irccd/test/debug_server.hpp
diffstat 12 files changed, 923 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.md	Wed Dec 20 16:36:48 2017 +0100
+++ b/CHANGES.md	Thu Dec 21 21:55:57 2017 +0100
@@ -12,6 +12,11 @@
     standard paths for both irccd and plugins (#611),
   - If Mercurial is found, the version is bundled in `irccd --version`.
 
+Irccd test:
+
+  - A brand new `irccd-test` program has been added to tests plugins on the
+    command line (#569).
+
 CMake:
 
   - CMake no longer create a fake installation directory while building (#674),
--- a/CMakeLists.txt	Wed Dec 20 16:36:48 2017 +0100
+++ b/CMakeLists.txt	Thu Dec 21 21:55:57 2017 +0100
@@ -89,6 +89,7 @@
 
 add_subdirectory(irccd)
 add_subdirectory(irccdctl)
+add_subdirectory(irccd-test)
 add_subdirectory(contrib)
 
 if (HAVE_JS)
@@ -114,6 +115,7 @@
 message("")
 
 message("Compiling irccd with following options:")
+message("    Libedit:          ${WITH_LIBEDIT_MSG}")
 message("    OpenSSL:          ${WITH_SSL_MSG}")
 message("    JS:               ${WITH_JS_MSG}")
 message("    Tests:            ${WITH_TESTS_MSG}")
--- a/cmake/IrccdOptions.cmake	Wed Dec 20 16:36:48 2017 +0100
+++ b/cmake/IrccdOptions.cmake	Thu Dec 21 21:55:57 2017 +0100
@@ -19,6 +19,7 @@
 #
 # Options that controls the build:
 #
+# WITH_LIBEDIT          Enable libedit support (default: on)
 # WITH_SSL              Enable OpenSSL (default: on)
 # WITH_JS               Enable JavaScript (default: on)
 # WITH_TESTS            Enable unit testing (default: off)
@@ -71,6 +72,7 @@
     set(DEFAULT_PKGCONFIG "No")
 endif ()
 
+option(WITH_LIBEDIT "Enable libedit support" On)
 option(WITH_SSL "Enable SSL" On)
 option(WITH_JS "Enable embedded Duktape" On)
 option(WITH_TESTS "Enable unit testing" Off)
@@ -126,6 +128,7 @@
 find_package(OpenSSL)
 find_package(Pandoc)
 find_package(TCL QUIET)
+find_package(Editline)
 
 if (NOT WITH_DOCS)
     set(WITH_HTML FALSE)
@@ -133,6 +136,17 @@
     set(WITH_MAN FALSE)
 endif ()
 
+if (WITH_LIBEDIT)
+    if (Editline_FOUND)
+        set(HAVE_LIBEDIT On)
+        set(WITH_LIBEDIT_MSG "Yes")
+    else ()
+        set(WITH_LIBEDIT_MSG "No (libedit not found)")
+    endif ()
+else ()
+    set(WITH_LIBEDIT_MSG "No (disabled by user)")
+endif ()
+
 if (WITH_SSL)
     if (OPENSSL_FOUND)
         set(HAVE_SSL On)
--- a/cmake/internal/sysconfig.hpp.in	Wed Dec 20 16:36:48 2017 +0100
+++ b/cmake/internal/sysconfig.hpp.in	Thu Dec 21 21:55:57 2017 +0100
@@ -73,6 +73,7 @@
 
 #cmakedefine HAVE_JS
 #cmakedefine HAVE_SSL
+#cmakedefine HAVE_LIBEDIT
 
 /*
  * Platform checks.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmake/packages/FindEditline.cmake	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,24 @@
+# FindEditline
+# -----------
+#
+# Find libedit library, this modules defines:
+#
+# Editline_INCLUDE_DIRS, where to find histedit.h
+# Editline_LIBRARIES, where to find library
+# Editline_FOUND, if it is found
+
+find_path(Editline_INCLUDE_DIR NAMES histedit.h)
+find_library(Editline_LIBRARY NAMES libedit edit)
+
+include(FindPackageHandleStandardArgs)
+
+find_package_handle_standard_args(
+    Editline
+    FOUND_VAR Editline_FOUND
+    REQUIRED_VARS Editline_LIBRARY Editline_INCLUDE_DIR
+)
+
+set(Editline_LIBRARIES ${Editline_LIBRARY})
+set(Editline_INCLUDE_DIRS ${Editline_INCLUDE_DIR})
+
+mark_as_advanced(Editline_INCLUDE_DIR Editline_LIBRARY)
--- a/doc/html/CMakeLists.txt	Wed Dec 20 16:36:48 2017 +0100
+++ b/doc/html/CMakeLists.txt	Thu Dec 21 21:55:57 2017 +0100
@@ -133,10 +133,11 @@
 set(
     HTML_SOURCES
     build.md
+    irccd.conf.md
+    irccdctl.conf.md
+    irccdctl.md
     irccd.md
-    irccd.conf.md
-    irccdctl.md
-    irccdctl.conf.md
+    irccd-test.md
 )
 
 set(
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/src/irccd-test.md	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,71 @@
+% irccd-test
+% David Demelier
+% 2017-12-21
+
+The `irccd-test` program is a simple utility to test plugins on the command
+line.
+
+It opens a prompt that waits for user input, each line consist of a specific
+plugin event. These are mostly the same as the Javascript API offers.
+
+If compiled with [libedit][] library, the prompt offers basic completion for the
+plugin events.
+
+When a event requires a server, a fake debugging server is created if it does
+not exists already. That fake server simply prints every command on the command
+line instead of sending them through IRC.
+
+# Synopsis
+
+    $ irccd-test [options] plugin-identifier
+    $ irccd-test [options] /path/to/plugin
+
+# Options
+
+The following options are available:
+
+  - `-c, --config file`: specify the configuration file.
+
+# Commands
+
+List of available commands:
+
+  - onCommand server origin channel message
+  - onConnect server
+  - onInvite server origin channel target
+  - onJoin server origin channel
+  - onKick server origin channel reason
+  - onLoad
+  - onMe server origin channel message
+  - onMessage server origin channel message
+  - onMode server origin channel mode limit user mask
+  - onNames server channel nick1 nick2 nickN
+  - onNick server origin nickname
+  - onNotice server origin channel nickname
+  - onPart server origin channel reason
+  - onReload
+  - onTopic server origin channel topic
+  - onUnload
+  - onWhois server nick user host realname chan1 chan2 chanN
+
+# Example
+
+Example by testing the **ask** plugin.
+
+    $ irccd-test ask
+    > onCommand local #test jean will I be rich?
+	local: connect
+	local: message jean #test, No
+	> onCommand local #test jean are you sure?
+	local: message jean #test, Yes
+
+As you can see in this example, the first onCommand generates two server
+commands, the first connect attempt is being made because irccd-test creates a
+new fake server on the fly as **local** was not existing yet. You can ignore
+this.
+
+Then, the server sent a message on the **#test** channel and said **No**. The
+second onCommand event did not generate a connect event because the local server
+was already present. It said on the same server **No** though.
+
+[libedit]: http://thrysoee.dk/editline/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/irccd-test/CMakeLists.txt	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,30 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_executable(
+    TARGET irccd-test
+    LIBRARIES
+        libirccd
+        libirccd-test
+        $<$<BOOL:${HAVE_LIBEDIT}>:${Editline_LIBRARIES}>
+        $<$<BOOL:${HAVE_JS}>:libirccd-js>
+    INCLUDES
+        $<$<BOOL:${HAVE_LIBEDIT}>:${Editline_INCLUDE_DIRS}>
+    DESCRIPTION "Plugin tester"
+    SOURCES main.cpp
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/irccd-test/main.cpp	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,538 @@
+/*
+ * main.cpp -- irccd-test main file
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <algorithm>
+#include <functional>
+#include <iostream>
+#include <string>
+#include <unordered_map>
+
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/filesystem/path.hpp>
+
+#if defined(HAVE_LIBEDIT)
+#   include <histedit.h>
+#endif
+
+#include <irccd/options.hpp>
+#include <irccd/string_util.hpp>
+
+#include <irccd/daemon/dynlib_plugin.hpp>
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/debug_server.hpp>
+
+#if defined(HAVE_JS)
+#   include <irccd/js/directory_jsapi.hpp>
+#   include <irccd/js/elapsed_timer_jsapi.hpp>
+#   include <irccd/js/file_jsapi.hpp>
+#   include <irccd/js/irccd_jsapi.hpp>
+#   include <irccd/js/js_plugin.hpp>
+#   include <irccd/js/logger_jsapi.hpp>
+#   include <irccd/js/plugin_jsapi.hpp>
+#   include <irccd/js/server_jsapi.hpp>
+#   include <irccd/js/system_jsapi.hpp>
+#   include <irccd/js/timer_jsapi.hpp>
+#   include <irccd/js/unicode_jsapi.hpp>
+#   include <irccd/js/util_jsapi.hpp>
+#endif
+
+namespace irccd {
+
+namespace su = string_util;
+
+namespace {
+
+boost::asio::io_service io;
+
+std::unique_ptr<irccd> daemon;
+std::shared_ptr<plugin> plugin;
+
+void usage()
+{
+    std::cerr << "usage: irccd-test [-c config] plugin-name" << std::endl;
+    std::exit(1);
+}
+
+std::shared_ptr<server> get_server(std::string name)
+{
+    name = boost::algorithm::trim_copy(name);
+
+    if (name.empty())
+        name = "test";
+
+    auto s = daemon->servers().get(name);
+
+    if (!s) {
+        s = std::make_shared<debug_server>(io, std::move(name));
+        daemon->servers().add(s);
+    }
+
+    return s;
+}
+
+std::string get_arg(const std::vector<std::string>& args, unsigned index)
+{
+    if (index >= args.size())
+        return "";
+
+    return args[index];
+}
+
+/*
+ * onCommand server origin channel message
+ */
+void on_command(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_command(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3)
+    });
+}
+
+/*
+ * onConnect server
+ */
+void on_connect(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    plugin->on_connect(*daemon, {get_server(get_arg(args, 0))});
+}
+
+/*
+ * onInvite server origin channel target
+ */
+void on_invite(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    plugin->on_invite(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3),
+    });
+}
+
+/*
+ * onJoin server origin channel
+ */
+void on_join(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    plugin->on_join(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2)
+    });
+}
+
+/*
+ * onKick server origin channel reason
+ */
+void on_kick(const std::string& data)
+{
+    auto args = su::split(data, " ", 5);
+
+    plugin->on_kick(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3),
+        get_arg(args, 4),
+    });
+}
+
+/*
+ * onLoad
+ */
+void on_load(const std::string&)
+{
+    plugin->on_load(*daemon);
+}
+
+/*
+ * onMe server origin channel message
+ */
+void on_me(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_me(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3)
+    });
+}
+
+/*
+ * onMessage server origin channel message
+ */
+void on_message(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_message(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3)
+    });
+}
+
+/*
+ * onMode server origin channel mode limit user mask
+ */
+void on_mode(const std::string& data)
+{
+    auto args = su::split(data, " ", 7);
+
+    plugin->on_mode(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3),
+        get_arg(args, 4),
+        get_arg(args, 5),
+        get_arg(args, 6),
+    });
+}
+
+/*
+ * onNames server channel nick1 nick2 nickN
+ */
+void on_names(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    names_event ev;
+
+    ev.server = get_server(get_arg(args, 0));
+    ev.channel = get_arg(args, 1);
+
+    if (args.size() >= 3U)
+        ev.names.insert(ev.names.begin(), args.begin() + 2, args.end());
+
+    plugin->on_names(*daemon, ev);
+}
+
+/*
+ * onNick server origin nickname
+ */
+void on_nick(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    plugin->on_nick(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2)
+    });
+}
+
+/*
+ * onNotice server origin channel nickname
+ */
+void on_notice(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_notice(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3)
+    });
+}
+
+/*
+ * onPart server origin channel reason
+ */
+void on_part(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_part(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3),
+    });
+}
+
+/*
+ * onReload
+ */
+void on_reload(const std::string&)
+{
+    plugin->on_reload(*daemon);
+}
+
+/*
+ * onTopic server origin channel topic
+ */
+void on_topic(const std::string& data)
+{
+    auto args = su::split(data, " ", 4);
+
+    plugin->on_topic(*daemon, {
+        get_server(get_arg(args, 0)),
+        get_arg(args, 1),
+        get_arg(args, 2),
+        get_arg(args, 3)
+    });
+}
+
+/*
+ * onUnload
+ */
+void on_unload(const std::string&)
+{
+    plugin->on_unload(*daemon);
+}
+
+/*
+ * onWhois server nick user host realname chan1 chan2 chanN
+ */
+void on_whois(const std::string& data)
+{
+    auto args = su::split(data, " ");
+
+    whois_event ev;
+
+    ev.server = get_server(get_arg(args, 0));
+    ev.whois.nick = get_arg(args, 1);
+    ev.whois.user = get_arg(args, 2);
+    ev.whois.host = get_arg(args, 3);
+    ev.whois.realname = get_arg(args, 4);
+
+    if (args.size() >= 5)
+        ev.whois.channels.insert(ev.whois.channels.begin(), args.begin() + 5, args.end());
+
+    plugin->on_whois(*daemon, ev);
+}
+
+/*
+ * Table of user functions.
+ */
+using function = std::function<void (const std::string&)>;
+using functions = std::unordered_map<std::string, function>;
+
+static const functions list{
+    { "onCommand",  &(on_command)   },
+    { "onConnect",  &(on_connect)   },
+    { "onInvite",   &(on_invite)    },
+    { "onJoin",     &(on_join)      },
+    { "onKick",     &(on_kick)      },
+    { "onLoad",     &(on_load)      },
+    { "onMe",       &(on_me)        },
+    { "onMessage",  &(on_message)   },
+    { "onMode",     &(on_mode)      },
+    { "onNames",    &(on_names)     },
+    { "onNick",     &(on_nick)      },
+    { "onNotice",   &(on_notice)    },
+    { "onPart",     &(on_part)      },
+    { "onReload",   &(on_reload)    },
+    { "onTopic",    &(on_topic)     },
+    { "onUnload",   &(on_unload)    },
+    { "onWhois",    &(on_whois)     }
+};
+
+void exec(const std::string& line)
+{
+    auto pos = line.find(' ');
+    auto it = list.find(line.substr(0, pos));
+
+    if (it != list.end())
+        it->second(pos == std::string::npos ? "" : line.substr(pos + 1));
+}
+
+#if defined(HAVE_LIBEDIT)
+
+const char* prompt(EditLine*)
+{
+    static const char* text = "> ";
+
+    return text;
+}
+
+std::string clean(std::string input)
+{
+    while (!input.empty() && (input.back() == '\n' || input.back() == '\r'))
+        input.pop_back();
+
+    return input;
+}
+
+std::vector<std::string> matches(const std::string& name)
+{
+    std::vector<std::string> result;
+
+    for (const auto& pair : list)
+        if (pair.first.compare(0U, name.size(), name) == 0U)
+            result.push_back(pair.first);
+
+    return result;
+}
+
+unsigned char complete(EditLine* el, int)
+{
+    const auto* lf = el_line(el);
+    const auto args = su::split(std::string(lf->buffer, lf->cursor), " ");
+
+    if (args.size() == 0U)
+        return CC_REFRESH;
+
+    const auto found = matches(args[0]);
+
+    if (found.size() != 1U)
+        return CC_REFRESH;
+
+    // Insert the missing text, e.g. onCom -> onCommand.
+    if (el_insertstr(el, &found[0].c_str()[args[0].size()]) < 0)
+        return CC_ERROR;
+
+    return CC_REFRESH;
+}
+
+void run()
+{
+    std::unique_ptr<EditLine, void (*)(EditLine*)> el(
+        el_init("irccd-test", stdin, stdout, stderr),
+        el_end
+    );
+    std::unique_ptr<History, void (*)(History*)> hist(
+        history_init(),
+        history_end
+    );
+    HistEvent hev;
+
+    history(hist.get(), &hev, H_SETSIZE, 1024);
+    el_set(el.get(), EL_EDITOR, "emacs");
+    el_set(el.get(), EL_PROMPT, prompt);
+    el_set(el.get(), EL_HIST, history, hist.get());
+    el_set(el.get(), EL_ADDFN, "ed-complete", "Complete command", complete);
+    el_set(el.get(), EL_BIND, "^I", "ed-complete", nullptr);
+
+    const char* s;
+    int size;
+
+    while ((s = el_gets(el.get(), &size)) && size >= 0) {
+        if (size > 0)
+            history(hist.get(), &hev, H_ENTER, s);
+
+        exec(clean(s));
+    }
+}
+
+#else
+
+void run()
+{
+    std::string line;
+
+    for (;;) {
+        std::cout << "> ";
+        std::getline(std::cin, line);
+        exec(line);
+    }
+}
+
+#endif
+
+void load_plugins(int argc, char** argv)
+{
+    if (argc <= 0)
+        usage();
+
+    daemon->plugins().load("test", boost::filesystem::exists(argv[0]) ? argv[0] : "");
+    plugin = daemon->plugins().get("test");
+}
+
+void load_options(int& argc, char**& argv)
+{
+    const option::options def{
+        { "-c",         true    },
+        { "--config",   true    }
+    };
+
+    auto result = option::read(argc, argv, def);
+    auto it = result.find("-c");
+
+    if (it == result.end())
+        it = result.find("--config");
+    if (it != result.end()) {
+        try {
+            daemon->set_config(it->second);
+        } catch (const std::exception& ex) {
+            throw std::runtime_error(su::sprintf("%s: %s", it->second, ex.what()));
+        }
+    }
+}
+
+void load(int argc, char** argv)
+{
+    daemon = std::make_unique<irccd>(io);
+
+#if defined(HAVE_JS)
+    auto loader = std::make_unique<js_plugin_loader>(*daemon);
+
+    loader->modules().push_back(std::make_unique<irccd_jsapi>());
+    loader->modules().push_back(std::make_unique<directory_jsapi>());
+    loader->modules().push_back(std::make_unique<elapsed_timer_jsapi>());
+    loader->modules().push_back(std::make_unique<file_jsapi>());
+    loader->modules().push_back(std::make_unique<logger_jsapi>());
+    loader->modules().push_back(std::make_unique<plugin_jsapi>());
+    loader->modules().push_back(std::make_unique<server_jsapi>());
+    loader->modules().push_back(std::make_unique<system_jsapi>());
+    loader->modules().push_back(std::make_unique<timer_jsapi>());
+    loader->modules().push_back(std::make_unique<unicode_jsapi>());
+    loader->modules().push_back(std::make_unique<util_jsapi>());
+
+    daemon->plugins().add_loader(std::move(loader));
+#endif
+
+    load_options(argc, argv);
+    load_plugins(argc, argv);
+}
+
+} // !namespace
+
+} // !irccd
+
+int main(int argc, char** argv)
+{
+    try {
+        irccd::load(--argc, ++argv);
+        irccd::run();
+    } catch (const std::exception& ex) {
+        std::cerr << "abort: " << ex.what() << std::endl;
+        return 1;
+    }
+}
--- a/libirccd-test/CMakeLists.txt	Wed Dec 20 16:36:48 2017 +0100
+++ b/libirccd-test/CMakeLists.txt	Thu Dec 21 21:55:57 2017 +0100
@@ -22,6 +22,8 @@
     TARGET libirccd-test
     SOURCES
         ${libirccd-test_SOURCE_DIR}/irccd/test/command_test.hpp
+        ${libirccd-test_SOURCE_DIR}/irccd/test/debug_server.cpp
+        ${libirccd-test_SOURCE_DIR}/irccd/test/debug_server.hpp
         ${libirccd-test_SOURCE_DIR}/irccd/test/journal_server.cpp
         ${libirccd-test_SOURCE_DIR}/irccd/test/journal_server.hpp
         $<$<BOOL:${HAVE_JS}>:${libirccd-test_SOURCE_DIR}/irccd/test/plugin_test.cpp>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/debug_server.cpp	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,109 @@
+/*
+ * debug_server.cpp -- server which prints everything in the console
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <iostream>
+
+#include "debug_server.hpp"
+
+namespace irccd {
+
+void debug_server::connect() noexcept
+{
+    std::cout << name() << ": connect" << std::endl;
+}
+
+void debug_server::disconnect() noexcept
+{
+    std::cout << name() << ": disconnect" << std::endl;
+}
+
+void debug_server::reconnect() noexcept
+{
+    std::cout << name() << ": reconnect" << std::endl;
+}
+
+void debug_server::invite(std::string target, std::string channel)
+{
+    std::cout << name() << ": invite " << target << " " << channel << std::endl;
+}
+
+void debug_server::join(std::string channel, std::string password)
+{
+    std::cout << name() << ": join " << channel << " " << password << std::endl;
+}
+
+void debug_server::kick(std::string target, std::string channel, std::string reason)
+{
+    std::cout << name() << ": kick " << target << " " << channel << " " << reason << std::endl;
+}
+
+void debug_server::me(std::string target, std::string message)
+{
+    std::cout << name() << ": me " << target << " " << message << std::endl;
+}
+
+void debug_server::message(std::string target, std::string message)
+{
+    std::cout << name() << ": message " << target << " " << message << std::endl;
+}
+
+void debug_server::mode(std::string channel,
+          std::string mode,
+          std::string limit,
+          std::string user,
+          std::string mask)
+{
+    std::cout << name() << ": mode "
+              << channel << " "
+              << mode << " "
+              << limit << " "
+              << user << " "
+              << mask << std::endl;
+}
+
+void debug_server::names(std::string channel)
+{
+    std::cout << name() << ": names " << channel << std::endl;
+}
+
+void debug_server::notice(std::string target, std::string message)
+{
+    std::cout << name() << ": notice " << target << " " << message << std::endl;
+}
+
+void debug_server::part(std::string channel, std::string reason)
+{
+    std::cout << name() << ": part " << channel << " " << reason << std::endl;
+}
+
+void debug_server::send(std::string raw)
+{
+    std::cout << name() << ": send " << raw << std::endl;
+}
+
+void debug_server::topic(std::string channel, std::string topic)
+{
+    std::cout << name() << ": topic " << channel << " " << topic << std::endl;
+}
+
+void debug_server::whois(std::string target)
+{
+    std::cout << name() << ": whois " << target << std::endl;
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/debug_server.hpp	Thu Dec 21 21:55:57 2017 +0100
@@ -0,0 +1,123 @@
+/*
+ * debug_server.hpp -- server which prints everything in the console
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_TEST_DEBUG_SERVER_HPP
+#define IRCCD_TEST_DEBUG_SERVER_HPP
+
+/**
+ * \file debug_server.hpp
+ * \brief Server which prints everything in the console.
+ */
+
+#include <irccd/daemon/server.hpp>
+
+namespace irccd {
+
+/**
+ * \brief Server which prints everything in the console.
+ */
+class debug_server : public server {
+public:
+    /**
+     * Inherited constructors.
+     */
+    using server::server;
+
+    /**
+     * \copydoc server::connect
+     */
+    void connect() noexcept override;
+
+    /**
+     * \copydoc server::connect
+     */
+    void disconnect() noexcept override;
+
+    /**
+     * \copydoc server::reconnect
+     */
+    void reconnect() noexcept override;
+
+    /**
+     * \copydoc server::invite
+     */
+    void invite(std::string target, std::string channel) override;
+
+    /**
+     * \copydoc server::join
+     */
+    void join(std::string channel, std::string password = "") override;
+
+    /**
+     * \copydoc server::kick
+     */
+    void kick(std::string target, std::string channel, std::string reason = "") override;
+
+    /**
+     * \copydoc server::me
+     */
+    void me(std::string target, std::string message) override;
+
+    /**
+     * \copydoc server::message
+     */
+    void message(std::string target, std::string message) override;
+
+    /**
+     * \copydoc server::mode
+     */
+    void mode(std::string channel,
+              std::string mode,
+              std::string limit = "",
+              std::string user = "",
+              std::string mask = "") override;
+
+    /**
+     * \copydoc server::names
+     */
+    void names(std::string channel) override;
+
+    /**
+     * \copydoc server::notice
+     */
+    void notice(std::string target, std::string message) override;
+
+    /**
+     * \copydoc server::part
+     */
+    void part(std::string channel, std::string reason = "") override;
+
+    /**
+     * \copydoc server::send
+     */
+    void send(std::string raw) override;
+
+    /**
+     * \copydoc server::topic
+     */
+    void topic(std::string channel, std::string topic) override;
+
+    /**
+     * \copydoc server::whois
+     */
+    void whois(std::string target) override;
+};
+
+} // !irccd
+
+#endif // !IRCCD_TEST_DEBUG_SERVER_HPP