second round of library config API and dump

David Bremner david at tethera.net
Tue Jan 12 19:10:06 PST 2016


Based on feedback on IRC from Austin and Tomi, I've simplified the API
and no longer expose the database level prefix to the caller.

This series does fix at least one memory ownership bug. Not a
particularly subtle one, I apparently only got half way through making
the current 'value' owned by the iterator.

There's also a few more tests, as the API starts to settle down.

Because some global renaming, the interdiff is somewhat noisy, but
hopefully mostly straightforward.

diff --git a/lib/Makefile.local b/lib/Makefile.local
index ccc1e49..eb442d1 100644
--- a/lib/Makefile.local
+++ b/lib/Makefile.local
@@ -48,7 +48,7 @@ libnotmuch_cxx_srcs =		\
 	$(dir)/index.cc		\
 	$(dir)/message.cc	\
 	$(dir)/query.cc		\
-	$(dir)/metadata.cc	\
+	$(dir)/config.cc	\
 	$(dir)/thread.cc
 
 libnotmuch_modules := $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o)
diff --git a/lib/metadata.cc b/lib/config.cc
similarity index 47%
rename from lib/metadata.cc
rename to lib/config.cc
index 34b46e0..4cd9314 100644
--- a/lib/metadata.cc
+++ b/lib/config.cc
@@ -22,88 +22,36 @@
 #include "notmuch-private.h"
 #include "database-private.h"
 
-struct _notmuch_metadata {
+static const std::string CONFIG_PREFIX="C";
+
+struct _notmuch_config_list {
     notmuch_database_t *notmuch;
     Xapian::TermIterator *iterator;
-    notmuch_metadata_class_t mclass;
     char *current_key;
+    char *current_val;
 };
 
 static int
-_notmuch_metadata_destroy (notmuch_metadata_t *list) {
+_notmuch_config_list_destroy (notmuch_config_list_t *list) {
     delete list->iterator;
     return 0;
 }
 
-typedef struct prefix {
-    notmuch_metadata_class_t mclass;
-    const char *prefix;
-} prefix_t;
-
-static prefix_t METADATA_PREFIX[] = {
-    {NOTMUCH_METADATA_CONFIG, "C"},
-};
-
-#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
-
-const char *
-notmuch_metadata_prefix_string (notmuch_metadata_class_t mclass)
-{
-    unsigned int i;
-
-    for (i=0; i < ARRAY_SIZE(METADATA_PREFIX); i++) {
-	if (METADATA_PREFIX[0].mclass == mclass)
-	    return METADATA_PREFIX[i].prefix;
-    }
-    return NULL;
-}
-
-notmuch_status_t _make_key(void *ctx, notmuch_metadata_class_t mclass,
-			    const char *in, const char **out)
-{
-    const char *term;
-    const char *prefix = NULL;
-
-    if (!out)
-	return NOTMUCH_STATUS_NULL_POINTER;
-
-    prefix = notmuch_metadata_prefix_string(mclass);
-    if (!prefix)
-	return NOTMUCH_STATUS_UNSUPPORTED_OPERATION;
-
-    term = talloc_asprintf (ctx, "%s%s",
-			    prefix, in);
-    if (!term)
-	return NOTMUCH_STATUS_OUT_OF_MEMORY;
-
-    *out = term;
-    return NOTMUCH_STATUS_SUCCESS;
-}
-
 notmuch_status_t
-notmuch_database_set_metadata (notmuch_database_t *notmuch,
-			       notmuch_metadata_class_t mclass,
-			       const char *key,
-			       const char *value)
+notmuch_database_set_config (notmuch_database_t *notmuch,
+			     const char *key,
+			     const char *value)
 {
     notmuch_status_t status;
     Xapian::WritableDatabase *db;
-    const char *key_term = NULL;
-    void *local;
-
-    local = talloc_new (NULL);
 
     status = _notmuch_database_ensure_writable (notmuch);
     if (status)
-	goto DONE;
-
-    status = _make_key (local, mclass, key, &key_term);
-    if (status)
-	goto DONE;
+	return status;
 
     try {
 	db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-	db->set_metadata (key_term, value);
+	db->set_metadata (CONFIG_PREFIX+key, value);
     } catch (const Xapian::Error &error) {
 	status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
 	notmuch->exception_reported = TRUE;
@@ -112,33 +60,18 @@ notmuch_database_set_metadata (notmuch_database_t *notmuch,
 				   error.get_msg().c_str());
 	}
     }
- DONE:
-    talloc_free (local);
-
-    return status;
+    return NOTMUCH_STATUS_SUCCESS;
 }
 
 static notmuch_status_t
 _metadata_value (notmuch_database_t *notmuch,
-		 notmuch_metadata_class_t mclass,
 		 const char *key,
 		 std::string &value)
 {
-    notmuch_status_t status;
-
-    const char *key_term = NULL;
-    void *local;
-
-    local = talloc_new (NULL);
-
-    status = _make_key (local, mclass, key, &key_term);
-    if (status)
-	goto DONE;
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
 
     try {
-
-	value = notmuch->xapian_db->get_metadata (key_term);
-
+	value = notmuch->xapian_db->get_metadata (CONFIG_PREFIX+key);
     } catch (const Xapian::Error &error) {
 	status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
 	notmuch->exception_reported = TRUE;
@@ -147,24 +80,20 @@ _metadata_value (notmuch_database_t *notmuch,
 				   error.get_msg().c_str());
 	}
     }
-
- DONE:
-    talloc_free (local);
     return status;
 }
 
 notmuch_status_t
