changeset 963:371e1cc2c697

tests: add 80% of the Javascript API
author David Demelier <markand@malikania.fr>
date Thu, 28 Jan 2021 14:20:58 +0100
parents 63208f5bb0f6
children 0dd6afe7386d
files MIGRATING.md irccd/main.c lib/CMakeLists.txt lib/irccd/config.h.in lib/irccd/js-plugin.c lib/irccd/js-plugin.h lib/irccd/jsapi-chrono.c lib/irccd/jsapi-directory.c lib/irccd/jsapi-directory.h lib/irccd/jsapi-file.c lib/irccd/jsapi-irccd.c lib/irccd/jsapi-util.c man/irccd-api-util.3 tests/CMakeLists.txt tests/data/example-dl-plugin.c tests/data/example-plugin.js tests/data/root/file-1.txt tests/data/root/level-1/level-2/file-2.txt tests/data/root/lines.txt tests/data/timer.js tests/example-dl-plugin.c tests/test-jsapi-chrono.c tests/test-jsapi-directory.c tests/test-jsapi-file.c tests/test-jsapi-irccd.c tests/test-jsapi-system.c tests/test-jsapi-timer.c tests/test-jsapi-unicode.c tests/test-jsapi-util.c
diffstat 29 files changed, 2432 insertions(+), 228 deletions(-) [+]
line wrap: on
line diff
--- a/MIGRATING.md	Mon Jan 25 22:56:05 2021 +0100
+++ b/MIGRATING.md	Thu Jan 28 14:20:58 2021 +0100
@@ -42,6 +42,8 @@
 ### Module File
 
 - The method `File.readline` is no longer marked as slow.
+- Methods `File.lines`, `File.read`, `File.readline` and `File.seek`,  now throw
+  an exception if the file was closed.
 
 ### Module Chrono
 
@@ -51,6 +53,8 @@
 ### Module Util
 
 - The method `Util.ticks` as been removed.
+- The method `Util.cut` now throws a `RangeError` exception if the number of
+  lines exceed `maxl` argument instead of returning null.
 
 ### Module Server
 
--- a/irccd/main.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/irccd/main.c	Thu Jan 28 14:20:58 2021 +0100
@@ -28,19 +28,23 @@
 #include <irccd/js-plugin.h>
 #include <irccd/rule.h>
 
+static int
+run(int argc, char **argv)
+{
+	(void)argc;
+
+	if (strcmp(argv[0], "version") == 0)
+		puts(IRCCD_VERSION);
+
+	return 0;
+}
+
 int
 main(int argc, char **argv)
 {
-	(void)argc;
-	(void)argv;
-
-	struct irc_server *s;
-
-	irc_bot_init();
+	--argc;
+	++argv;
 
-	s = irc_server_new("mlk", "circ", "circ", "circ", "malikania.fr", 6667);
-	irc_server_join(s, "#test", NULL);
-	irc_bot_server_add(s);
-	irc_bot_plugin_add(irc_js_plugin_open("/Users/markand/test.js"));
-	irc_bot_run();
+	if (argc > 0)
+		return run(argc, argv);
 }
--- a/lib/CMakeLists.txt	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/CMakeLists.txt	Thu Jan 28 14:20:58 2021 +0100
@@ -60,6 +60,8 @@
 		irccd/js-plugin.h
 		irccd/jsapi-chrono.c
 		irccd/jsapi-chrono.h
+		irccd/jsapi-directory.c
+		irccd/jsapi-directory.h
 		irccd/jsapi-file.c
 		irccd/jsapi-file.h
 		irccd/jsapi-irccd.c
--- a/lib/irccd/config.h.in	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/config.h.in	Thu Jan 28 14:20:58 2021 +0100
@@ -19,9 +19,10 @@
 #ifndef IRCCD_CONFIG_H
 #define IRCCD_CONFIG_H
 
-#define IRCCD_VERSION_MAJOR @IRCCD_VERSION_MAJOR@
-#define IRCCD_VERSION_MINOR @IRCCD_VERSION_MINOR@
-#define IRCCD_VERSION_PATCH @IRCCD_VERSION_PATCH@
+#define IRCCD_VERSION_MAJOR     @IRCCD_VERSION_MAJOR@
+#define IRCCD_VERSION_MINOR     @IRCCD_VERSION_MINOR@
+#define IRCCD_VERSION_PATCH     @IRCCD_VERSION_PATCH@
+#define IRCCD_VERSION           "@IRCCD_VERSION_MAJOR@.@IRCCD_VERSION_MINOR@.@IRCCD_VERSION_PATCH@"
 
 #cmakedefine IRCCD_WITH_JS
 #cmakedefine IRCCD_WITH_SSL
--- a/lib/irccd/js-plugin.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/js-plugin.c	Thu Jan 28 14:20:58 2021 +0100
@@ -24,11 +24,11 @@
 #include <string.h>
 #include <unistd.h>
 
-#include <duktape.h>
-
 #include "channel.h"
 #include "event.h"
 #include "js-plugin.h"
+#include "jsapi-chrono.h"
+#include "jsapi-directory.h"
 #include "jsapi-file.h"
 #include "jsapi-irccd.h"
 #include "jsapi-logger.h"
@@ -44,17 +44,6 @@
 #include "server.h"
 #include "util.h"
 
-struct self {
-	duk_context *ctx;
-	char **options;
-	char **templates;
-	char **paths;
-	char *license;
-	char *version;
-	char *author;
-	char *description;
-};
-
 static void
 freelist(char **table)
 {
@@ -187,7 +176,7 @@
 static void
 set_template(struct irc_plugin *plg, const char *key, const char *value)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	set_key_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_TEMPLATES, key, value);
 }
@@ -195,7 +184,7 @@
 static const char *
 get_template(struct irc_plugin *plg, const char *key)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_TEMPLATES, key);
 }
@@ -203,7 +192,7 @@
 static const char **
 get_templates(struct irc_plugin *plg)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_table(js->ctx, IRC_JSAPI_PLUGIN_PROP_TEMPLATES, &js->templates);
 }
@@ -211,7 +200,7 @@
 static void
 set_path(struct irc_plugin *plg, const char *key, const char *value)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	set_key_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_PATHS, key, value);
 }
@@ -219,7 +208,7 @@
 static const char *
 get_path(struct irc_plugin *plg, const char *key)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_PATHS, key);
 }
@@ -227,7 +216,7 @@
 static const char **
 get_paths(struct irc_plugin *plg)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_table(js->ctx, IRC_JSAPI_PLUGIN_PROP_PATHS, &js->paths);
 }
@@ -235,7 +224,7 @@
 static void
 set_option(struct irc_plugin *plg, const char *key, const char *value)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	set_key_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_OPTIONS, key, value);
 }
@@ -243,7 +232,7 @@
 static const char *
 get_option(struct irc_plugin *plg, const char *key)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_value(js->ctx, IRC_JSAPI_PLUGIN_PROP_OPTIONS, key);
 }
@@ -251,7 +240,7 @@
 static const char **
 get_options(struct irc_plugin *plg)
 {
-	struct self *js = plg->data;
+	struct irc_js_plugin_data *js = plg->data;
 
 	return get_table(js->ctx, IRC_JSAPI_PLUGIN_PROP_OPTIONS, &js->options);
 }
