changeset 949:b4e8551e2064

server: several improvements
author David Demelier <markand@malikania.fr>
date Sat, 16 Jan 2021 22:48:30 +0100
parents 21a91311c8ea
children 9fcb0038fe0a
files .hgignore irccd/main.c lib/CMakeLists.txt lib/irccd/irccd.c lib/irccd/jsapi-server.c lib/irccd/list.h lib/irccd/server.c lib/irccd/server.h tests/CMakeLists.txt tests/test-bot.c
diffstat 10 files changed, 378 insertions(+), 164 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sat Jan 16 17:58:46 2021 +0100
+++ b/.hgignore	Sat Jan 16 22:48:30 2021 +0100
@@ -11,32 +11,10 @@
 \.swp$
 \.swo$
 
-# libcompat.
-^extern/libcompat/src/.*\.h$
-^extern/libcompat/trycompile$
-^extern/libcompat/tryinclude$
-^extern/libcompat/trylib$
-
 # executables.
 ^irccd/irccd$
 ^irccdctl/irccdctl$
 
-# temporary files.
-^lib/irccd/config\.h$
-\.a$
-\.o$
-\.d$
-\.so$
-\.dylib$
-
-# tests.
-^tests/test-channel$
-^tests/test-dl-plugin$
-^tests/test-log$
-^tests/test-rule$
-^tests/test-subst$
-^tests/test-util$
-
 # macOS specific.
 \.DS_Store$
 \.dSYM
--- a/irccd/main.c	Sat Jan 16 17:58:46 2021 +0100
+++ b/irccd/main.c	Sat Jan 16 22:48:30 2021 +0100
@@ -45,9 +45,9 @@
 	irc_bot_init();
 
 	irc_transport_bind("/tmp/irccd.sock");
-	irc_server_join(&s, "#test", NULL);
-	irc_js_plugin_open(&p, "test.js");
-	irc_bot_add_server(irc_util_memdup(&s, sizeof (s)));
+	//irc_server_join(&s, "#test", NULL);
+	irc_js_plugin_open(&p, "/Users/markand/test.js");
+	//irc_bot_add_server(irc_util_memdup(&s, sizeof (s)));
 	irc_bot_add_plugin(&p);
 	irc_bot_run();
 }
--- a/lib/CMakeLists.txt	Sat Jan 16 17:58:46 2021 +0100
+++ b/lib/CMakeLists.txt	Sat Jan 16 22:48:30 2021 +0100
@@ -41,6 +41,7 @@
 	irccd/server.c
 	irccd/server.h
 	irccd/set.h
+	irccd/list.h
 	irccd/subst.c
 	irccd/subst.h
 	irccd/transport.c
--- a/lib/irccd/irccd.c	Sat Jan 16 17:58:46 2021 +0100
+++ b/lib/irccd/irccd.c	Sat Jan 16 22:48:30 2021 +0100
@@ -26,6 +26,7 @@
 
 #include "event.h"
 #include "irccd.h"
+#include "list.h"
 #include "log.h"
 #include "peer.h"
 #include "plugin.h"
@@ -113,6 +114,9 @@
 static void
 process(struct pkg *pkg)
 {
+	struct irc_server *s;
+	struct irc_peer peer;
+	struct irc_event ev;
 
 	if (poll(pkg->fds, pkg->fdsz, 1000) < 0)
 		err(1, "poll");
@@ -123,18 +127,11 @@
 	 * not.
 	 */
 	for (size_t i = 0; i < pkg->fdsz; ++i) {
-		struct irc_peer peer;
-
 		pipe_flush(&pkg->fds[i]);
 
-#if 0
-		for (size_t s = 0; s < irc.serversz; ++s)
-			irc_server_flush(irc.servers[s], &pkg->fds[i]);
-#endif
-		for (struct irc_server *s = irc.servers; s; s = s->next)
+		IRC_LIST_FOREACH(irc.servers, s)
 			irc_server_flush(s, &pkg->fds[i]);
 
-
 		/* Accept new transport client. */
 		if (irc_transport_flush(&pkg->fds[i], &peer))
 			IRC_SET_ALLOC_PUSH(&irc.peers, &irc.peersz, &peer, cmp_peer);
@@ -153,12 +150,9 @@
 	 * For every server, poll any kind of new event and pass them to the
 	 * plugin unless the rules explicitly disallow us to do so.
 	 */
-	for (struct irc_server *s = irc.servers; s; s = s->next) {
-		struct irc_event ev;
-
+	IRC_LIST_FOREACH(irc.servers, s)
 		while (irc_server_poll(s, &ev))
 			invoke(&ev);
-	}
 }
 
 static void
@@ -184,8 +178,8 @@
 	irc_server_incref(s);
 	irc_server_connect(s);
 
-	s->next = irc.servers;
-	irc.servers = s;
+	IRC_LIST_ADD(irc.servers, s);
+
 	irc.serversz++;
 }
 