-notmuch_database_get_metadata (notmuch_database_t *notmuch,
-			       notmuch_metadata_class_t mclass,
-			       const char *key,
-			       char **value) {
+notmuch_database_get_config (notmuch_database_t *notmuch,
+			     const char *key,
+			     char **value) {
     std::string strval;
     notmuch_status_t status;
 
     if (!value)
 	return NOTMUCH_STATUS_NULL_POINTER;
 
-    status = _metadata_value (notmuch, mclass, key, strval);
+    status = _metadata_value (notmuch, key, strval);
     if (status)
 	return status;
 
@@ -174,31 +103,28 @@ notmuch_database_get_metadata (notmuch_database_t *notmuch,
 }
 
 notmuch_status_t
-notmuch_database_get_all_metadata (notmuch_database_t *notmuch,
-				   notmuch_metadata_class_t mclass,
-				   notmuch_metadata_t **out)
+notmuch_database_get_config_list (notmuch_database_t *notmuch,
+				 const char *prefix,
+				 notmuch_config_list_t **out)
 {
-    notmuch_metadata_t *list = NULL;
+    notmuch_config_list_t *list = NULL;
     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
 
-    const char *prefix = notmuch_metadata_prefix_string (mclass);
-
-    list = talloc (notmuch, notmuch_metadata_t);
+    list = talloc (notmuch, notmuch_config_list_t);
     if (!list) {
 	status = NOTMUCH_STATUS_OUT_OF_MEMORY;
 	goto DONE;
     }
 
-    talloc_set_destructor(list, _notmuch_metadata_destroy);
+    talloc_set_destructor(list, _notmuch_config_list_destroy);
     list->iterator = new Xapian::TermIterator;
     list->notmuch = notmuch;
-    list->mclass = mclass;
     list->current_key = NULL;
+    list->current_val = NULL;
 
     try {
 
-	*list->iterator = notmuch->xapian_db->metadata_keys_begin();
-	list->iterator->skip_to (prefix);
+	*list->iterator = notmuch->xapian_db->metadata_keys_begin(CONFIG_PREFIX + (prefix ? prefix : NULL));
 
     } catch (const Xapian::Error &error) {
 	_notmuch_database_log (notmuch, "A Xapian exception occurred getting metadata iterator: %s.\n",
@@ -217,49 +143,52 @@ notmuch_database_get_all_metadata (notmuch_database_t *notmuch,
 }
 
 notmuch_bool_t
-notmuch_metadata_valid (notmuch_metadata_t *metadata)
+notmuch_config_list_valid (notmuch_config_list_t *metadata)
 {
-    const char *prefix = notmuch_metadata_prefix_string (metadata->mclass);
     if (*(metadata->iterator) == metadata->notmuch->xapian_db->metadata_keys_end())
 	return FALSE;
 
-    return (strncmp((**(metadata->iterator)).c_str (), prefix, strlen (prefix)) == 0);
+    return TRUE;
 }
 
 const char *
-notmuch_metadata_key (notmuch_metadata_t *metadata)
+notmuch_config_list_key (notmuch_config_list_t *list)
 {
-    const char *prefix = notmuch_metadata_prefix_string (metadata->mclass);
+    if (list->current_key)
+	talloc_free (list->current_key);
 
-    if (metadata->current_key)
-	talloc_free (metadata->current_key);
-    metadata->current_key = talloc_strdup (metadata, (**(metadata->iterator)).c_str () + strlen (prefix));
+    list->current_key = talloc_strdup (list, (**(list->iterator)).c_str () + CONFIG_PREFIX.length ());
 
-    return  metadata->current_key;
+    return  list->current_key;
 }
 
 const char *
-notmuch_metadata_value (notmuch_metadata_t *metadata)
+notmuch_config_list_value (notmuch_config_list_t *list)
 {
-    const char *key = notmuch_metadata_key (metadata);
-    char *val;
+    std::string strval;
     notmuch_status_t status;
+    const char *key = notmuch_config_list_key (list);
 
-    status=notmuch_database_get_metadata (metadata->notmuch, metadata->mclass, key, &val);
+    /* TODO: better error reporting?? */
+    status = _metadata_value (list->notmuch, key, strval);
     if (status)
 	return NULL;
 
-    return val;
+    if (list->current_val)
+	talloc_free(list->current_val);
+
+    list->current_val = talloc_strdup(list, strval.c_str ());
+    return list->current_val;
 }
 
 void
-notmuch_metadata_move_to_next (notmuch_metadata_t *metadata)
+notmuch_config_list_move_to_next (notmuch_config_list_t *list)
 {
-    (*(metadata->iterator))++;
+    (*(list->iterator))++;
 }
 
 void
-notmuch_metadata_destroy (notmuch_metadata_t *metadata)
+notmuch_config_list_destroy (notmuch_config_list_t *list)
 {
-    talloc_free (metadata);
+    talloc_free (list);
 }
diff --git a/lib/notmuch.h b/lib/notmuch.h
index 657a7d6..b439c88 100644
--- a/lib/notmuch.h
+++ b/lib/notmuch.h
@@ -197,7 +197,7 @@ typedef struct _notmuch_message notmuch_message_t;
 typedef struct _notmuch_tags notmuch_tags_t;
 typedef struct _notmuch_directory notmuch_directory_t;
 typedef struct _notmuch_filenames notmuch_filenames_t;
-typedef struct _notmuch_metadata notmuch_metadata_t;
+typedef struct _notmuch_config_list notmuch_config_list_t;
 #endif /* __DOXYGEN__ */
 
 /**
@@ -1830,78 +1830,69 @@ notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
 void
 notmuch_filenames_destroy (notmuch_filenames_t *filenames);
 
-/**
- * metadata class
- */
-
-typedef enum _notmuch_metadata_class {
-    NOTMUCH_METADATA_FIRST_CLASS = 1,
-    NOTMUCH_METADATA_CONFIG = 1,
-    NOTMUCH_METADATA_LAST_CLASS
-} notmuch_metadata_class_t;
 
 /**
- * set metadata
+ * set config 'key' to 'value'
  *
  */
 notmuch_status_t
-notmuch_database_set_metadata (notmuch_database_t *db, notmuch_metadata_class_t mclass, const char *key, const char *value);
+notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value);
 
 /**
- * retrieve one metadata value
+ * retrieve config item 'key', assign to  'value'
+ *
+ * keys which have not been previously set with n_d_set_config will
+ * return an empty string.
  *
- * return value is allocated by malloc and should be freed by the caller.
+ * return value is allocated by malloc and should be freed by the
+ * caller.
  */
 notmuch_status_t
-notmuch_database_get_metadata (notmuch_database_t *db, notmuch_metadata_class_t mclass, const char *key, char **value);
+notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value);
 
 /**
- * get all metadata of a given class
+ * Create an iterator for all config items with keys matching a given prefix
  */
 notmuch_status_t
-notmuch_database_get_all_metadata (notmuch_database_t *db, notmuch_metadata_class_t mclass, notmuch_metadata_t **out);
+notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, notmuch_config_list_t **out);
 
 /**
- * Is 'metadata' iterator valid (i.e. _key, _value, _move_to_next can be called).
+ * Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called).
  */
 notmuch_bool_t
-notmuch_metadata_valid (notmuch_metadata_t *metadata);
+notmuch_config_list_valid (notmuch_config_list_t *config_list);
 
 /**
- * return key for current metadata pair
+ * return key for current config pair
  *
  * return value is owned by the iterator, and will be destroyed by the
- * next call to notmuch_metadata_key or notmuch_metadata_destroy.
+ * next call to notmuch_config_list_key or notmuch_config_list_destroy.
  */
 const char *
-notmuch_metadata_key (notmuch_metadata_t *metadata);
+notmuch_config_list_key (notmuch_config_list_t *config_list);
 
 /**
- * return 'value' for current metadata pair
+ * return 'value' for current config pair
  *
  * return value is owned by the iterator, and will be destroyed by the
- * next call to notmuch_metadata_value
+ * next call to notmuch_config_list_value or notmuch config_list_destroy
  */
 const char *
-notmuch_metadata_value (notmuch_metadata_t *metadata);
+notmuch_config_list_value (notmuch_config_list_t *config_list);
+
 
 /**
- * move 'metadata' iterator to the next pair
+ * move 'config_list' iterator to the next pair
  */
 void
-notmuch_metadata_move_to_next (notmuch_metadata_t *metadata);
+notmuch_config_list_move_to_next (notmuch_config_list_t *config_list);
 
 /**
- * free any resources held by 'metadata'
+ * free any resources held by 'config_list'
  */
 void
-notmuch_metadata_destroy (notmuch_metadata_t * metadata);
+notmuch_config_list_destroy (notmuch_config_list_t *config_list);
 
-/**
- * convert enum to string
- */
-const char *
-notmuch_metadata_prefix_string (notmuch_metadata_class_t mclass);
 /* @} */
 
 NOTMUCH_END_DECLS
diff --git a/notmuch-client.h b/notmuch-client.h
index 8ee6e0e..2dca83c 100644
--- a/notmuch-client.h
+++ b/notmuch-client.h
@@ -445,7 +445,7 @@ typedef enum dump_formats {
 
 typedef enum dump_includes {
     DUMP_INCLUDE_TAGS=1,
-    DUMP_INCLUDE_METADATA=2,
+    DUMP_INCLUDE_CONFIG=2,
 } dump_include_t;
 
 int
diff --git a/notmuch-dump.c b/notmuch-dump.c
index a88b5d5..4909493 100644
--- a/notmuch-dump.c
+++ b/notmuch-dump.c
@@ -24,25 +24,21 @@
 #include <zlib.h>
 
 static notmuch_status_t
-database_dump_metadata(notmuch_database_t *notmuch, gzFile output)
+database_dump_config(notmuch_database_t *notmuch, gzFile output)
 {
-    notmuch_metadata_class_t mclass;
-    notmuch_metadata_t *meta;
+    notmuch_config_list_t *list;
     notmuch_status_t status;
-
-    for (mclass = NOTMUCH_METADATA_FIRST_CLASS; mclass < NOTMUCH_METADATA_LAST_CLASS; mclass++) {
-	status = notmuch_database_get_all_metadata (notmuch, NOTMUCH_METADATA_CONFIG, &meta);
-	if (status)
-	    return status;
-
-	for (; notmuch_metadata_valid (meta); notmuch_metadata_move_to_next (meta)) {
-	    /* FIXME hexencode key and values */
-	    gzprintf(output, "#@ %s %s %s\n",
-		     notmuch_metadata_prefix_string(mclass),
-		     notmuch_metadata_key (meta), notmuch_metadata_value (meta));
-	}
-	notmuch_metadata_destroy (meta);
+    status = notmuch_database_get_config_list (notmuch, NULL, &list);
+    if (status)
+	return status;
+
+    for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+	/* FIXME hexencode key and values */
+	gzprintf(output, "#@ %s %s\n",
+		 notmuch_config_list_key (list), notmuch_config_list_value (list));
     }
+    notmuch_config_list_destroy (list);
+
     return NOTMUCH_STATUS_SUCCESS;
 }
 
@@ -55,9 +51,9 @@ database_dump_file (notmuch_database_t *notmuch, gzFile output,
     notmuch_message_t *message;
     notmuch_tags_t *tags;
 
-    if (include | DUMP_INCLUDE_METADATA) {
+    if (include | DUMP_INCLUDE_CONFIG) {
 	if (print_status_database ("notmuch dump", notmuch,
-				   database_dump_metadata(notmuch,output)))
+				   database_dump_config(notmuch,output)))
 	    return EXIT_FAILURE;
     }
 
@@ -267,7 +263,7 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[])
 				  { "batch-tag", DUMP_FORMAT_BATCH_TAG },
 				  { 0, 0 } } },
 	{ NOTMUCH_OPT_KEYWORD_FLAGS, &include, "include", 'i',
-	  (notmuch_keyword_t []){ { "metadata", DUMP_INCLUDE_METADATA },
+	  (notmuch_keyword_t []){ { "metadata", DUMP_INCLUDE_CONFIG },
 				  { "tags", DUMP_INCLUDE_TAGS} } },
 	{ NOTMUCH_OPT_STRING, &output_file_name, "output", 'o', 0  },
 	{ NOTMUCH_OPT_BOOLEAN, &gzip_output, "gzip", 'z', 0 },
@@ -282,7 +278,7 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[])
     notmuch_process_shared_options (argv[0]);
 
     if (include == 0)
