From 599d9b5bbc5d0908dc8ca9a6bd58917d1e7c5c8a Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan@rename-it.nl>
Date: Sun, 19 Oct 2008 15:19:54 +0200
Subject: [PATCH] Testsuite: added support for basic result checking.

---
 TODO                                          |   3 -
 src/lib-sieve/sieve-result.c                  |  42 +++
 src/lib-sieve/sieve-result.h                  |  13 +-
 src/testsuite/Makefile.am                     |  21 +-
 src/testsuite/ext-testsuite.c                 |   6 +-
 src/testsuite/testsuite-common.c              |  66 ++--
 src/testsuite/testsuite-common.h              |   3 +
 src/testsuite/testsuite-result.c              |  43 +++
 src/testsuite/testsuite-result.h              |  14 +
 src/testsuite/tst-test-error.c                |  62 ++--
 src/testsuite/tst-test-result.c               | 318 ++++++++++++++++++
 tests/execute/actions.svtest                  |  38 +++
 tests/execute/actions/fileinto.sieve          |  11 +-
 tests/execute/actions/redirect.sieve          |   9 +-
 tests/extensions/reject/execute.svtest        |  10 +
 tests/extensions/vacation/execute.svtest      |  24 ++
 .../extensions/vacation/execute/action.sieve  |   4 +
 17 files changed, 609 insertions(+), 78 deletions(-)
 create mode 100644 src/testsuite/testsuite-result.c
 create mode 100644 src/testsuite/testsuite-result.h
 create mode 100644 src/testsuite/tst-test-result.c
 create mode 100644 tests/extensions/vacation/execute/action.sieve

diff --git a/TODO b/TODO
index f1c6674e1..0a2b73888 100644
--- a/TODO
+++ b/TODO
@@ -1,8 +1,5 @@
 Next (in order of descending priority/precedence):
 
-* Finish the test suite for the base functionality
-	- Add capability to test the result.
-
 * ## MAKE A FIRST RELEASE (0.1.x) ##
 
 * Fix remaining RFC deviations:
diff --git a/src/lib-sieve/sieve-result.c b/src/lib-sieve/sieve-result.c
index a8c13ae5e..ba406a210 100644
--- a/src/lib-sieve/sieve-result.c
+++ b/src/lib-sieve/sieve-result.c
@@ -760,6 +760,48 @@ int sieve_result_execute
 	return SIEVE_EXEC_OK;
 }
 
+/*
+ * Result evaluation
+ */
+
+struct sieve_result_iterate_context {
+	struct sieve_result *result;
+	struct sieve_result_action *action;
+};
+
+struct sieve_result_iterate_context *sieve_result_iterate_init
+(struct sieve_result *result)
+{
+	struct sieve_result_iterate_context *rictx = 
+		t_new(struct sieve_result_iterate_context, 1);
+	
+	rictx->result = result;
+	rictx->action = result->first_action;
+
+	return rictx;
+}
+
+const struct sieve_action *sieve_result_iterate_next
+	(struct sieve_result_iterate_context *rictx, void **context)
+{
+	struct sieve_result_action *act;
+
+	if ( rictx == NULL )
+		return  NULL;
+
+	act = rictx->action;
+	if ( act != NULL ) {
+		rictx->action = act->next;
+
+		if ( context != NULL )
+			*context = act->context;
+	
+		return act->action;
+	}
+
+	return NULL;
+}
+
 /*
  * Side effects list
  */
diff --git a/src/lib-sieve/sieve-result.h b/src/lib-sieve/sieve-result.h
index c0f8d0f8d..025a3eee3 100644
--- a/src/lib-sieve/sieve-result.h
+++ b/src/lib-sieve/sieve-result.h
@@ -90,7 +90,18 @@ bool sieve_result_implicit_keep
 int sieve_result_execute
 	(struct sieve_result *result, const struct sieve_message_data *msgdata,
 		const struct sieve_script_env *senv, struct sieve_exec_status *estatus);
-		
+
+/*
+ * Result evaluation
+ */
+
+struct sieve_result_iterate_context;
+
+struct sieve_result_iterate_context *sieve_result_iterate_init
+	(struct sieve_result *result);
+const struct sieve_action *sieve_result_iterate_next
+	(struct sieve_result_iterate_context *rictx, void **context);
+	
 /*
  * Side effects list
  */
diff --git a/src/testsuite/Makefile.am b/src/testsuite/Makefile.am
index d320a3284..d8e7f508c 100644
--- a/src/testsuite/Makefile.am
+++ b/src/testsuite/Makefile.am
@@ -20,19 +20,19 @@ libs = \
 	$(dovecot_incdir)/src/lib-storage/index/raw/libstorage_raw.a \
 	$(dovecot_incdir)/src/lib-storage/index/maildir/libstorage_maildir.a \
 	$(dovecot_incdir)/src/lib-storage/index/mbox/libstorage_mbox.a \
-    $(dovecot_incdir)/src/lib-storage/index/libstorage_index.a \
-    $(dovecot_incdir)/src/lib-storage/libstorage.a \
-    $(dovecot_incdir)/src/lib-index/libindex.a \
-    $(dovecot_incdir)/src/lib-imap/libimap.a \
-    $(dovecot_incdir)/src/lib-mail/libmail.a \
-    $(dovecot_incdir)/src/lib-charset/libcharset.a \
+	$(dovecot_incdir)/src/lib-storage/index/libstorage_index.a \
+	$(dovecot_incdir)/src/lib-storage/libstorage.a \
+	$(dovecot_incdir)/src/lib-index/libindex.a \
+	$(dovecot_incdir)/src/lib-imap/libimap.a \
+	$(dovecot_incdir)/src/lib-mail/libmail.a \
+	$(dovecot_incdir)/src/lib-charset/libcharset.a \
 	$(dovecot_incdir)/src/lib/liblib.a
 
 ldadd = \
 	$(libs) \
  	$(LIBICONV) \
-    $(RAND_LIBS) \
-    $(MODULE_LIBS)
+	$(RAND_LIBS) \
+	$(MODULE_LIBS)
 
 testsuite_LDADD = $(ldadd)
 testsuite_DEPENDENCIES = $(libs)
@@ -45,13 +45,15 @@ commands = \
 tests = \
 	tst-test-compile.c \
 	tst-test-execute.c \
-	tst-test-error.c
+	tst-test-error.c \
+	tst-test-result.c
 
 testsuite_SOURCES = \
 	namespaces.c \
 	mail-raw.c \
 	testsuite-common.c \
 	testsuite-objects.c \
+	testsuite-result.c \
 	$(commands) \
 	$(tests) \
 	ext-testsuite.c \
@@ -60,6 +62,7 @@ testsuite_SOURCES = \
 noinst_HEADERS = \
 	testsuite-common.h \
 	testsuite-objects.h \
+	testsuite-result.c \
 	namespaces.h \
 	mail-raw.h
 
diff --git a/src/testsuite/ext-testsuite.c b/src/testsuite/ext-testsuite.c
index e648fa29e..b01c50cbb 100644
--- a/src/testsuite/ext-testsuite.c
+++ b/src/testsuite/ext-testsuite.c
@@ -57,7 +57,8 @@ const struct sieve_operation *testsuite_operations[] = {
 	&test_set_operation,
 	&test_compile_operation,
 	&test_execute_operation,
-	&test_error_operation
+	&test_error_operation,
+	&test_result_operation
 };
 
 /* 
@@ -113,7 +114,8 @@ static bool ext_testsuite_validator_load(struct sieve_validator *valdtr)
 	sieve_validator_register_command(valdtr, &tst_test_compile);
 	sieve_validator_register_command(valdtr, &tst_test_execute);
 	sieve_validator_register_command(valdtr, &tst_test_error);
-	
+	sieve_validator_register_command(valdtr, &tst_test_result);	
+
 	return testsuite_validator_context_initialize(valdtr);
 }
 
diff --git a/src/testsuite/testsuite-common.c b/src/testsuite/testsuite-common.c
index a395c06ee..64e3e23d5 100644
--- a/src/testsuite/testsuite-common.c
+++ b/src/testsuite/testsuite-common.c
@@ -23,8 +23,9 @@
 #include "sieve-result.h"
 #include "sieve-dump.h"
 
-#include "testsuite-objects.h"
 #include "testsuite-common.h"
+#include "testsuite-objects.h"
+#include "testsuite-result.h"
 
 /*
  * Global data
@@ -96,7 +97,8 @@ static void _testsuite_message_set(string_t *message)
 	if ( sender == NULL ) 
 		sender = "sender@example.com";
 
-	memset(&testsuite_msgdata, 0, sizeof(testsuite_msgdata));	testsuite_msgdata.mail = mail;
+	memset(&testsuite_msgdata, 0, sizeof(testsuite_msgdata));	
+	testsuite_msgdata.mail = mail;
 	testsuite_msgdata.auth_user = testsuite_user;
 	testsuite_msgdata.return_path = sender;
 	testsuite_msgdata.to_address = recipient;
@@ -321,20 +323,20 @@ static void _testsuite_script_verror
 
 static struct sieve_error_handler *_testsuite_script_ehandler_create(void)
 {
-    pool_t pool;
-    struct sieve_error_handler *ehandler;
+	pool_t pool;
+	struct sieve_error_handler *ehandler;
 
-    /* Pool is not strictly necessary, but other handler types will need a pool,
-     * so this one will have one too.
-     */
-    pool = pool_alloconly_create
-        ("testsuite_script_error_handler", sizeof(struct sieve_error_handler));
-    ehandler = p_new(pool, struct sieve_error_handler, 1);
-    sieve_error_handler_init(ehandler, pool, 0);
+	/* Pool is not strictly necessary, but other handler types will need a pool,
+	 * so this one will have one too.
+	 */
+	pool = pool_alloconly_create
+		("testsuite_script_error_handler", sizeof(struct sieve_error_handler));
+	ehandler = p_new(pool, struct sieve_error_handler, 1);
+	sieve_error_handler_init(ehandler, pool, 0);
 
-    ehandler->verror = _testsuite_script_verror;
+	ehandler->verror = _testsuite_script_verror;
 
-    return ehandler;
+	return ehandler;
 }
 
 static void testsuite_script_clear_messages(void)
@@ -345,8 +347,8 @@ static void testsuite_script_clear_messages(void)
 		pool_unref(&_testsuite_scriptmsg_pool);
 	}
 
-	 _testsuite_scriptmsg_pool = pool_alloconly_create
-        ("testsuite_script_messages", 8192);
+	_testsuite_scriptmsg_pool = pool_alloconly_create
+		("testsuite_script_messages", 8192);
 	
 	p_array_init(&_testsuite_script_errors, _testsuite_scriptmsg_pool, 128);	
 
@@ -376,7 +378,7 @@ const char *testsuite_script_get_error_next(bool location)
 static void testsuite_script_init(void)
 {
 	test_script_ehandler = _testsuite_script_ehandler_create(); 	
-    sieve_error_handler_accept_infolog(test_script_ehandler, TRUE);
+	sieve_error_handler_accept_infolog(test_script_ehandler, TRUE);
 
 	testsuite_script_clear_messages();
 
@@ -390,22 +392,22 @@ bool testsuite_script_compile(const char *script_path)
 
 	testsuite_script_clear_messages();
 
-	    /* Initialize environment */
-    sieve_dir = strrchr(script_path, '/');
-    if ( sieve_dir == NULL )
-        sieve_dir= "./";
-    else
-        sieve_dir = t_strdup_until(script_path, sieve_dir+1);
+	/* Initialize environment */
+	sieve_dir = strrchr(script_path, '/');
+	if ( sieve_dir == NULL )
+		sieve_dir= "./";
+	else
+		sieve_dir = t_strdup_until(script_path, sieve_dir+1);
 
-    /* Currently needed for include (FIXME) */
-    env_put(t_strconcat("SIEVE_DIR=", sieve_dir, "included", NULL));
-    env_put(t_strconcat("SIEVE_GLOBAL_DIR=", sieve_dir, "included-global", NULL));
+	/* Currently needed for include (FIXME) */
+	env_put(t_strconcat("SIEVE_DIR=", sieve_dir, "included", NULL));
+	env_put(t_strconcat("SIEVE_GLOBAL_DIR=", sieve_dir, "included-global", NULL));
 
-    if ( (sbin = sieve_compile(script_path, test_script_ehandler)) == NULL )
-        return FALSE;
+	if ( (sbin = sieve_compile(script_path, test_script_ehandler)) == NULL )
+		return FALSE;
 
 	if ( _testsuite_compiled_script != NULL ) {
-	    sieve_close(&_testsuite_compiled_script);
+		sieve_close(&_testsuite_compiled_script);
 	}
 
 	_testsuite_compiled_script = sbin;
@@ -452,6 +454,8 @@ bool testsuite_script_execute(const struct sieve_runtime_env *renv)
 	ret = sieve_interpreter_run(interp, renv->msgdata, &scriptenv, &result, &estatus);
 
 	sieve_interpreter_free(&interp);
+
+	testsuite_result_assign(result);
 	
 	return ( ret > 0 );
 }
@@ -461,8 +465,8 @@ static void testsuite_script_deinit(void)
 	sieve_error_handler_unref(&test_script_ehandler);
 
 	if ( _testsuite_compiled_script != NULL ) {
-        sieve_close(&_testsuite_compiled_script);
-    }
+		sieve_close(&_testsuite_compiled_script);
+	}
 
 	pool_unref(&_testsuite_scriptmsg_pool);
 	//str_free(test_script_error_buf);
@@ -476,10 +480,12 @@ void testsuite_init(void)
 {
 	testsuite_test_context_init();
 	testsuite_script_init();
+	testsuite_result_init();
 }
 
 void testsuite_deinit(void)
 {
+	testsuite_result_deinit();
 	testsuite_script_deinit();
 	testsuite_test_context_deinit();
 }