@@ -204,7 +198,7 @@
 void
 irc_bot_remove_server(const char *name)
 {
-	struct irc_server *s, *p;
+	struct irc_server *s;
 
 	if (!(s = irc_bot_find_server(name)))
 		return;
@@ -217,17 +211,7 @@
 		.server = s
 	});
 
-	if (s == irc.servers)
-		irc.servers = irc.servers->next;
-	else {
-		/* x -> y -> z */
-		/*      ^      */
-		/*      s      */
-		for (p = irc.servers->next; p->next != s; p = p->next)
-			continue;
-
-		p->next = s->next;
-	}
+	IRC_LIST_REMOVE(irc.servers, s);
 
 	irc_server_decref(s);
 	irc.serversz--;
@@ -238,14 +222,8 @@
 {
 	struct irc_server *s, *next;
 
-	if (!(s = irc.servers))
-		return;
-
-	while (s) {
-		next = s->next;
+	IRC_LIST_FOREACH_SAFE(irc.servers, s, next)
 		irc_bot_remove_server(s->name);
-		s = next;
-	}
 }
 
 void
--- a/lib/irccd/jsapi-server.c	Sat Jan 16 17:58:46 2021 +0100
+++ b/lib/irccd/jsapi-server.c	Sat Jan 16 22:48:30 2021 +0100
@@ -23,6 +23,7 @@
 #include "channel.h"
 #include "irccd.h"
 #include "jsapi-server.h"
+#include "list.h"
 #include "server.h"
 #include "util.h"
 
@@ -60,6 +61,79 @@
 	return sv;
 }
 
+static inline void
+get_port(duk_context *ctx, struct irc_server *s)
+{
+	duk_get_prop_string(ctx, 0, "port");
+
+	if (!duk_is_number(ctx, -1))
+		duk_error(ctx, DUK_ERR_ERROR, "invalid 'port' property");
+
+	s->port = duk_to_int(ctx, -1);
+	duk_pop(ctx);
+}
+
+static inline void
+get_ip(duk_context *ctx, struct irc_server *s)
+{
+	enum irc_server_flags flags = IRC_SERVER_FLAGS_IPV4 |
+				      IRC_SERVER_FLAGS_IPV6;
+
+	duk_get_prop_string(ctx, 0, "ipv4");
+	duk_get_prop_string(ctx, 0, "ipv6");
+
+	if (duk_is_boolean(ctx, -1) && !duk_to_boolean(ctx, -1))
+		flags &= ~(IRC_SERVER_FLAGS_IPV4);
+	if (duk_is_boolean(ctx, -2) && !duk_to_boolean(ctx, -2))
+		flags &= ~(IRC_SERVER_FLAGS_IPV6);
+
+	s->flags |= flags;
+	duk_pop_n(ctx, 2);
+}
+
+static inline void
+get_ssl(duk_context *ctx, struct irc_server *s)
+{
+	duk_get_prop_string(ctx, 0, "ssl");
+
+	if (duk_is_boolean(ctx, -1) && duk_to_boolean(ctx, -1))
+		s->flags |= IRC_SERVER_FLAGS_SSL;
+
+	duk_pop(ctx);
+}
+
+static inline void
+get_string(duk_context *ctx, const char *n, bool required, char *dst, size_t dstsz)
+{
+	duk_get_prop_string(ctx, 0, n);
+
+	if (duk_is_string(ctx, -1))
+		strlcpy(dst, duk_to_string(ctx, -1), dstsz);
+	else if (required)
+		duk_error(ctx, DUK_ERR_ERROR, "invalid or missing '%s' property", n);
+
+	duk_pop(ctx);
+}
+
+static inline void
+get_channels(duk_context *ctx, struct irc_server *s)
+{
+	duk_get_prop_string(ctx, 0, "channels");
+
+	for (duk_enum(ctx, -1, 0); duk_next(ctx, -1, true); ) {
+		duk_get_prop_string(ctx, -1, "name");
+		duk_get_prop_string(ctx, -2, "password");
+
+		if (!duk_is_string(ctx, -2))
+			duk_error(ctx, DUK_ERR_ERROR, "invalid channel 'name' property");
+
+		irc_server_join(s, duk_to_string(ctx, -2), duk_opt_string(ctx, -1, NULL));
+		duk_pop_n(ctx, 4);
+	}
+
+	duk_pop_n(ctx, 2);
+}
+
 static duk_ret_t
 Server_prototype_info(duk_context *ctx)
 {
@@ -362,89 +436,6 @@
 	return 1;
 }
 
-static inline void
-get_name(duk_context *ctx, struct irc_server *s)
-{
-	duk_get_prop_string(ctx, 0, "name");
-
-	if (!duk_is_string(ctx, -1))
-		duk_error(ctx, DUK_ERR_ERROR, "invalid 'name' property");
-
-	strlcpy(s->name, duk_to_string(ctx, -1), sizeof (s->name));
-	duk_pop(ctx);
-}
-
-static inline void
-get_port(duk_context *ctx, struct irc_server *s)
-{
-	duk_get_prop_string(ctx, 0, "port");
-
-	if (!duk_is_number(ctx, -1))
-		duk_error(ctx, DUK_ERR_ERROR, "invalid 'port' property");
-
-	s->port = duk_to_int(ctx, -1);
-	duk_pop(ctx);
-}
-
-static inline void
-get_ip(duk_context *ctx, struct irc_server *s)
-{
-	enum irc_server_flags flags = IRC_SERVER_FLAGS_IPV4 |
-	                              IRC_SERVER_FLAGS_IPV6;
-
-	duk_get_prop_string(ctx, 0, "ipv4");
-	duk_get_prop_string(ctx, 0, "ipv6");
-
-	if (duk_is_boolean(ctx, -1) && !duk_to_boolean(ctx, -1))
-		flags &= ~(IRC_SERVER_FLAGS_IPV4);
-	if (duk_is_boolean(ctx, -2) && !duk_to_boolean(ctx, -2))
-		flags &= ~(IRC_SERVER_FLAGS_IPV6);
-
-	s->flags |= flags;
-	duk_pop_n(ctx, 2);
-}
-
-static inline void
-get_ssl(duk_context *ctx, struct irc_server *s)
-{
-	duk_get_prop_string(ctx, 0, "ssl");
-
-	if (duk_is_boolean(ctx, -1) && duk_to_boolean(ctx, -1))
-		s->flags |= IRC_SERVER_FLAGS_SSL;
-
-	duk_pop(ctx);
-}
-
-static inline void
-get_string(duk_context *ctx, const char *n, char *dst, size_t dstsz)
-{
-	duk_get_prop_string(ctx, 0, n);
-
-	if (duk_is_string(ctx, -1) && duk_is_string(ctx ,-1))
-		strlcpy(dst, duk_to_string(ctx, -1), dstsz);
-
-	duk_pop(ctx);
-}
-
-static inline void
-get_channels(duk_context *ctx, struct irc_server *s)
-{
-	duk_get_prop_string(ctx, 0, "channels");
-
-	for (duk_enum(ctx, -1, 0); duk_next(ctx, -1, true); ) {
-		duk_get_prop_string(ctx, -1, "name");
-		duk_get_prop_string(ctx, -2, "password");
-
-		if (!duk_is_string(ctx, -2))
-			duk_error(ctx, DUK_ERR_ERROR, "invalid channel 'name' property");
-
-		irc_server_join(s, duk_to_string(ctx, -2), duk_opt_string(ctx, -1, NULL));
-		duk_pop_n(ctx, 4);
-	}
-
-	duk_pop_n(ctx, 2);
-}
-
 static duk_ret_t
 Server_constructor(duk_context *ctx)
 {
@@ -452,14 +443,15 @@
 
 	duk_require_object(ctx, 0);
 
-	get_name(ctx, &s);
+	get_string(ctx, "name", true, s.name, sizeof (s.name));
+	get_string(ctx, "hostname", true, s.hostname, sizeof (s.hostname));
 	get_port(ctx, &s);
 	get_ip(ctx, &s);
 	get_ssl(ctx, &s);
-	get_string(ctx, "nickname", s.nickname, sizeof (s.nickname));
-	get_string(ctx, "username", s.username, sizeof (s.username));
-	get_string(ctx, "realname", s.realname, sizeof (s.realname));
-	get_string(ctx, "commandChar", s.commandchar, sizeof (s.commandchar));
+	get_string(ctx, "nickname", false, s.nickname, sizeof (s.nickname));
+	get_string(ctx, "username", false, s.username, sizeof (s.username));
+	get_string(ctx, "realname", false, s.realname, sizeof (s.realname));
+	get_string(ctx, "commandChar", false, s.commandchar, sizeof (s.commandchar));
 	get_channels(ctx, &s);
 
 	p = irc_util_memdup(&s, sizeof (s));
@@ -473,8 +465,6 @@
 	return 0;
 }
 
