changeset 678:a4082de4e94e

Tests: rework cli_test to spawn irccd daemon in a thread, continue #785 Instead of spawning irccd executable, use the irccd class for better flexibility in test code. For example, for server tests we will need to use journal_server to see if commands have been called instead of checking a real IRC server. While here, add rule-cli-remove test that was forgotten.
author David Demelier <markand@malikania.fr>
date Thu, 12 Apr 2018 20:14:07 +0200
parents d24200bdeaf8
children 4960c858fff5
files libcommon/irccd/socket_acceptor.hpp libirccd-test/CMakeLists.txt libirccd-test/irccd/test/cli_test.cpp libirccd-test/irccd/test/cli_test.hpp libirccd-test/irccd/test/plugin_cli_test.cpp libirccd-test/irccd/test/plugin_cli_test.hpp libirccd-test/irccd/test/rule_cli_test.cpp libirccd-test/irccd/test/rule_cli_test.hpp libirccd/irccd/daemon/service/transport_service.hpp tests/CMakeLists.txt tests/data/bar.js tests/data/foo.js tests/data/irccd-multiple-rules.conf tests/data/irccd-plugins.conf tests/data/irccd-rules.conf tests/src/irccdctl/CMakeLists.txt tests/src/irccdctl/cli-plugin-config/main.cpp tests/src/irccdctl/cli-plugin-info/main.cpp tests/src/irccdctl/cli-plugin-list/main.cpp tests/src/irccdctl/cli-plugin-load/main.cpp tests/src/irccdctl/cli-plugin-reload/main.cpp tests/src/irccdctl/cli-plugin-unload/main.cpp tests/src/irccdctl/cli-rule-add/main.cpp tests/src/irccdctl/cli-rule-edit/main.cpp tests/src/irccdctl/cli-rule-info/main.cpp tests/src/irccdctl/cli-rule-list/main.cpp tests/src/irccdctl/cli-rule-move/main.cpp tests/src/irccdctl/cli-rule-remove/CMakeLists.txt tests/src/irccdctl/cli-rule-remove/main.cpp
diffstat 29 files changed, 761 insertions(+), 360 deletions(-) [+]
line wrap: on
line diff
--- a/libcommon/irccd/socket_acceptor.hpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/libcommon/irccd/socket_acceptor.hpp	Thu Apr 12 20:14:07 2018 +0200
@@ -46,6 +46,11 @@
 class socket_acceptor : public acceptor {
 public:
     /**
+     * Convenient endpoint alias.
+     */
+    using endpoint = typename Protocol::endpoint;
+
+    /**
      * Convenient acceptor alias.
      */
     using acceptor = typename Protocol::acceptor;
--- a/libirccd-test/CMakeLists.txt	Thu Apr 12 19:52:10 2018 +0200
+++ b/libirccd-test/CMakeLists.txt	Thu Apr 12 20:14:07 2018 +0200
@@ -28,6 +28,10 @@
         ${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
+        ${libirccd-test_SOURCE_DIR}/irccd/test/plugin_cli_test.cpp
+        ${libirccd-test_SOURCE_DIR}/irccd/test/plugin_cli_test.hpp
+        ${libirccd-test_SOURCE_DIR}/irccd/test/rule_cli_test.cpp
+        ${libirccd-test_SOURCE_DIR}/irccd/test/rule_cli_test.hpp
         $<$<BOOL:${HAVE_JS}>:${libirccd-test_SOURCE_DIR}/irccd/test/plugin_test.cpp>
         $<$<BOOL:${HAVE_JS}>:${libirccd-test_SOURCE_DIR}/irccd/test/plugin_test.hpp>
         $<$<BOOL:${HAVE_JS}>:${libirccd-test_SOURCE_DIR}/irccd/test/js_test.hpp>
--- a/libirccd-test/irccd/test/cli_test.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/libirccd-test/irccd/test/cli_test.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -17,14 +17,12 @@
  */
 
 #include <chrono>
-#include <future>
 #include <sstream>
-#include <thread>
-
-#include <boost/asio.hpp>
-#include <boost/process.hpp>
 
 #include <irccd/string_util.hpp>
+#include <irccd/socket_acceptor.hpp>
+
+#include <irccd/daemon/service/transport_service.hpp>
 
 #include "cli_test.hpp"
 
@@ -32,56 +30,59 @@
 
 namespace irccd {
 
-void cli_test::run_irccd(const std::string& config)
+cli_test::cli_test()
 {
-    std::ostringstream oss;
+    std::remove(CMAKE_BINARY_DIR "/tmp/irccd.sock");
+
+    io::local_acceptor::endpoint endpoint(CMAKE_BINARY_DIR "/tmp/irccd.sock");
+    io::local_acceptor::acceptor acceptor(service_, std::move(endpoint));
 
-    oss << IRCCD_EXECUTABLE << " -fc ";
-    oss << CMAKE_BINARY_DIR "/tmp/" << config;
+    irccd_.transports().add(std::make_unique<transport_server>(
+        std::make_unique<io::local_acceptor>(std::move(acceptor))));
+}
 
-    irccd_ = proc::child(oss.str());
-
-    // Let irccd bind correctly to the socket.
-    std::this_thread::sleep_for(std::chrono::milliseconds(250U));
+cli_test::~cli_test()
+{
+    service_.stop();
+    thread_.join();
 }
 
-cli_test::outputs cli_test::run_irccdctl(const std::vector<std::string>& args)
+void cli_test::start()
 {
+    thread_ = std::thread([this] { service_.run(); });
+
+    // Let irccd bind correctly.
+    std::this_thread::sleep_for(std::chrono::milliseconds(250));
+}
+
+cli_test::outputs cli_test::exec(const std::vector<std::string>& args)
+{
+    static const std::string irccdctl = IRCCDCTL_EXECUTABLE;
+    static const std::string conf = CMAKE_BINARY_DIR "/tmp/irccdctl.conf";
+
     std::ostringstream oss;
-    std::future<std::string> out;
-    std::future<std::string> err;
 
-    oss << IRCCDCTL_EXECUTABLE << " -c ";
-    oss << CMAKE_BINARY_DIR << "/tmp/irccdctl.conf ";
+    oss << irccdctl << " -c " << conf << " ";
     oss << string_util::join(args, " ");
 
-    boost::asio::io_service io;
-    proc::child irccdctl(
+    proc::ipstream out;
+    proc::ipstream err;
+
+    proc::system(
         oss.str(),
         proc::std_in.close(),
         proc::std_out > out,
-        proc::std_err > err,
-        io
+        proc::std_err > err
     );
 
-    io.run();
+    outputs result;
 
-    auto result = std::make_pair(
-        string_util::split(out.get(), "\n"),
-        string_util::split(err.get(), "\n")
-    );
-
-    irccdctl.join();
+    for (std::string line; out && std::getline(out, line); )
+        result.first.push_back(line);
+    for (std::string line; err && std::getline(err, line); )
+        result.second.push_back(line);
 
     return result;
 }
 
-cli_test::outputs cli_test::run(const std::string& config,
-                                const std::vector<std::string>& args)
-{
-    run_irccd(config);
-
-    return run_irccdctl(args);
-}
-
 } // !irccd
--- a/libirccd-test/irccd/test/cli_test.hpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/libirccd-test/irccd/test/cli_test.hpp	Thu Apr 12 20:14:07 2018 +0200
@@ -24,19 +24,41 @@
  * \brief Test fixture for irccdctl frontend.
  */
 
+#include <thread>
 #include <utility>
 #include <vector>
 
+#include <irccd/daemon/irccd.hpp>
+
+#include <boost/asio.hpp>
 #include <boost/process.hpp>
 
 namespace irccd {
 
 /**
  * \brief Test fixture for irccdctl frontend.
+ *
+ * This class will run irccd daemon in a thread when member function `start` is
+ * called.
+ *
+ * Before starting the daemon, the test can manually modify irccd instance
+ * through `irccd_` member variable. Once started, call `exec` with arguments
+ * you want to pass through irccdctl utility.
  */
 class cli_test {
 private:
-    boost::process::child irccd_;
+    using io_service = boost::asio::io_service;
+
+    std::thread thread_;
+    io_service service_;
+
+protected:
+    /**
+     * Irccd instance.
+     *
+     * \warning Do not modify once `start()` has been called.
+     */
+    irccd irccd_{service_};
 
 public:
     /**
@@ -50,26 +72,30 @@
     using outputs = std::pair<output, output>;
 
     /**
-     * Start irccd daemon with the configuration file (relative to tests/data).
-     *
-     * \param config the path to the config
+     * Construct and initialize and irccd daemon running in a thread.
      */
-    void run_irccd(const std::string& config);
+    cli_test();
+
+    /**
+     * Stop irccd and close everything.
+     */
+    ~cli_test();
 
     /**
-     * Start irccdctl and returns its stdout/stderr.
+     * Start irccd daemon.
+     *
+     * A thread will be running and closed when the destructor is called, you
+     * MUST not modify irccd while running.
+     */
+    void start();
+
+    /**
+     * Execute irccdctl.
      *
      * \param args the arguments to irccdctl
+     * \return the stdout/stderr result pair
      */
-    outputs run_irccdctl(const std::vector<std::string>& args);
-
-    /**
-     * Convenient function that starts irccd and irccdctl for oneshot tests.
-     *
-     * \param config the base configuration name for irccd
-     * \param args the arguments to irccdctl
-     */
-    outputs run(const std::string& config, const std::vector<std::string>& args);
+    outputs exec(const std::vector<std::string>& args);
 };
 
 } // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/plugin_cli_test.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,42 @@
+/*
+ * plugin_cli_test.cpp -- test fixture for irccdctl frontend (plugins support)
+ *
+ * Copyright (c) 2013-2018 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/daemon/command/plugin_config_command.hpp>
+#include <irccd/daemon/command/plugin_info_command.hpp>
+#include <irccd/daemon/command/plugin_list_command.hpp>
+#include <irccd/daemon/command/plugin_load_command.hpp>
+#include <irccd/daemon/command/plugin_reload_command.hpp>
+#include <irccd/daemon/command/plugin_unload_command.hpp>
+
+#include <irccd/daemon/service/transport_service.hpp>
+
+#include "plugin_cli_test.hpp"
+
+namespace irccd {
+
+plugin_cli_test::plugin_cli_test()
+{
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_config_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_info_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_list_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_load_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_reload_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<plugin_unload_command>());
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/plugin_cli_test.hpp	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,50 @@
+/*
+ * plugin_cli_test.hpp -- test fixture for irccdctl frontend (plugins support)
+ *
+ * Copyright (c) 2013-2018 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_PLUGIN_CLI_TEST_HPP
+#define IRCCD_TEST_PLUGIN_CLI_TEST_HPP
+
+/**
+ * \file plugin_cli_test.hpp
+ * \brief Test fixture for irccdctl frontend (plugins support).
+ */
+
+#include <irccd/daemon/service/plugin_service.hpp>
+
+#include <irccd/daemon/plugin.hpp>
+
+#include "cli_test.hpp"
+
+namespace irccd {
+
+/**
+ * \file plugin_cli_test.hpp
+ *
+ * This class adds all plugin related transport commands to irccd.
+ */
+class plugin_cli_test : public cli_test {
+public:
+    /**
+     * Default constructor.
+     */
+    plugin_cli_test();
+};
+
+} // !irccd
+
+#endif // !IRCCD_TEST_PLUGIN_CLI_TEST_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/rule_cli_test.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,42 @@
+/*
+ * rule_cli_test.cpp -- test fixture for irccdctl frontend (rule support)
+ *
+ * Copyright (c) 2013-2018 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/daemon/command/rule_add_command.hpp>
+#include <irccd/daemon/command/rule_edit_command.hpp>
+#include <irccd/daemon/command/rule_info_command.hpp>
+#include <irccd/daemon/command/rule_list_command.hpp>
+#include <irccd/daemon/command/rule_move_command.hpp>
+#include <irccd/daemon/command/rule_remove_command.hpp>
+
+#include <irccd/daemon/service/transport_service.hpp>
+
+#include "rule_cli_test.hpp"
+
+namespace irccd {
+
+rule_cli_test::rule_cli_test()
+{
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_add_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_edit_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_info_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_list_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_move_command>());
+    irccd_.transports().get_commands().push_back(std::make_unique<rule_remove_command>());
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-test/irccd/test/rule_cli_test.hpp	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,50 @@
+/*
+ * rule_cli_test.hpp -- test fixture for irccdctl frontend (rule support)
+ *
+ * Copyright (c) 2013-2018 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_RULE_CLI_TEST_HPP
+#define IRCCD_TEST_RULE_CLI_TEST_HPP
+
+/**
+ * \file rule_cli_test.hpp
+ * \brief Test fixture for irccdctl frontend (rule support).
+ */
+
+#include <irccd/daemon/service/rule_service.hpp>
+
+#include <irccd/daemon/rule.hpp>
+
+#include "cli_test.hpp"
+
+namespace irccd {
+
+/**
+ * \file rule_cli_test.hpp
+ *
+ * This class adds all rule related transport commands to irccd.
+ */
+class rule_cli_test : public cli_test {
+public:
+    /**
+     * Default constructor.
+     */
+    rule_cli_test();
+};
+
+} // !irccd
+
+#endif // !IRCCD_TEST_RULE_CLI_TEST_HPP
--- a/libirccd/irccd/daemon/service/transport_service.hpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/libirccd/irccd/daemon/service/transport_service.hpp	Thu Apr 12 20:14:07 2018 +0200
@@ -31,6 +31,7 @@
 
 class command;
 class config;
+class irccd;
 
 /**
  * \brief manage transport servers and clients.
--- a/tests/CMakeLists.txt	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/CMakeLists.txt	Thu Apr 12 20:14:07 2018 +0200
@@ -18,22 +18,12 @@
 
 project(tests)
 
-set(
-    CONFIGS
-    irccdctl.conf
-    irccd-multiple-rules.conf
-    irccd-plugins.conf
-    irccd-rules.conf
+# for test-cli-* tests.
+configure_file(
+    ${tests_SOURCE_DIR}/data/irccdctl.conf
+    ${CMAKE_BINARY_DIR}/tmp/irccdctl.conf
 )
 
-foreach (c ${CONFIGS})
-    configure_file(
-        ${tests_SOURCE_DIR}/data/${c}
-        ${CMAKE_BINARY_DIR}/tmp/${c}
-        @ONLY
-    )
-endforeach ()
-
 add_subdirectory(src/libcommon)
 add_subdirectory(src/libirccd)
 add_subdirectory(src/irccdctl)
--- a/tests/data/bar.js	Thu Apr 12 19:52:10 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-info = {
-    author: "David Demelier <markand@malikania.fr>",
-    license: "ISC",
-    summary: "bar",
-    version: "0.0"
-};
-
-function onLoad()
-{
-}
--- a/tests/data/foo.js	Thu Apr 12 19:52:10 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-info = {
-    author: "David Demelier <markand@malikania.fr>",
-    license: "ISC",
-    summary: "foo",
-    version: "0.0"
-};
-
-function onLoad()
-{
-}
-
-function onReload()
-{
-    Irccd.Plugin.config["reloaded"] = "true";
-}
--- a/tests/data/irccd-multiple-rules.conf	Thu Apr 12 19:52:10 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-[transport]
-type        = "unix"
-path        = "@CMAKE_BINARY_DIR@/tmp/irccd.sock"
-
-[rule]
-servers     = "s1"
-channels    = "c1"
-plugins     = "p1"
-events      = "onTopic"
-action      = "accept"
-
-[rule]
-servers     = "s2"
-channels    = "c2"
-plugins     = "p2"
-events      = "onCommand"
-action      = "drop"
-
-[rule]
-servers     = "s3"
-channels    = "c3"
-plugins     = "p3"
-events      = "onMessage"
-action      = "accept"
-
-# vim: ft=cfg:
--- a/tests/data/irccd-plugins.conf	Thu Apr 12 19:52:10 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-[transport]
-type    = "unix"
-path    = "@CMAKE_BINARY_DIR@/tmp/irccd.sock"
-
-[plugins]
-foo     = "@tests_SOURCE_DIR@/data/foo.js"
-bar     = "@tests_SOURCE_DIR@/data/bar.js"
-
-[plugin.bar]
-v1      = "123"
-v2      = "456"
-
-# vim: ft=cfg:
--- a/tests/data/irccd-rules.conf	Thu Apr 12 19:52:10 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-[transport]
-type        = "unix"
-path        = "@CMAKE_BINARY_DIR@/tmp/irccd.sock"
-
-[rule]
-servers     = ( "s1", "s2" )
-channels    = ( "c1", "c2" )
-plugins     = ( "p1", "p2" )
-events      = ( "onCommand", "onMessage" )
-action      = "drop"
-
-# vim: ft=cfg:
--- a/tests/src/irccdctl/CMakeLists.txt	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/CMakeLists.txt	Thu Apr 12 20:14:07 2018 +0200
@@ -30,3 +30,4 @@
 add_subdirectory(cli-rule-info)
 add_subdirectory(cli-rule-list)
 add_subdirectory(cli-rule-move)
+add_subdirectory(cli-rule-remove)
--- a/tests/src/irccdctl/cli-plugin-config/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-config/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,19 +19,58 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-config"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_config_suite, cli_test)
+namespace {
+
+class custom_plugin : public plugin {
+private:
+    plugin_config config_;
+
+public:
+    using plugin::plugin;
+
+    plugin_config get_config() override
+    {
+        return config_;
+    }
+
+    void set_config(plugin_config config) override
+    {
+        config_ = std::move(config);
+    }
+};
+
+class custom_plugin_cli_test : public plugin_cli_test {
+public:
+    custom_plugin_cli_test()
+    {
+        auto conf1 = std::make_unique<custom_plugin>("conf1", "local");
+        auto conf2 = std::make_unique<custom_plugin>("conf2", "local");
+
+        conf1->set_config({
+            { "v1", "123" },
+            { "v2", "456" }
+        });
+
+        irccd_.plugins().add(std::move(conf1));
+        irccd_.plugins().add(std::move(conf2));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_config_suite, custom_plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(set_and_get)
 {
-    run_irccd("irccd-plugins.conf");
+    start();
 
     // First, configure. No output yet
     {
-        const auto result = run_irccdctl({ "plugin-config", "foo", "verbose", "false" });
+        const auto result = exec({ "plugin-config", "conf2", "verbose", "false" });
 
         // no output yet.
         BOOST_TEST(result.first.size() == 0U);
@@ -40,9 +79,9 @@
 
     // Get the newly created value.
     {
-        const auto result = run_irccdctl({ "plugin-config", "foo", "verbose" });
+        const auto result = exec({ "plugin-config", "conf2", "verbose" });
 
-        BOOST_TEST(result.first.size() == 2U);
+        BOOST_TEST(result.first.size() == 1U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0] == "false");
     }
@@ -50,9 +89,11 @@
 
 BOOST_AUTO_TEST_CASE(getall)
 {
-    const auto result = run("irccd-plugins.conf", { "plugin-config", "bar" });
+    start();
 
-    BOOST_TEST(result.first.size() == 3U);
+    const auto result = exec({ "plugin-config", "conf1" });
+
+    BOOST_TEST(result.first.size() == 2U);
     BOOST_TEST(result.second.size() == 0U);
     BOOST_TEST(result.first[0] == "v1               : 123");
     BOOST_TEST(result.first[1] == "v2               : 456");
--- a/tests/src/irccdctl/cli-plugin-info/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-info/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,17 +19,27 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-info"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_info_suite, cli_test)
+BOOST_FIXTURE_TEST_SUITE(plugin_info_suite, plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-    const auto result = run("irccd-plugins.conf", { "plugin-info", "foo" });
+    auto p = std::make_unique<plugin>("p", "local");
+
+    p->set_author("David Demelier <markand@malikania.fr>");
+    p->set_license("ISC");
+    p->set_summary("foo");
+    p->set_version("0.0");
 
-    BOOST_TEST(result.first.size() == 5U);
+    irccd_.plugins().add(std::move(p));
+    start();
+
+    const auto result = exec({ "plugin-info", "p" });
+
+    BOOST_TEST(result.first.size() == 4U);
     BOOST_TEST(result.second.size() == 0U);
     BOOST_TEST(result.first[0] == "Author         : David Demelier <markand@malikania.fr>");
     BOOST_TEST(result.first[1] == "License        : ISC");
--- a/tests/src/irccdctl/cli-plugin-list/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-list/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,20 +19,24 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-list"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_list_suite, cli_test)
+BOOST_FIXTURE_TEST_SUITE(plugin_list_suite, plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(output)
 {
-    const auto result = run("irccd-plugins.conf", { "plugin-list" });
+    irccd_.plugins().add(std::make_unique<plugin>("p1", "local"));
+    irccd_.plugins().add(std::make_unique<plugin>("p2", "local"));
+    start();
 
-    BOOST_TEST(result.first.size() == 3U);
+    const auto result = exec({ "plugin-list" });
+
+    BOOST_TEST(result.first.size() == 2U);
     BOOST_TEST(result.second.size() == 0U);
-    BOOST_TEST(result.first[0] == "foo");
-    BOOST_TEST(result.first[1] == "bar");
+    BOOST_TEST(result.first[0] == "p1");
+    BOOST_TEST(result.first[1] == "p2");
 }
 
 BOOST_AUTO_TEST_SUITE_END()
--- a/tests/src/irccdctl/cli-plugin-load/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-load/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,19 +19,44 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-load"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_load_suite, cli_test)
+namespace {
+
+class custom_plugin_loader : public plugin_loader {
+public:
+    custom_plugin_loader()
+        : plugin_loader({}, { "none" })
+    {
+    }
+
+    std::shared_ptr<plugin> find(const std::string& id) override
+    {
+        return std::make_unique<plugin>(id, "local");
+    }
+
+    std::shared_ptr<plugin> open(const std::string& id, const std::string& path) override
+    {
+        return std::make_unique<plugin>(id, path);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_load_suite, plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-    run_irccd("irccd-plugins.conf");
+    irccd_.plugins().add(std::make_unique<plugin>("p1", "local"));
+    irccd_.plugins().add(std::make_unique<plugin>("p2", "local"));
+    irccd_.plugins().add_loader(std::make_unique<custom_plugin_loader>());
+    start();
 
     // Load a plugin first.
     {
-        const auto result = run_irccdctl({ "plugin-load", "test-cli-plugin-load" });
+        const auto result = exec({ "plugin-load", "test-cli-plugin-load" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
@@ -39,13 +64,13 @@
 
     // Get the new list of plugins.
     {
-        const auto result = run_irccdctl({ "plugin-list" });
+        const auto result = exec({ "plugin-list" });
 
-    BOOST_TEST(result.first.size() == 4U);
-    BOOST_TEST(result.second.size() == 0U);
-    BOOST_TEST(result.first[0] == "foo");
-    BOOST_TEST(result.first[1] == "bar");
-    BOOST_TEST(result.first[2] == "test-cli-plugin-load");
+        BOOST_TEST(result.first.size() == 3U);
+        BOOST_TEST(result.second.size() == 0U);
+        BOOST_TEST(result.first[0] == "p1");
+        BOOST_TEST(result.first[1] == "p2");
+        BOOST_TEST(result.first[2] == "test-cli-plugin-load");
     }
 }
 
--- a/tests/src/irccdctl/cli-plugin-reload/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-reload/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,31 +19,43 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-reload"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_reload_suite, cli_test)
+namespace {
+
+class custom_plugin : public plugin {
+public:
+    bool reloaded_{false};
+
+    custom_plugin()
+        : plugin("p", "local")
+    {
+    }
+
+    void handle_reload(irccd&) override
+    {
+        reloaded_ = true;
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_reload_suite, plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-    run_irccd("irccd-plugins.conf");
+    const auto plugin = std::make_shared<custom_plugin>();
 
-    {
-        // onReload will update the config.
-        const auto result = run_irccdctl({ "plugin-reload", "foo" });
+    irccd_.plugins().add(plugin);
+    start();
 
-        BOOST_TEST(result.first.size() == 0U);
-        BOOST_TEST(result.second.size() == 0U);
-    }
+    const auto result = exec({ "plugin-reload", "p" });
 
-    {
-        const auto result = run_irccdctl({ "plugin-config", "foo", "reloaded" });
-
-        BOOST_TEST(result.first.size() == 2U);
-        BOOST_TEST(result.second.size() == 0U);
-        BOOST_TEST(result.first[0] == "true");
-    }
+    BOOST_TEST(result.first.size() == 0U);
+    BOOST_TEST(result.second.size() == 0U);
+    BOOST_TEST(plugin->reloaded_);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
--- a/tests/src/irccdctl/cli-plugin-unload/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-plugin-unload/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,30 +19,43 @@
 #define BOOST_TEST_MODULE "irccdctl plugin-unload"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/plugin_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(plugin_unload_suite, cli_test)
+namespace {
+
+class custom_plugin : public plugin {
+public:
+    bool unloaded_{false};
+
+    custom_plugin()
+        : plugin("p", "local")
+    {
+    }
+
+    void handle_unload(irccd&) override
+    {
+        unloaded_ = true;
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_unload_suite, plugin_cli_test)
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-    run_irccd("irccd-plugins.conf");
+    const auto plugin = std::make_shared<custom_plugin>();
 
-    {
-        const auto result = run_irccdctl({ "plugin-unload", "foo" });
+    irccd_.plugins().add(plugin);
+    start();
 
-        BOOST_TEST(result.first.size() == 0U);
-        BOOST_TEST(result.second.size() == 0U);
-    }
+    const auto result = exec({ "plugin-unload", "p" });
 
-    {
-        const auto result = run_irccdctl({ "plugin-list" });
-
-        BOOST_TEST(result.first.size() == 2U);
-        BOOST_TEST(result.second.size() == 0U);
-        BOOST_TEST(result.first[0] == "bar");
-    }
+    BOOST_TEST(result.first.size() == 0U);
+    BOOST_TEST(result.second.size() == 0U);
+    BOOST_TEST(!irccd_.plugins().has("p"));
 }
 
 BOOST_AUTO_TEST_SUITE_END()
--- a/tests/src/irccdctl/cli-rule-add/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-rule-add/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,22 +19,22 @@
 #define BOOST_TEST_MODULE "irccdctl rule-add"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/rule_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(rule_add_suite, cli_test)
+BOOST_FIXTURE_TEST_SUITE(rule_add_suite, rule_cli_test)
 
 BOOST_AUTO_TEST_CASE(all)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-add",
-            "-c tc1",       "--add-channel tc2",
+        const auto result = exec({ "rule-add",
+            "-c c1",        "--add-channel c2",
             "-e onMessage", "--add-event onCommand",
-            "-p tp1",       "--add-plugin tp2",
-            "-s ts1",       "--add-server ts2",
+            "-p p1",        "--add-plugin p2",
+            "-s s1",        "--add-server s2",
             "drop"
         });
 
@@ -43,73 +43,9 @@
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
-
-        BOOST_TEST(result.first.size() == 15U);
-        BOOST_TEST(result.second.size() == 0U);
-        BOOST_TEST(result.first[0]  == "rule:        0");
-        BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
-        BOOST_TEST(result.first[2]  == "channels:    c1 c2 ");
-        BOOST_TEST(result.first[3]  == "plugins:     p1 p2 ");
-        BOOST_TEST(result.first[4]  == "events:      onCommand onMessage ");
-        BOOST_TEST(result.first[5]  == "action:      drop");
-        BOOST_TEST(result.first[6]  == "");
-        BOOST_TEST(result.first[7]  == "rule:        1");
-        BOOST_TEST(result.first[8]  == "servers:     ts1 ts2 ");
-        BOOST_TEST(result.first[9]  == "channels:    tc1 tc2 ");
-        BOOST_TEST(result.first[10] == "plugins:     tp1 tp2 ");
-        BOOST_TEST(result.first[11] == "events:      onCommand onMessage ");
-        BOOST_TEST(result.first[12] == "action:      drop");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(server)
-{
-    run_irccd("irccd-rules.conf");
-
-    {
-        const auto result = run_irccdctl({ "rule-add", "-s ts1", "--add-server ts2", "drop" });
-
-        BOOST_TEST(result.first.size() == 0U);
-        BOOST_TEST(result.second.size() == 0U);
-    }
-
-    {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 15U);
-        BOOST_TEST(result.second.size() == 0U);
-        BOOST_TEST(result.first[0]  == "rule:        0");
-        BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
-        BOOST_TEST(result.first[2]  == "channels:    c1 c2 ");
-        BOOST_TEST(result.first[3]  == "plugins:     p1 p2 ");
-        BOOST_TEST(result.first[4]  == "events:      onCommand onMessage ");
-        BOOST_TEST(result.first[5]  == "action:      drop");
-        BOOST_TEST(result.first[6]  == "");
-        BOOST_TEST(result.first[7]  == "rule:        1");
-        BOOST_TEST(result.first[8]  == "servers:     ts1 ts2 ");
-        BOOST_TEST(result.first[9]  == "channels:    ");
-        BOOST_TEST(result.first[10] == "plugins:     ");
-        BOOST_TEST(result.first[11] == "events:      ");
-        BOOST_TEST(result.first[12] == "action:      drop");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(channel)
-{
-    run_irccd("irccd-rules.conf");
-
-    {
-        const auto result = run_irccdctl({ "rule-add", "-c tc1", "--add-channel tc2", "drop" });
-
-        BOOST_TEST(result.first.size() == 0U);
-        BOOST_TEST(result.second.size() == 0U);
-    }
-
-    {
-        const auto result = run_irccdctl({ "rule-list" });
-
-        BOOST_TEST(result.first.size() == 15U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -117,77 +53,106 @@
         BOOST_TEST(result.first[3]  == "plugins:     p1 p2 ");
         BOOST_TEST(result.first[4]  == "events:      onCommand onMessage ");
         BOOST_TEST(result.first[5]  == "action:      drop");
-        BOOST_TEST(result.first[6]  == "");
-        BOOST_TEST(result.first[7]  == "rule:        1");
-        BOOST_TEST(result.first[8]  == "servers:     ");
-        BOOST_TEST(result.first[9]  == "channels:    tc1 tc2 ");
-        BOOST_TEST(result.first[10] == "plugins:     ");
-        BOOST_TEST(result.first[11] == "events:      ");
-        BOOST_TEST(result.first[12] == "action:      drop");
     }
 }
 
-BOOST_AUTO_TEST_CASE(plugin)
+BOOST_AUTO_TEST_CASE(server)
 {
-    run_irccd("irccd-rules.conf");
+    start();
+
+    {
+        const auto result = exec({ "rule-add", "-s s1", "--add-server s2", "drop" });
+
+        BOOST_TEST(result.first.size() == 0U);
+        BOOST_TEST(result.second.size() == 0U);
+    }
 
     {
-        const auto result = run_irccdctl({ "rule-add", "-p tp1", "--add-plugin tp2", "drop" });
+        const auto result = exec({ "rule-list" });
+
+        BOOST_TEST(result.first.size() == 7U);
+        BOOST_TEST(result.second.size() == 0U);
+        BOOST_TEST(result.first[0]  == "rule:        0");
+        BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
+        BOOST_TEST(result.first[2]  == "channels:    ");
+        BOOST_TEST(result.first[3]  == "plugins:     ");
+        BOOST_TEST(result.first[4]  == "events:      ");
+        BOOST_TEST(result.first[5]  == "action:      drop");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(channel)
+{
+    start();
+
+    {
+        const auto result = exec({ "rule-add", "-c c1", "--add-channel c2", "drop" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 15U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
-        BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
+        BOOST_TEST(result.first[1]  == "servers:     ");
         BOOST_TEST(result.first[2]  == "channels:    c1 c2 ");
-        BOOST_TEST(result.first[3]  == "plugins:     p1 p2 ");
-        BOOST_TEST(result.first[4]  == "events:      onCommand onMessage ");
+        BOOST_TEST(result.first[3]  == "plugins:     ");
+        BOOST_TEST(result.first[4]  == "events:      ");
         BOOST_TEST(result.first[5]  == "action:      drop");
-        BOOST_TEST(result.first[6]  == "");
-        BOOST_TEST(result.first[7]  == "rule:        1");
-        BOOST_TEST(result.first[8]  == "servers:     ");
-        BOOST_TEST(result.first[9]  == "channels:    ");
-        BOOST_TEST(result.first[10] == "plugins:     tp1 tp2 ");
-        BOOST_TEST(result.first[11] == "events:      ");
-        BOOST_TEST(result.first[12] == "action:      drop");
     }
 }
 
-BOOST_AUTO_TEST_CASE(event)
+BOOST_AUTO_TEST_CASE(plugin)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-add", "-e onMessage", "--add-event onCommand", "drop" });
+        const auto result = exec({ "rule-add", "-p p1", "--add-plugin p2", "drop" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 15U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
-        BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
-        BOOST_TEST(result.first[2]  == "channels:    c1 c2 ");
+        BOOST_TEST(result.first[1]  == "servers:     ");
+        BOOST_TEST(result.first[2]  == "channels:    ");
         BOOST_TEST(result.first[3]  == "plugins:     p1 p2 ");
+        BOOST_TEST(result.first[4]  == "events:      ");
+        BOOST_TEST(result.first[5]  == "action:      drop");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(event)
+{
+    start();
+
+    {
+        const auto result = exec({ "rule-add", "-e onMessage", "--add-event onCommand", "drop" });
+
+        BOOST_TEST(result.first.size() == 0U);
+        BOOST_TEST(result.second.size() == 0U);
+    }
+
+    {
+        const auto result = exec({ "rule-list" });
+
+        BOOST_TEST(result.first.size() == 7U);
+        BOOST_TEST(result.second.size() == 0U);
+        BOOST_TEST(result.first[0]  == "rule:        0");
+        BOOST_TEST(result.first[1]  == "servers:     ");
+        BOOST_TEST(result.first[2]  == "channels:    ");
+        BOOST_TEST(result.first[3]  == "plugins:     ");
         BOOST_TEST(result.first[4]  == "events:      onCommand onMessage ");
         BOOST_TEST(result.first[5]  == "action:      drop");
-        BOOST_TEST(result.first[6]  == "");
-        BOOST_TEST(result.first[7]  == "rule:        1");
-        BOOST_TEST(result.first[8]  == "servers:     ");
-        BOOST_TEST(result.first[9]  == "channels:    ");
-        BOOST_TEST(result.first[10] == "plugins:     ");
-        BOOST_TEST(result.first[11] == "events:      onCommand onMessage ");
-        BOOST_TEST(result.first[12] == "action:      drop");
     }
 }
 
--- a/tests/src/irccdctl/cli-rule-edit/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-rule-edit/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,19 +19,38 @@
 #define BOOST_TEST_MODULE "irccdctl rule-edit"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/rule_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(rule_edit_suite, cli_test)
+namespace {
+
+class custom_rule_cli_test : public rule_cli_test {
+public:
+    custom_rule_cli_test()
+    {
+        irccd_.rules().add({
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onCommand", "onMessage" },
+            rule::action::drop
+        });
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_edit_suite, custom_rule_cli_test)
 
 BOOST_AUTO_TEST_CASE(server)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit",
-            "-s ts1",  "--add-server ts2",
+        const auto result = exec({ "rule-edit",
+            "-s ts1",   "--add-server ts2",
             "-S s1",    "--remove-server s2",
             "0"
         });
@@ -41,9 +60,9 @@
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     ts1 ts2 ");
@@ -56,10 +75,10 @@
 
 BOOST_AUTO_TEST_CASE(channel)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit",
+        const auto result = exec({ "rule-edit",
             "-c tc1",   "--add-channel tc2",
             "-C c1",    "--remove-channel c2",
             "0"
@@ -70,9 +89,9 @@
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -85,10 +104,10 @@
 
 BOOST_AUTO_TEST_CASE(plugin)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit",
+        const auto result = exec({ "rule-edit",
             "-p tp1",   "--add-plugin tp2",
             "-P p1",    "--remove-plugin p2",
             "0"
@@ -99,9 +118,9 @@
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -114,10 +133,10 @@
 
 BOOST_AUTO_TEST_CASE(event)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit",
+        const auto result = exec({ "rule-edit",
             "-e onKick",    "--add-event onNickname",
             "-E onMessage", "--remove-event onCommand",
             "0"
@@ -128,9 +147,9 @@
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -143,19 +162,19 @@
 
 BOOST_AUTO_TEST_CASE(action_1)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit", "-a accept", "0" });
+        const auto result = exec({ "rule-edit", "-a accept", "0" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -168,19 +187,19 @@
 
 BOOST_AUTO_TEST_CASE(action_2)
 {
-    run_irccd("irccd-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-edit", "--action accept", "0" });
+        const auto result = exec({ "rule-edit", "--action accept", "0" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 8U);
+        BOOST_TEST(result.first.size() == 7U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
@@ -194,4 +213,3 @@
 BOOST_AUTO_TEST_SUITE_END()
 
 } // !irccd
-
--- a/tests/src/irccdctl/cli-rule-info/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-rule-info/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,17 +19,27 @@
 #define BOOST_TEST_MODULE "irccdctl rule-info"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/rule_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(rule_info_suite, cli_test)
+BOOST_FIXTURE_TEST_SUITE(rule_info_suite, rule_cli_test)
 
 BOOST_AUTO_TEST_CASE(info)
 {
-    const auto result = run("irccd-rules.conf", { "rule-info", "0" });
+    irccd_.rules().add({
+        { "s1", "s2" },
+        { "c1", "c2" },
+        { "o1", "o2" },
+        { "p1", "p2" },
+        { "onCommand", "onMessage" },
+        rule::action::drop
+    });
+    start();
 
-    BOOST_TEST(result.first.size() == 8U);
+    const auto result = exec({ "rule-info", "0" });
+
+    BOOST_TEST(result.first.size() == 7U);
     BOOST_TEST(result.second.size() == 0U);
     BOOST_TEST(result.first[0]  == "rule:        0");
     BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
--- a/tests/src/irccdctl/cli-rule-list/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-rule-list/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,17 +19,27 @@
 #define BOOST_TEST_MODULE "irccdctl rule-list"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/rule_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(rule_list_suite, cli_test)
+BOOST_FIXTURE_TEST_SUITE(rule_list_suite, rule_cli_test)
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-    const auto result = run("irccd-rules.conf", { "rule-list" });
+    irccd_.rules().add({
+        { "s1", "s2" },
+        { "c1", "c2" },
+        { "o1", "o2" },
+        { "p1", "p2" },
+        { "onCommand", "onMessage" },
+        rule::action::drop
+    });
+    start();
 
-    BOOST_TEST(result.first.size() == 8U);
+    const auto result = exec({ "rule-list" });
+
+    BOOST_TEST(result.first.size() == 7U);
     BOOST_TEST(result.second.size() == 0U);
     BOOST_TEST(result.first[0]  == "rule:        0");
     BOOST_TEST(result.first[1]  == "servers:     s1 s2 ");
--- a/tests/src/irccdctl/cli-rule-move/main.cpp	Thu Apr 12 19:52:10 2018 +0200
+++ b/tests/src/irccdctl/cli-rule-move/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -19,27 +19,62 @@
 #define BOOST_TEST_MODULE "irccdctl rule-move"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/test/cli_test.hpp>
+#include <irccd/test/rule_cli_test.hpp>
 
 namespace irccd {
 
-BOOST_FIXTURE_TEST_SUITE(rule_move_suite, cli_test)
+namespace {
+
+class custom_rule_cli_test : public rule_cli_test {
+public:
+    custom_rule_cli_test()
+    {
+        irccd_.rules().add({
+            { "s1" },
+            { "c1" },
+            { "o1" },
+            { "p1" },
+            { "onTopic" },
+            rule::action::accept
+        });
+        irccd_.rules().add({
+            { "s2" },
+            { "c2" },
+            { "o2" },
+            { "p2" },
+            { "onCommand" },
+            rule::action::drop
+        });
+        irccd_.rules().add({
+            { "s3" },
+            { "c3" },
+            { "o3" },
+            { "p3" },
+            { "onMessage" },
+            rule::action::accept
+        });
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_move_suite, custom_rule_cli_test)
 
 BOOST_AUTO_TEST_CASE(from_0_to_1)
 {
-    run_irccd("irccd-multiple-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-move", "0", "1" });
+        const auto result = exec({ "rule-move", "0", "1" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 22U);
+        BOOST_TEST(result.first.size() == 21U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s2 ");
@@ -67,19 +102,19 @@
 
 BOOST_AUTO_TEST_CASE(from_2_to_0)
 {
-    run_irccd("irccd-multiple-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-move", "2", "0" });
+        const auto result = exec({ "rule-move", "2", "0" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 22U);
+        BOOST_TEST(result.first.size() == 21U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s3 ");
@@ -107,19 +142,19 @@
 
 BOOST_AUTO_TEST_CASE(same)
 {
-    run_irccd("irccd-multiple-rules.conf");
+    start();
 
     {
-        const auto result = run_irccdctl({ "rule-move", "2", "2" });
+        const auto result = exec({ "rule-move", "2", "2" });
 
         BOOST_TEST(result.first.size() == 0U);
         BOOST_TEST(result.second.size() == 0U);
     }
 
     {
-        const auto result = run_irccdctl({ "rule-list" });
+        const auto result = exec({ "rule-list" });
 
-        BOOST_TEST(result.first.size() == 22U);
+        BOOST_TEST(result.first.size() == 21U);
         BOOST_TEST(result.second.size() == 0U);
         BOOST_TEST(result.first[0]  == "rule:        0");
         BOOST_TEST(result.first[1]  == "servers:     s1 ");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/irccdctl/cli-rule-remove/CMakeLists.txt	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,24 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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_test(
+    NAME cli-rule-remove
+    SOURCES main.cpp
+    LIBRARIES libcommon
+    DEPENDS irccd irccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/irccdctl/cli-rule-remove/main.cpp	Thu Apr 12 20:14:07 2018 +0200
@@ -0,0 +1,98 @@
+/*
+ * main.cpp -- test irccdctl rule-remove
+ *
+ * Copyright (c) 2013-2018 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.
+ */
+
+#define BOOST_TEST_MODULE "irccdctl rule-remove"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/rule_cli_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class custom_rule_cli_test : public rule_cli_test {
+public:
+    custom_rule_cli_test()
+    {
+        irccd_.rules().add({
+            { "s1" },
+            { "c1" },
+            { "o1" },
+            { "p1" },
+            { "onTopic" },
+            rule::action::accept
+        });
+        irccd_.rules().add({
+            { "s2" },
+            { "c2" },
+            { "o2" },
+            { "p2" },
+            { "onCommand" },
+            rule::action::drop
+        });
+        irccd_.rules().add({
+            { "s3" },
+            { "c3" },
+            { "o3" },
+            { "p3" },
+            { "onMessage" },
+            rule::action::accept
+        });
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_move_suite, custom_rule_cli_test)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    start();
+
+    {
+        const auto result = exec({ "rule-remove", "0" });
+
+        BOOST_TEST(result.first.size() == 0U);
+        BOOST_TEST(result.second.size() == 0U);
+    }
+
+    {
+        const auto result = exec({ "rule-list" });
+
+        BOOST_TEST(result.first.size() == 14U);
+        BOOST_TEST(result.second.size() == 0U);
+        BOOST_TEST(result.first[0]  == "rule:        0");
+        BOOST_TEST(result.first[1]  == "servers:     s2 ");
+        BOOST_TEST(result.first[2]  == "channels:    c2 ");
+        BOOST_TEST(result.first[3]  == "plugins:     p2 ");
+        BOOST_TEST(result.first[4]  == "events:      onCommand ");
+        BOOST_TEST(result.first[5]  == "action:      drop");
+        BOOST_TEST(result.first[6]  == "");
+        BOOST_TEST(result.first[7]  == "rule:        1");
+        BOOST_TEST(result.first[8]  == "servers:     s3 ");
+        BOOST_TEST(result.first[9]  == "channels:    c3 ");
+        BOOST_TEST(result.first[10] == "plugins:     p3 ");
+        BOOST_TEST(result.first[11] == "events:      onMessage ");
+        BOOST_TEST(result.first[12] == "action:      accept");
+        BOOST_TEST(result.first[13] == "");
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd