summary refs log tree commit diff
path: root/libnm-core/tests
diff options
context:
space:
mode:
Diffstat (limited to 'libnm-core/tests')
-rw-r--r--libnm-core/tests/meson.build2
-rw-r--r--libnm-core/tests/test-general.c516
-rw-r--r--libnm-core/tests/test-keyfile.c98
-rw-r--r--libnm-core/tests/test-setting.c368
4 files changed, 884 insertions, 100 deletions
diff --git a/libnm-core/tests/meson.build b/libnm-core/tests/meson.build
index 7d772c80..1afb9c4b 100644
--- a/libnm-core/tests/meson.build
+++ b/libnm-core/tests/meson.build
@@ -34,7 +34,7 @@ foreach test_unit: test_units
     'libnm-core-' + test_unit,
     [test_unit + '.c'] + enum,
     dependencies: [
-      nm_core_dep,
+      libnm_core_dep,
       libnm_systemd_shared_no_logging_dep,
     ],
     c_args: [
diff --git a/libnm-core/tests/test-general.c b/libnm-core/tests/test-general.c
index 7d97296a..a6abf391 100644
--- a/libnm-core/tests/test-general.c
+++ b/libnm-core/tests/test-general.c
@@ -23,8 +23,8 @@
 
 #include "nm-default.h"
 
-#include "nm-utils/c-list-util.h"
-#include "nm-utils/nm-enum-utils.h"
+#include "nm-std-aux/c-list-util.h"
+#include "nm-glib-aux/nm-enum-utils.h"
 
 #include "nm-utils.h"
 #include "nm-setting-private.h"
@@ -63,8 +63,8 @@
 #include "nm-setting-wpan.h"
 #include "nm-simple-connection.h"
 #include "nm-keyfile-internal.h"
-#include "nm-utils/nm-dedup-multi.h"
-#include "nm-ethtool-utils.h"
+#include "nm-glib-aux/nm-dedup-multi.h"
+#include "nm-libnm-core-intern/nm-ethtool-utils.h"
 
 #include "test-general-enums.h"
 
@@ -80,6 +80,29 @@ G_STATIC_ASSERT (sizeof (bool) <= sizeof (int));
 
 /*****************************************************************************/
 
+static void
+test_nm_ascii_spaces (void)
+{
+	int i;
+	const char *const S = NM_ASCII_SPACES;
+
+	for (i = 0; S[i]; i++)
+		g_assert (!strchr (&S[i + 1], S[i]));
+
+	for (i = 0; S[i] != '\0'; i++)
+		g_assert (g_ascii_isspace (S[i]));
+
+	g_assert (!g_ascii_isspace ((char) 0));
+	for (i = 1; i < 0x100; i++) {
+		if (g_ascii_isspace ((char) i))
+			g_assert (strchr (S, (char) i));
+		else
+			g_assert (!strchr (S, (char) i));
+	}
+}
+
+/*****************************************************************************/
+
 typedef struct _nm_packed {
 	int v0;
 	char v1;
@@ -236,51 +259,324 @@ test_nm_g_slice_free_fcn (void)
 /*****************************************************************************/
 
 static void
-_do_test_nm_utils_strsplit_set (gboolean escape, const char *str, ...)
+_do_test_nm_utils_strsplit_set_f_one (NMUtilsStrsplitSetFlags flags,
+                                      const char *str,
+                                      gsize words_len,
+                                      const char *const*exp_words)
 {
-	gs_unref_ptrarray GPtrArray *args_array = g_ptr_array_new ();
-	const char *const*args;
+#define DELIMITERS           " \n"
+#define DELIMITERS_C         ' ', '\n'
+
 	gs_free const char **words = NULL;
-	const char *arg;
-	gsize i;
-	va_list ap;
+	gsize i, j, k;
+	const gboolean f_allow_escaping = NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING);
+	const gboolean f_preserve_empty = NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY);
+	const char *s1;
+	gsize initial_offset;
+	gs_strfreev char **words_g = NULL;
+
+	g_assert (!NM_FLAGS_ANY (flags, ~(  NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING
+	                                  | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY)));
+
+	/* assert that the epected words are valid (and don't contain unescaped delimiters). */
+	for (i = 0; i < words_len; i++) {
+		const char *w = exp_words[i];
+
+		g_assert (w);
+		if (!f_preserve_empty)
+			g_assert (w[0]);
+		for (k = 0; w[k]; ) {
+			if (   f_allow_escaping
+			    && w[k] == '\\') {
+				k++;
+				if (w[k] == '\0')
+					break;
+				k++;
+				continue;
+			}
+			g_assert (!NM_IN_SET (w[k], DELIMITERS_C));
+			k++;
+		}
+		if (!f_allow_escaping)
+			g_assert (!NM_STRCHAR_ANY (w, ch, NM_IN_SET (ch, DELIMITERS_C)));
+	}
 
-	va_start (ap, str);
-	while ((arg = va_arg (ap, const char *)))
-		g_ptr_array_add (args_array, (gpointer) arg);
-	va_end (ap);
-	g_ptr_array_add (args_array, NULL);
+	initial_offset = (f_preserve_empty || !str)
+	                 ? 0u
+	                 : strspn (str, DELIMITERS);
+
+	/* first compare our expected values with what g_strsplit_set() would
+	 * do. */
+	words_g = str ? g_strsplit_set (str, DELIMITERS, -1) : NULL;
+	if (str == NULL) {
+		g_assert_cmpint (words_len, ==, 0);
+		g_assert (!words_g);
+	} else if (nm_streq0 (str, "")) {
+		g_assert_cmpint (words_len, ==, 0);
+		g_assert (words_g);
+		g_assert (!words_g[0]);
+	} else {
+		g_assert (words_g);
+		g_assert (words_g[0]);
+		if (!f_allow_escaping) {
+			if (!f_preserve_empty) {
+				for (i = 0, j = 0; words_g[i]; i++) {
+					if (words_g[i][0] == '\0')
+						g_free (words_g[i]);
+					else
+						words_g[j++] = words_g[i];
+				}
+				words_g[j] = NULL;
+			}
+			if (f_preserve_empty)
+				g_assert_cmpint (words_len, >, 0);
+			for (i = 0; i < words_len; i++) {
+				g_assert (exp_words[i]);
+				g_assert_cmpstr (exp_words[i], ==, words_g[i]);
+			}
+			g_assert (words_g[words_len] == NULL);
+			g_assert_cmpint (NM_PTRARRAY_LEN (words_g), ==, words_len);
+			g_assert (_nm_utils_strv_cmp_n (exp_words, words_len, NM_CAST_STRV_CC (words_g), -1) == 0);
+		}
+	}
 
-	args = (const char *const*) args_array->pdata;
+	if (   flags == NM_UTILS_STRSPLIT_SET_FLAGS_NONE
+	    && nmtst_get_rand_bool ())
+		words = nm_utils_strsplit_set (str, DELIMITERS);
+	else if (   flags == NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
+	         && nmtst_get_rand_bool ())
+		words = nm_utils_strsplit_set_with_empty (str, DELIMITERS);
+	else
+		words = nm_utils_strsplit_set_full (str, DELIMITERS, flags);
 
-	words = nm_utils_strsplit_set (str, " \t\n", escape);
+	g_assert_cmpint (NM_PTRARRAY_LEN (words), ==, words_len);
 
-	if (!args[0]) {
+	if (words_len == 0) {
 		g_assert (!words);
 		g_assert (   !str
-		          || NM_STRCHAR_ALL (str, ch, NM_IN_SET (ch, ' ', '\t', '\n')));
+		          || NM_STRCHAR_ALL (str, ch, NM_IN_SET (ch, DELIMITERS_C)));
 		return;
 	}
+
 	g_assert (words);
-	for (i = 0; args[i] || words[i]; i++) {
-		g_assert (args[i]);
-		g_assert (words[i]);
-		g_assert (args[i][0]);
-		g_assert (escape || NM_STRCHAR_ALL (args[i], ch, !NM_IN_SET (ch, ' ', '\t', '\n')));
-		g_assert_cmpstr (args[i], ==, words[i]);
+	for (i = 0; i < words_len; i++)
+		g_assert_cmpstr (exp_words[i], ==, words[i]);
+	g_assert (words[words_len] == NULL);
+
+	g_assert (_nm_utils_strv_cmp_n (exp_words, words_len, words, -1) == 0);
+
+	s1 = words[0];
+	g_assert (s1 >= (char *) &words[words_len + 1]);
+	s1 = &s1[strlen (str)];
+	for (i = 1; i < words_len; i++) {
+		g_assert (&(words[i - 1])[strlen (words[i - 1])] < words[i]);
+		g_assert (words[i] <= s1);
+	}
+
+	/* while strsplit removes all delimiters, we can relatively easily find them
+	 * in the original string. Assert that the original string and the pointer offsets
+	 * of words correspond. In particular, find idx_delim_after and idx_delim_before
+	 * to determine which delimiter was after/before a word. */
+	{
+		gsize idx_word_start;
+		gsize idx_delim_after_old = G_MAXSIZE;
+
+		idx_word_start = initial_offset;
+		for (i = 0; i < words_len; i++) {
+			const gsize l_i = strlen (words[i]);
+			gsize idx_delim_after;
+			gsize idx_delim_before;
+
+			/* find the delimiter *after* words[i]. We can do that by looking at the next
+			 * word and calculating the pointer difference.
+			 *
+			 * The delimiter after the very last word is '\0' and requires strlen() to find. */
+			idx_delim_after = initial_offset + ((words[i] - words[0]) + l_i);
+			if (idx_delim_after != idx_word_start + l_i) {
+				g_assert (!f_preserve_empty);
+				g_assert_cmpint (idx_word_start + l_i, <, idx_delim_after);
+				idx_word_start = idx_delim_after - l_i;
+			}
+			if (i + 1 < words_len) {
+				gsize x = initial_offset + ((words[i + 1] - words[0]) - 1);
+
+				if (idx_delim_after != x) {
+					g_assert (!f_preserve_empty);
+					g_assert_cmpint (idx_delim_after, <, x);
+					for (k = idx_delim_after; k <= x; k++)
+						g_assert (NM_IN_SET (str[k], DELIMITERS_C));
+				}
+				g_assert (NM_IN_SET (str[idx_delim_after], DELIMITERS_C));
+			} else {
+				if (f_preserve_empty)
+					g_assert (NM_IN_SET (str[idx_delim_after], '\0'));
+				else
+					g_assert (NM_IN_SET (str[idx_delim_after], '\0', DELIMITERS_C));
+			}
+
+			/* find the delimiter *before* words[i]. */
+			if (i == 0) {
+				/* there is only a delimiter *before*, with !f_preserve_empty and leading
+				 * delimiters. */
+				idx_delim_before = G_MAXSIZE;
+				if (initial_offset > 0) {
+					g_assert (!f_preserve_empty);
+					idx_delim_before = initial_offset - 1;
+				}
+			} else
+				idx_delim_before = initial_offset + (words[i] - words[0]) - 1;
+			if (idx_delim_before != G_MAXSIZE)
+				g_assert (NM_IN_SET (str[idx_delim_before], DELIMITERS_C));
+			if (idx_delim_after_old != idx_delim_before) {
+				g_assert (!f_preserve_empty);
+				if (i == 0) {
+					g_assert_cmpint (initial_offset, >, 0);
+					g_assert_cmpint (idx_delim_before, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_before, ==, initial_offset - 1);
+				} else {
+					g_assert_cmpint (idx_delim_after_old, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_before, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_after_old, <, idx_delim_before);
+					for (k = idx_delim_after_old; k <= idx_delim_before; k++)
+						g_assert (NM_IN_SET (str[k], DELIMITERS_C));
+				}
+			}
+
+			for (k = 0; k < l_i; ) {
+				if (   f_allow_escaping
+				    && str[idx_word_start + k] == '\\') {
+					k++;
+					if (k >= l_i)
+						break;
+					k++;
+					continue;
+				}
+				g_assert (!NM_IN_SET (str[idx_word_start + k], DELIMITERS_C));
+				k++;
+			}
+			g_assert (strncmp (words[i], &str[idx_word_start], l_i) == 0);
+
+			if (i > 0) {
+				const char *s = &(words[i - 1])[strlen (words[i - 1]) + 1];
+
+				if (s != words[i]) {
+					g_assert (!f_preserve_empty);
+					g_assert (s < words[i]);
+				}
+			}
+
+			idx_word_start += l_i + 1;
+			idx_delim_after_old = idx_delim_after;
+		}
 	}
 }
 
-#define do_test_nm_utils_strsplit_set(str, ...) \
-	_do_test_nm_utils_strsplit_set (str, ##__VA_ARGS__, NULL)
+static void
+_do_test_nm_utils_strsplit_set_f (NMUtilsStrsplitSetFlags flags,
+                                  const char *str,
+                                  gsize words_len,
+                                  const char *const*exp_words)
+{
+	_do_test_nm_utils_strsplit_set_f_one (flags, str, words_len, exp_words);
+
+	if (NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY)) {
+		gs_unref_ptrarray GPtrArray *exp_words2 = NULL;
+		gsize k;
+
+		exp_words2 = g_ptr_array_new ();
+		for (k = 0; k < words_len; k++) {
+			if (exp_words[k][0] != '\0')
+				g_ptr_array_add (exp_words2, (gpointer) exp_words[k]);
+		}
+
+		_do_test_nm_utils_strsplit_set_f_one (flags & (~NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY),
+		                                      str,
+		                                      exp_words2->len,
+		                                      (const char *const*) exp_words2->pdata);
+	}
+}
+
+#define do_test_nm_utils_strsplit_set_f(flags, str, ...) \
+	_do_test_nm_utils_strsplit_set_f (flags, \
+	                                  str, \
+	                                  NM_NARG (__VA_ARGS__), \
+	                                  NM_MAKE_STRV (__VA_ARGS__))
+
+#define do_test_nm_utils_strsplit_set(allow_escaping, str, ...) \
+	do_test_nm_utils_strsplit_set_f (  (allow_escaping) \
+	                                 ? NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING \
+	                                 : NM_UTILS_STRSPLIT_SET_FLAGS_NONE, \
+	                                 str, \
+	                                 ##__VA_ARGS__)
+
+static void
+_do_test_nm_utils_strsplit_set_simple (NMUtilsStrsplitSetFlags flags,
+                                       const char *str,
+                                       gsize words_len,
+                                       const char *const*exp_words)
+{
+	gs_free const char **tokens = NULL;
+	gsize n_tokens;
+
+	tokens = nm_utils_strsplit_set_full (str, DELIMITERS, flags);
+
+	if (!tokens) {
+		g_assert_cmpint (words_len, ==, 0);
+		return;
+	}
+
+	g_assert (str && str[0]);
+	g_assert_cmpint (words_len, >, 0);
+	n_tokens = NM_PTRARRAY_LEN (tokens);
+
+	if (_nm_utils_strv_cmp_n (exp_words, words_len, tokens, -1) != 0) {
+		gsize i;
+
+		g_print (">>> split \"%s\" (flags %x) got %zu tokens (%zu expected)\n", str, (guint) flags, n_tokens, words_len);
+		for (i = 0; i < NM_MAX (n_tokens, words_len); i++) {
+			const char *s1 = i < n_tokens  ? tokens[i]    : NULL;
+			const char *s2 = i < words_len ? exp_words[i] : NULL;
+
+			g_print (">>> [%zu]: %s - %s%s%s vs. %s%s%s\n",
+			         i,
+			         nm_streq0 (s1, s2) ? "same" : "diff",
+			         NM_PRINT_FMT_QUOTE_STRING (s1),
+			         NM_PRINT_FMT_QUOTE_STRING (s2));
+		}
+		g_assert_not_reached ();
+	}
+	g_assert_cmpint (words_len, ==, NM_PTRARRAY_LEN (tokens));
+}
+#define do_test_nm_utils_strsplit_set_simple(flags, str, ...) \
+	_do_test_nm_utils_strsplit_set_simple ((flags), (str), NM_NARG (__VA_ARGS__), NM_MAKE_STRV (__VA_ARGS__))
 
 static void
 test_nm_utils_strsplit_set (void)
 {
+	gs_unref_ptrarray GPtrArray *words_exp = NULL;
+	guint test_run;
+
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, NULL);
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, " ");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, "a  b", "a", "b");
+
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, NULL);
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, " ", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "  ", "", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "a  ", "a", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "a  b", "a", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, " ab  b", "", "ab", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "ab  b", "ab", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb", "abb");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb  bb ", "abb", "", "bb", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb bcb ", "abb", "bcb", "");
+
 	do_test_nm_utils_strsplit_set (FALSE, NULL);
 	do_test_nm_utils_strsplit_set (FALSE, "");
