diff --git a/tests/Makefile b/tests/Makefile
index 7439c8c3..78755edd 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -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
index 00000000..b2a0511a
--- /dev/null
+++ b/tests/hwtable.c
@@ -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;
+}