diff --git a/src/testsuite/testsuite-common.h b/src/testsuite/testsuite-common.h
index 4cb1d5a98..7e6300d75 100644
--- a/src/testsuite/testsuite-common.h
+++ b/src/testsuite/testsuite-common.h
@@ -63,6 +63,7 @@ extern const struct sieve_command cmd_test_set;
 extern const struct sieve_command tst_test_compile;
 extern const struct sieve_command tst_test_execute;
 extern const struct sieve_command tst_test_error;
+extern const struct sieve_command tst_test_result;
 
 /* 
  * Operations 
@@ -76,6 +77,7 @@ enum testsuite_operation_code {
 	TESTSUITE_OPERATION_TEST_COMPILE,
 	TESTSUITE_OPERATION_TEST_EXECUTE,
 	TESTSUITE_OPERATION_TEST_ERROR,
+	TESTSUITE_OPERATION_TEST_RESULT
 };
 
 extern const struct sieve_operation test_operation;
@@ -85,6 +87,7 @@ extern const struct sieve_operation test_set_operation;
 extern const struct sieve_operation test_compile_operation;
 extern const struct sieve_operation test_execute_operation;
 extern const struct sieve_operation test_error_operation;
+extern const struct sieve_operation test_result_operation;
 
 /* 
  * Operands 
diff --git a/src/testsuite/testsuite-result.c b/src/testsuite/testsuite-result.c
new file mode 100644
index 000000000..fc48aa68a
--- /dev/null
+++ b/src/testsuite/testsuite-result.c
@@ -0,0 +1,43 @@
+/* Copyright (c) 2002-2008 Dovecot Sieve authors, see the included COPYING file
+ */
+
+#include "sieve-common.h"
+#include "sieve-actions.h"
+#include "sieve-result.h"
+
+#include "testsuite-common.h"
+#include "testsuite-result.h"
+
+unsigned int _testsuite_result_index; /* Yuck */
+static struct sieve_result *_testsuite_result;
+
+void testsuite_result_init(void)
+{
+	_testsuite_result = NULL;
+	_testsuite_result_index = 0;
+}
+
+void testsuite_result_deinit(void)
+{
+	if ( _testsuite_result != NULL ) {
+		sieve_result_unref(&_testsuite_result);
+	}
+}
+
+void testsuite_result_assign(struct sieve_result *result)
+{
+	if ( _testsuite_result != NULL ) {
+		sieve_result_unref(&_testsuite_result);
+	}
+
+	_testsuite_result = result;
+}
+
+struct sieve_result_iterate_context *testsuite_result_iterate_init(void)
+{
+	if ( _testsuite_result == NULL )
+		return NULL;
+
+	return sieve_result_iterate_init(_testsuite_result);
+}
+
diff --git a/src/testsuite/testsuite-result.h b/src/testsuite/testsuite-result.h
new file mode 100644
index 000000000..404b40e1a
--- /dev/null
+++ b/src/testsuite/testsuite-result.h
@@ -0,0 +1,14 @@
+/* Copyright (c) 2002-2008 Dovecot Sieve authors, see the included COPYING file
+ */
+
+#ifndef __TESTSUITE_RESULT_H
+#define __TESTSUITE_RESULT_H
+
+void testsuite_result_init(void);
+void testsuite_result_deinit(void);
+
+void testsuite_result_assign(struct sieve_result *result);
+
+struct sieve_result_iterate_context *testsuite_result_iterate_init(void);
+
+#endif /* __TESTSUITE_RESULT_H */
diff --git a/src/testsuite/tst-test-error.c b/src/testsuite/tst-test-error.c
index e7d23afe1..39eda3429 100644
--- a/src/testsuite/tst-test-error.c
+++ b/src/testsuite/tst-test-error.c
@@ -74,10 +74,10 @@ static bool tst_test_error_validate_index_tag
 		struct sieve_command_context *cmd);
 
 static const struct sieve_argument test_error_index_tag = {
-    "index",
-    NULL, NULL,
-    tst_test_error_validate_index_tag,
-    NULL, NULL
+	"index",
+	NULL, NULL,
+	tst_test_error_validate_index_tag,
+	NULL, NULL
 };
 
 enum tst_test_error_optional {
@@ -106,9 +106,9 @@ static bool tst_test_error_validate_index_tag
 		return FALSE;
 	}
 
-    /* Skip parameter */
-    *arg = sieve_ast_argument_next(*arg);
-    return TRUE;
+	/* Skip parameter */
+	*arg = sieve_ast_argument_next(*arg);
+	return TRUE;
 }
 
 