-	include = DUMP_INCLUDE_METADATA | DUMP_INCLUDE_TAGS;
+	include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_TAGS;
 
     if (opt_index < argc) {
 	query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
diff --git a/notmuch-new.c b/notmuch-new.c
index 1f8050d..fd2ff82 100644
--- a/notmuch-new.c
+++ b/notmuch-new.c
@@ -1041,7 +1041,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[])
 	    }
 
 	    if (notmuch_database_dump (notmuch, backup_name, "",
-				       DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_METADATA | DUMP_INCLUDE_TAGS, TRUE)) {
+				       DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_TAGS, TRUE)) {
 		fprintf (stderr, "Backup failed. Aborting upgrade.");
 		return EXIT_FAILURE;
 	    }
diff --git a/test/T590-metadata.sh b/test/T590-libconfig.sh
similarity index 35%
rename from test/T590-metadata.sh
rename to test/T590-libconfig.sh
index 45a49be..4fe6bd1 100755
--- a/test/T590-metadata.sh
+++ b/test/T590-libconfig.sh
@@ -1,5 +1,5 @@
 #!/usr/bin/env bash
-test_description="metadata API"
+test_description="library config API"
 
 . ./test-lib.sh || exit 1
 
@@ -36,14 +36,14 @@ cat <<EOF > c_tail
 }
 EOF
 