-	do_test_nm_utils_strsplit_set (FALSE, "\t");
-	do_test_nm_utils_strsplit_set (FALSE, " \t\n");
+	do_test_nm_utils_strsplit_set (FALSE, "\n");
+	do_test_nm_utils_strsplit_set (TRUE, " \t\n", "\t");
 	do_test_nm_utils_strsplit_set (FALSE, "a", "a");
 	do_test_nm_utils_strsplit_set (FALSE, "a b", "a", "b");
 	do_test_nm_utils_strsplit_set (FALSE, "a\rb", "a\rb");
@@ -300,6 +596,76 @@ test_nm_utils_strsplit_set (void)
 	do_test_nm_utils_strsplit_set (TRUE, "foo\\", "foo\\");
 	do_test_nm_utils_strsplit_set (TRUE, "bar foo\\", "bar", "foo\\");
 	do_test_nm_utils_strsplit_set (TRUE, "\\ a b\\ \\  c", "\\ a", "b\\ \\ ", "c");
+
+	words_exp = g_ptr_array_new_with_free_func (g_free);
+	for (test_run = 0; test_run < 100; test_run++) {
+		gboolean f_allow_escaping = nmtst_get_rand_bool ();
+		guint words_len = nmtst_get_rand_int () % 100;
+		gs_free char *str = NULL;
+		guint i;
+
+		g_ptr_array_set_size (words_exp, 0);
+		for (i = 0; i < words_len; i++) {
+			guint word_len;
+			char *word;
+			guint j;
+
+			word_len = nmtst_get_rand_int ();
+			if ((word_len % 100) < 30)
+				word_len = 0;
+			else
+				word_len = (word_len >> 10) % 100;
+			word = g_new (char, word_len + 3);
+			for (j = 0; j < word_len; ) {
+				guint32 p = nmtst_get_rand_int ();
+				static const char delimiters_arr[] = { DELIMITERS_C };
+				static const char regular_chars[] = "abcdefghijklmnopqrstuvwxyz";
+
+				if (   !f_allow_escaping
+				    || (p % 1000) < 700) {
+					if (((p >> 20) % 100) < 20)
+						word[j++] = '\\';
+					word[j++] = regular_chars[(p >> 11) % (G_N_ELEMENTS (regular_chars) - 1)];
+					continue;
+				}
+				word[j++] = '\\';
+				word[j++] = delimiters_arr[(p >> 11) % G_N_ELEMENTS (delimiters_arr)];
+			}
+			word[j] = '\0';
+			g_ptr_array_add (words_exp, word);
+		}
+		g_ptr_array_add (words_exp, NULL);
+
+		str = g_strjoinv (" ", (char **) words_exp->pdata);
+
+		if (   str[0] == '\0'
+		    && words_len > 0) {
+			g_assert (words_len == 1);
+			g_assert_cmpstr (words_exp->pdata[0], ==, "");
+			words_len = 0;
+		}
+
+		_do_test_nm_utils_strsplit_set_f (  (f_allow_escaping ? NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING : NM_UTILS_STRSPLIT_SET_FLAGS_NONE)
+		                                  | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+		                                  str,
+		                                  words_len,
+		                                  (const char *const*) words_exp->pdata);
+	}
+
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\t", "\t");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\t");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+	                                      "\t", "");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+	                                      "\t\\\t\t\t\\\t", "\t\t\t\t");
+
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta", "\ta");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta", "a");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta\\ b\t\\ ", "\ta b\t ");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta\\ b\t\\ \t", "a b\t ");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "a\\  b", "a ", "b");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta\\  b", "\ta ", "b");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta\\  b", "a ", "b");
 }
 
 /*****************************************************************************/