@@ -123,8 +123,8 @@ static bool tst_test_error_registered
 	sieve_comparators_link_tag(validator, cmd_reg, SIEVE_MATCH_OPT_COMPARATOR);
 	sieve_match_types_link_tags(validator, cmd_reg, SIEVE_MATCH_OPT_MATCH_TYPE);
 
-	 sieve_validator_register_tag
-        (validator, cmd_reg, &test_error_index_tag, OPT_INDEX);
+	sieve_validator_register_tag
+		(validator, cmd_reg, &test_error_index_tag, OPT_INDEX);
 
 	return TRUE;
 }
@@ -139,15 +139,15 @@ static bool tst_test_error_validate
 	struct sieve_ast_argument *arg = tst->first_positional;
 	
 	if ( !sieve_validate_positional_argument
-        (valdtr, tst, arg, "key list", 2, SAAT_STRING_LIST) ) {
-        return FALSE;
-    }
+		(valdtr, tst, arg, "key list", 2, SAAT_STRING_LIST) ) {
+		return FALSE;
+	}
 
-    if ( !sieve_validator_argument_activate(valdtr, tst, arg, FALSE) )
-        return FALSE;
+	if ( !sieve_validator_argument_activate(valdtr, tst, arg, FALSE) )
+		return FALSE;
 
-    /* Validate the key argument to a specified match type */
-    return sieve_match_type_validate
+	/* Validate the key argument to a specified match type */
+	return sieve_match_type_validate
 		(valdtr, tst, arg, &is_match_type, &i_octet_comparator);
 }
 
@@ -265,10 +265,10 @@ static int tst_test_error_operation_execute
 
 	testsuite_script_get_error_init();
 
-    /* Initialize match */
-    mctx = sieve_match_begin(renv->interp, mtch, cmp, NULL, key_list);
+	/* Initialize match */
+	mctx = sieve_match_begin(renv->interp, mtch, cmp, NULL, key_list);
 
-    /* Iterate through all errors to match */
+	/* Iterate through all errors to match */
 	error = NULL;
 	matched = FALSE;
 	cur_index = 1;
@@ -285,22 +285,22 @@ static int tst_test_error_operation_execute
 
 		matched = ret > 0;
 		cur_index++;
-    }
+	}
 
-    /* Finish match */
-    if ( (ret=sieve_match_end(mctx)) < 0 )
-        result = FALSE;
-    else
-        matched = ( ret > 0 || matched );
+	/* Finish match */
+	if ( (ret=sieve_match_end(mctx)) < 0 )
+		result = FALSE;
+	else
+		matched = ( ret > 0 || matched );
 
 	/* Set test result for subsequent conditional jump */
-    if ( result ) {
-        sieve_interpreter_set_test_result(renv->interp, matched);
-        return SIEVE_EXEC_OK;
-    }
+	if ( result ) {
+		sieve_interpreter_set_test_result(renv->interp, matched);
+		return SIEVE_EXEC_OK;
+	}
 
-    sieve_runtime_trace_error(renv, "invalid string-list item");
-    return SIEVE_EXEC_BIN_CORRUPT;
+	sieve_runtime_trace_error(renv, "invalid string-list item");
+	return SIEVE_EXEC_BIN_CORRUPT;
 }
 
 