-#if 0
-
 static duk_ret_t
 Server_destructor(duk_context *ctx)
 {
@@ -491,8 +481,6 @@
 	return 0;
 }
 
-#endif
-
 static duk_ret_t
 Server_add(duk_context *ctx)
 {
@@ -520,9 +508,11 @@
 static duk_ret_t
 Server_list(duk_context *ctx)
 {
+	struct irc_server *s;
+
 	duk_push_object(ctx);
 
-	for (struct irc_server *s = irc.servers; s; s = s->next) {
+	IRC_LIST_FOREACH(irc.servers, s) {
 		irc_jsapi_server_push(ctx, s);
 		duk_put_prop_string(ctx, -2, s->name);
 	}
@@ -577,6 +567,8 @@
 	duk_put_function_list(ctx, -1, functions);
 	duk_push_object(ctx);
 	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, Server_destructor, 1);
+	duk_set_finalizer(ctx, -2);
 	duk_dup_top(ctx);
 	duk_put_global_string(ctx, PROTOTYPE);
 	duk_put_prop_string(ctx, -2, "prototype");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/list.h	Sat Jan 16 22:48:30 2021 +0100
@@ -0,0 +1,48 @@
+/*
+ * list.h -- generic macros to manipulate linked lists
+ *
+ * Copyright (c) 2013-2021 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 IRCD_LIST_H
+#define IRCD_LIST_H
+
+#define IRC_LIST_ADD(h, o)                                              \
+do {                                                                    \
+        if ((h))                                                        \
+                (h)->prev = (o);                                        \
+        (o)->next = (h);                                                \
+        (h) = (o);                                                      \
+} while (0)
+
+#define IRC_LIST_REMOVE(h, o)                                           \
+do {                                                                    \
+        if ((o)->prev)                                                  \
+                (o)->prev->next = (o)->next;                            \
+        if ((o)->next)                                                  \
+                (o)->next->prev = (o)->prev;                            \
+        if ((o) == (h))                                                 \
+                (h) = (o)->next;                                        \
+        (o)->next = (o)->prev = NULL;                                   \
+} while (0)
+
+
+#define IRC_LIST_FOREACH(h, s)                                          \
+	for ((s) = (h); (s); (s) = (s)->next)
+
+#define IRC_LIST_FOREACH_SAFE(h, s, tmp)                                \
+        for ((s) = (h); s && ((tmp) = (s)->next, 1); (s) = (tmp))
+
+#endif /* IRC_LIST_H */
--- a/lib/irccd/server.c	Sat Jan 16 17:58:46 2021 +0100
+++ b/lib/irccd/server.c	Sat Jan 16 22:48:30 2021 +0100
@@ -573,6 +573,26 @@
 	}
 }
 