@@ -809,54 +1175,54 @@ test_setting_vpn_items (void)
 	nm_setting_vpn_remove_data_item (s_vpn, "foobar4-flags");
 
 	/* Try to add some blank values and make sure they are rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, NULL, NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "foobar1", NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (item) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "foobar1", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, NULL, "blahblah1");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "", "blahblah1");
 	g_test_assert_expected_messages ();
 
 	nm_setting_vpn_foreach_data_item (s_vpn, vpn_check_empty_func, NULL);
 
 	/* Try to add some blank secrets and make sure they are rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, NULL, NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, "", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0]));
 	nm_setting_vpn_add_secret (s_vpn, "foobar1", NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (secret) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0]));
 	nm_setting_vpn_add_secret (s_vpn, "foobar1", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, NULL, "blahblah1");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, "", "blahblah1");
 	g_test_assert_expected_messages ();
 
@@ -2347,7 +2713,7 @@ test_setting_connection_permissions_helpers (void)
 	g_assert (!success);
 
 	/* Ensure a bad [type] is rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (ptype));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (ptype && ptype[0]));
 	success = nm_setting_connection_add_permission (s_con, NULL, "blah", NULL);
 	g_test_assert_expected_messages ();
 	g_assert (!success);
@@ -2687,6 +3053,7 @@ test_connection_diff_a_only (void)
 			{ NM_SETTING_IP_CONFIG_ROUTES,             NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_ROUTE_METRIC,       NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_ROUTE_TABLE,        NM_SETTING_DIFF_RESULT_IN_A },
+			{ NM_SETTING_IP_CONFIG_ROUTING_RULES,      NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_IGNORE_AUTO_ROUTES, NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_IGNORE_AUTO_DNS,    NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP4_CONFIG_DHCP_CLIENT_ID,    NM_SETTING_DIFF_RESULT_IN_A },
@@ -4713,6 +5080,7 @@ test_connection_normalize_infiniband_mtu (void)
 {
 	gs_unref_object NMConnection *con = NULL;
 	NMSettingInfiniband *s_infini;
+	guint mtu_regular = nmtst_rand_select (2044, 2045, 65520);
 
 	con = nmtst_create_minimal_connection ("test_connection_normalize_infiniband_mtu", NULL,
 	                                       NM_SETTING_INFINIBAND_SETTING_NAME, NULL);
@@ -4725,26 +5093,26 @@ test_connection_normalize_infiniband_mtu (void)
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "datagram",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 2044,
+	              NM_SETTING_INFINIBAND_MTU, (guint) mtu_regular,
 	              NULL);
 	nmtst_assert_connection_verifies_and_normalizable (con);
 	nmtst_connection_normalize (con);
-	g_assert_cmpint (2044, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (mtu_regular, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "datagram",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 2045,
+	              NM_SETTING_INFINIBAND_MTU, (guint) 65521,
 	              NULL);
 	nmtst_assert_connection_verifies_after_normalization (con, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
 	nmtst_connection_normalize (con);
-	g_assert_cmpint (2044, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (65520, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "connected",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 65520,
+	              NM_SETTING_INFINIBAND_MTU, (guint) mtu_regular,
 	              NULL);
 	nmtst_assert_connection_verifies_without_normalization (con);
-	g_assert_cmpint (65520, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (mtu_regular, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "connected",
@@ -5540,7 +5908,7 @@ _sock_addr_endpoint (const char *endpoint,
 	SockAddrUnion sockaddr = { };
 
 	g_assert (endpoint);
-	g_assert (!host == (port == -1));
+	g_assert ((!host) == (port == -1));
 	g_assert (port >= -1 && port <= G_MAXUINT16);
 
 	ep = nm_sock_addr_endpoint_new (endpoint);
@@ -7706,53 +8074,6 @@ test_ethtool_offload (void)
 	g_assert_cmpstr (d->optname, ==, NM_ETHTOOL_OPTNAME_FEATURE_RXHASH);
 }
 
-static void
-test_nm_utils_escape_spaces (void)
-{
-	char *to_free;
-
-	g_assert_cmpstr (_nm_utils_escape_spaces (NULL, &to_free), ==, NULL);
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("", &to_free), ==, "");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces (" ", &to_free), ==, "\\ ");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("\t ", &to_free), ==, "\\\t\\ ");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc", &to_free), ==, "abc");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc def", &to_free), ==, "abc\\ def");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc\tdef", &to_free), ==, "abc\\\tdef");
-	g_free (to_free);
-}
-
-static void
-test_nm_utils_unescape_spaces (void)
-{
-#define CHECK_STR(in, out) \
-	G_STMT_START { \
-		gs_free char *str = g_strdup (in); \
-		\
-		g_assert_cmpstr (_nm_utils_unescape_spaces (str), ==, out); \
-	} G_STMT_END
-
-	CHECK_STR ("\\a", "\\a");
-	CHECK_STR ("foobar", "foobar");
-	CHECK_STR ("foo bar", "foo bar");
-	CHECK_STR ("foo\\ bar", "foo bar");
-	CHECK_STR ("foo\\", "foo\\");
-	CHECK_STR ("\\\\\t", "\\\t");
-
-#undef CHECK_STR
-}
-
 /*****************************************************************************/
 
 NMTST_DEFINE ();