@@ -259,7 +248,7 @@
 static void
 vcall(struct irc_plugin *plg, const char *function, const char *fmt, va_list ap)
 {
-	struct self *self = plg->data;
+	struct irc_js_plugin_data *self = plg->data;
 	int nargs = 0;
 
 	duk_get_global_string(self->ctx, function);
@@ -430,11 +419,13 @@
 static bool
 init(struct irc_plugin *plg, const char *script)
 {
-	struct self js = {0};
+	struct irc_js_plugin_data js = {0};
 
 	/* Load all modules. */
 	js.ctx = duk_create_heap(wrap_malloc, wrap_realloc, wrap_free, NULL, NULL);
 	irc_jsapi_load(js.ctx);
+	irc_jsapi_chrono_load(js.ctx);
+	irc_jsapi_directory_load(js.ctx);
 	irc_jsapi_file_load(js.ctx);
 	irc_jsapi_logger_load(js.ctx);
 	irc_jsapi_plugin_load(js.ctx, plg);
@@ -480,7 +471,7 @@
 static void
 finish(struct irc_plugin *plg)
 {
-	struct self *self = plg->data;
+	struct irc_js_plugin_data *self = plg->data;
 
 	if (self->ctx)
 		duk_destroy_heap(self->ctx);
--- a/lib/irccd/js-plugin.h	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/js-plugin.h	Thu Jan 28 14:20:58 2021 +0100
@@ -21,8 +21,21 @@
 
 #include <stdbool.h>
 
+#include <duktape.h>
+
 struct irc_plugin;
 
+struct irc_js_plugin_data {
+	duk_context *ctx;
+	char **options;
+	char **templates;
+	char **paths;
+	char *license;
+	char *version;
+	char *author;
+	char *description;
+};
+
 struct irc_plugin *
 irc_js_plugin_open(const char *);
 
--- a/lib/irccd/jsapi-chrono.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/jsapi-chrono.c	Thu Jan 28 14:20:58 2021 +0100
@@ -73,18 +73,18 @@
 {
 	struct timer *timer;
 
-	timer = irc_util_calloc(1, sizeof (*self));
+	timer = irc_util_calloc(1, sizeof (*timer));
 	timespec_get(&timer->start, TIME_UTC);
 
 	duk_push_this(ctx);
 	duk_push_pointer(ctx, timer);
 	duk_put_prop_string(ctx, -2, SIGNATURE);
-	duk_pop(ctx);
 
 	/* this.elapsed property. */
 	duk_push_string(ctx, "elapsed");
 	duk_push_c_function(ctx, Chrono_prototype_elapsed, 0);
 	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+	duk_pop(ctx);
 
 	return 0;
 }
@@ -106,7 +106,7 @@
 };
 
 void
-irc_js_chrono_load(duk_context *ctx)
+irc_jsapi_chrono_load(duk_context *ctx)
 {
 	duk_get_global_string(ctx, "Irccd");
 	duk_push_c_function(ctx, Chrono_constructor, 0);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/jsapi-directory.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,425 @@
+/*
+ * jsapi-directory.c -- Irccd.Directory API
+ *
+ * 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 <sys/stat.h>
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <unistd.h>
+
+#if defined(_WIN32)
+#       include <windows.h>
+#endif
+
+#include <duktape.h>
+
+#include "jsapi-system.h"
+
+enum {
+	LIST_DOT = (1 << 0),
+	LIST_DOT_DOT = (1 << 1)
+};
+
+struct cursor {
+	char path[PATH_MAX];
+	char entry[FILENAME_MAX];
+	bool recursive;
+	void *data;
+	bool (*fn)(const struct cursor *);
+};
+
+struct finder {
+	union {
+		const char *pattern;
+		regex_t regex;
+	};
+	struct cursor curs;
+	void (*finish)(struct finder *);
+};
+
+static int
+recursedir(int dirfd, struct cursor *cs)
+{
+	DIR *dp;
+	struct dirent *entry;
+	struct stat st;
+	size_t entrylen;
+	int childfd, ret = 0;
+
+	if (!(dp = fdopendir(dirfd)))
+		return -1;
+
+	while ((entry = readdir(dp))) {
+		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+			continue;
+		if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0)
+			continue;
+
+		entrylen = strlen(entry->d_name);
+
+		/*
+		 * Append full path for the given entry.
+		 * e.g. /foo/bar/ -> /foo/bar/quux.txt
+		 */
+		strlcat(cs->path, entry->d_name, sizeof (cs->path));
+
+		/* Go recursively if it's a directory and activated. */
+		if (S_ISDIR(st.st_mode) && cs->recursive) {
+			strlcat(cs->path, "/", sizeof (cs->path));
+
+			entrylen += 1;
+	
+			if ((childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY)) < 0)
+				continue;
+			if ((ret = recursedir(childfd, cs))) {
+				close(childfd);
+				goto stop;
+			}
+
+			close(childfd);
+		}
+
+		strlcpy(cs->entry, entry->d_name, sizeof (cs->entry));
+
+		if (cs->fn(cs)) {
+			ret = 1;
+			goto stop;
+		}
+
+		cs->path[strlen(cs->path) - entrylen] = '\0';
+	}
+stop:
+	closedir(dp);
+
+	return ret;
+}
+
+static int
+recurse(const char *path, struct cursor *cs)
+{
+	int fd, ret;
+	size_t pathlen;
+
+	if ((fd = open(path, O_RDONLY | O_DIRECTORY)) < 0)
+		return -1;
+
+	pathlen = strlen(path);
+
+	if (strlcpy(cs->path, path, sizeof (cs->path)) >= sizeof (cs->path))
+		return errno = ENOMEM, -1;
+	if (cs->path[pathlen - 1] != '/' && strlcat(cs->path, "/", sizeof (cs->path)) >= sizeof (cs->path))
+		return errno = ENOMEM, -1;
+
+	ret = recursedir(fd, cs);
+	close(fd);
+
+	return ret;
+}
+
+static inline const char *
+path(duk_context *ctx)
+{
+	const char *ret;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, "path");
+
+	if (duk_get_type(ctx, -1) != DUK_TYPE_STRING)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "not a Directory object");
+
+	ret = duk_get_string(ctx, -1);
+
+	if (!ret || !ret[0])
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "directory object has empty path");
+
+	duk_pop_n(ctx, 2);
+
+	return ret;
+}
+
+static bool
+find_regex(const struct cursor *curs)
+{
+	const struct finder *fd = curs->data;
+
+	return regexec(&fd->regex, curs->entry, 0, NULL, 0) == 0;
+}
+
+static bool
+find_name(const struct cursor *curs)
+{
+	const struct finder *fd = curs->data;
+
+	return strcmp(curs->entry, fd->pattern) == 0;
+}
+
+static void
+find_regex_finish(struct finder *fd)
+{
+	regfree(&fd->regex);
+}
+
+static int
+find_helper(duk_context *ctx, const char *base, bool recursive, int pattern_index)
+{
+	struct finder finder = {
+		.curs = {
+			.recursive = recursive,
+			.data = &finder,
+		}
+	};
+	int status;
+
+	if (duk_is_string(ctx, pattern_index)) {
+		finder.pattern = duk_get_string(ctx, pattern_index);
+		finder.curs.fn = find_name;
+	} else {
+		/* Check if it's a RegExp object. */
+		duk_get_global_string(ctx, "RegExp");
+
+		if (!duk_instanceof(ctx, pattern_index, -1))
+			/* TODO: better error. */
+			return duk_error(ctx, DUK_ERR_TYPE_ERROR, "pattern arg error");
+
+		duk_get_prop_string(ctx, pattern_index, "source");
+
+		if (regcomp(&finder.regex, duk_to_string(ctx, -1), REG_EXTENDED) != 0)
+			return duk_error(ctx, DUK_ERR_ERROR, "RegExp error");
+
+		finder.curs.fn = find_regex;
+		finder.finish = find_regex_finish;
+		duk_pop_n(ctx, 2);
+	}
+
+	status = recurse(base, &finder.curs);
+
+	if (finder.finish)
+		finder.finish(&finder);
+
+	if (status == 1)
+		duk_push_string(ctx, finder.curs.path);
+	else
+		duk_push_null(ctx);
+
+	return 1;
+}
+
+static bool
+rm(const struct cursor *curs)
+{
+	return remove(curs->path), false;
+}
+
+static int
+rm_helper(duk_context *ctx, const char *base, bool recursive)
+{
+	struct stat st;
+	struct cursor curs = {
+		.recursive = true,
+		.fn = rm
+	};
+
+	if (stat(base, &st) < 0)
+		return irc_jsapi_system_raise(ctx), 0;
+	else if (!S_ISDIR(st.st_mode)) {
+		errno = ENOTDIR;
+		return irc_jsapi_system_raise(ctx), 0;
+	}
+
+	if (recursive)
+		recurse(base, &curs);
+
+	remove(base);
+
+	return 0;
+}
+
+static inline void
+mkpath(duk_context *ctx, const char *path)
+{
+#ifdef _WIN32
+	/* TODO: convert code to errno. */
+	if (!CreateDirectoryA(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
+		errno = EPERM;
+		irc_jsapi_system_raise(ctx);
+#else
+	if (mkdir(path, 0755) < 0 && errno != EEXIST)
+		irc_jsapi_system_raise(ctx);
+#endif
+}
+
+static inline char *
+normalize(char *str)
+{
+	for (char *p = str; *p; ++p)
+		if (*p == '\\')
+			*p = '/';
+
+	return str;
+}
+
+static int
+Directory_prototype_find(duk_context *ctx)
+{
+	return find_helper(ctx, path(ctx), duk_opt_boolean(ctx, 1, false), 0);
+}
+
+static int
+Directory_prototype_remove(duk_context *ctx)
+{
+	return rm_helper(ctx, path(ctx), duk_opt_boolean(ctx, 0, false));
+}
+
+static int
+Directory_constructor(duk_context *ctx)
+{
+	const char *path = duk_require_string(ctx, 0);
+	const int flags = duk_opt_int(ctx, 1, 0);
+	DIR *dp;
+	struct dirent *entry;
+
+	if (!duk_is_constructor_call(ctx))
+		return 0;
+
+	duk_push_this(ctx);
+
+	/* this.entries property. */
+	duk_push_string(ctx, "entries");
+	duk_push_array(ctx);
+
+	if (!(dp = opendir(path)))
+		irc_jsapi_system_raise(ctx);
+
+	for (int i = 0; (entry = readdir(dp)); ) {
+		if (strcmp(entry->d_name, ".") == 0 && !(flags & LIST_DOT))
+			continue;
+		if (strcmp(entry->d_name, "..") == 0 && !(flags & LIST_DOT_DOT))
+			continue;
+
+		duk_push_object(ctx);
+		duk_push_string(ctx, entry->d_name);
+		duk_put_prop_string(ctx, -2, "name");
+		duk_push_int(ctx, entry->d_type);
+		duk_put_prop_string(ctx, -2, "type");
+		duk_put_prop_index(ctx, -2, i++);
+	}
+
+	closedir(dp);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_VALUE);
+
+	/* this.path property. */
+	duk_push_string(ctx, "path");
+	duk_push_string(ctx, path);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_VALUE);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+Directory_find(duk_context *ctx)
+{
+	const char *path = duk_require_string(ctx, 0);
+	bool recursive = duk_opt_boolean(ctx, 2, false);
+
+	return find_helper(ctx, path, recursive, 1);
+}
+
+static duk_ret_t
+Directory_remove(duk_context* ctx)
+{
+	return rm_helper(ctx, duk_require_string(ctx, 0), duk_opt_boolean(ctx, 1, false));
+}
+
+static duk_ret_t
+Directory_mkdir(duk_context* ctx)
+{
+	char path[PATH_MAX], *p;
+
+	/* Copy the directory to normalize and iterate over '/'. */
+	strlcpy(path, duk_require_string(ctx, 0), sizeof (path));
+	normalize(path);
+
+#if defined(_WIN32)
+	/* Remove drive letter that we don't need. */
+	if ((p = strchr(path, ':')))
+		++p;
+	else
+		p = path;
+#else
+	p = path;
+#endif
+
+	for (p = p + 1; *p; ++p) {
+		if (*p == '/') {
+			*p = 0;
+			mkpath(ctx, path);
+			*p = '/';
+		}
+	}
+
+	mkpath(ctx, path);
+
+	return 0;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "find",               Directory_prototype_find,       DUK_VARARGS     },
+	{ "remove",             Directory_prototype_remove,     1               },
+	{ NULL,                 NULL,                           0               }
+};
+
+static const duk_function_list_entry functions[] = {
+	{ "find",               Directory_find,                 DUK_VARARGS     },
+	{ "mkdir",              Directory_mkdir,                DUK_VARARGS     },
+	{ "remove",             Directory_remove,               DUK_VARARGS     },
+	{ NULL,                 NULL,                           0               }
+};
+
+static const duk_number_list_entry constants[] = {
+	{ "Dot",                LIST_DOT        },
+	{ "DotDot",             LIST_DOT_DOT    },
+	{ "TypeFile",           DT_REG          },
+	{ "TypeDir",            DT_DIR          },
+	{ "TypeLink",           DT_LNK          },
+	{ "TypeBlock",          DT_BLK          },
+	{ "TypeCharacter",      DT_CHR          },
+	{ "TypeFifo",           DT_FIFO         },
+	{ "TypeSocket",         DT_SOCK         },
+	{ "TypeUnknown",        DT_UNKNOWN      },
+	{ NULL,                 0               }
+};
+
+void
+irc_jsapi_directory_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_get_global_string(ctx, "Irccd");
+	duk_push_c_function(ctx, Directory_constructor, 2);
+	duk_put_number_list(ctx, -1, constants);
+	duk_put_function_list(ctx, -1, functions);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Directory");
+	duk_pop(ctx);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/jsapi-directory.h	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,27 @@
+/*
+ * jsapi-directory.h -- Irccd.Directory API
+ *
+ * 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 IRCCD_JSAPI_DIRECTORY_H
+#define IRCCD_JSAPI_DIRECTORY_H
+
+#include <duktape.h>
+
+void
+irc_jsapi_directory_load(duk_context *);
+
+#endif /* !IRCCD_JSAPI_DIRECTORY_H */
--- a/lib/irccd/jsapi-file.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/jsapi-file.c	Thu Jan 28 14:20:58 2021 +0100
@@ -257,8 +257,12 @@
 		irc_jsapi_system_raise(ctx);
 	}
 