diff --git a/src/testsuite/tst-test-result.c b/src/testsuite/tst-test-result.c
new file mode 100644
index 000000000..c417789cb
--- /dev/null
+++ b/src/testsuite/tst-test-result.c
@@ -0,0 +1,318 @@
+/* Copyright (c) 2002-2008 Dovecot Sieve authors, see the included COPYING file
+ */
+
+/* FIXME: this file is very similar to tst-test-error.c. Maybe it is best to 
+ * implement errors and actions as testsuite-objects and implement a common
+ * interface to test these.
+ */
+
+#include "sieve-common.h"
+#include "sieve-error.h"
+#include "sieve-script.h"
+#include "sieve-commands.h"
+#include "sieve-actions.h"
+#include "sieve-comparators.h"
+#include "sieve-match-types.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-code.h"
+#include "sieve-binary.h"
+#include "sieve-result.h"
+#include "sieve-dump.h"
+#include "sieve-match.h"
+
+#include "testsuite-common.h"
+#include "testsuite-result.h"
+
+/*
+ * test_result command
+ *
+ * Syntax:   
+ *   test [MATCH-TYPE] [COMPARATOR] [:index number] <key-list: string-list>
+ */
+
+static bool tst_test_result_registered
+    (struct sieve_validator *validator, struct sieve_command_registration *cmd_reg);
+static bool tst_test_result_validate
+	(struct sieve_validator *validator, struct sieve_command_context *cmd);
+static bool tst_test_result_generate
+	(const struct sieve_codegen_env *cgenv, struct sieve_command_context *ctx);
+
+const struct sieve_command tst_test_result = { 
+	"test_result", 
+	SCT_TEST, 
+	1, 0, FALSE, FALSE,
+	tst_test_result_registered, 
+	NULL,
+	tst_test_result_validate, 
+	tst_test_result_generate, 
+	NULL 
+};
+
+/* 
+ * Operation 
+ */
+
+static bool tst_test_result_operation_dump
+	(const struct sieve_operation *op,
+		const struct sieve_dumptime_env *denv, sieve_size_t *address);
+static int tst_test_result_operation_execute
+	(const struct sieve_operation *op, 
+		const struct sieve_runtime_env *renv, sieve_size_t *address);
+
+const struct sieve_operation test_result_operation = { 
+	"test_result",
+	&testsuite_extension, 
+	TESTSUITE_OPERATION_TEST_RESULT,
+	tst_test_result_operation_dump, 
+	tst_test_result_operation_execute 
+};
+
+/*
+ * Tagged arguments
+ */ 
+
+/* NOTE: This will be merged with the date-index extension when it is 
+ * implemented.
+ */
+
+/* FIXME: at least merge this with the test_error version of this tag */
+
+static bool tst_test_result_validate_index_tag
+	(struct sieve_validator *validator, struct sieve_ast_argument **arg,
+		struct sieve_command_context *cmd);
+
+static const struct sieve_argument test_result_index_tag = {
+    "index",
+    NULL, NULL,
+    tst_test_result_validate_index_tag,
+    NULL, NULL
+};
+
+enum tst_test_result_optional {
+	OPT_INDEX = SIEVE_MATCH_OPT_LAST,
+};
+
+/*
+ * Argument implementation
+ */
+
+static bool tst_test_result_validate_index_tag
+(struct sieve_validator *validator, struct sieve_ast_argument **arg,
+	struct sieve_command_context *cmd)
+{
+	struct sieve_ast_argument *tag = *arg;
+
+	/* Detach the tag itself */
+	*arg = sieve_ast_arguments_detach(*arg,1);
+
+	/* Check syntax:
+	 *   :index number
+	 */
+	if ( !sieve_validate_tag_parameter
+		(validator, cmd, tag, *arg, SAAT_NUMBER) ) {
+		return FALSE;
+	}
+
+	/* Skip parameter */
+	*arg = sieve_ast_argument_next(*arg);
+	return TRUE;
+}
+
+
+/*
+ * Command registration
+ */
+
+static bool tst_test_result_registered
+(struct sieve_validator *validator, struct sieve_command_registration *cmd_reg)
+{
+	/* The order of these is not significant */
+	sieve_comparators_link_tag(validator, cmd_reg, SIEVE_MATCH_OPT_COMPARATOR);
+	sieve_match_types_link_tags(validator, cmd_reg, SIEVE_MATCH_OPT_MATCH_TYPE);
+
+	sieve_validator_register_tag
+		(validator, cmd_reg, &test_result_index_tag, OPT_INDEX);
+
+	return TRUE;
+}
+
+/* 
+ * Validation 
+ */
+
+static bool tst_test_result_validate
+(struct sieve_validator *valdtr ATTR_UNUSED, struct sieve_command_context *tst) 
+{
+	struct sieve_ast_argument *arg = tst->first_positional;
+	
+	if ( !sieve_validate_positional_argument
+		(valdtr, tst, arg, "key list", 2, SAAT_STRING_LIST) ) {
+		return FALSE;
+	}
+
+	if ( !sieve_validator_argument_activate(valdtr, tst, arg, FALSE) )
+		return FALSE;
+
+	/* Validate the key argument to a specified match type */
+	return sieve_match_type_validate
+		(valdtr, tst, arg, &is_match_type, &i_octet_comparator);
+}
+
+/* 
+ * Code generation 
+ */
+
+static inline struct testsuite_generator_context *
+_get_generator_context(struct sieve_generator *gentr)
+{
+	return (struct testsuite_generator_context *) 
+		sieve_generator_extension_get_context(gentr, &testsuite_extension);
+}
+
+static bool tst_test_result_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_command_context *tst)
+{
+	sieve_operation_emit_code(cgenv->sbin, &test_result_operation);
+
+	/* Generate arguments */
+	return sieve_generate_arguments(cgenv, tst, NULL);
+}
+
+/* 
+ * Code dump
+ */
+ 
+static bool tst_test_result_operation_dump
+(const struct sieve_operation *op ATTR_UNUSED,
+	const struct sieve_dumptime_env *denv, sieve_size_t *address)
+{
+	int opt_code = 0;
+
+	sieve_code_dumpf(denv, "TEST_RESULT:");
+	sieve_code_descend(denv);
+
+	/* Handle any optional arguments */
+	do {
+		if ( !sieve_match_dump_optional_operands(denv, address, &opt_code) )
+			return FALSE;
+
+		switch ( opt_code ) {
+		case SIEVE_MATCH_OPT_END:
+			break;
+		case OPT_INDEX:
+			if ( !sieve_opr_number_dump(denv, address, "index") )
+				return FALSE;
+			break;
+		default:
+			return FALSE;
+		}
+	} while ( opt_code != SIEVE_MATCH_OPT_END );
+
+	return sieve_opr_stringlist_dump(denv, address, "key list");
+}
+
+/*
+ * Intepretation
+ */
+
+static int tst_test_result_operation_execute
+(const struct sieve_operation *op ATTR_UNUSED,
+	const struct sieve_runtime_env *renv, sieve_size_t *address)
+{	
+	int opt_code = 0;
+	bool result = TRUE;
+	const struct sieve_comparator *cmp = &i_octet_comparator;
+	const struct sieve_match_type *mtch = &is_match_type;
+	struct sieve_match_context *mctx;
+	struct sieve_coded_stringlist *key_list;
+	bool matched;
+	struct sieve_result_iterate_context *rictx;
+	const struct sieve_action *action;
+	int cur_index = 0, index = 0;
+	int ret;
+
+	/*
+	 * Read operands
+	 */
+
+	/* Handle optional operands */
+	do {
+		sieve_number_t number; 
+
+		if ( (ret=sieve_match_read_optional_operands
+			(renv, address, &opt_code, &cmp, &mtch)) <= 0 )
+ 			return ret;
+
+		switch ( opt_code ) {
+		case SIEVE_MATCH_OPT_END:
+			break;
+		case OPT_INDEX:
+			if ( !sieve_opr_number_read(renv, address, &number) ) {
+				sieve_runtime_trace_error(renv, "invalid index operand");
+				return SIEVE_EXEC_BIN_CORRUPT;
+			}
+			index = (int) number;
+			break;
+		default:
+			sieve_runtime_trace_error(renv, "invalid optional operand");
+			return SIEVE_EXEC_BIN_CORRUPT;
+		}	
+	} while ( opt_code != SIEVE_MATCH_OPT_END);
+
+	/* Read key-list */
+	if ( (key_list=sieve_opr_stringlist_read(renv, address)) == NULL ) {
+		sieve_runtime_trace_error(renv, "invalid key-list operand");
+		return SIEVE_EXEC_BIN_CORRUPT;
+	}
+
+	/*
+	 * Perform operation
+	 */
+	
+	sieve_runtime_trace(renv, "TEST_RESULT test (index: %d)", index);
+
+	rictx = testsuite_result_iterate_init();
+
+  /* Initialize match */
+  mctx = sieve_match_begin(renv->interp, mtch, cmp, NULL, key_list);
+
+  /* Iterate through all errors to match */
+	matched = FALSE;
+	cur_index = 1;
+	ret = 0;
+	while ( result && !matched &&
+		(action=sieve_result_iterate_next(rictx, NULL)) != NULL ) {
+		const char *act_name = action->name;
+
+		if ( index == 0 || index == cur_index ) {
+			if ( (ret=sieve_match_value(mctx, act_name, strlen(act_name))) < 0 ) {
+				result = FALSE;
+				break;
+			}
+		}
+
+		matched = ret > 0;
+		cur_index++;
+	}
+
+	/* Finish match */
+	if ( (ret=sieve_match_end(mctx)) < 0 )
+		result = FALSE;
+	else
+		matched = ( ret > 0 || matched );
+
+	/* Set test result for subsequent conditional jump */
+	if ( result ) {
+		sieve_interpreter_set_test_result(renv->interp, matched);
+		return SIEVE_EXEC_OK;
+	}
+
+	sieve_runtime_trace_error(renv, "invalid string-list item");
+	return SIEVE_EXEC_BIN_CORRUPT;
+}
+
+
+
+
diff --git a/tests/execute/actions.svtest b/tests/execute/actions.svtest
index 066016728..d7c2b1d1f 100644
--- a/tests/execute/actions.svtest
+++ b/tests/execute/actions.svtest
@@ -1,4 +1,6 @@
 require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
 
 test_set "message" text:
 To: nico@vestingbar.nl
