tests/hwtable: tests for config file handling and hwentry merging
authorMartin Wilck <mwilck@suse.com>
Fri, 8 Jun 2018 10:20:23 +0000 (12:20 +0200)
committerChristophe Varoqui <christophe.varoqui@opensvc.com>
Thu, 21 Jun 2018 07:38:55 +0000 (09:38 +0200)
Add unit tests for parsing "device" and "blacklist" configuration entries and
hwtable entry merging. The tests work by creating a multipath.conf file and one
or more config_dir/*.conf files, reading device entries from both, simulating
discovery of fake paths and multipaths, and verifying these are configured as
expected. The focus is on the behavior if several hwtable or blacklist entries
match a single device.

Most of these tests in this file serve as regression test for follow-up changes.
But Some of the tests are meant to illustrate problems problems with
the current code, which behaves inconsistently in certain cases. By
changing the BROKEN macro at the top of the file, the user can control if broken
behavior causes test failure, or succeeds with a warning. Subsequent patches
will remove the BROKEN tests. Reviewers are encouraged to focus on the BROKEN
tests, which annotate current behavior that follow-up patches are going to change.

Problems with the current code are mainly found in two areas:

 1. It currently makes a difference whether several entries matching the same
 device are in in the same configuration file (default "multipath.conf") or
 distributed over "multipath.conf" and other files under config_dir. This is
 highly unexpected behavior.
 Moreover, in this context, the built-in hwtable can be viewed as yet another
 "configuration file" which is parsed before "multipath.conf". The same problem
 arises: adding a hwentry to "multipath.conf" doesn't necessarily have the same
 effect as adding the same entry to the built-in hwtable and recompiling,
 because different merging logic applies (in short: entries from different
 "configuration files" are merged, but entries from the same "file" aren't).

 This is a major blocker for using the configuration dumped with
 "multipath -t" as input for multipath-tools, because a built-in entry which
 is dumped ends up in "multipath.conf" or a config_dir file, which may change
 the way it's merged with other entries.

  2. The vendor/product/rev IDs are regular expressions, and merging of entries
  by regular expressions simply doesn't work. Entries are merged if a later
  regex, interpreted as a string, matches an earlier added regex. But that's
  incorrect. Except in trivial cases, matching of a regex r2 by another regex
  r1 doesn't say anything about whether strings matched by r2 will be matched
  by r1 as well.

Another, minor issue is the treatment of double quotes.

The way these tests are implemented using the test_driver() function may seem
weird in the first place. The reason is that in a later patch, when we have
configuration-dumping-and-reloading basically working, test_driver() will be
extended to run the same test both with the initial and dumped configuration
and ensure that there are no differences. If I didn't introduce test_driver()
right away, I'd have major, confusing changes in the test code when this
functionality is added.

Signed-off-by: Martin Wilck <mwilck@suse.com>
tests/Makefile
tests/hwtable.c [new file with mode: 0644]

index 7439c8c..78755ed 100644 (file)
@@ -3,7 +3,7 @@ include ../Makefile.inc
 CFLAGS += $(BIN_CFLAGS) -I$(multipathdir) -I$(mpathcmddir)
 LIBDEPS += -L$(multipathdir) -lmultipath -lcmocka
 
-TESTS := uevent parser util dmevents
+TESTS := uevent parser util dmevents hwtable
 
 .SILENT: $(TESTS:%=%.o)
 .PRECIOUS: $(TESTS:%=%-test)
@@ -19,11 +19,17 @@ all:        $(TESTS:%=%.out)
 # XYZ-test_LIBDEPS: Additional libs to link for this test
 
 dmevents-test_LIBDEPS = -lpthread -ldevmapper -lurcu
+hwtable-test_TESTDEPS := test-lib.o
+hwtable-test_OBJDEPS := ../libmultipath/discovery.o ../libmultipath/blacklist.o \
+       ../libmultipath/prio.o ../libmultipath/callout.o ../libmultipath/structs.o
+hwtable-test_LIBDEPS := -ludev -lpthread -ldl
 
 %.out: %-test
        @echo == running $< ==
        @LD_LIBRARY_PATH=$(multipathdir):$(mpathcmddir) ./$< >$@
 
+OBJS = $(TESTS:%=%.o) test-lib.o
+
 clean: dep_clean
        $(RM) $(TESTS:%=%-test) $(TESTS:%=%.out) $(OBJS)
 
diff --git a/tests/hwtable.c b/tests/hwtable.c
new file mode 100644 (file)
index 0000000..b2a0511
--- /dev/null
@@ -0,0 +1,1689 @@
+/* Set BROKEN to 1 to treat broken behavior as success */
+#define BROKEN 1
+#define VERBOSITY 2
+
+#include <stdbool.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdlib.h>
+#include <cmocka.h>
+#include <libudev.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/sysmacros.h>
+#include "structs.h"
+#include "structs_vec.h"
+#include "config.h"
+#include "debug.h"
+#include "defaults.h"
+#include "pgpolicies.h"
+#include "test-lib.h"
+
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0]))
+#define N_CONF_FILES 2
+
+static const char tmplate[] = "/tmp/hwtable-XXXXXX";
+/* pretend new dm, use minio_rq */
+static const unsigned int dm_tgt_version[3] = { 1, 1, 1 };
+
+struct key_value {
+       const char *key;
+       const char *value;
+};
+
+struct hwt_state {
+       char *tmpname;
+       char *dirname;
+       FILE *config_file;
+       FILE *conf_dir_file[N_CONF_FILES];
+       struct vectors *vecs;
+       void (*test)(const struct hwt_state *);
+       const char *test_name;
+};
+
+#define SET_TEST_FUNC(hwt, func) do {          \
+               hwt->test = func;               \
+               hwt->test_name = #func;         \
+       } while (0)
+
+static struct config *_conf;
+struct udev *udev;
+int logsink;
+
+struct config *get_multipath_config(void)
+{
+       return _conf;
+}
+
+void put_multipath_config(void *arg)
+{}
+
+void make_config_file_path(char *buf, int buflen,
+                         const struct hwt_state *hwt, int i)
+{
+       static const char fn_template[] = "%s/test-%02d.conf";
+
+       if (i == -1)
+               /* main config file */
+               snprintf(buf, buflen, fn_template, hwt->tmpname, 0);
+       else
+               snprintf(buf, buflen, fn_template, hwt->dirname, i);
+}
+
+static void reset_vecs(struct vectors *vecs)
+{
+       remove_maps(vecs);
+       free_pathvec(vecs->pathvec, FREE_PATHS);
+
+       vecs->pathvec = vector_alloc();
+       assert_ptr_not_equal(vecs->pathvec, NULL);
+       vecs->mpvec = vector_alloc();
+       assert_ptr_not_equal(vecs->mpvec, NULL);
+}
+
+static void free_hwt(struct hwt_state *hwt)
+{
+       char buf[PATH_MAX];
+       int i;
+
+       if (hwt->config_file != NULL)
+               fclose(hwt->config_file);
+       for (i = 0; i < N_CONF_FILES; i++) {
+               if (hwt->conf_dir_file[i] != NULL)
+                       fclose(hwt->conf_dir_file[i]);
+       }
+
+       if (hwt->tmpname != NULL) {
+               make_config_file_path(buf, sizeof(buf), hwt, -1);
+               unlink(buf);
+               rmdir(hwt->tmpname);
+               free(hwt->tmpname);
+       }
+
+       if (hwt->dirname != NULL) {
+               for (i = 0; i < N_CONF_FILES; i++) {
+                       make_config_file_path(buf, sizeof(buf), hwt, i);
+                       unlink(buf);
+               }
+               rmdir(hwt->dirname);
+               free(hwt->dirname);
+       }
+
+       if (hwt->vecs != NULL) {
+               if (hwt->vecs->mpvec != NULL)
+                       remove_maps(hwt->vecs);
+               if (hwt->vecs->pathvec != NULL)
+                       free_pathvec(hwt->vecs->pathvec, FREE_PATHS);
+               pthread_mutex_destroy(&hwt->vecs->lock.mutex);
+               free(hwt->vecs);
+       }
+       free(hwt);
+}
+
+static int setup(void **state)
+{
+       struct hwt_state *hwt;
+       char buf[PATH_MAX];
+       int i;
+
+       *state = NULL;
+       hwt = calloc(1, sizeof(*hwt));
+       if (hwt == NULL)
+               return -1;
+
+       snprintf(buf, sizeof(buf), "%s", tmplate);
+       if (mkdtemp(buf) == NULL) {
+               condlog(0, "mkdtemp: %s", strerror(errno));
+               goto err;
+       }
+       hwt->tmpname = strdup(buf);
+
+       snprintf(buf, sizeof(buf), "%s", tmplate);
+       if (mkdtemp(buf) == NULL) {
+               condlog(0, "mkdtemp (2): %s", strerror(errno));
+               goto err;
+       }
+       hwt->dirname = strdup(buf);
+
+       make_config_file_path(buf, sizeof(buf), hwt, -1);
+       hwt->config_file = fopen(buf, "w+");
+       if (hwt->config_file == NULL)
+               goto err;
+
+       for (i = 0; i < N_CONF_FILES; i++) {
+               make_config_file_path(buf, sizeof(buf), hwt, i);
+               hwt->conf_dir_file[i] = fopen(buf, "w+");
+               if (hwt->conf_dir_file[i] == NULL)
+                       goto err;
+       }
+
+       hwt->vecs = calloc(1, sizeof(*hwt->vecs));
+       if (hwt->vecs == NULL)
+               goto err;
+       pthread_mutex_init(&hwt->vecs->lock.mutex, NULL);
+       hwt->vecs->pathvec = vector_alloc();
+       hwt->vecs->mpvec = vector_alloc();
+       if (hwt->vecs->pathvec == NULL || hwt->vecs->mpvec == NULL)
+               goto err;
+
+       *state = hwt;
+       return 0;
+
+err:
+       free_hwt(hwt);
+       return -1;
+}
+
+static int teardown(void **state)
+{
+       if (state == NULL || *state == NULL)
+               return -1;
+
+       free_hwt(*state);
+       *state = NULL;
+
+       return 0;
+}
+
+/*
+ * Helpers for creating the config file(s)
+ */
+
+static void reset_config(FILE *ff)
+{
+       if (ff == NULL)
+               return;
+       rewind(ff);
+       if (ftruncate(fileno(ff), 0) == -1)
+               condlog(1, "ftruncate: %s", strerror(errno));
+}
+
+static void reset_configs(const struct hwt_state *hwt)
+{
+       int i;
+
+       reset_config(hwt->config_file);
+       for (i = 0; i < N_CONF_FILES; i++)
+               reset_config(hwt->conf_dir_file[i]);
+}
+
+static void write_key_values(FILE *ff, int nkv, const struct key_value *kv)
+{
+       int i;
+
+       for (i = 0; i < nkv; i++) {
+               if (strchr(kv[i].value, ' ') == NULL &&
+                   strchr(kv[i].value, '\"') == NULL)
+                       fprintf(ff, "\t%s %s\n", kv[i].key, kv[i].value);
+               else
+                       fprintf(ff, "\t%s \"%s\"\n", kv[i].key, kv[i].value);
+       }
+}
+
+static void begin_section(FILE *ff, const char *section)
+{
+       fprintf(ff, "%s {\n", section);
+}
+
+static void end_section(FILE *ff)
+{
+       fprintf(ff, "}\n");
+}
+
+static void write_section(FILE *ff, const char *section,
+                         int nkv, const struct key_value *kv)
+{
+       begin_section(ff, section);
+       write_key_values(ff, nkv, kv);
+       end_section(ff);
+}
+
+static void write_defaults(const struct hwt_state *hwt)
+{
+       static const char bindings_name[] = "bindings";
+       static struct key_value defaults[] = {
+               { "config_dir", NULL },
+               { "bindings_file", NULL },
+               { "detect_prio", "no" },
+               { "detect_checker", "no" },
+       };
+       char buf[sizeof(tmplate) + sizeof(bindings_name)];
+
+       snprintf(buf, sizeof(buf), "%s/%s", hwt->tmpname, bindings_name);
+       defaults[0].value = hwt->dirname;
+       defaults[1].value = buf;
+       write_section(hwt->config_file, "defaults",
+                     ARRAY_SIZE(defaults), defaults);
+}
+
+static void begin_config(const struct hwt_state *hwt)
+{
+       reset_configs(hwt);
+       write_defaults(hwt);
+}
+
+static void begin_section_all(const struct hwt_state *hwt, const char *section)
+{
+       int i;
+
+       begin_section(hwt->config_file, section);
+       for (i = 0; i < N_CONF_FILES; i++)
+               begin_section(hwt->conf_dir_file[i], section);
+}
+
+static void end_section_all(const struct hwt_state *hwt)
+{
+       int i;
+
+       end_section(hwt->config_file);
+       for (i = 0; i < N_CONF_FILES; i++)
+               end_section(hwt->conf_dir_file[i]);
+}
+
+static void finish_config(const struct hwt_state *hwt)
+{
+       int i;
+
+       fflush(hwt->config_file);
+       for (i = 0; i < N_CONF_FILES; i++) {
+               fflush(hwt->conf_dir_file[i]);
+       }
+}
+
+static void write_device(FILE *ff, int nkv, const struct key_value *kv)
+{
+       write_section(ff, "device", nkv, kv);
+}
+
+/*
+ * Some macros to avoid boilerplace code
+ */
+
+#define CHECK_STATE(state) ({ \
+       assert_ptr_not_equal(state, NULL); \
+       assert_ptr_not_equal(*(state), NULL);   \
+       *state; })
+
+#define WRITE_EMPTY_CONF(hwt) do {                             \
+               begin_config(hwt);                              \
+               finish_config(hwt);                             \
+       } while (0)
+
+#define WRITE_ONE_DEVICE(hwt, kv) do {                                 \
+               begin_config(hwt);                                      \
+               begin_section_all(hwt, "devices");                      \
+               write_device(hwt->config_file, ARRAY_SIZE(kv), kv);     \
+               end_section_all(hwt);                                   \
+               finish_config(hwt);                                     \
+       } while (0)
+
+#define WRITE_TWO_DEVICES(hwt, kv1, kv2) do {                          \
+               begin_config(hwt);                                      \
+               begin_section_all(hwt, "devices");                      \
+               write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);   \
+               write_device(hwt->config_file, ARRAY_SIZE(kv2), kv2);   \
+               end_section_all(hwt);                                   \
+               finish_config(hwt);                                     \
+       } while (0)
+
+#define WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2) do {                    \
+               begin_config(hwt);                                      \
+               begin_section_all(hwt, "devices");                      \
+               write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);   \
+               write_device(hwt->conf_dir_file[0],                     \
+                            ARRAY_SIZE(kv2), kv2);                     \
+               end_section_all(hwt);                                   \
+               finish_config(hwt);                                     \
+       } while (0)
+
+#define LOAD_CONFIG(hwt) ({ \
+       char buf[PATH_MAX];        \
+       struct config *__cf;                                            \
+                                                                       \
+       make_config_file_path(buf, sizeof(buf), hwt, -1);               \
+       __cf = load_config(buf);                                        \
+       assert_ptr_not_equal(__cf, NULL);                               \
+       assert_ptr_not_equal(__cf->hwtable, NULL);                      \
+       __cf->verbosity = VERBOSITY;                                    \
+       memcpy(&__cf->version, dm_tgt_version, sizeof(__cf->version));  \
+       __cf; })
+
+#define FREE_CONFIG(conf) do {                 \
+               free_config(conf);              \
+               conf = NULL;                    \
+       } while (0)
+
+#define TEST_PROP(prop, val) do {                              \
+               if (val == NULL)                                \
+                       assert_ptr_equal(prop, NULL);           \
+               else {                                          \
+                       assert_ptr_not_equal(prop, NULL);       \
+                       assert_string_equal(prop, val);         \
+               }                                               \
+       } while (0)
+
+#if BROKEN
+#define TEST_PROP_BROKEN(name, prop, bad, good) do {                   \
+               condlog(1, "%s: WARNING: Broken test for %s == \"%s\" on line %d, should be \"%s\"", \
+                       __func__, name, bad ? bad : "NULL",             \
+                       __LINE__, good ? good : "NULL");                        \
+               TEST_PROP(prop, bad);                                   \
+       } while (0)
+#else
+#define TEST_PROP_BROKEN(name, prop, bad, good) TEST_PROP(prop, good)
+#endif
+
+/*
+ * Some predefined key/value pairs
+ */
+
+static const char _wwid[] = "wwid";
+static const char _vendor[] = "vendor";
+static const char _product[] = "product";
+static const char _prio[] = "prio";
+static const char _checker[] = "path_checker";
+static const char _getuid[] = "getuid_callout";
+static const char _uid_attr[] = "uid_attribute";
+static const char _bl_product[] = "product_blacklist";
+static const char _minio[] = "rr_min_io_rq";
+static const char _no_path_retry[] = "no_path_retry";
+
+/* Device identifiers */
+static const struct key_value vnd_foo = { _vendor, "foo" };
+static const struct key_value prd_bar = { _product, "bar" };
+static const struct key_value prd_bam = { _product, "bam" };
+static const struct key_value prd_baq = { _product, "\"bar\"" };
+static const struct key_value prd_baqq = { _product, "\"\"bar\"\"" };
+static const struct key_value prd_barz = { _product, "barz" };
+static const struct key_value vnd_boo = { _vendor, "boo" };
+static const struct key_value prd_baz = { _product, "baz" };
+static const struct key_value wwid_test = { _wwid, default_wwid };
+
+/* Regular expresssions */
+static const struct key_value vnd__oo = { _vendor, ".oo" };
+static const struct key_value vnd_t_oo = { _vendor, "^.oo" };
+static const struct key_value prd_ba_ = { _product, "ba." };
+static const struct key_value prd_ba_s = { _product, "(bar|baz|ba\\.)$" };
+/* Pathological cases, see below */
+static const struct key_value prd_barx = { _product, "ba[[rxy]" };
+static const struct key_value prd_bazy = { _product, "ba[zy]" };
+static const struct key_value prd_bazy1 = { _product, "ba(z|y)" };
+
+/* Properties */
+static const struct key_value prio_emc = { _prio, "emc" };
+static const struct key_value prio_hds = { _prio, "hds" };
+static const struct key_value prio_rdac = { _prio, "rdac" };
+static const struct key_value chk_hp = { _checker, "hp_sw" };
+static const struct key_value gui_foo = { _getuid, "/tmp/foo" };
+static const struct key_value uid_baz = { _uid_attr, "BAZ_ATTR" };
+static const struct key_value bl_bar = { _bl_product, "bar" };
+static const struct key_value bl_baz = { _bl_product, "baz" };
+static const struct key_value bl_barx = { _bl_product, "ba[[rxy]" };
+static const struct key_value bl_bazy = { _bl_product, "ba[zy]" };
+static const struct key_value minio_99 = { _minio, "99" };
+static const struct key_value npr_37 = { _no_path_retry, "37" };
+static const struct key_value npr_queue = { _no_path_retry, "queue" };
+
+/***** BEGIN TESTS SECTION *****/
+
+/*
+ * Run the given test case. This will later be extended
+ * to run the test several times in a row.
+ */
+static void test_driver(void **state)
+{
+       const struct hwt_state *hwt;
+
+       hwt = CHECK_STATE(state);
+       _conf = LOAD_CONFIG(hwt);
+       hwt->test(hwt);
+
+       reset_vecs(hwt->vecs);
+       FREE_CONFIG(_conf);
+}
+
+/*
+ * Sanity check for the test itself, because defaults may be changed
+ * in libmultipath.
+ *
+ * Our checking for match or non-match relies on the defaults being
+ * different from what our device sections contain.
+ */
+static void test_sanity_globals(void **state)
+{
+       assert_string_not_equal(prio_emc.value, DEFAULT_PRIO);
+       assert_string_not_equal(prio_hds.value, DEFAULT_PRIO);
+       assert_string_not_equal(chk_hp.value, DEFAULT_CHECKER);
+       assert_int_not_equal(MULTIBUS, DEFAULT_PGPOLICY);
+       assert_int_not_equal(NO_PATH_RETRY_QUEUE, DEFAULT_NO_PATH_RETRY);
+       assert_int_not_equal(atoi(minio_99.value), DEFAULT_MINIO_RQ);
+       assert_int_not_equal(atoi(npr_37.value), DEFAULT_NO_PATH_RETRY);
+}
+
+/*
+ * Regression test for internal hwtable. NVME is an example of two entries
+ * in the built-in hwtable, one if which matches a subset of the other.
+ */
+static void test_internal_nvme(const struct hwt_state *hwt)
+{
+       struct path *pp;
+       struct multipath *mp;
+
+       /*
+        * Generic NVMe: expect defaults for pgpolicy and no_path_retry
+        */
+       pp = mock_path("NVME", "NoName");
+       mp = mock_multipath(pp);
+       assert_ptr_not_equal(mp, NULL);
+       TEST_PROP(pp->checker.name, NONE);
+       TEST_PROP(pp->uid_attribute, "ID_WWN");
+       assert_int_equal(mp->pgpolicy, DEFAULT_PGPOLICY);
+       assert_int_equal(mp->no_path_retry, DEFAULT_NO_PATH_RETRY);
+       assert_int_equal(mp->retain_hwhandler, RETAIN_HWHANDLER_OFF);
+
+       /*
+        * NetApp NVMe: expect special values for pgpolicy and no_path_retry
+        */
+       pp = mock_path_wwid("NVME", "NetApp ONTAP Controller",
+                           default_wwid_1);
+       mp = mock_multipath(pp);
+       assert_ptr_not_equal(mp, NULL);
+       TEST_PROP(pp->checker.name, NONE);
+       TEST_PROP(pp->uid_attribute, "ID_WWN");
+       assert_int_equal(mp->pgpolicy, MULTIBUS);
+       assert_int_equal(mp->no_path_retry, NO_PATH_RETRY_QUEUE);
+       assert_int_equal(mp->retain_hwhandler, RETAIN_HWHANDLER_OFF);
+}
+
+static int setup_internal_nvme(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_EMPTY_CONF(hwt);
+       SET_TEST_FUNC(hwt, test_internal_nvme);
+
+       return 0;
+}
+
+/*
+ * Device section with a simple entry qith double quotes ('foo:"bar"')
+ */
+static void test_quoted_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+       /* foo:"bar" matches */
+       pp = mock_path(vnd_foo.value, prd_baq.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+
+       /* foo:bar doesn't match */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+}
+
+static int setup_quoted_hwe(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+       const struct key_value kv[] = { vnd_foo, prd_baqq, prio_emc };
+
+       WRITE_ONE_DEVICE(hwt, kv);
+       SET_TEST_FUNC(hwt, test_quoted_hwe);
+       return 0;
+}
+
+/*
+ * Device section with a single simple entry ("foo:bar")
+ */
+static void test_string_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:bar matches */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+
+       /* boo:bar doesn't match */
+       pp = mock_path(vnd_boo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+}
+
+static int setup_string_hwe(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+       const struct key_value kv[] = { vnd_foo, prd_bar, prio_emc };
+
+       WRITE_ONE_DEVICE(hwt, kv);
+       SET_TEST_FUNC(hwt, test_string_hwe);
+       return 0;
+}
+
+/*
+ * Device section with a single regex entry ("^.foo:(bar|baz|ba\.)$")
+ */
+static void test_regex_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:bar matches */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+
+       /* foo:baz matches */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+
+       /* boo:baz matches */
+       pp = mock_path(vnd_boo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+
+       /* foo:BAR doesn't match */
+       pp = mock_path(vnd_foo.value, "BAR");
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+
+       /* bboo:bar doesn't match */
+       pp = mock_path("bboo", prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+}
+
+static int setup_regex_hwe(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+       const struct key_value kv[] = { vnd_t_oo, prd_ba_s, prio_emc };
+
+       WRITE_ONE_DEVICE(hwt, kv);
+       SET_TEST_FUNC(hwt, test_regex_hwe);
+       return 0;
+}
+
+/*
+ * Two device entries, kv1 is a regex match ("^.foo:(bar|baz|ba\.)$"),
+ * kv2 a string match (foo:bar) which matches a subset of the regex.
+ * Both are added to the main config file.
+ *
+ * Expected: Devices matching both get properties from both, kv2 taking
+ * precedence. Devices matching kv1 only just get props from kv1.
+ *
+ * Current: These entries are currently _NOT_ merged, therefore getuid is
+ * default for kv1 matches, and checker is default on kv2 matches.
+ */
+static void test_regex_string_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz matches kv1 */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* boo:baz matches kv1 */
+       pp = mock_path(vnd_boo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .oo:ba. matches kv1 */
+       pp = mock_path(vnd__oo.value, prd_ba_.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .foo:(bar|baz|ba\.) doesn't match */
+       pp = mock_path(vnd__oo.value, prd_ba_s.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches kv2 and kv1 */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       /*
+        * You'd expect that the two entries above be merged,
+        * but that isn't the case if they're in the same input file.
+        */
+       TEST_PROP_BROKEN(_checker, pp->checker.name,
+                        DEFAULT_CHECKER, chk_hp.value);
+}
+
+static int setup_regex_string_hwe(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+       const struct key_value kv1[] = { vnd_t_oo, prd_ba_s, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+
+       WRITE_TWO_DEVICES(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_regex_string_hwe);
+       return 0;
+}
+
+/*
+ * Two device entries, kv1 is a regex match ("^.foo:(bar|baz|ba\.)$"),
+ * kv2 a string match (foo:bar) which matches a subset of the regex.
+ * kv1 is added to the main config file, kv2 to a config_dir file.
+ * This case is more important as you may think, because it's equivalent
+ * to kv1 being in the built-in hwtable and kv2 in multipath.conf.
+ *
+ * Expected: Devices matching kv2 (and thus, both) get properties
+ * from both, kv2 taking precedence.
+ * Devices matching kv1 only just get props from kv1.
+ *
+ * Current: behaves as expected.
+ */
+static void test_regex_string_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz matches kv1 */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* boo:baz matches kv1 */
+       pp = mock_path(vnd_boo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .oo:ba. matches kv1 */
+       pp = mock_path(vnd__oo.value, prd_ba_.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .oo:(bar|baz|ba\.)$ doesn't match */
+       pp = mock_path(vnd__oo.value, prd_ba_s.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches kv2 */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       /* Later match takes prio */
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       /* This time it's merged */
+       TEST_PROP(pp->checker.name, chk_hp.value);
+}
+
+static int setup_regex_string_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_t_oo, prd_ba_s, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_regex_string_hwe_dir);
+       return 0;
+}
+
+/*
+ * Three device entries, kv1 is a regex match and kv2 and kv3 string
+ * matches, where kv3 is a substring of kv2. All in different config
+ * files.
+ *
+ * Expected: Devices matching kv3 get props from all, devices matching
+ * kv2 from kv2 and kv1, and devices matching kv1 only just from kv1.
+ */
+static void test_regex_2_strings_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz matches kv1 */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->uid_attribute, DEFAULT_UID_ATTRIBUTE);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* boo:baz doesn't match */
+       pp = mock_path(vnd_boo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->uid_attribute, DEFAULT_UID_ATTRIBUTE);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches kv2 and kv1 */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->uid_attribute, uid_baz.value);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* foo:barz matches kv3 and kv2 and kv1 */
+       pp = mock_path_flags(vnd_foo.value, prd_barz.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_rdac.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP(pp->uid_attribute, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+}
+
+static int setup_regex_2_strings_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_ba_, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, uid_baz };
+       const struct key_value kv3[] = { vnd_foo, prd_barz,
+                                        prio_rdac, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "devices");
+       write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);
+       write_device(hwt->conf_dir_file[0], ARRAY_SIZE(kv2), kv2);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv3), kv3);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_regex_2_strings_hwe_dir);
+       return 0;
+}
+
+/*
+ * Like test_regex_string_hwe_dir, but the order of kv1 and kv2 is exchanged.
+ *
+ * Expected: Devices matching kv1 (and thus, both) get properties
+ * from both, kv1 taking precedence.
+ * Devices matching kv1 only just get props from kv1.
+ *
+ * Current: kv2 never matches, because kv1 is more generic and encountered
+ * first; thus properties from kv2 aren't used.
+ */
+static void test_string_regex_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:bar matches kv2 and kv1 */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value,
+                            BROKEN == 1 ? 0 : USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP_BROKEN(_getuid, pp->getuid, (char *)NULL, gui_foo.value);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* foo:baz matches kv1 */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* boo:baz matches kv1 */
+       pp = mock_path(vnd_boo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .oo:ba. matches kv1 */
+       pp = mock_path(vnd__oo.value, prd_ba_.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* .oo:(bar|baz|ba\.)$ doesn't match */
+       pp = mock_path(vnd__oo.value, prd_ba_s.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+}
+
+static int setup_string_regex_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_t_oo, prd_ba_s, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv2, kv1);
+       SET_TEST_FUNC(hwt, test_string_regex_hwe_dir);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, trival regex ("string").
+ * Both are added to the main config file.
+ * These entries are NOT merged.
+ * This could happen in a large multipath.conf file.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ *
+ * Current: devices get props from kv2 only.
+ */
+static void test_2_ident_strings_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both, but only kv2 is seen */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name, DEFAULT_CHECKER,
+                        chk_hp.value);
+}
+
+static int setup_2_ident_strings_hwe(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_ident_strings_hwe);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, trival regex ("string").
+ * Both are added to an extra config file.
+ * This could happen in a large multipath.conf file.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ */
+static void test_2_ident_strings_both_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name, DEFAULT_CHECKER,
+                        chk_hp.value);
+}
+
+static int setup_2_ident_strings_both_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "devices");
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv1), kv1);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_2_ident_strings_both_dir);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, trival regex ("string").
+ * Both are added to an extra config file.
+ * An empty entry kv0 with the same string exists in the main config file.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ */
+static void test_2_ident_strings_both_dir_w_prev(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name, DEFAULT_CHECKER,
+                        chk_hp.value);
+}
+
+static int setup_2_ident_strings_both_dir_w_prev(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       const struct key_value kv0[] = { vnd_foo, prd_bar };
+       const struct key_value kv1[] = { vnd_foo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+
+       begin_config(hwt);
+       begin_section_all(hwt, "devices");
+       write_device(hwt->config_file, ARRAY_SIZE(kv0), kv0);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv1), kv1);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_2_ident_strings_both_dir_w_prev);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, trival regex ("string").
+ * kv1 is added to the main config file, kv2 to a config_dir file.
+ * These entries are merged.
+ * This case is more important as you may think, because it's equivalent
+ * to kv1 being in the built-in hwtable and kv2 in multipath.conf.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ *
+ * Current: behaves as expected.
+ */
+static void test_2_ident_strings_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+}
+
+static int setup_2_ident_strings_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_ident_strings_hwe_dir);
+       return 0;
+}
+
+/*
+ * Like test_2_ident_strings_hwe_dir, but this time the config_dir file
+ * contains an additional, empty entry (kv0).
+ *
+ * Expected: matching devices get props from kv1 and kv2, kv2 taking precedence.
+ *
+ * Current: kv0 and kv1 are merged into kv0, and then ignored because kv2 takes
+ * precedence. Thus the presence of the empty kv0 changes how kv1 is treated.
+ */
+static void test_3_ident_strings_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name, DEFAULT_CHECKER,
+                        chk_hp.value);
+}
+
+static int setup_3_ident_strings_hwe_dir(void **state)
+{
+       const struct key_value kv0[] = { vnd_foo, prd_bar };
+       const struct key_value kv1[] = { vnd_foo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "devices");
+       write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv0), kv0);
+       write_device(hwt->conf_dir_file[1], ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_3_ident_strings_hwe_dir);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, non-trival regex that matches
+ * itself (string ".oo" matches regex ".oo").
+ * kv1 is added to the main config file, kv2 to a config_dir file.
+ * This case is more important as you may think, because it's equivalent
+ * to kv1 being in the built-in hwtable and kv2 in multipath.conf.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ *
+ * Current: behaves as expected.
+ */
+static void test_2_ident_self_matching_re_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+}
+
+static int setup_2_ident_self_matching_re_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd__oo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd__oo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_ident_self_matching_re_hwe_dir);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, non-trival regex that matches
+ * itself (string ".oo" matches regex ".oo").
+ * kv1 and kv2 are added to the main config file.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ *
+ * Current: Devices get properties from kv2 only (kv1 and kv2 are not merged).
+ */
+static void test_2_ident_self_matching_re_hwe(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name,
+                        DEFAULT_CHECKER, chk_hp.value);
+}
+
+static int setup_2_ident_self_matching_re_hwe(void **state)
+{
+       const struct key_value kv1[] = { vnd__oo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd__oo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_ident_self_matching_re_hwe);
+       return 0;
+}
+
+/*
+ * Two identical device entries kv1 and kv2, non-trival regex that doesn't
+ * match itself (string "^.oo" doesn't match regex "^.oo").
+ * kv1 is added to the main config file, kv2 to a config_dir file.
+ * This case is more important as you may think, see above.
+ *
+ * Expected: matching devices get props from both, kv2 taking precedence.
+ *
+ * Current: devices get props from kv2 only.
+ */
+static void
+test_2_ident_not_self_matching_re_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:baz doesn't match */
+       pp = mock_path(vnd_foo.value, prd_baz.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /* foo:bar matches both, but only kv2 is seen */
+       pp = mock_path_flags(vnd_foo.value, prd_bar.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name,
+                        DEFAULT_CHECKER, chk_hp.value);
+}
+
+static int setup_2_ident_not_self_matching_re_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_t_oo, prd_bar, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_t_oo, prd_bar, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_ident_not_self_matching_re_hwe_dir);
+       return 0;
+}
+
+/*
+ * Two different non-trivial regexes kv1, kv2. The 1st one matches the 2nd, but
+ * it doesn't match all possible strings matching the second.
+ * ("ba[zy]" matches regex "ba[[rxy]", but "baz" does not).
+ * This causes the first entry to be merged into the second, but both entries
+ * to be kept.
+ *
+ * Expected: Devices matching both regexes get properties from both, kv2
+ * taking precedence. Devices matching just one regex get properties from
+ * that one regex only.
+ *
+ * Current: behaves as expected, except for devices that match only kv2.
+ * Those get properties from kv1, too.
+ */
+static void test_2_matching_res_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:bar matches k1 only */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* foo:bay matches k1 and k2 */
+       pp = mock_path_flags(vnd_foo.value, "bay", USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP(pp->checker.name, chk_hp.value);
+
+       /* foo:baz matches k2 only. */
+       pp = mock_path_flags(vnd_foo.value, prd_baz.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name,
+                        chk_hp.value, DEFAULT_CHECKER);
+}
+
+static int setup_2_matching_res_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_barx, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bazy, prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_matching_res_hwe_dir);
+       return 0;
+}
+
+/*
+ * Two different non-trivial regexes which match the same set of strings.
+ * But they don't match each other.
+ * "baz" matches both regex "ba[zy]" and "ba(z|y)"
+ *
+ * Expected: matching devices get properties from both, kv2 taking precedence.
+ *
+ * Current: matching devices get properties from kv2 only.
+ */
+static void test_2_nonmatching_res_hwe_dir(const struct hwt_state *hwt)
+{
+       struct path *pp;
+
+       /* foo:bar doesn't match */
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       TEST_PROP(prio_name(&pp->prio), DEFAULT_PRIO);
+       TEST_PROP(pp->getuid, NULL);
+       TEST_PROP(pp->checker.name, DEFAULT_CHECKER);
+
+       /*
+        * foo:baz matches k2 and k1. Yet it sees the value from k2 only.
+        */
+       pp = mock_path_flags(vnd_foo.value, prd_baz.value, USE_GETUID);
+       TEST_PROP(prio_name(&pp->prio), prio_hds.value);
+       TEST_PROP(pp->getuid, gui_foo.value);
+       TEST_PROP_BROKEN(_checker, pp->checker.name,
+                        DEFAULT_CHECKER, chk_hp.value);
+}
+
+static int setup_2_nonmatching_res_hwe_dir(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bazy, prio_emc, chk_hp };
+       const struct key_value kv2[] = { vnd_foo, prd_bazy1,
+                                        prio_hds, gui_foo };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES_W_DIR(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_2_nonmatching_res_hwe_dir);
+       return 0;
+}
+
+/*
+ * Simple blacklist test.
+ *
+ * NOTE: test failures in blacklisting tests will manifest as cmocka errors
+ * "Could not get value to mock function XYZ", because pathinfo() takes
+ * different code paths for blacklisted devices.
+ */
+static void test_blacklist(const struct hwt_state *hwt)
+{
+       mock_path_flags(vnd_foo.value, prd_bar.value, BL_BY_DEVICE);
+       mock_path(vnd_foo.value, prd_baz.value);
+}
+
+static int setup_blacklist(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "blacklist");
+       write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist);
+       return 0;
+}
+
+/*
+ * Simple blacklist test with regex and exception
+ */
+static void test_blacklist_regex(const struct hwt_state *hwt)
+{
+       mock_path(vnd_foo.value, prd_bar.value);
+       mock_path_flags(vnd_foo.value, prd_baz.value, BL_BY_DEVICE);
+       mock_path(vnd_foo.value, prd_bam.value);
+}
+
+static int setup_blacklist_regex(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_ba_s };
+       const struct key_value kv2[] = { vnd_foo, prd_bar };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       hwt = CHECK_STATE(state);
+       begin_config(hwt);
+       begin_section_all(hwt, "blacklist");
+       write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);
+       end_section_all(hwt);
+       begin_section_all(hwt, "blacklist_exceptions");
+       write_device(hwt->conf_dir_file[0], ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist_regex);
+       return 0;
+}
+
+/*
+ * Simple blacklist test with regex and exception
+ * config file order inverted wrt test_blacklist_regex
+ */
+static int setup_blacklist_regex_inv(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_ba_s };
+       const struct key_value kv2[] = { vnd_foo, prd_bar };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "blacklist");
+       write_device(hwt->conf_dir_file[0], ARRAY_SIZE(kv1), kv1);
+       end_section_all(hwt);
+       begin_section_all(hwt, "blacklist_exceptions");
+       write_device(hwt->config_file, ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist_regex);
+       return 0;
+}
+
+/*
+ * Simple blacklist test with regex and exception
+ * config file order inverted wrt test_blacklist_regex
+ */
+static void test_blacklist_regex_matching(const struct hwt_state *hwt)
+{
+       mock_path_flags(vnd_foo.value, prd_bar.value, BL_BY_DEVICE);
+       mock_path_flags(vnd_foo.value, prd_baz.value, BL_BY_DEVICE);
+       mock_path(vnd_foo.value, prd_bam.value);
+}
+
+static int setup_blacklist_regex_matching(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_barx };
+       const struct key_value kv2[] = { vnd_foo, prd_bazy };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "blacklist");
+       write_device(hwt->config_file, ARRAY_SIZE(kv1), kv1);
+       write_device(hwt->conf_dir_file[0], ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist_regex_matching);
+       return 0;
+}
+
+/*
+ * Test for blacklisting by WWID
+ *
+ * Note that default_wwid is a substring of default_wwid_1. Because
+ * matching is done by regex, both paths are blacklisted.
+ */
+static void test_blacklist_wwid(const struct hwt_state *hwt)
+{
+       mock_path_flags(vnd_foo.value, prd_bar.value, BL_BY_WWID);
+       mock_path_wwid_flags(vnd_foo.value, prd_baz.value, default_wwid_1,
+                            BL_BY_WWID);
+}
+
+static int setup_blacklist_wwid(void **state)
+{
+       const struct key_value kv[] = { wwid_test };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       write_section(hwt->config_file, "blacklist", ARRAY_SIZE(kv), kv);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist_wwid);
+       return 0;
+}
+
+/*
+ * Test for blacklisting by WWID
+ *
+ * Here the blacklist contains only default_wwid_1. Thus the path
+ * with default_wwid is NOT blacklisted.
+ */
+static void test_blacklist_wwid_1(const struct hwt_state *hwt)
+{
+       mock_path(vnd_foo.value, prd_bar.value);
+       mock_path_wwid_flags(vnd_foo.value, prd_baz.value, default_wwid_1,
+                            BL_BY_WWID);
+}
+
+static int setup_blacklist_wwid_1(void **state)
+{
+       const struct key_value kv[] = { { _wwid, default_wwid_1 }, };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       write_section(hwt->config_file, "blacklist", ARRAY_SIZE(kv), kv);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_blacklist_wwid_1);
+       return 0;
+}
+
+/*
+ * Test for product_blacklist. Two entries blacklisting each other.
+ *
+ * Expected: Both are blacklisted.
+ */
+static void test_product_blacklist(const struct hwt_state *hwt)
+{
+       mock_path_flags(vnd_foo.value, prd_baz.value, BL_BY_DEVICE);
+       mock_path_flags(vnd_foo.value, prd_bar.value, BL_BY_DEVICE);
+       mock_path(vnd_foo.value, prd_bam.value);
+}
+
+static int setup_product_blacklist(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar, bl_baz };
+       const struct key_value kv2[] = { vnd_foo, prd_baz, bl_bar };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_product_blacklist);
+       return 0;
+}
+
+/*
+ * Test for product_blacklist. The second regex "matches" the first.
+ * This is a pathological example.
+ *
+ * Expected: "foo:bar", "foo:baz" are blacklisted.
+ */
+static void test_product_blacklist_matching(const struct hwt_state *hwt)
+{
+       mock_path_flags(vnd_foo.value, prd_bar.value, BL_BY_DEVICE);
+#if BROKEN == 1
+       condlog(1, "%s: WARNING: broken blacklist test on line %d",
+               __func__, __LINE__ + 1);
+       mock_path(vnd_foo.value, prd_baz.value);
+#else
+       mock_path_blacklisted(vnd_foo.value, prd_baz.value);
+#endif
+       mock_path(vnd_foo.value, prd_bam.value);
+}
+
+static int setup_product_blacklist_matching(void **state)
+{
+       const struct key_value kv1[] = { vnd_foo, prd_bar, bl_barx };
+       const struct key_value kv2[] = { vnd_foo, prd_baz, bl_bazy };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       WRITE_TWO_DEVICES(hwt, kv1, kv2);
+       SET_TEST_FUNC(hwt, test_product_blacklist_matching);
+       return 0;
+}
+
+/*
+ * Basic test for multipath-based configuration.
+ *
+ * Expected: properties, including pp->prio, are taken from multipath
+ * section.
+ */
+static void test_multipath_config(const struct hwt_state *hwt)
+{
+       struct path *pp;
+       struct multipath *mp;
+
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       mp = mock_multipath(pp);
+       assert_ptr_not_equal(mp->mpe, NULL);
+       TEST_PROP(prio_name(&pp->prio), prio_rdac.value);
+       assert_int_equal(mp->minio, atoi(minio_99.value));
+       TEST_PROP(pp->uid_attribute, uid_baz.value);
+
+       /* test different wwid */
+       pp = mock_path_wwid(vnd_foo.value, prd_bar.value, default_wwid_1);
+       mp = mock_multipath(pp);
+       // assert_ptr_equal(mp->mpe, NULL);
+       TEST_PROP(prio_name(&pp->prio), prio_emc.value);
+       assert_int_equal(mp->minio, DEFAULT_MINIO_RQ);
+       TEST_PROP(pp->uid_attribute, uid_baz.value);
+}
+
+static int setup_multipath_config(void **state)
+{
+       struct hwt_state *hwt = CHECK_STATE(state);
+       const struct key_value kvm[] = { wwid_test, prio_rdac, minio_99 };
+       const struct key_value kvp[] = { vnd_foo, prd_bar, prio_emc, uid_baz };
+
+       begin_config(hwt);
+       begin_section_all(hwt, "devices");
+       write_section(hwt->conf_dir_file[0], "device", ARRAY_SIZE(kvp), kvp);
+       end_section_all(hwt);
+       begin_section_all(hwt, "multipaths");
+       write_section(hwt->config_file, "multipath", ARRAY_SIZE(kvm), kvm);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_multipath_config);
+       return 0;
+}
+
+/*
+ * Basic test for multipath-based configuration. Two sections for the same wwid.
+ *
+ * Expected: properties are taken from both multipath sections, later taking
+ * precedence
+ *
+ * Current: gets properties from first entry only.
+ */
+static void test_multipath_config_2(const struct hwt_state *hwt)
+{
+       struct path *pp;
+       struct multipath *mp;
+
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       mp = mock_multipath(pp);
+       assert_ptr_not_equal(mp, NULL);
+       assert_ptr_not_equal(mp->mpe, NULL);
+       TEST_PROP(prio_name(&pp->prio), prio_rdac.value);
+#if BROKEN
+       condlog(1, "%s: WARNING: broken test on %d", __func__, __LINE__ + 1);
+       assert_int_equal(mp->minio, DEFAULT_MINIO_RQ);
+       condlog(1, "%s: WARNING: broken test on %d", __func__, __LINE__ + 1);
+       assert_int_equal(mp->no_path_retry, NO_PATH_RETRY_QUEUE);
+#else
+       assert_int_equal(mp->minio, atoi(minio_99.value));
+       assert_int_equal(mp->no_path_retry, atoi(npr_37.value));
+#endif
+}
+
+static int setup_multipath_config_2(void **state)
+{
+       const struct key_value kv1[] = { wwid_test, prio_rdac, npr_queue };
+       const struct key_value kv2[] = { wwid_test, minio_99, npr_37 };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "multipaths");
+       write_section(hwt->config_file, "multipath", ARRAY_SIZE(kv1), kv1);
+       write_section(hwt->conf_dir_file[1], "multipath", ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_multipath_config_2);
+       return 0;
+}
+
+/*
+ * Same as test_multipath_config_2, both entries in the same config file.
+ *
+ * Expected: properties are taken from both multipath sections.
+ */
+static void test_multipath_config_3(const struct hwt_state *hwt)
+{
+       struct path *pp;
+       struct multipath *mp;
+
+       pp = mock_path(vnd_foo.value, prd_bar.value);
+       mp = mock_multipath(pp);
+       assert_ptr_not_equal(mp, NULL);
+       assert_ptr_not_equal(mp->mpe, NULL);
+       TEST_PROP(prio_name(&pp->prio), prio_rdac.value);
+#if BROKEN
+       condlog(1, "%s: WARNING: broken test on %d", __func__, __LINE__ + 1);
+       assert_int_equal(mp->minio, DEFAULT_MINIO_RQ);
+       condlog(1, "%s: WARNING: broken test on %d", __func__, __LINE__ + 1);
+       assert_int_equal(mp->no_path_retry, NO_PATH_RETRY_QUEUE);
+#else
+       assert_int_equal(mp->minio, atoi(minio_99.value));
+       assert_int_equal(mp->no_path_retry, atoi(npr_37.value));
+#endif
+}
+
+static int setup_multipath_config_3(void **state)
+{
+       const struct key_value kv1[] = { wwid_test, prio_rdac, npr_queue };
+       const struct key_value kv2[] = { wwid_test, minio_99, npr_37 };
+       struct hwt_state *hwt = CHECK_STATE(state);
+
+       begin_config(hwt);
+       begin_section_all(hwt, "multipaths");
+       write_section(hwt->config_file, "multipath", ARRAY_SIZE(kv1), kv1);
+       write_section(hwt->config_file, "multipath", ARRAY_SIZE(kv2), kv2);
+       end_section_all(hwt);
+       finish_config(hwt);
+       SET_TEST_FUNC(hwt, test_multipath_config_3);
+       return 0;
+}
+
+/*
+ * Create wrapper functions around test_driver() to avoid that cmocka
+ * always uses the same test name. That makes it easier to read test results.
+ */
+
+#define define_test(x)                         \
+       static void run_##x(void **state)       \
+       {                                       \
+               return test_driver(state);      \
+       }
+
+define_test(string_hwe)
+define_test(quoted_hwe)
+define_test(internal_nvme)
+define_test(regex_hwe)
+define_test(regex_string_hwe)
+define_test(regex_string_hwe_dir)
+define_test(regex_2_strings_hwe_dir)
+define_test(string_regex_hwe_dir)
+define_test(2_ident_strings_hwe)
+define_test(2_ident_strings_both_dir)
+define_test(2_ident_strings_both_dir_w_prev)
+define_test(2_ident_strings_hwe_dir)
+define_test(3_ident_strings_hwe_dir)
+define_test(2_ident_self_matching_re_hwe_dir)
+define_test(2_ident_self_matching_re_hwe)
+define_test(2_ident_not_self_matching_re_hwe_dir)
+define_test(2_matching_res_hwe_dir)
+define_test(2_nonmatching_res_hwe_dir)
+define_test(blacklist)
+define_test(blacklist_wwid)
+define_test(blacklist_wwid_1)
+define_test(blacklist_regex)
+define_test(blacklist_regex_inv)
+define_test(blacklist_regex_matching)
+define_test(product_blacklist)
+define_test(product_blacklist_matching)
+define_test(multipath_config)
+define_test(multipath_config_2)
+define_test(multipath_config_3)
+
+#define test_entry(x) \
+       cmocka_unit_test_setup(run_##x, setup_##x)
+
+static int test_hwtable(void)
+{
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_sanity_globals),
+               test_entry(internal_nvme),
+               test_entry(string_hwe),
+               test_entry(quoted_hwe),
+               test_entry(regex_hwe),
+               test_entry(regex_string_hwe),
+               test_entry(regex_string_hwe_dir),
+               test_entry(regex_2_strings_hwe_dir),
+               test_entry(string_regex_hwe_dir),
+               test_entry(2_ident_strings_hwe),
+               test_entry(2_ident_strings_both_dir),
+               test_entry(2_ident_strings_both_dir_w_prev),
+               test_entry(2_ident_strings_hwe_dir),
+               test_entry(3_ident_strings_hwe_dir),
+               test_entry(2_ident_self_matching_re_hwe_dir),
+               test_entry(2_ident_self_matching_re_hwe),
+               test_entry(2_ident_not_self_matching_re_hwe_dir),
+               test_entry(2_matching_res_hwe_dir),
+               test_entry(2_nonmatching_res_hwe_dir),
+               test_entry(blacklist),
+               test_entry(blacklist_wwid),
+               test_entry(blacklist_wwid_1),
+               test_entry(blacklist_regex),
+               test_entry(blacklist_regex_inv),
+               test_entry(blacklist_regex_matching),
+               test_entry(product_blacklist),
+               test_entry(product_blacklist_matching),
+               test_entry(multipath_config),
+               test_entry(multipath_config_2),
+               test_entry(multipath_config_3),
+       };
+
+       return cmocka_run_group_tests(tests, setup, teardown);
+}
+
+int main(void)
+{
+       int ret = 0;
+
+       ret += test_hwtable();
+       return ret;
+}