+static bool
+set_nonblock(struct irc_server *s)
+{
+	int cflags = 0;
+
+	if ((cflags = fcntl(s->fd, F_GETFL)) < 0 || fcntl(s->fd, F_SETFL, cflags | O_NONBLOCK) < 0)
+		return false;
+
+	return true;
+}
+
+static bool
+create(struct irc_server *s)
+{
+	s->fd = socket(s->aip->ai_family, s->aip->ai_socktype,
+	    s->aip->ai_protocol);
+
+	return set_nonblock(s);
+}
+
 static void
 dial(struct irc_server *s)
 {
@@ -583,27 +603,15 @@
 	}
 
 	for (; s->aip; s->aip = s->aip->ai_next) {
-		int cflags;
-
 		/* We may need to close a socket that was open earlier. */
 		if (s->fd != 0)
 			close(s->fd);
 
-		s->fd = socket(s->aip->ai_family, s->aip->ai_socktype,
-		    s->aip->ai_protocol);
-
-		if (s->fd < 0) {
+		if (!create(s)) {
 			irc_log_warn("server %s: %s", s->name, strerror(errno));
 			continue;
 		}
 
-		if ((cflags = fcntl(s->fd, F_GETFL)) < 0) {
-			irc_log_warn("server %s: %s", s->name, strerror(errno));
-			continue;
-		}
-
-		fcntl(s->fd, F_SETFL, cflags | O_NONBLOCK);
-
 		/*
 		 * With some luck, the connection completes immediately,
 		 * otherwise we will need to wait until the socket is writable.
--- a/lib/irccd/server.h	Sat Jan 16 17:58:46 2021 +0100
+++ b/lib/irccd/server.h	Sat Jan 16 22:48:30 2021 +0100
@@ -102,6 +102,7 @@
 	/* Reference count. */
 	size_t refc;
 	struct irc_server *next;
+	struct irc_server *prev;
 
 	/* IRC server settings. */
 	char chantypes[8];
--- a/tests/CMakeLists.txt	Sat Jan 16 17:58:46 2021 +0100
+++ b/tests/CMakeLists.txt	Sat Jan 16 22:48:30 2021 +0100
@@ -20,6 +20,7 @@
 
 set(
 	TESTS
+	test-bot
 	test-channel
 	#test-dl-plugin
 	test-log
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-bot.c	Sat Jan 16 22:48:30 2021 +0100
@@ -0,0 +1,207 @@
+/*
+ * test-bot.c -- test bot.h functions
+ *
+ * Copyright (c) 2013-2021 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 <string.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/irccd.h>
+#include <irccd/server.h>
+#include <irccd/util.h>
+
+static struct irc_server *
+server_new(const char *name)
+{
+	struct irc_server *s;
+
+	s = irc_util_calloc(1, sizeof (*s));
+	strlcpy(s->name, name, sizeof (s->name));
+
+	return s;
+}
+
+static void
+clean(void *udata)
+{
+	(void)udata;
+
+	irc_bot_clear_servers();
+}
+
+GREATEST_TEST
+servers_add(void)
+{
+	struct irc_server *s1, *s2, *s3;
+
+	s1 = server_new("malikania");
+	s2 = server_new("freenode");
+	s3 = server_new("oftc");
+
+	/* irc.servers -> s1 */
+	irc_bot_add_server(s1);
+	GREATEST_ASSERT_EQ(1, irc.serversz);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(s1, irc.servers);
+	GREATEST_ASSERT_EQ(NULL, s1->prev);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+
+	/* irc.servers -> s2 -> s1 */
+	irc_bot_add_server(s2);
+	GREATEST_ASSERT_EQ(2, irc.serversz);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(s2, irc.servers);
+	GREATEST_ASSERT_EQ(s1, s2->next);
+	GREATEST_ASSERT_EQ(NULL, s2->prev);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+	GREATEST_ASSERT_EQ(s2, s1->prev);
+
+	/* irc.servers -> s3 -> s2 -> s1 */
+	irc_bot_add_server(s3);
+	GREATEST_ASSERT_EQ(3, irc.serversz);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(1, s3->refc);
+	GREATEST_ASSERT_EQ(s3, irc.servers);
+	GREATEST_ASSERT_EQ(s2, s3->next);
+	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	GREATEST_ASSERT_EQ(s1, s2->next);
+	GREATEST_ASSERT_EQ(s3, s2->prev);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+	GREATEST_ASSERT_EQ(s2, s1->prev);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+servers_remove(void)
+{
+	struct irc_server *s1, *s2, *s3;
+
+	s1 = server_new("1");
+	s2 = server_new("2");
+	s3 = server_new("3");
+
+	/* Protect deletion from irc_bot_remove_server. */
+	irc_server_incref(s1);
+	irc_server_incref(s2);
+	irc_server_incref(s3);
+
+	/* irc.servers -> s3 -> s2 -> s1 */
+	irc_bot_add_server(s1);
+	irc_bot_add_server(s2);
+	irc_bot_add_server(s3);
+
+	/* irc.servers -> s3 -> [s2] -> s1 */
+	/* irc.servers -> s3 -> s1 */
+	irc_bot_remove_server(s2->name);
+	GREATEST_ASSERT_EQ(2, irc.serversz);
+	GREATEST_ASSERT_EQ(2, s1->refc);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(2, s3->refc);
+	GREATEST_ASSERT_EQ(NULL, s2->next);
+	GREATEST_ASSERT_EQ(NULL, s2->prev);
+	GREATEST_ASSERT_EQ(s1, s3->next);
+	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+	GREATEST_ASSERT_EQ(s3, s1->prev);
+
+	/* irc.servers -> s3 -> [s1] */
+	/* irc.servers -> s3 */
+	irc_bot_remove_server(s1->name);
+	GREATEST_ASSERT_EQ(1, irc.serversz);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(2, s3->refc);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+	GREATEST_ASSERT_EQ(NULL, s1->prev);
+	GREATEST_ASSERT_EQ(NULL, s3->next);
+	GREATEST_ASSERT_EQ(NULL, s3->prev);
+
+	/* irc.servers -> [s3] */
+	/* irc.servers -> NULL */
+	irc_bot_remove_server(s3->name);
+	GREATEST_ASSERT_EQ(0, irc.serversz);
+	GREATEST_ASSERT_EQ(NULL, irc.servers);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(1, s3->refc);
+	GREATEST_ASSERT_EQ(NULL, s3->next);
+	GREATEST_ASSERT_EQ(NULL, s3->prev);
+
+	irc_server_decref(s1);
+	irc_server_decref(s2);
+	irc_server_decref(s3);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+servers_clear(void)
+{
+	struct irc_server *s1, *s2, *s3;
+
+	s1 = server_new("1");
+	s2 = server_new("2");
+	s3 = server_new("3");
+
+	/* Protect deletion from irc_bot_remove_server. */
+	irc_server_incref(s1);
+	irc_server_incref(s2);
+	irc_server_incref(s3);
+
+	irc_bot_add_server(s1);
+	irc_bot_add_server(s2);
+	irc_bot_add_server(s3);
+	irc_bot_clear_servers();
+
+	GREATEST_ASSERT_EQ(0, irc.serversz);
+	GREATEST_ASSERT_EQ(NULL, irc.servers);
+	GREATEST_ASSERT_EQ(1, s1->refc);
+	GREATEST_ASSERT_EQ(NULL, s1->next);
+	GREATEST_ASSERT_EQ(NULL, s1->prev);
+	GREATEST_ASSERT_EQ(1, s2->refc);
+	GREATEST_ASSERT_EQ(NULL, s2->next);
+	GREATEST_ASSERT_EQ(NULL, s2->prev);
+	GREATEST_ASSERT_EQ(1, s3->refc);
+	GREATEST_ASSERT_EQ(NULL, s3->next);
+	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_servers)
+{
+	GREATEST_SET_SETUP_CB(clean, NULL);
+	GREATEST_SET_TEARDOWN_CB(clean, NULL);
+	GREATEST_RUN_TEST(servers_add);
+	GREATEST_RUN_TEST(servers_remove);
+	GREATEST_RUN_TEST(servers_clear);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_servers);
+	GREATEST_MAIN_END();
+
+	return 0;
+}