Introduction
Introduction Statistics Contact Development Disclaimer Help
tAdd basic infrastructure for sort of automated tests - ledit - Text editor (WI…
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log
Files
Refs
README
LICENSE
---
commit 225b064c19ce96a38c5472b3d73367922f671dc7
parent 155a081f559a9ecfddd8b086c7d91187c2af4bc2
Author: lumidify <[email protected]>
Date: Sat, 4 Nov 2023 19:11:19 +0100
Add basic infrastructure for sort of automated tests
Now that I have the infrastructure, I can forget about
writing the actual tests. That's how it works, right?
Diffstat:
M Makefile | 3 ++-
M keys_basic.c | 9 +--------
M keys_basic.h | 2 +-
M keys_command.c | 38 ++++++++++++++++++++++-------…
M keys_command.h | 2 +-
M ledit.c | 365 +++++++++++++++++++++++++++++…
A tests/README | 59 +++++++++++++++++++++++++++++…
M undo.c | 27 +++++++++++++++------------
M undo.h | 4 ++++
M view.h | 2 +-
10 files changed, 466 insertions(+), 45 deletions(-)
---
diff --git a/Makefile b/Makefile
t@@ -12,6 +12,7 @@ MAN5 = leditrc.5
MISCFILES = Makefile README LICENSE IDEAS NOTES TODO
DEBUG=0
+TEST=0
SANITIZE=0
ENABLE_UTF8PROC=1
t@@ -78,7 +79,7 @@ EXTRA_LDFLAGS_UTF8PROC1 = `pkg-config --libs libutf8proc`
# Xcursor isn't actually needed right now since I'm not using the drag 'n drop…
# of ctrlsel yet, but since it's moderately likely that I will use that in the…
# decided to just leave it in.
-CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}…
+CFLAGS_LEDIT = -DTEST=${TEST} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAG…
LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBU…
all: ${BIN}
diff --git a/keys_basic.c b/keys_basic.c
t@@ -2709,14 +2709,7 @@ repeat_command(ledit_view *view, char *text, size_t len…
}
struct action
-basic_key_handler(ledit_view *view, XEvent *event, int lang_index) {
- char *buf = NULL;
- KeySym sym = NoSymbol;
- int n;
-
- unsigned int key_state = event->xkey.state;
- preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
-
+basic_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char *…
struct repetition_stack_elem *re = push_repetition_stack();
re->key_text = ledit_strndup(buf, (size_t)n);
re->len = (size_t)n;
diff --git a/keys_basic.h b/keys_basic.h
t@@ -11,6 +11,6 @@ int basic_key_cb_modemask_is_valid(basic_key_cb *cb, ledit_m…
/* perform cleanup of global data */
void basic_key_cleanup(void);
-struct action basic_key_handler(ledit_view *view, XEvent *event, int lang_inde…
+struct action basic_key_handler(ledit_view *view, unsigned int key_state, KeyS…
#endif
diff --git a/keys_command.c b/keys_command.c
t@@ -215,11 +215,16 @@ static int parse_range(
static int handle_cmd(ledit_view *view, char *cmd, size_t len, size_t lang_ind…
/* FIXME: USE LEN EVERYWHERE INSTEAD OF RELYING ON cmd BEING NUL-TERMINATED */
-/* FIXME: return error so write_quit knows when to quit */
+/* returns 1 on error, 0 otherwise */
static int
-handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
- (void)l1;
- (void)l2;
+handle_write_base(ledit_view *view, char *cmd) {
+ #if TEST
+ /* disallow normal file writing in test mode so no
+ file can accidentally be destroyed by fuzz testing */
+ (void)view;
+ (void)cmd;
+ return 0;
+ #else
/* FIXME: allow writing only part of file */
char *filename = view->buffer->filename;
int stored = 1;
t@@ -248,6 +253,7 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size_…
"%s: file modification time changed; use ! to over…
filename
);
+ return 1;
/* FIXME: I guess the file can still exist if stat returns an …
but the writing itself will probably fail then as well. */
} else if (!ret && !force && !stored) {
t@@ -256,8 +262,10 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size…
"%s: file exists; use ! to override",
filename
);
+ return 1;
} else if (buffer_write_to_filename(view->buffer, filename, &e…
window_show_message_fmt(view->window, "Error writing %…
+ return 1;
} else {
/* FIXME: better message */
window_show_message_fmt(view->window, "Wrote file %s",…
t@@ -270,8 +278,18 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size…
}
} else {
window_show_message(view->window, "No file name", -1);
+ return 1;
}
return 0;
+ #endif
+}
+
+static int
+handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
+ (void)l1;
+ (void)l2;
+ handle_write_base(view, cmd);
+ return 0;
}
static int
t@@ -320,7 +338,10 @@ close_view(ledit_view *view, char *cmd, size_t l1, size_t…
static int
handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2) {
- handle_write(view, cmd, l1, l2);
+ (void)l1;
+ (void)l2;
+ if (handle_write_base(view, cmd))
+ return 0;
ledit_cleanup();
exit(0);
return 0;
t@@ -984,14 +1005,9 @@ edit_discard(ledit_view *view, char *key_text, size_t le…
}
struct action
-command_key_handler(ledit_view *view, XEvent *event, int lang_index) {
- char *buf = NULL;
- KeySym sym = NoSymbol;
- int n;
+command_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char…
command_key_array *cur_keys = config_get_command_keys(lang_index);
size_t num_keys = cur_keys->num_keys;
- unsigned int key_state = event->xkey.state;
- preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
int grabkey = 1, found = 0;
command_key_cb_flags flags = KEY_FLAG_NONE;
for (size_t i = 0; i < num_keys; i++) {
diff --git a/keys_command.h b/keys_command.h
t@@ -17,6 +17,6 @@ void search_next(ledit_view *view);
void search_prev(ledit_view *view);
void command_key_cleanup(void);
-struct action command_key_handler(ledit_view *view, XEvent *event, int lang_in…
+struct action command_key_handler(ledit_view *view, unsigned int key_state, Ke…
#endif
diff --git a/ledit.c b/ledit.c
t@@ -12,6 +12,9 @@
#include <pwd.h>
#include <time.h>
+#if TEST
+#include <fcntl.h>
+#endif
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
t@@ -45,15 +48,322 @@ static void setup(int argc, char *argv[]);
static void redraw(void);
static void change_keyboard(char *lang);
-static void key_press(ledit_view *view, XEvent *event);
+static void key_press_event(ledit_view *view, XEvent *event);
+static void key_press(ledit_view *view, unsigned int key_state, KeySym sym, ch…
ledit_common common;
ledit_clipboard *clipboard = NULL;
ledit_buffer *buffer = NULL;
size_t cur_lang = 0;
+#if TEST
+static struct {
+ char *read; /* text read from stdin */
+ size_t read_len; /* length of text in read buffer */
+ size_t read_alloc; /* size of read buffer */
+ size_t line_start; /* start of current line */
+ size_t read_cur; /* length of text already read */
+} test_status = {NULL, 0, 0, 0, 0};
+
+#define READ_BLK_SIZE 128
+
+/* Read up to READ_BLK_SIZE bytes from stdin.
+ Returns 1 if an error occurred, -1 if not new data available, 0 otherwise. …
+static int
+read_input(void) {
+ if (test_status.read_cur > 0) {
+ memmove(test_status.read, test_status.read + test_status.read_…
+ test_status.read_len -= test_status.read_cur;
+ test_status.read_cur = 0;
+ }
+ int nread;
+ test_status.read_alloc = ideal_array_size(test_status.read_alloc, test…
+ test_status.read = ledit_realloc(test_status.read, test_status.read_al…
+ nread = read(fileno(stdin), test_status.read + test_status.read_len, R…
+ if (nread == -1 && errno == EAGAIN)
+ return -1;
+ else if (nread == -1 || nread == 0)
+ return 1;
+ test_status.read_len += nread;
+
+ return 0;
+}
+
+/* based partially on OpenBSD's strtonum */
+int
+read_rangeint(long long *ret, int end, long long min, long long max) {
+ if (test_status.read_cur >= test_status.read_len || test_status.read[t…
+ return 1;
+ char end_char = end ? '\n' : ' ';
+ size_t len = 0;
+ test_status.read_cur++;
+ char *str = test_status.read + test_status.read_cur;
+ int found = 0;
+ for (; test_status.read_cur < test_status.read_len; test_status.read_c…
+ if (test_status.read[test_status.read_cur] == end_char) {
+ found = 1;
+ break;
+ }
+ len++;
+ }
+ if (!found || len == 0)
+ return 1;
+ /* the string needs to be nul-terminated
+ if it contains more than 11 characters (10 digits + sign),
+ it's illegal anyways (at least for these testing purposes...) */
+ if (len > 11)
+ return 1;
+ char nstr[12];
+ strncpy(nstr, str, len);
+ nstr[len] = '\0';
+ char *num_end;
+ long long ll = strtoll(nstr, &num_end, 10);
+ if (nstr == num_end || *num_end != '\0' ||
+ ll < min || ll > max || ((ll == LLONG_MIN ||
+ ll == LLONG_MAX) && errno == ERANGE)) {
+ return 1;
+ }
+ *ret = ll;
+ if (end)
+ test_status.read_cur++;
+ return 0;
+}
+
+int
+read_uint(unsigned int *ret, int end) {
+ long long l;
+ int err = read_rangeint(&l, end, 0, UINT_MAX);
+ *ret = (unsigned int)l;
+ return err;
+}
+
+int
+read_int(int *ret, int end) {
+ long long l;
+ int err = read_rangeint(&l, end, INT_MIN, INT_MAX);
+ *ret = (int)l;
+ return err;
+}
+
+int
+read_text(char **text, size_t *text_len) {
+ if (test_status.read_cur >= test_status.read_len || test_status.read[t…
+ return 1;
+ int bs = 0;
+ int offset = 0;
+ test_status.read_cur++;
+ size_t start = test_status.read_cur;
+ *text = test_status.read + test_status.read_cur;
+ int found = 0;
+ for (; test_status.read_cur < test_status.read_len; test_status.read_c…
+ if (test_status.read[test_status.read_cur] == '\\') {
+ bs++;
+ if (bs / 2)
+ offset++;
+ bs %= 2;
+ test_status.read[test_status.read_cur - offset] = '\\';
+ } else if (test_status.read[test_status.read_cur] == '\n') {
+ if (!bs) {
+ found = 1;
+ break;
+ } else {
+ bs = 0;
+ offset++;
+ test_status.read[test_status.read_cur - offset…
+ }
+ } else {
+ test_status.read[test_status.read_cur - offset] = test…
+ bs = 0;
+ }
+ }
+ if (!found)
+ return 1;
+ *text_len = test_status.read_cur - start - offset;
+ test_status.read_cur++;
+ return 0;
+}
+
+int
+read_filename(char **text, size_t *text_len) {
+ if (read_text(text, text_len))
+ return 1;
+ for (size_t i = 0; i < *text_len; i++) {
+ if ((*text)[i] == '/' || (*text)[i] == '\0')
+ return 1;
+ }
+ return 0;
+}
+
+static unsigned int view_num = 0;
+/* Process commands in test_status.
+ Returns 0 if no complete commands are contained in read buffer, 1 otherwise…
+static int
+process_commands(void) {
+ int bs = 0;
+ int found = 0;
+ size_t nl_index = 0;
+ for (size_t i = test_status.read_cur; i < test_status.read_len; i++) {
+ if (test_status.read[i] == '\\') {
+ bs++;
+ bs %= 2;
+ } else if (test_status.read[i] == '\n' && bs == 0) {
+ found = 1;
+ nl_index = i;
+ break;
+ } else {
+ bs = 0;
+ }
+ }
+ if (!found)
+ return 0;
+ unsigned int key_state, button_num, keysym, new_view;
+ char *text, *term, *errstr;
+ size_t text_len;
+ int x, y;
+ XEvent e;
+ FILE *file;
+ test_status.read_cur += 1;
+ ledit_view *view = buffer->views[view_num];
+ switch (test_status.read[test_status.read_cur-1]) {
+ case 'k':
+ /* key press */
+ /* k key_state keysym text */
+ if (read_uint(&key_state, 0))
+ goto error;
+ if (read_uint(&keysym, 0))
+ goto error;
+ if (read_text(&text, &text_len))
+ goto error;
+ key_press(view, key_state, keysym, text, (int)text_len…
+ break;
+ case 'p':
+ /* mouse button press */
+ /* p button_num x y */
+ if (read_uint(&button_num, 0))
+ goto error;
+ if (read_int(&x, 0))
+ goto error;
+ if (read_int(&y, 1))
+ goto error;
+ e = (XEvent){.xbutton = {.type = ButtonPress, .button …
+ window_register_button_press(view->window, &e);
+ break;
+ case 'r':
+ /* mouse button release */
+ /* r button_num x y */
+ if (read_uint(&button_num, 0))
+ goto error;
+ if (read_int(&x, 0))
+ goto error;
+ if (read_int(&y, 1))
+ goto error;
+ e = (XEvent){.xbutton = {.type = ButtonRelease, .butto…
+ window_button_release(view->window, &e);
+ break;
+ case 'm':
+ /* mouse motion */
+ /* m x y */
+ if (read_int(&x, 0))
+ goto error;
+ if (read_int(&y, 1))
+ goto error;
+ e = (XEvent){.xmotion = {.type = MotionNotify, .x = x,…
+ window_register_motion(view->window, &e);
+ break;
+ case 'l':
+ /* language switch */
+ /* l lang_name */
+ if (read_text(&text, &text_len))
+ goto error;
+ term = ledit_strndup(text, text_len);
+ change_keyboard(term);
+ free(term);
+ break;
+ case 's':
+ /* switch view */
+ /* s view_num */
+ if (read_uint(&new_view, 1))
+ goto error;
+ if (new_view >= buffer->views_num)
+ fprintf(stderr, "Invalid view number %u\n", ne…
+ else
+ view_num = new_view;
+ break;
+ case 'w':
+ /* write contents of buffer */
+ /* w file_name */
+ if (read_filename(&text, &text_len))
+ goto error;
+ term = ledit_strndup(text, text_len);
+ if (buffer_write_to_filename(buffer, term, &errstr))
+ fprintf(stderr, "Error writing %s: %s\n", term…
+ free(term);
+ break;
+ case 'd':
+ /* dump other info to file */
+ /* d file_name */
+ if (read_filename(&text, &text_len))
+ goto error;
+ term = ledit_strndup(text, text_len);
+ file = fopen(term, "w");
+ if (!file) {
+ fprintf(stderr, "Unable to open file %s\n", te…
+ } else {
+ fprintf(
+ file,
+ "cursor_line: %zu, cursor_byte: %zu, sel_v…
+ "sel_line1: %zu, sel_byte1: %zu, "
+ "sel_line2: %zu, sel_byte2: %zu\n",
+ view->cur_line, view->cur_index, view->sel…
+ view->sel.line1, view->sel.byte1,
+ view->sel.line2, view->sel.byte2
+ );
+ fclose(file);
+ }
+ free(term);
+ break;
+ case 'u':
+ /* dump undo stack to file */
+ if (read_filename(&text, &text_len))
+ goto error;
+ /* u file_name */
+ term = ledit_strndup(text, text_len);
+ file = fopen(term, "w");
+ if (!file) {
+ fprintf(stderr, "Unable to open file %s\n", te…
+ } else {
+ dump_undo_stack(file, buffer->undo);
+ fclose(file);
+ }
+ free(term);
+ break;
+ default:
+ goto error;
+ }
+ return 1;
+error:
+ fprintf(stderr, "Error parsing command.\n");
+ test_status.read_cur = nl_index + 1;
+ return 1;
+}
+#endif
+
+/* can only be set to 1 when compiled with TEST */
+static int test_extra = 0;
+
static void
mainloop(void) {
+ #if TEST
+ int flags = fcntl(fileno(stdin), F_GETFL, 0);
+ if (flags == -1) {
+ fprintf(stderr, "Unable to set non-blocking mode on stdin.\n");
+ return;
+ }
+ if (fcntl(fileno(stdin), F_SETFL, flags | O_NONBLOCK)) {
+ fprintf(stderr, "Unable to set non-blocking mode on stdin.\n");
+ return;
+ }
+ #endif
XEvent event;
int xkb_event_type;
int major, minor;
t@@ -138,16 +448,20 @@ mainloop(void) {
window_register_resize(view->window, &event);
break;
case ButtonPress:
- window_register_button_press(view->window, &ev…
+ if (!test_extra)
+ window_register_button_press(view->win…
break;
case ButtonRelease:
- window_button_release(view->window, &event);
+ if (!test_extra)
+ window_button_release(view->window, &e…
break;
case MotionNotify:
- window_register_motion(window, &event);
+ if (!test_extra)
+ window_register_motion(window, &event);
break;
case KeyPress:
- key_press(view, &event);
+ if (!test_extra)
+ key_press_event(view, &event);
break;
case ClientMessage:
if ((Atom)event.xclient.data.l[0] == view->win…
t@@ -161,11 +475,22 @@ mainloop(void) {
}
};
+ #if TEST
+ int ret;
+ if ((ret = read_input()) == 1) {
+ fprintf(stderr, "Unable to read text from stdin.\n");
+ } else if (ret == 0) {
+ while (process_commands()) {
+ /* NOP */
+ }
+ }
+ #endif
+
for (size_t i = 0; i < buffer->views_num; i++) {
window_handle_filtered_events(buffer->views[i]->window…
}
- if (change_kbd) {
+ if (!test_extra && change_kbd) {
change_kbd = 0;
XkbStateRec s;
XkbGetState(common.dpy, XkbUseCoreKbd, &s);
t@@ -201,11 +526,21 @@ setup(int argc, char *argv[]) {
char c;
char *opt_filename = NULL;
- while ((c = getopt(argc, argv, "c:")) != -1) {
+ #if TEST
+ char *opts = "tc:";
+ #else
+ char *opts = "c:";
+ #endif
+ while ((c = getopt(argc, argv, opts)) != -1) {
switch (c) {
case 'c':
opt_filename = optarg;
break;
+ #if TEST
+ case 't':
+ test_extra = 1;
+ break;
+ #endif
default:
fprintf(stderr, "USAGE: ledit [-c config] [file]\n");
exit(1);
t@@ -517,16 +852,26 @@ change_keyboard(char *lang) {
}
static void
-key_press(ledit_view *view, XEvent *event) {
+key_press(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int…
/* FIXME: just let view handle this since the action is part
of it anyways now */
if (view->cur_action.type == ACTION_GRABKEY && view->cur_action.callba…
- view->cur_action = view->cur_action.callback(view, event, cur_…
+ view->cur_action = view->cur_action.callback(view, key_state, …
} else {
- view->cur_action = basic_key_handler(view, event, cur_lang);
+ view->cur_action = basic_key_handler(view, key_state, sym, buf…
}
}
+static void
+key_press_event(ledit_view *view, XEvent *event) {
+ char *buf = NULL;
+ KeySym sym = NoSymbol;
+ int n;
+ unsigned int key_state = event->xkey.state;
+ preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
+ key_press(view, key_state, sym, buf, n);
+}
+
int
main(int argc, char *argv[]) {
setup(argc, argv);
diff --git a/tests/README b/tests/README
t@@ -0,0 +1,59 @@
+There aren't any proper tests currently, but some infrastructure is in place t…
+
+When compiled with TEST=1, ledit accepts commands on standard input to generat…
+Each command ends in newline. If the last argument is text, it may also contai…
+they are escaped with backslash. Single backslashes that are not in front of a…
+just taken verbatim, but two backslashes are collapsed into one. Filenames are…
+case because they are not allowed to contain '/' or '\0'.
+
+The commands to generate events take raw integers instead of symbolic names fo…
+and other parameters. These need to be given using the definitions from Xlib.
+
+The commands currently supported are the following:
+
+k <key_state> <keysym> <text>
+
+Generate a keypress event. <key_state> and <keysym> are the modifier state and…
+
+p <button_num> <x> <y>
+
+Generate a mouse button press event.
+
+r <button_num> <x> <y>
+
+Generate a mouse button release event.
+
+m <x> <y>
+
+Generate a mouse motion event.
+
+l <lang>
+
+Switch to keyboard layout <lang>.
+
+s <view_num>
+
+Switch to view <view_num>, if it exists.
+
+w <filename>
+
+Write the contents of the buffer to <filename>.
+
+d <filename>
+
+Dump various information to <filename>. Currently, the cursor position and
+information about the selection is given. See ledit.c for the exact format.
+
+u <filename>
+
+Dump the undo stack to <filename>. See undo.c for the exact format.
+
+TODO: Add more commands, e.g. for dumping the repetition stack.
+
+
+When compiled with TEST=1, ledit supports an additional command-line argument …
+This disables handling of the regular key press, mouse, and language switch ev…
+in order to avoid messing with the results.
+
+Note that regular file writing using :w is disabled when compiled with TEST=1
+in order to avoid overwriting anything important if fuzz testing is done.
diff --git a/undo.c b/undo.c
t@@ -101,22 +101,27 @@ undo_change_mode_group(undo_stack *undo) {
undo->change_mode_group = 1;
}
-/*
-static void
-dump_undo(undo_stack *undo) {
- printf("START UNDO STACK\n");
- printf("cur: %zu\n", undo->cur);
+#if TEST
+void
+dump_undo_stack(FILE *file, undo_stack *undo) {
+ fprintf(
+ file,
+ "cur: %zu, cur_valid: %d, change_mode_group: %d, len: %zu, cap: %z…
+ undo->cur, undo->cur_valid, undo->change_mode_group, undo->len, un…
+ );
for (size_t i = 0; i < undo->len; i++) {
undo_elem *e = &undo->stack[i];
- printf(
- "type %d, mode %d, group %d, mode_group %d, text '%.*s', r…
+ fprintf(
+ file,
+ "type %d, mode %d, group %d, mode_group %d, text '%.*s', "
+ "op_range (%zu,%zu;%zu,%zu), cursor_range (%zu,%zu;%zu,%zu…
e->type, e->mode, e->group, e->mode_group, (int)e->text->l…
- e->op_range.byte1, e->op_range.byte2
+ e->op_range.line1, e->op_range.byte1, e->op_range.line2, e…
+ e->cursor_range.line1, e->cursor_range.byte1, e->cursor_ra…
);
}
- printf("END UNDO STACK\n");
}
-*/
+#endif
static void
push_undo(
t@@ -140,7 +145,6 @@ push_undo(
txtbuf_copy(e->text, text);
else
e->text = txtbuf_dup(text);
- /* dump_undo(undo); */
}
void
t@@ -243,7 +247,6 @@ ledit_undo(undo_stack *undo, ledit_mode mode, void *callba…
*min_line_ret = min_line;
if (mode == NORMAL || mode == VISUAL)
undo_change_mode_group(undo);
- /* dump_undo(undo); */
return UNDO_NORMAL;
}
diff --git a/undo.h b/undo.h
t@@ -133,4 +133,8 @@ void undo_change_last_cur_range(undo_stack *undo, ledit_ra…
*/
char *undo_state_to_str(undo_status s);
+#if TEST
+void dump_undo_stack(FILE *file, undo_stack *undo);
+#endif
+
#endif
diff --git a/view.h b/view.h
t@@ -26,7 +26,7 @@ enum action_type {
main event manager what key handler to call next */
struct action {
enum action_type type;
- struct action (*callback)(ledit_view *view, XEvent *event, int lang_in…
+ struct action (*callback)(ledit_view *view, unsigned int key_state, Ke…
};
typedef struct {
You are viewing proxied material from lumidify.org. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.