@@ -17,6 +19,22 @@ test "Fileinto" {
 	if not test_execute {
 		test_fail "execute failed";
 	}
+
+	if not test_result :count "eq" :comparator "i;ascii-numeric" "3" {
+		test_fail "wrong number of actions in result";
+	} 
+
+	if not test_result :index 1 "store" {
+		test_fail "first action is not 'store'";
+	} 
+
+	if not test_result :index 2 "store" {
+		test_fail "second action is not 'store'";
+	} 
+
+	if not test_result :index 3 "store" {
+		test_fail "third action is not 'store'";
+	} 
 }
 
 test "Redirect" {
@@ -27,5 +45,25 @@ test "Redirect" {
 	if not test_execute {
 		test_fail "execute failed";
 	}
+
+	if not test_result :count "eq" :comparator "i;ascii-numeric" "4" {
+		test_fail "wrong number of actions in result";
+	} 
+
+	if not test_result :index 1 "redirect" {
+		test_fail "first action is not 'redirect'";
+	} 
+
+	if not test_result :index 2 "store" {
+		test_fail "second action is not 'store'";
+	} 
+
+	if not test_result :index 3 "redirect" {
+		test_fail "third action is not 'redirect'";
+	} 
+
+	if not test_result :index 4 "redirect" {
+		test_fail "fourth action is not 'redirect'";
+	} 
 }
 
diff --git a/tests/execute/actions/fileinto.sieve b/tests/execute/actions/fileinto.sieve
index 41ab6b6a8..c58af8691 100644
--- a/tests/execute/actions/fileinto.sieve
+++ b/tests/execute/actions/fileinto.sieve
@@ -1,8 +1,17 @@
 require "fileinto";
 
+/* Three store actions */
+
 if address :contains "to" "vestingbar" {
+	/* #1 */
 	fileinto "INBOX.VB";
-	stop;
 }
 
+/* #2 */
+fileinto "INBOX.backup";
+
+/* #3 */
 keep;
+
+/* Duplicate of keep */
+fileinto "INBOX";
diff --git a/tests/execute/actions/redirect.sieve b/tests/execute/actions/redirect.sieve
index 6d02c6edc..601faf986 100644
--- a/tests/execute/actions/redirect.sieve
+++ b/tests/execute/actions/redirect.sieve
@@ -1,10 +1,17 @@
 if address :contains "to" "vestingbar" {
+	/* #1 */
 	redirect "stephan@example.com";
+	
+	/* #2 */
 	keep;
 }
 
+/* #3 */
 redirect "stephan@rename-it.nl";
+
+/* #4 */
 redirect "nico@example.nl";
-redirect "stephan@example.com";
 
+/* Duplicates */
+redirect "Stephan Bosch <stephan@example.com>";
 keep;
diff --git a/tests/extensions/reject/execute.svtest b/tests/extensions/reject/execute.svtest
index 7400de8bd..840ee2715 100644
--- a/tests/extensions/reject/execute.svtest
+++ b/tests/extensions/reject/execute.svtest
@@ -1,4 +1,6 @@
 require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
 
 test_set "message" text:
 To: nico@vestingbar.nl
@@ -17,4 +19,12 @@ test "Execute" {
 	if not test_execute {
 		test_fail "execute failed";
 	}
+
+	if not test_result :count "eq" :comparator "i;ascii-numeric" "1" {
+		test_fail "invalid number of actions in result";
+	}
+
+	if not test_result :index 1 "reject" {
+		test_fail "reject action missing from result";
+	}
 }
diff --git a/tests/extensions/vacation/execute.svtest b/tests/extensions/vacation/execute.svtest
index 6ad15b86b..a8cdd6b88 100644
--- a/tests/extensions/vacation/execute.svtest
+++ b/tests/extensions/vacation/execute.svtest
@@ -1,4 +1,28 @@
 require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Action" {
+	if not test_compile "execute/action.sieve" {
+		test_fail "script compile failed";
+	}
+
+	if not test_execute {
+		test_fail "script execute failed";
+	}
+
+	if not test_result :count "eq" :comparator "i;ascii-numeric" "2" {
+		test_fail "invalid number of actions in result";
+	}
+
+	if not test_result :index 1 "vacation" {
+		test_fail "vacation action is not present as first item in result";
+	}
+	
+	if not test_result :index 2 "store" {
+		test_fail "store action is missing in result";
+	}
+}
 
 test "No :handle specified" {
 	if not test_compile "execute/no-handle.sieve" {
diff --git a/tests/extensions/vacation/execute/action.sieve b/tests/extensions/vacation/execute/action.sieve
new file mode 100644
index 000000000..7fb6d78fd
--- /dev/null
+++ b/tests/extensions/vacation/execute/action.sieve
@@ -0,0 +1,4 @@
+require "vacation";
+
+vacation "I am not at home today";
+keep;
-- 
GitLab