-	if (getline(&line, &linesz, file->fp) < 0 || ferror(file->fp)) {
+	if (getline(&line, &linesz, file->fp) < 0) {
 		free(line);
+
+		if (feof(file->fp))
+			return 0;
+
 		irc_jsapi_system_raise(ctx);
 	}
 
@@ -312,7 +316,7 @@
 	long position;
 
 	if (!file->fp || (position = ftell(file->fp)) < 0)
-		irc_jsapi_system_raise(ctx);
+		return irc_jsapi_system_raise(ctx), 0;
 
 	duk_push_number(ctx, position);
 
--- a/lib/irccd/jsapi-irccd.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/jsapi-irccd.c	Thu Jan 28 14:20:58 2021 +0100
@@ -24,7 +24,7 @@
 #include "config.h"
 #include "util.h"
 
-static duk_ret_t
+static int
 SystemError_constructor(duk_context *ctx)
 {
 	duk_push_this(ctx);
@@ -283,6 +283,14 @@
 
 /* }}} */
 
+static int
+print(duk_context *ctx)
+{
+	puts(duk_require_string(ctx, 0));
+
+	return 0;
+}
+
 void
 irc_jsapi_load(duk_context *ctx)
 {
@@ -318,4 +326,8 @@
 
 	/* Set Irccd as global. */
 	duk_put_global_string(ctx, "Irccd");
+
+	/* Convenient global "print" function. */
+	duk_push_c_function(ctx, print, 1);
+	duk_put_global_string(ctx, "print");
 }
--- a/lib/irccd/jsapi-util.c	Mon Jan 25 22:56:05 2021 +0100
+++ b/lib/irccd/jsapi-util.c	Thu Jan 28 14:20:58 2021 +0100
@@ -159,18 +159,20 @@
 }
 
 static int
-limit(duk_context *ctx, duk_idx_t index, const char *name, int value)
+limit(duk_context *ctx, duk_idx_t index, const char *name, size_t value)
 {
+	int newvalue;
+
 	if (duk_get_top(ctx) < index || !duk_is_number(ctx, index))
 		return value;
 
-	value = duk_to_int(ctx, index);
+	newvalue = duk_to_int(ctx, index);
 
-	if (value <= 0)
+	if (newvalue <= 0)
 		(void)duk_error(ctx, DUK_ERR_RANGE_ERROR,
 		    "argument %d (%s) must be positive", index, name);
 
-	return value;
+	return newvalue;
 }
 
 static char *
@@ -178,7 +180,7 @@
 {
 	FILE *fp;
 	char *out = NULL;
-	size_t outsz = 0, linesz = 0, tokensz;
+	size_t outsz = 0, linesz = 0, tokensz, lineavail = maxl;
 	struct string *token;
 
 	if (!(fp = open_memstream(&out, &outsz)))
@@ -187,10 +189,10 @@
 	TAILQ_FOREACH(token, tokens, link) {
 		tokensz = strlen(token->value);
 
-		if (tokensz >= maxc) {
+		if (tokensz > maxc) {
 			fclose(fp);
 			duk_push_error_object(ctx, DUK_ERR_RANGE_ERROR,
-			    "token '%s' could not fit in maxc (%zu)", token, maxc);
+			    "token '%s' could not fit in maxc limit (%zu)", token->value, maxc);
 			return NULL;
 		}
 
@@ -207,15 +209,14 @@
 		 * a "new" one.
 		 */
 		if (linesz + tokensz > maxc) {
-			if (maxl == 0) {
+			if (--lineavail == 0) {
 				fclose(fp);
-				duk_push_error_object(ctx, "lines exceeds maxl (%zu)", maxl);
+				duk_push_error_object(ctx, DUK_ERR_RANGE_ERROR, "number of lines exceeds maxl (%zu)", maxl);
 				return NULL;
 			}
 
 			fputc('\n', fp);
 			linesz = 0;
-			maxl -= 1;
 		}
 
 		linesz += fprintf(fp, "%s%s", linesz > 0 ? " " : "", token->value);
--- a/man/irccd-api-util.3	Mon Jan 25 22:56:05 2021 +0100
+++ b/man/irccd-api-util.3	Thu Jan 28 14:20:58 2021 +0100
@@ -42,19 +42,11 @@
 The argument
 .Fa maxc
 controls the maximum of characters allowed per line, it can be a positive
-integer. If undefined is given, a default of 72 is used.
-.Pp
-The argument
+integer. If undefined is given, a default of 72 is used. The argument
 .Fa maxl
-controls the maximum of lines allowed. It can be a positive integer or undefined
-for an infinite list.
-.Pp
-If
-.Fa maxl
-is used as a limit and the data can not fit within the bounds,
-undefined is returned.
-.Pp
-An empty list may be returned if empty strings were found.
+controls the maximum of lines allowed. It can be a positive integer or
+undefined for an infinite list. An empty list may be returned if empty strings
+were found.
 .Pp
 .\" Irccd.Util.format
 The
@@ -118,7 +110,10 @@
 are negative numbers.
 .It Bq Er RangeError
 If one word length was bigger than
-.Fa maxc .
+.Fa maxc
+or the number of lines would exceed
+.Fa maxl
+argument.
 .It Bq Er TypeError
 If
 .Fa data
--- a/tests/CMakeLists.txt	Mon Jan 25 22:56:05 2021 +0100
+++ b/tests/CMakeLists.txt	Thu Jan 28 14:20:58 2021 +0100
@@ -30,9 +30,31 @@
 	test-util
 )
 
+if (IRCCD_WITH_JS)
+	list(
+		APPEND TESTS
+		test-jsapi-chrono
+		test-jsapi-directory
+		test-jsapi-file
+		test-jsapi-irccd
+		test-jsapi-system
+		test-jsapi-timer
+		test-jsapi-unicode
+		test-jsapi-util
+	)
+endif ()
+
 foreach (t ${TESTS})
 	add_executable(${t} ${t}.c)
 	add_test(${t} ${t})
 	target_link_libraries(${t} libirccd libirccd-greatest)
 	set_target_properties(${t} PROPERTIES FOLDER "tests")
+	target_compile_definitions(
+		${t}
+		PRIVATE
+			IRCCD_EXECUTABLE="$<TARGET_FILE:irccd>"
+			BINARY="${tests_BINARY_DIR}"
+			SOURCE="${tests_SOURCE_DIR}"
+	)
+	add_dependencies(${t} irccd)
 endforeach ()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/example-dl-plugin.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,160 @@
+/*
+ * example-dl-plugin.c -- simple plugin for unit tests
+ *
+ * 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>
+
+#include <irccd/dl-plugin.h>
+#include <irccd/event.h>
+#include <irccd/util.h>
+
+struct kw {
+	const char *key;
+	char value[256];
+};
+
+/*
+ * Options.
+ */
+static struct kw options[] = {
+	{ "option-1", "value-1" }
+};
+
+static const char *options_list[] = {
+	"option-1",
+	NULL
+};
+
+/*
+ * Templates.
+ */
+static struct kw templates[] = {
+	{ "template-1", "Welcome #{target}" }
+};
+
+static const char *templates_list[] = {
+	"template-1",
+	NULL
+};
+
+/*
+ * Paths.
+ */
+static struct kw paths[] = {
+	{ "path-1", "/usr/local/etc" }
+};
+
+static const char *paths_list[] = {
+	"path-1",
+	NULL
+};
+
+static void
+set(struct kw *table, size_t tablesz, const char *key, const char *value)
+{
+	for (size_t i = 0; i < tablesz; ++i) {
+		if (strcmp(table[i].key, key) == 0) {
+			strlcpy(table[i].value, value, sizeof (table[i].value));
+			break;
+		}
+	}
+}
+
+static const char *
+get(const struct kw *table, size_t tablesz, const char *key)
+{
+	for (size_t i = 0; i < tablesz; ++i)
+		if (strcmp(table[i].key, key) == 0)
+			return table[i].value;
+
+	return NULL;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_option(const char *key, const char *value)
+{
+	set(options, IRC_UTIL_SIZE(options), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_option(const char *key)
+{
+	return get(options, IRC_UTIL_SIZE(options), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_options(void)
+{
+	return options_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_template(const char *key, const char *value)
+{
+	set(templates, IRC_UTIL_SIZE(templates), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_template(const char *key)
+{
+	return get(templates, IRC_UTIL_SIZE(templates), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_templates(void)
+{
+	return templates_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_path(const char *key, const char *value)
+{
+	set(paths, IRC_UTIL_SIZE(paths), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_path(const char *key)
+{
+	return get(paths, IRC_UTIL_SIZE(paths), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_paths(void)
+{
+	return paths_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_event(const struct irc_event *ev)
+{
+	(void)ev;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_load(void)
+{
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_reload(void)
+{
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_unload(void)
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/example-plugin.js	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,1 @@
+/* Just a simple file. */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/root/file-1.txt	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,1 @@
+file-1.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/root/level-1/level-2/file-2.txt	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,1 @@
+file-2.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/root/lines.txt	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,3 @@
+a
+b
+c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/timer.js	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,13 @@
+count = 0;
+
+function onLoad()
+{
+	if (typeof (type) === "undefined")
+		throw Error("global timer type not defined");
+
+	t = new Irccd.Timer(type, 50, function () {
+		count += 1;
+	});
+
+	t.start();
+}
--- a/tests/example-dl-plugin.c	Mon Jan 25 22:56:05 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-/*
- * example-dl-plugin.c -- simple plugin for unit tests
- *
- * 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>
-
-#include <irccd/dl-plugin.h>
-#include <irccd/event.h>
-#include <irccd/util.h>
-
-struct kw {
-	const char *key;
-	char value[256];
-};
-
-/*
- * Options.
- */
-static struct kw options[] = {
-	{ "option-1", "value-1" }
-};
-
-static const char *options_list[] = {
-	"option-1",
-	NULL
-};
-
-/*
- * Templates.
- */
-static struct kw templates[] = {
-	{ "template-1", "Welcome #{target}" }
-};
-
-static const char *templates_list[] = {
-	"template-1",
-	NULL
-};
-
-/*
- * Paths.
- */
-static struct kw paths[] = {
-	{ "path-1", "/usr/local/etc" }
-};
-
-static const char *paths_list[] = {
-	"path-1",
-	NULL
-};
-
-static void
-set(struct kw *table, size_t tablesz, const char *key, const char *value)
-{
-	for (size_t i = 0; i < tablesz; ++i) {
-		if (strcmp(table[i].key, key) == 0) {
-			strlcpy(table[i].value, value, sizeof (table[i].value));
-			break;
-		}
-	}
-}
-
-static const char *
-get(const struct kw *table, size_t tablesz, const char *key)
-{
-	for (size_t i = 0; i < tablesz; ++i)
-		if (strcmp(table[i].key, key) == 0)
-			return table[i].value;
-
-	return NULL;
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_set_option(const char *key, const char *value)
-{
-	set(options, IRC_UTIL_SIZE(options), key, value);
-}
-
-IRC_DL_EXPORT const char *
-example_dl_plugin_get_option(const char *key)
-{
-	return get(options, IRC_UTIL_SIZE(options), key);
-}
-
-IRC_DL_EXPORT const char **
-example_dl_plugin_get_options(void)
-{
-	return options_list;
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_set_template(const char *key, const char *value)
-{
-	set(templates, IRC_UTIL_SIZE(templates), key, value);
-}
-
-IRC_DL_EXPORT const char *
-example_dl_plugin_get_template(const char *key)
-{
-	return get(templates, IRC_UTIL_SIZE(templates), key);
-}
-
-IRC_DL_EXPORT const char **
-example_dl_plugin_get_templates(void)
-{
-	return templates_list;
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_set_path(const char *key, const char *value)
-{
-	set(paths, IRC_UTIL_SIZE(paths), key, value);
-}
-
-IRC_DL_EXPORT const char *
-example_dl_plugin_get_path(const char *key)
-{
-	return get(paths, IRC_UTIL_SIZE(paths), key);
-}
-
-IRC_DL_EXPORT const char **
-example_dl_plugin_get_paths(void)
-{
-	return paths_list;
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_event(const struct irc_event *ev)
-{
-	(void)ev;
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_load(void)
-{
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_reload(void)
-{
-}
-
-IRC_DL_EXPORT void
-example_dl_plugin_unload(void)
-{
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-chrono.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,112 @@
+/*
+ * test-jsapi-chrono.c -- test Irccd.Chrono API
+ *
+ * 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 <unistd.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+basics_simple(void)
+{
+	if (duk_peval_string(data->ctx, "timer = new Irccd.Chrono();") != 0)
+		GREATEST_FAIL();
+
+	sleep(1);
+
+	if (duk_peval_string(data->ctx, "result = timer.elapsed;") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "result");
+
+	GREATEST_ASSERT_IN_RANGE(1000U, duk_get_uint(data->ctx, -1), 100);
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_reset(void)
+{
+	/*
+	 * Create a timer and wait for it to accumulate some time. Then use
+	 * start to reset its value and wait for 1s. The elapsed time must not
+	 * be greater than 1s.
+	 */
+	if (duk_peval_string(data->ctx, "timer = new Irccd.Chrono()") != 0)
+		GREATEST_FAIL();
+
+	sleep(1);
+
+	if (duk_peval_string(data->ctx, "timer.reset();") != 0)
+		GREATEST_FAIL();
+
+	sleep(1);
+
+	if (duk_peval_string(data->ctx, "result = timer.elapsed") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "result");
+
+	GREATEST_ASSERT_IN_RANGE(1000U, duk_get_uint(data->ctx, -1), 100);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(basics_simple);
+	GREATEST_RUN_TEST(basics_reset);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-directory.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,257 @@
+/*
+ * test-jsapi-directory.c -- test Irccd.Directory API
+ *
+ * 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 <sys/stat.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+
+	duk_push_string(data->ctx, SOURCE);
+	duk_put_global_string(data->ctx, "SOURCE");
+
+	duk_push_string(data->ctx, BINARY);
+	duk_put_global_string(data->ctx, "BINARY");
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+object_constructor(void)
+{
+	const char *script =
+		"d = new Irccd.Directory(SOURCE + '/data/root');"
+		"p = d.path;"
+		"l = d.entries.length;";
+
+	if (duk_peval_string(data->ctx, script) != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "l");
+	GREATEST_ASSERT_EQ(3U, duk_get_uint(data->ctx, -1));
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT(duk_is_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_find(void)
+{
+	const char *script = "d = new Irccd.Directory(SOURCE + '/data/root');";
+
+	if (duk_peval_string(data->ctx, script) != 0)
+		GREATEST_FAIL();
+
+	/* Find "lines.txt" not recursively. */
+	if (duk_peval_string(data->ctx, "p = d.find('lines.txt');") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root/lines.txt", duk_get_string(data->ctx, -1));
+
+	/* Find "unknown.txt" not recursively (not found). */
+	if (duk_peval_string(data->ctx, "p = d.find('unknown.txt');") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT(duk_is_null(data->ctx, -1));
+
+	/* Find "file-2.txt" not recursively (exists but in sub directory). */
+	if (duk_peval_string(data->ctx, "p = d.find('file-2.txt');") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT(duk_is_null(data->ctx, -1));
+
+	/* Find "file-2.txt" recursively. */
+	if (duk_peval_string(data->ctx, "p = d.find('file-2.txt', true);") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root/level-1/level-2/file-2.txt", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_remove(void)
+{
+	struct stat st;
+
+	/* First create an empty directory. */
+	mkdir(BINARY "/empty", 0700);
+
+	if (duk_peval_string(data->ctx, "d = new Irccd.Directory(BINARY + '/empty')") != 0)
+		GREATEST_FAIL();
+
+	/* Not recursive. */
+	if (duk_peval_string(data->ctx, "d.remove()") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT_EQ(-1, stat(BINARY "/empty", &st));
+
+	mkdir(BINARY "/notempty", 0700);
+	mkdir(BINARY "/notempty/empty", 0700);
+
+	if (duk_peval_string(data->ctx, "d = new Irccd.Directory(BINARY + '/notempty')") != 0)
+		GREATEST_FAIL();
+
+	/* Not recursive. */
+	if (duk_peval_string(data->ctx, "d.remove(true)") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT_EQ(-1, stat(BINARY "/notempty", &st));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_object)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(object_constructor);
+	GREATEST_RUN_TEST(object_find);
+	GREATEST_RUN_TEST(object_remove);
+}
+
+GREATEST_TEST
+free_find(void)
+{
+	/* Find "lines.txt" not recursively. */
+	if (duk_peval_string(data->ctx, "p = Irccd.Directory.find(SOURCE + '/data/root', 'lines.txt');") != 0) {
+		puts(duk_to_string(data->ctx, -1));
+		GREATEST_FAIL();
+	}
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root/lines.txt", duk_get_string(data->ctx, -1));
+
+	/* Find "unknown.txt" not recursively (not found). */
+	if (duk_peval_string(data->ctx, "p = Irccd.Directory.find(SOURCE + '/data/root', 'unknown.txt');") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT(duk_is_null(data->ctx, -1));
+
+	/* Find "file-2.txt" not recursively (exists but in sub directory). */
+	if (duk_peval_string(data->ctx, "p = Irccd.Directory.find(SOURCE + '/data/root', 'file-2.txt');") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT(duk_is_null(data->ctx, -1));
+
+	/* Find "file-2.txt" recursively. */
+	if (duk_peval_string(data->ctx, "p = Irccd.Directory.find(SOURCE + '/data/root', 'file-2.txt', true);") != 0)
+		GREATEST_FAIL();
+
+	duk_get_global_string(data->ctx, "p");
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root/level-1/level-2/file-2.txt", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_remove(void)
+{
+	struct stat st;
+
+	/* First create an empty directory. */
+	mkdir(BINARY "/empty", 0700);
+
+	/* Not recursive. */
+	if (duk_peval_string(data->ctx, "Irccd.Directory.remove(BINARY + '/empty')") != 0) {
+		puts(duk_to_string(data->ctx, -1));
+		GREATEST_FAIL();
+	}
+
+	GREATEST_ASSERT_EQ(-1, stat(BINARY "/empty", &st));
+
+	mkdir(BINARY "/notempty", 0700);
+	mkdir(BINARY "/notempty/empty", 0700);
+
+	/* Not recursive. */
+	if (duk_peval_string(data->ctx, "Irccd.Directory.remove(BINARY + '/notempty', true)") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT_EQ(-1, stat(BINARY "/notempty", &st));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_mkdir(void)
+{
+	struct stat st;
+
+	remove(BINARY "/1/2");
+	remove(BINARY "/1");
+
+	if (duk_peval_string(data->ctx, "Irccd.Directory.mkdir(BINARY + '/1/2')") != 0) {
+		puts(duk_to_string(data->ctx, -1));
+		GREATEST_FAIL();
+	}
+
+	GREATEST_ASSERT_EQ(0, stat(BINARY "/1/2", &st));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_free)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(free_find);
+	GREATEST_RUN_TEST(free_remove);
+	GREATEST_RUN_TEST(free_mkdir);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_object);
+	GREATEST_RUN_SUITE(suite_free);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-file.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,452 @@
+/*
+ * test-jsapi-file.c -- test Irccd.File API
+ *
+ * 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 <sys/stat.h>
+#include <stdio.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+
+	duk_push_string(data->ctx, SOURCE);
+	duk_put_global_string(data->ctx, "SOURCE");
+
+	duk_push_string(data->ctx, BINARY);
+	duk_put_global_string(data->ctx, "BINARY");
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+free_basename(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.File.basename('/usr/local/etc/irccd.conf');"))
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("irccd.conf", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_dirname(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.File.dirname('/usr/local/etc/irccd.conf');"))
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("/usr/local/etc", duk_get_string(data->ctx, -1));
+	
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_exists(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.File.exists(SOURCE + '/data/root/file-1.txt')"))
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_exists2(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.File.exists('file_which_does_not_exist.txt')"))
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(!duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+free_remove(void)
+{
+	FILE *fp;
+	struct stat st;
+
+	if (!(fp = fopen(BINARY "/test.bin", "w")))
+		GREATEST_FAIL();
+
+	fclose(fp);
+
+	if (duk_peval_string(data->ctx, "Irccd.File.remove(BINARY + '/test.bin')") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(stat(BINARY "/test.bin", &st) < 0);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_free)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(free_basename);
+	GREATEST_RUN_TEST(free_dirname);
+	GREATEST_RUN_TEST(free_exists);
+	GREATEST_RUN_TEST(free_exists2);
+	GREATEST_RUN_TEST(free_remove);
+}
+
+GREATEST_TEST
+object_basename(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"result = f.basename();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("file-1.txt", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_basename_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"f.close();"
+		"result = f.basename();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("file-1.txt", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_dirname(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"result = f.dirname();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_dirname_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"f.close();"
+		"result = f.dirname();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ(SOURCE "/data/root", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_lines(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"result = new Irccd.File(SOURCE + '/data/root/lines.txt', 'r').lines();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_EQ(3, duk_get_length(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -1, 0));
+	GREATEST_ASSERT_STR_EQ("a", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -2, 1));
+	GREATEST_ASSERT_STR_EQ("b", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -3, 2));
+	GREATEST_ASSERT_STR_EQ("c", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_lines_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  f = new Irccd.File(SOURCE + '/data/root/lines.txt', 'r');"
+		"  f.close();"
+		"  f.lines();"
+		"} catch (e) {"
+		"  name = e.name;"
+		"}"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_seek1(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"f.seek(Irccd.File.SeekSet, 6);"
+		"result = f.read(1);"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ(".", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_seek2(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"f.seek(Irccd.File.SeekSet, 2);"
+		"f.seek(Irccd.File.SeekCur, 4);"
+		"result = f.read(1);"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ(".", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_seek3(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"f.seek(Irccd.File.SeekEnd, -2);"
+		"result = f.read(1);"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("t", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_seek_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"  f.close();"
+		"  f.seek(Irccd.File.SeekEnd, -2);"
+		"} catch (e) {"
+		"  name = e.name"
+		"}"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_read(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"result = f.read();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("file-1.txt\n", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_read_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  f = new Irccd.File(SOURCE + '/data/root/file-1.txt', 'r');"
+		"  f.close();"
+		"  f.read();"
+		"} catch (e) {"
+		"  name = e.name;"
+		"}"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_readline(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"result = [];"
+		"f = new Irccd.File(SOURCE + '/data/root/lines.txt', 'r');"
+		"for (var s; s = f.readline(); ) {"
+		"  result.push(s);"
+		"}"
+	);
+
+	if (ret != 0) {
+		puts(duk_to_string(data->ctx, -1));
+		GREATEST_FAIL();
+	}
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_EQ(3, duk_get_length(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -1, 0));
+	GREATEST_ASSERT_STR_EQ("a", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -2, 1));
+	GREATEST_ASSERT_STR_EQ("b", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_prop_index(data->ctx, -3, 2));
+	GREATEST_ASSERT_STR_EQ("c", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+object_readline_closed(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  result = [];"
+		"  f = new Irccd.File(SOURCE + '/data/root/lines.txt', 'r');"
+		"  f.close();"
+		"  for (var s; s = f.readline(); ) {"
+		"    result.push(s);"
+		"  }"
+		"} catch (e) {"
+		"  name = e.name;"
+		"}\n"
+
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_EQ(0, duk_get_length(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_object)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(object_basename);
+	GREATEST_RUN_TEST(object_basename_closed);
+	GREATEST_RUN_TEST(object_dirname);
+	GREATEST_RUN_TEST(object_dirname_closed);
+	GREATEST_RUN_TEST(object_lines);
+	GREATEST_RUN_TEST(object_lines_closed);
+	GREATEST_RUN_TEST(object_seek1);
+	GREATEST_RUN_TEST(object_seek2);
+	GREATEST_RUN_TEST(object_seek3);
+	GREATEST_RUN_TEST(object_seek_closed);
+	GREATEST_RUN_TEST(object_read);
+	GREATEST_RUN_TEST(object_read_closed);
+	GREATEST_RUN_TEST(object_readline);
+	GREATEST_RUN_TEST(object_readline_closed);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_free);
+	GREATEST_RUN_SUITE(suite_object);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-irccd.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,167 @@
+/*
+ * test-jsapi-irccd.c -- test Irccd API
+ *
+ * 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 <errno.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <config.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/jsapi-system.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+static int
+throw(duk_context *ctx)
+{
+	errno = EINVAL;
+	irc_jsapi_system_raise(ctx);
+
+	return 0;
+}
+
+GREATEST_TEST
+basics_version(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"major = Irccd.version.major;"
+		"minor = Irccd.version.minor;"
+		"patch = Irccd.version.patch;"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "major"));
+	GREATEST_ASSERT_EQ(IRCCD_VERSION_MAJOR, duk_get_int(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "minor"));
+	GREATEST_ASSERT_EQ(IRCCD_VERSION_MINOR, duk_get_int(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "patch"));
+	GREATEST_ASSERT_EQ(IRCCD_VERSION_PATCH, duk_get_int(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_system_error_from_js(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  throw new Irccd.SystemError(1, 'test');"
+		"} catch (e) {"
+		"  errno = e.errno;"
+		"  name = e.name;"
+		"  message = e.message;"
+		"  v1 = (e instanceof Error);"
+		"  v2 = (e instanceof Irccd.SystemError);"
+		"}"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "errno"));
+	GREATEST_ASSERT_EQ(1, duk_get_int(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "message"));
+	GREATEST_ASSERT_STR_EQ("test", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "v1"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "v2"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_system_error_from_c(void)
+{
+	duk_push_c_function(data->ctx, throw, 0);
+	duk_put_global_string(data->ctx, "f");
+
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  f();"
+		"} catch (e) {"
+		"  errno = e.errno;"
+		"  name = e.name;"
+		"  v1 = (e instanceof Error);"
+		"  v2 = (e instanceof Irccd.SystemError);"
+		"}"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "errno"));
+	GREATEST_ASSERT_EQ(EINVAL, duk_get_int(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("SystemError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "v1"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "v2"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(basics_version);
+	GREATEST_RUN_TEST(basics_system_error_from_js);
+	GREATEST_RUN_TEST(basics_system_error_from_c);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-system.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,121 @@
+/*
+ * test-jsapi-system.c -- test Irccd.System API
+ *
+ * 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.
+ */
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+// TODO: irccd/
+#include <config.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+basics_popen(void)
+{
+	int ret = duk_peval_string(data->ctx,
+		"f = Irccd.System.popen(\"" IRCCD_EXECUTABLE " version\", \"r\");"
+		"r = f.readline();"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "r"));
+	GREATEST_ASSERT_STR_EQ(IRCCD_VERSION, duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_sleep(void)
+{
+	time_t start, now;
+
+	start = time(NULL);
+
+	if (duk_peval_string(data->ctx, "Irccd.System.sleep(2)") != 0)
+		GREATEST_FAIL();
+
+	now = time(NULL);
+
+	GREATEST_ASSERT_IN_RANGE(2000LL, difftime(now, start) * 1000LL, 100LL);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_usleep(void)
+{
+	time_t start, now;
+
+	start = time(NULL);
+
+	if (duk_peval_string(data->ctx, "Irccd.System.usleep(2000000)") != 0)
+		GREATEST_FAIL();
+
+	now = time(NULL);
+
+	GREATEST_ASSERT_IN_RANGE(2000LL, difftime(now, start) * 1000LL, 100LL);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(basics_popen);
+	GREATEST_RUN_TEST(basics_sleep);
+	GREATEST_RUN_TEST(basics_usleep);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-timer.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,104 @@
+/*
+ * test-jsapi-timer.c -- test Irccd.System API
+ *
+ * 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.
+ */
+
+/* TODO: We need proper bot function to dispatch */
+
+#if 0
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+// TODO: irccd/
+#include <config.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/timer.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+static void
+set_type(const char *name)
+{
+	duk_get_global_string(data->ctx, "Irccd");
+	duk_get_prop_string(data->ctx, -1, "Timer");
+	duk_get_prop_string(data->ctx, -1, name.c_str());
+	duk_put_global_string(data->ctx, "type");
+	duk_pop_n(data->ctx, 2);
+
+	plugin_->open();
+	plugin_->handle_load(bot_);
+}
+
+BOOST_AUTO_TEST_CASE(single)
+{
+	boost::timer::cpu_timer timer;
+
+	set_type("Single");
+
+	while (timer.elapsed().wall / 1000000LL < 3000) {
+		ctx_.reset();
+		ctx_.poll();
+	}
+
+	BOOST_TEST(duk_get_global_string(data->ctx, "count"));
+	BOOST_TEST(duk_get_int(data->ctx, -1) == 1);
+}
+
+BOOST_AUTO_TEST_CASE(repeat)
+{
+	boost::timer::cpu_timer timer;
+
+	set_type("Repeat");
+
+	while (timer.elapsed().wall / 1000000LL < 3000) {
+		ctx_.reset();
+		ctx_.poll();
+	}
+
+	BOOST_TEST(duk_get_global_string(data->ctx, "count"));
+	BOOST_TEST(duk_get_int(data->ctx, -1) >= 5);
+}
+
+#endif
+
+int
+main(void)
+{
+	
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-unicode.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,116 @@
+/*
+ * test-jsapi-unicode.c -- test Irccd.Unicode API
+ *
+ * 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.
+ */
+
+/*
+ * /!\ Be sure that this file is kept saved in UTF-8 /!\
+ */
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+// TODO: irccd/
+#include <config.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+basics_is_letter(void)
+{
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isLetter(String('é').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isLetter(String('€').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(!duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_is_lower(void)
+{
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isLower(String('é').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isLower(String('É').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(!duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_is_upper(void)
+{
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isUpper(String('É').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(duk_get_boolean(data->ctx, -1));
+
+	duk_peval_string_noresult(data->ctx, "result = Irccd.Unicode.isUpper(String('é').charCodeAt(0));");
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT(!duk_get_boolean(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(basics_is_letter);
+	GREATEST_RUN_TEST(basics_is_lower);
+	GREATEST_RUN_TEST(basics_is_upper);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-jsapi-util.c	Thu Jan 28 14:20:58 2021 +0100
@@ -0,0 +1,355 @@
+/*
+ * test-jsapi-util.c -- test Irccd.Util API
+ *
+ * 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.
+ */
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/js-plugin.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin *plugin;
+static struct irc_js_plugin_data *data;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	plugin = irc_js_plugin_open(SOURCE "/data/example-plugin.js");
+	data = plugin->data;
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(plugin);
+
+	plugin = NULL;
+	data = NULL;
+}
+
+GREATEST_TEST
+basics_splituser(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.Util.splituser(\"user!~user@hyper/super/host\");") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("user", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_splithost(void)
+{
+	if (duk_peval_string(data->ctx, "result = Irccd.Util.splithost(\"user!~user@hyper/super/host\");") != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("hyper/super/host", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(basics_splituser);
+	GREATEST_RUN_TEST(basics_splithost);
+}
+
+GREATEST_TEST
+format_simple(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"result = Irccd.Util.format(\"#{target}\", { target: \"markand\" })"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "result"));
+	GREATEST_ASSERT_STR_EQ("markand", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_format)
+{
+	GREATEST_RUN_TEST(format_simple);
+}
+
+GREATEST_TEST
+cut_string_simple(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut('hello world');\n"
+		"line0 = lines[0];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_double(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut('hello world', 5);\n"
+		"line0 = lines[0];\n"
+		"line1 = lines[1];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line1"));
+	GREATEST_ASSERT_STR_EQ("world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_dirty(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut('	 hello	world	 ', 5);\n"
+		"line0 = lines[0];\n"
+		"line1 = lines[1];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line1"));
+	GREATEST_ASSERT_STR_EQ("world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_too_much_lines(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {"
+		"  lines = Irccd.Util.cut('abc def ghi jkl', 3, 3);"
+		"} catch (e) {\n"
+		"  name = e.name;\n"
+		"  message = e.message;\n"
+		"}\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("RangeError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "message"));
+	GREATEST_ASSERT_STR_EQ("number of lines exceeds maxl (3)", duk_get_string(data->ctx, -1));
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_token_too_big(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {\n"
+		"  lines = Irccd.Util.cut('hello world', 3);\n"
+		"} catch (e) {\n"
+		"  name = e.name;\n"
+		"  message = e.message;\n"
+		"}\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("RangeError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "message"));
+	GREATEST_ASSERT_STR_EQ("token 'hello' could not fit in maxc limit (3)", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_negative_maxc(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {\n"
+		"  lines = Irccd.Util.cut('hello world', -3);\n"
+		"} catch (e) {\n"
+		"  name = e.name;\n"
+		"  message = e.message;\n"
+		"}\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("RangeError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "message"));
+	GREATEST_ASSERT_STR_EQ("argument 1 (maxc) must be positive", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_string_negative_maxl(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {\n"
+		"  lines = Irccd.Util.cut('hello world', undefined, -1);\n"
+		"} catch (e) {\n"
+		"  name = e.name;\n"
+		"  message = e.message;\n"
+		"}\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("RangeError", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "message"));
+	GREATEST_ASSERT_STR_EQ("argument 2 (maxl) must be positive", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_array_simple(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut([ 'hello', 'world' ]);\n"
+		"line0 = lines[0];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_array_double(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut([ 'hello', 'world' ], 5);\n"
+		"line0 = lines[0];\n"
+		"line1 = lines[1];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line1"));
+	GREATEST_ASSERT_STR_EQ("world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_array_dirty(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"lines = Irccd.Util.cut([ '   ', ' hello  ', '  world ', '	'], 5);\n"
+		"line0 = lines[0];\n"
+		"line1 = lines[1];\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line0"));
+	GREATEST_ASSERT_STR_EQ("hello", duk_get_string(data->ctx, -1));
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "line1"));
+	GREATEST_ASSERT_STR_EQ("world", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+cut_invalid_data(void)
+{
+	const int ret = duk_peval_string(data->ctx,
+		"try {\n"
+		"  lines = Irccd.Util.cut(123);\n"
+		"} catch (e) {\n"
+		"  name = e.name;\n"
+		"  message = e.message;\n"
+		"}\n"
+	);
+
+	if (ret != 0)
+		GREATEST_FAIL();
+
+	GREATEST_ASSERT(duk_get_global_string(data->ctx, "name"));
+	GREATEST_ASSERT_STR_EQ("TypeError", duk_get_string(data->ctx, -1));
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_cut)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(cut_string_simple);
+	GREATEST_RUN_TEST(cut_string_double);
+	GREATEST_RUN_TEST(cut_string_dirty);
+	GREATEST_RUN_TEST(cut_string_too_much_lines);
+	GREATEST_RUN_TEST(cut_string_token_too_big);
+	GREATEST_RUN_TEST(cut_string_negative_maxc);
+	GREATEST_RUN_TEST(cut_string_negative_maxl);
+	GREATEST_RUN_TEST(cut_array_simple);
+	GREATEST_RUN_TEST(cut_array_double);
+	GREATEST_RUN_TEST(cut_array_dirty);
+	GREATEST_RUN_TEST(cut_invalid_data);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_RUN_SUITE(suite_cut);
+	GREATEST_MAIN_END();
+
+	return 0;
+}