@@ -7761,6 +8082,7 @@ int main (int argc, char **argv)
 {
 	nmtst_init (&argc, &argv, TRUE);
 
+	g_test_add_func ("/core/general/test_nm_ascii_spaces", test_nm_ascii_spaces);
 	g_test_add_func ("/core/general/test_nm_hash", test_nm_hash);
 	g_test_add_func ("/core/general/test_nm_g_slice_free_fcn", test_nm_g_slice_free_fcn);
 	g_test_add_func ("/core/general/test_c_list_sort", test_c_list_sort);
@@ -7905,8 +8227,6 @@ int main (int argc, char **argv)
 	g_test_add_func ("/core/general/_nm_utils_dns_option_find_idx", test_nm_utils_dns_option_find_idx);
 	g_test_add_func ("/core/general/_nm_utils_validate_json", test_nm_utils_check_valid_json);
 	g_test_add_func ("/core/general/_nm_utils_team_config_equal", test_nm_utils_team_config_equal);
-	g_test_add_func ("/core/general/_nm_utils_escape_spaces", test_nm_utils_escape_spaces);
-	g_test_add_func ("/core/general/_nm_utils_unescape_spaces", test_nm_utils_unescape_spaces);
 	g_test_add_func ("/core/general/test_nm_utils_enum", test_nm_utils_enum);
 	g_test_add_func ("/core/general/nm-set-out", test_nm_set_out);
 	g_test_add_func ("/core/general/route_attributes/parse", test_route_attributes_parse);
diff --git a/libnm-core/tests/test-keyfile.c b/libnm-core/tests/test-keyfile.c
index 9b9d1c04..157e8f18 100644
--- a/libnm-core/tests/test-keyfile.c
+++ b/libnm-core/tests/test-keyfile.c
@@ -744,6 +744,102 @@ test_vpn_1 (void)
 
 /*****************************************************************************/
 
+static void
+test_bridge_vlans (void)
+{
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	gs_unref_object NMConnection *con = NULL;
+	NMSettingBridge *s_bridge;
+	NMBridgeVlan *vlan;
+	guint16 vid, vid_end;
+
+	con = nmtst_create_connection_from_keyfile (
+	      "[connection]\n"
+	      "id=t\n"
+	      "type=bridge\n"
+	      "interface-name=br4\n"
+	      "\n"
+	      "[bridge]\n"
+	      "vlans=900 ,  1 pvid  untagged, 100-123 untagged\n"
+	      "",
+	      "/test_bridge_port/vlans");
+	s_bridge = NM_SETTING_BRIDGE (nm_connection_get_setting (con, NM_TYPE_SETTING_BRIDGE));
+	g_assert (s_bridge);
+	g_assert_cmpuint (nm_setting_bridge_get_num_vlans (s_bridge), ==, 3);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 0);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 1);
+	g_assert_cmpuint (vid_end, ==, 1);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, TRUE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 1);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 100);
+	g_assert_cmpuint (vid_end, ==, 123);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 2);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 900);
+	g_assert_cmpuint (vid_end, ==, 900);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, FALSE);
+
+	CLEAR (&con, &keyfile);
+}
+
+static void
+test_bridge_port_vlans (void)
+{
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	gs_unref_object NMConnection *con = NULL;
+	NMSettingBridgePort *s_port;
+	NMBridgeVlan *vlan;
+	guint16 vid_start, vid_end;
+
+	con = nmtst_create_connection_from_keyfile (
+	      "[connection]\n"
+	      "id=t\n"
+	      "type=dummy\n"
+	      "interface-name=dummy1\n"
+	      "master=br0\n"
+	      "slave-type=bridge\n"
+	      "\n"
+	      "[bridge-port]\n"
+	      "vlans=4094 pvid , 10-20 untagged\n"
+	      "",
+	      "/test_bridge_port/vlans");
+	s_port = NM_SETTING_BRIDGE_PORT (nm_connection_get_setting (con, NM_TYPE_SETTING_BRIDGE_PORT));
+	g_assert (s_port);
+	g_assert_cmpuint (nm_setting_bridge_port_get_num_vlans (s_port), ==, 2);
+
+	vlan = nm_setting_bridge_port_get_vlan (s_port, 0);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+	g_assert_cmpuint (vid_start, ==, 10);
+	g_assert_cmpuint (vid_end, ==, 20);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_port_get_vlan (s_port, 1);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+	g_assert_cmpuint (vid_start, ==, 4094);
+	g_assert_cmpuint (vid_end, ==, 4094);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, TRUE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, FALSE);
+
+	CLEAR (&con, &keyfile);
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE ();
 
 int main (int argc, char **argv)