-test_begin_subtest "notmuch_database_{set,get}_metadata"
+test_begin_subtest "notmuch_database_{set,get}_config"
 cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
 {
-   RUN(notmuch_database_set_metadata (db, NOTMUCH_METADATA_CONFIG, "testkey1", "testvalue1"));
-   RUN(notmuch_database_set_metadata (db, NOTMUCH_METADATA_CONFIG, "testkey2", "testvalue2"));
-   RUN(notmuch_database_get_metadata (db, NOTMUCH_METADATA_CONFIG, "testkey1", &val));
+   RUN(notmuch_database_set_config (db, "testkey1", "testvalue1"));
+   RUN(notmuch_database_set_config (db, "testkey2", "testvalue2"));
+   RUN(notmuch_database_get_config (db, "testkey1", &val));
    printf("testkey1 = %s\n", val);
-   RUN(notmuch_database_get_metadata (db, NOTMUCH_METADATA_CONFIG, "testkey2", &val));
+   RUN(notmuch_database_get_config (db, "testkey2", &val));
    printf("testkey2 = %s\n", val);
 }
 EOF
@@ -55,33 +55,73 @@ testkey2 = testvalue2
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
-test_begin_subtest "get all metadata in one class"
+
+test_begin_subtest "notmuch_database_get_config_list: empty list"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   notmuch_config_list_t *list;
+   RUN(notmuch_database_get_config_list (db, "nonexistent", &list));
+   printf("valid = %d\n", notmuch_config_list_valid (list));
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+valid = 0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "notmuch_database_get_config_list: all pairs"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   notmuch_config_list_t *list;
+   RUN(notmuch_database_set_config (db, "zzzafter", "afterval"));
+   RUN(notmuch_database_set_config (db, "aaabefore", "beforeval"));
+   RUN(notmuch_database_get_config_list (db, "", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+aaabefore beforeval
+testkey1 testvalue1
+testkey2 testvalue2
+zzzafter afterval
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: one prefix"
 cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
 {
-   notmuch_metadata_t *meta;
-   RUN(notmuch_database_get_all_metadata (db, NOTMUCH_METADATA_CONFIG, &meta));
-   for (; notmuch_metadata_valid (meta); notmuch_metadata_move_to_next (meta)) {
-      printf("key = %s\n", notmuch_metadata_key (meta));
-      printf("val = %s\n", notmuch_metadata_value (meta));
+   notmuch_config_list_t *list;
+   RUN(notmuch_database_get_config_list (db, "testkey", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
    }
-   notmuch_metadata_destroy (meta);
+   notmuch_config_list_destroy (list);
 }
 EOF
 cat <<'EOF' >EXPECTED
 == stdout ==
-key = testkey1
-val = testvalue1
-key = testkey2
-val = testvalue2
+testkey1 testvalue1
+testkey2 testvalue2
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
-test_begin_subtest "dump metadata"
+test_begin_subtest "dump config"
 notmuch dump --include=metadata >OUTPUT
 cat <<'EOF' >EXPECTED
-#@ C testkey1 testvalue1
-#@ C testkey2 testvalue2
+#@ aaabefore beforeval
+#@ testkey1 testvalue1
+#@ testkey2 testvalue2
+#@ zzzafter afterval
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 


More information about the notmuch mailing list