@@ -757,6 +853,8 @@ int main (int argc, char **argv)
 	g_test_add_func ("/core/keyfile/test_team_conf_read/invalid", test_team_conf_read_invalid);
 	g_test_add_func ("/core/keyfile/test_user/1", test_user_1);
 	g_test_add_func ("/core/keyfile/test_vpn/1", test_vpn_1);
+	g_test_add_func ("/core/keyfile/bridge/vlans", test_bridge_vlans);
+	g_test_add_func ("/core/keyfile/bridge-port/vlans", test_bridge_port_vlans);
 
 	return g_test_run ();
 }
diff --git a/libnm-core/tests/test-setting.c b/libnm-core/tests/test-setting.c
index 2011273a..03100a03 100644
--- a/libnm-core/tests/test-setting.c
+++ b/libnm-core/tests/test-setting.c
@@ -20,6 +20,7 @@
 #include "nm-default.h"
 
 #include <linux/pkt_sched.h>
+#include <net/if.h>
 
 #include "nm-utils.h"
 #include "nm-utils-private.h"
@@ -1658,6 +1659,76 @@ test_sriov_parse_vlans (void)
 	test_sriov_parse_vlan_one ("1.2.ad;2.0.q;5;3", TRUE, {1, 2, 1}, {2, 0, 0}, {3, 0, 0}, {5, 0, 0});
 }
 
+static void
+test_bridge_vlans (void)
+{
+	NMBridgeVlan *v1, *v2;
+	GError *error = NULL;
+	guint16 vid_start, vid_end;
+	char *str;
+
+	v1 = nm_bridge_vlan_from_str ("1 foobar", &error);
+	nmtst_assert_no_success (v1, error);
+	g_clear_error (&error);
+
+	v1 = nm_bridge_vlan_from_str ("4095", &error);
+	nmtst_assert_no_success (v1, error);
+	g_clear_error (&error);
+
+	/* test ranges */
+	v1 = nm_bridge_vlan_from_str ("2-1000 untagged", &error);
+	nmtst_assert_success (v1, error);
+	g_assert_cmpint (nm_bridge_vlan_get_vid_range (v1, &vid_start, &vid_end), ==, TRUE);
+	g_assert_cmpuint (vid_start, ==, 2);
+	g_assert_cmpuint (vid_end, ==, 1000);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, TRUE);
+	nm_bridge_vlan_unref (v1);
+
+	/* test comparison (1) */
+	v1 = nm_bridge_vlan_from_str ("10 untagged", &error);
+	nmtst_assert_success (v1, error);
+
+	g_assert_cmpint (nm_bridge_vlan_get_vid_range (v1, &vid_start, &vid_end), ==, FALSE);
+	g_assert_cmpuint (vid_start, ==, 10);
+	g_assert_cmpuint (vid_end, ==, 10);
+	g_assert_cmpint (nm_bridge_vlan_is_sealed (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, TRUE);
+
+	nm_bridge_vlan_set_pvid (v1, TRUE);
+	nm_bridge_vlan_set_untagged (v1, FALSE);
+	nm_bridge_vlan_seal (v1);
+
+	g_assert_cmpint (nm_bridge_vlan_is_sealed (v1), ==, TRUE);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, TRUE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, FALSE);
+
+	str = nm_bridge_vlan_to_str (v1, &error);
+	nmtst_assert_success (str, error);
+	g_assert_cmpstr (str, ==, "10 pvid");
+	g_clear_pointer (&str, g_free);
+
+	v2 = nm_bridge_vlan_from_str ("  10  pvid  ", &error);
+	nmtst_assert_success (v2, error);
+
+	g_assert_cmpint (nm_bridge_vlan_cmp (v1, v2), ==, 0);
+
+	nm_bridge_vlan_unref (v1);
+	nm_bridge_vlan_unref (v2);
+
+	/* test comparison (2) */
+	v1 = nm_bridge_vlan_from_str ("10", &error);
+	nmtst_assert_success (v1, error);
+	v2 = nm_bridge_vlan_from_str ("20", &error);
+	nmtst_assert_success (v2, error);
+
+	g_assert_cmpint (nm_bridge_vlan_cmp (v1, v2), <, 0);
+
+	nm_bridge_vlan_unref (v1);
+	nm_bridge_vlan_unref (v2);
+}
+
 /*****************************************************************************/
 
 static void
@@ -2304,7 +2375,16 @@ test_roundtrip_conversion (gconstpointer test_data)
 	NMSettingConnection *s_con = NULL;
 	NMSettingWired *s_eth = NULL;
 	NMSettingWireGuard *s_wg = NULL;
+	union {
+		struct {
+			NMSettingIPConfig  *s_6;
+			NMSettingIPConfig  *s_4;
+		};
+		NMSettingIPConfig *s_x[2];
+	} s_ip;
+	int is_ipv4;
 	guint i;
+	gboolean success;
 
 	switch (MODE) {
 	case 0:
@@ -2491,6 +2571,89 @@ test_roundtrip_conversion (gconstpointer test_data)
 		_rndt_wg_peers_assert_equal (s_wg, wg_peers, TRUE, TRUE, FALSE);
 		break;
 
+	case 3:
+		con = nmtst_create_minimal_connection (ID, UUID, NM_SETTING_WIRED_SETTING_NAME, &s_con);
+		g_object_set (s_con,
+		              NM_SETTING_CONNECTION_INTERFACE_NAME,
+		              INTERFACE_NAME,
+		              NULL);
+		nmtst_connection_normalize (con);
+
+		s_eth = NM_SETTING_WIRED (nm_connection_get_setting (con, NM_TYPE_SETTING_WIRED));
+		g_assert (NM_IS_SETTING_WIRED (s_eth));
+
+		g_object_set (s_eth,
+		              NM_SETTING_WIRED_MTU,
+		              ETH_MTU,
+		              NULL);
+
+		s_ip.s_4 = NM_SETTING_IP_CONFIG (nm_connection_get_setting (con, NM_TYPE_SETTING_IP4_CONFIG));
+		g_assert (NM_IS_SETTING_IP4_CONFIG (s_ip.s_4));
+
+		s_ip.s_6 = NM_SETTING_IP_CONFIG (nm_connection_get_setting (con, NM_TYPE_SETTING_IP6_CONFIG));
+		g_assert (NM_IS_SETTING_IP6_CONFIG (s_ip.s_6));
+
+		for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
+			g_assert (NM_IS_SETTING_IP_CONFIG (s_ip.s_x[is_ipv4]));
+			for (i = 0; i < 3; i++) {
+				char addrstr[NM_UTILS_INET_ADDRSTRLEN];
+
+				nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr = NULL;
+
+				rr = nm_ip_routing_rule_new (is_ipv4 ? AF_INET : AF_INET6);
+				nm_ip_routing_rule_set_priority (rr, i + 1);
+				if (i > 0) {
+					if (is_ipv4)
+						nm_sprintf_buf (addrstr, "192.168.%u.0", i);
+					else
+						nm_sprintf_buf (addrstr, "1:2:3:%x::", 10 + i);
+					nm_ip_routing_rule_set_from (rr, addrstr, is_ipv4 ? 24 + i : 64 + i);
+				}
+				nm_ip_routing_rule_set_table (rr, 1000 + i);
+
+				success = nm_ip_routing_rule_validate (rr, &error);
+				nmtst_assert_success (success, error);
+
+				nm_setting_ip_config_add_routing_rule (s_ip.s_x[is_ipv4], rr);
+			}
+		}
+
+		g_ptr_array_add (kf_data_arr,
+		    g_strdup_printf ("[connection]\n"
+		                     "id=%s\n"
+		                     "uuid=%s\n"
+		                     "type=ethernet\n"
+		                     "interface-name=%s\n"
+		                     "permissions=\n"
+		                     "\n"
+		                     "[ethernet]\n"
+		                     "mac-address-blacklist=\n"
+		                     "%s" /* mtu */
+		                     "\n"
+		                     "[ipv4]\n"
+		                     "dns-search=\n"
+		                     "method=auto\n"
+		                     "routing-rule1=priority 1 from 0.0.0.0/0 table 1000\n"
+		                     "routing-rule2=priority 2 from 192.168.1.0/25 table 1001\n"
+		                     "routing-rule3=priority 3 from 192.168.2.0/26 table 1002\n"
+		                     "\n"
+		                     "[ipv6]\n"
+		                     "addr-gen-mode=stable-privacy\n"
+		                     "dns-search=\n"
+		                     "method=auto\n"
+		                     "routing-rule1=priority 1 from ::/0 table 1000\n"
+		                     "routing-rule2=priority 2 from 1:2:3:b::/65 table 1001\n"
+		                     "routing-rule3=priority 3 from 1:2:3:c::/66 table 1002\n"
+		                     "",
+		                     ID,
+		                     UUID,
+		                     INTERFACE_NAME,
+		                       (ETH_MTU != 0)
+		                     ? nm_sprintf_bufa (100, "mtu=%u\n", ETH_MTU)
+		                     : ""));
+
+		break;
+
 	default:
 		g_assert_not_reached ();
 	}
@@ -2618,6 +2781,204 @@ test_roundtrip_conversion (gconstpointer test_data)
 
 /*****************************************************************************/
 
+static NMIPRoutingRule *
+_rr_from_str_get_impl (const char *str, const char *const*aliases)
+{
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr = NULL;
+	gs_free_error GError *error = NULL;
+	gboolean vbool;
+	int addr_family;
+	int i;
+	NMIPRoutingRuleAsStringFlags to_string_flags;
+
+	rr = nm_ip_routing_rule_from_string (str,
+	                                     NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE,
+	                                     NULL,
+	                                     &error);
+	nmtst_assert_success (rr, error);
+
+	addr_family = nm_ip_routing_rule_get_addr_family (rr);
+	g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+
+	if (addr_family == AF_INET)
+		to_string_flags = NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET;
+	else
+		to_string_flags = NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6;
+
+	for (i = 0; TRUE; i++) {
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr2 = NULL;
+		gs_free char *str1 = NULL;
+		gs_unref_variant GVariant *variant1 = NULL;
+		const char *cstr1;
+
+		switch (i) {
+		case 0:
+			rr2 = nm_ip_routing_rule_ref (rr);
+			break;
+
+		case 1:
+			rr2 = nm_ip_routing_rule_from_string (str,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		case 2:
+			str1 = nm_ip_routing_rule_to_string (rr,
+			                                       NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                     NULL,
+			                                     &error);
+			nmtst_assert_success (str1 && str1[0], error);
+
+			g_assert_cmpstr (str, ==, str1);
+
+			rr2 = nm_ip_routing_rule_from_string (str1,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		case 3:
+			variant1 = nm_ip_routing_rule_to_dbus (rr);
+			g_assert (variant1);
+			g_assert (g_variant_is_floating (variant1));
+			g_assert (g_variant_is_of_type (variant1, G_VARIANT_TYPE_VARDICT));
+
+			rr2 = nm_ip_routing_rule_from_dbus (variant1,
+			                                    TRUE,
+			                                    &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		default:
+			if (!aliases || !aliases[0])
+				goto done;
+			cstr1 = (aliases++)[0];
+			rr2 = nm_ip_routing_rule_from_string (cstr1,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+		}
+
+		g_assert (rr2);
+		vbool = nm_ip_routing_rule_validate (rr, &error);
+		nmtst_assert_success (vbool, error);
+		vbool = nm_ip_routing_rule_validate (rr2, &error);
+		nmtst_assert_success (vbool, error);
+
+		g_assert_cmpint (nm_ip_routing_rule_cmp (rr, rr2), ==, 0);
+		g_assert_cmpint (nm_ip_routing_rule_cmp (rr2, rr), ==, 0);
+	}
+
+done:
+	return g_steal_pointer (&rr);
+}
+#define _rr_from_str_get(a, ...) _rr_from_str_get_impl (a, &(NM_MAKE_STRV (NULL, ##__VA_ARGS__))[1])
+
+#define _rr_from_str(...) \
+	G_STMT_START { \
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *_rr = NULL; \
+		\
+		_rr = _rr_from_str_get (__VA_ARGS__); \
+		g_assert (_rr); \
+	} G_STMT_END
+
+static void
+test_routing_rule (gconstpointer test_data)
+{
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr1 = NULL;
+	gboolean success;
+	char ifname_buf[16];
+
+	_rr_from_str ("priority 5 from 0.0.0.0 table 1",
+	              "  from 0.0.0.0  priority  5 lookup 1 ");
+	_rr_from_str ("priority 5 from 0.0.0.0/0 table 4");
+	_rr_from_str ("priority 5 to 0.0.0.0 table 6");
+	_rr_from_str ("priority 5 to 0.0.0.0 table 254",
+	              "priority 5 to 0.0.0.0/32");
+	_rr_from_str ("priority 5 from 1.2.3.4 table 15",
+	              "priority 5 from 1.2.3.4/32 table  0xF ",
+	              "priority 5 from 1.2.3.4/32 to 0.0.0.0/0 lookup 15 ");
+	_rr_from_str ("priority 5 from 1.2.3.4 to 0.0.0.0 table 8");
+	_rr_from_str ("priority 5 to a:b:c:: tos 0x16 table 25",
+	              "priority 5 to a:b:c::/128 table 0x19 tos 16",
+	              "priority 5 to a:b:c::/128 lookup 0x19 dsfield 16",
+	              "priority 5 to a:b:c::/128 lookup 0x19 dsfield 16 fwmark 0/0x00",
+	              "priority 5 to a:b:c:: from all lookup 0x19 dsfield 16 fwmark 0x0/0");
+	_rr_from_str ("priority 5 from :: fwmark 0 table 25",
+	              "priority 5 from ::/128 to all table 0x19 fwmark 0/0xFFFFFFFF",
+	              "priority 5 from :: to ::/0 table 0x19 fwmark 0x00/4294967295");
+	_rr_from_str ("priority 5 from :: iif aab table 25");
+	_rr_from_str ("priority 5 from :: iif aab oif er table 25",
+	              "priority 5 from :: table 0x19 dev aab oif er");
+	_rr_from_str ("priority 5 from :: iif a\\\\303b table 25");
+	_rr_from_str ("priority 5 to 0.0.0.0 sport 10 table 6",
+	              "priority 5 to 0.0.0.0 sport 10-10 table 6");
+	_rr_from_str ("not priority 5 to 0.0.0.0 dport 10-133 table 6",
+	              "priority 5 to 0.0.0.0 not dport 10-133 not table 6",
+	              "priority 5 to 0.0.0.0 not dport 10-\\ 133 not table 6");
+	_rr_from_str ("priority 5 to 0.0.0.0 ipproto 10 sport 10 table 6");
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif aab table 25");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "aab");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "aab");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif a\\\\303\\\\261xb table 254");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "a\\303\\261xb");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "a\303\261xb");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: oif \\\\101=\\\\303\\\\261xb table 7");
+	g_assert_cmpstr (nm_ip_routing_rule_get_oifname (rr1), ==, "\\101=\\303\\261xb");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "A=\303\261xb");
+	g_assert (success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert (!success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 to 0.0.0.0 tos 0x10 table 7");
+	g_assert_cmpstr (NULL, ==, nm_ip_routing_rule_get_from (rr1));
+	g_assert (!nm_ip_routing_rule_get_from_bin (rr1));
+	g_assert_cmpint (0, ==, nm_ip_routing_rule_get_from_len (rr1));
+	g_assert_cmpstr ("0.0.0.0", ==, nm_ip_routing_rule_get_to (rr1));
+	g_assert (nm_ip_addr_is_null (AF_INET, nm_ip_routing_rule_get_to_bin (rr1)));
+	g_assert_cmpint (32, ==, nm_ip_routing_rule_get_to_len (rr1));
+	g_assert_cmpint (7, ==, nm_ip_routing_rule_get_table (rr1));
+	g_assert_cmpint (0x10, ==, nm_ip_routing_rule_get_tos (rr1));
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif a\\\\303\\\\261,x;b table 254",
+	                        "priority 5 from :: iif a\\\\303\\\\261,x;b table 254");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "a\\303\\261,x;b");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "a\303\261,x;b");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE ();
 
 int
@@ -2663,6 +3024,8 @@ main (int argc, char **argv)
 	g_test_add_func ("/libnm/settings/tc_config/setting/duplicates", test_tc_config_setting_duplicates);
 	g_test_add_func ("/libnm/settings/tc_config/dbus", test_tc_config_dbus);
 
+	g_test_add_func ("/libnm/settings/bridge/vlans", test_bridge_vlans);
+
 #if WITH_JSON_VALIDATION
 	g_test_add_func ("/libnm/settings/team/sync_runner_from_config_roundrobin",
 	                 test_runner_roundrobin_sync_from_config);
@@ -2694,9 +3057,12 @@ main (int argc, char **argv)
 	g_test_add_func ("/libnm/settings/team-port/sycn_from_config_full", test_team_port_full_config);
 #endif
 
-	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/0", GINT_TO_POINTER (0), test_roundtrip_conversion);
+	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/0",   GINT_TO_POINTER (0), test_roundtrip_conversion);
 	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/wireguard/1", GINT_TO_POINTER (1), test_roundtrip_conversion);
 	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/wireguard/2", GINT_TO_POINTER (2), test_roundtrip_conversion);
+	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/3",   GINT_TO_POINTER (3), test_roundtrip_conversion);
+
+	g_test_add_data_func ("/libnm/settings/routing-rule/1", GINT_TO_POINTER (0), test_routing_rule);
 
 	return g_test_run ();
 }