From 59621f2d8e43e2d25a3ef64499cb43490c301ee8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Jun 2026 15:58:05 +0200 Subject: [PATCH] test: extend tests for new machine tag rules --- src/test/test-condition.c | 63 +++++++++++++++++++++++++++++++++++ src/test/test-hostname-util.c | 51 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/test/test-condition.c b/src/test/test-condition.c index 4f450ab12d8..a4c57dc1979 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -1474,6 +1474,69 @@ TEST(condition_test_machine_tag) { ASSERT_OK_ZERO(condition_test(condition, environ)); condition_free(condition); + /* Key/value (parameterized) tags */ + ASSERT_OK(write_string_file(f, "TAGS=\"role=webserver:env=prod:berlin\"\n", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE)); + + /* Exact match of a key/value tag */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=webserver", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + + /* Glob on the value */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=web*", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + + /* Glob on the key */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "*=prod", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + + /* A bare key does not match an assignment (the "=" is part of the tag) */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_ZERO(condition_test(condition, environ)); + condition_free(condition); + + /* Right key, wrong value → no match */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=database", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_ZERO(condition_test(condition, environ)); + condition_free(condition); + + /* A plain (non-parameterized) tag still matches alongside key/value tags */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "berlin", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + + /* Negation against a key/value tag */ + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=database", /* trigger= */ false, /* negate= */ true))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=webserver", /* trigger= */ false, /* negate= */ true))); + ASSERT_OK_ZERO(condition_test(condition, environ)); + condition_free(condition); + + /* Conflicting values for the same key: graceful parsing keeps the first (lexicographically smallest) + * value and drops the rest, so only "env=prod" remains visible. */ + ASSERT_OK(write_string_file(f, "TAGS=\"env=staging:env=prod\"\n", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE)); + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "env=prod", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "env=staging", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_ZERO(condition_test(condition, environ)); + condition_free(condition); + + /* Invalid key/value tags in the file are ignored, valid ones still match */ + ASSERT_OK(write_string_file(f, "TAGS=\"bad-=x:role=good\"\n", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE)); + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "bad-=x", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_ZERO(condition_test(condition, environ)); + condition_free(condition); + ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "role=good", /* trigger= */ false, /* negate= */ false))); + ASSERT_OK_POSITIVE(condition_test(condition, environ)); + condition_free(condition); + ASSERT_OK(set_unset_env("SYSTEMD_ETC_MACHINE_INFO", saved, /* overwrite= */ true)); } diff --git a/src/test/test-hostname-util.c b/src/test/test-hostname-util.c index 8ec4034af7f..513c8edc9b8 100644 --- a/src/test/test-hostname-util.c +++ b/src/test/test-hostname-util.c @@ -124,6 +124,15 @@ TEST(machine_tag_is_valid) { assert_se(machine_tag_is_valid("foo-bar.baz")); assert_se(machine_tag_is_valid("Webserver01")); assert_se(machine_tag_is_valid("a")); + assert_se(machine_tag_is_valid("a=")); /* empty value is OK */ + assert_se(machine_tag_is_valid("a=b")); + assert_se(machine_tag_is_valid("foo.bar=")); + assert_se(machine_tag_is_valid("foo.bar-baz=zuziuziuz")); + assert_se(machine_tag_is_valid("foo=bar.baz")); /* "." and "-" are fine inside a value */ + assert_se(machine_tag_is_valid("foo=bar-")); /* even as the very last char of a value */ + assert_se(machine_tag_is_valid("foo=.bar")); /* and as the very first char of a value */ + assert_se(machine_tag_is_valid("foo=bar=")); /* a value may itself contain a "=" */ + assert_se(machine_tag_is_valid("a=b=c")); /* only the first "=" is the separator */ assert_se(!machine_tag_is_valid(NULL)); assert_se(!machine_tag_is_valid("")); @@ -136,6 +145,15 @@ TEST(machine_tag_is_valid) { assert_se(!machine_tag_is_valid("foo-")); assert_se(!machine_tag_is_valid(".foo")); assert_se(!machine_tag_is_valid("foo.")); + assert_se(!machine_tag_is_valid("=b")); + assert_se(!machine_tag_is_valid("=")); + assert_se(!machine_tag_is_valid(".foo=asd")); /* "." not allowed as first char */ + assert_se(!machine_tag_is_valid("foo.=asd")); /* "." not allowed as last char of key */ + assert_se(!machine_tag_is_valid("foo-=asd")); /* "-" not allowed as last char of key */ + assert_se(!machine_tag_is_valid("_foo=asd")); /* "_" is not in the charset */ + assert_se(!machine_tag_is_valid("foo_=sda")); + assert_se(!machine_tag_is_valid("foo=a_b")); /* ... not even in the value */ + assert_se(!machine_tag_is_valid("foo=a:b")); /* colon is the separator, not allowed in a value */ /* Length boundary: 255 characters is fine, 256 is too long */ _cleanup_free_ char *max = strrep("a", 255), *over = strrep("a", 256); @@ -149,9 +167,18 @@ TEST(machine_tag_list_is_valid) { assert_se(machine_tag_list_is_valid(NULL)); /* empty list is valid */ assert_se(machine_tag_list_is_valid(STRV_MAKE("a"))); assert_se(machine_tag_list_is_valid(STRV_MAKE("foo", "bar", "c-d.e"))); + assert_se(machine_tag_list_is_valid(STRV_MAKE("foo=uuu", "bar=qqqq", "c-d.e"))); + assert_se(machine_tag_list_is_valid(STRV_MAKE("foo=aa", "foo=aa"))); /* same key + same value is OK */ + assert_se(machine_tag_list_is_valid(STRV_MAKE("foo", "foo=aa"))); /* bare key and assignment coexist */ + assert_se(machine_tag_list_is_valid(STRV_MAKE("foo=1", "foobar=2"))); /* one key is a prefix of the other */ + assert_se(machine_tag_list_is_valid(STRV_MAKE("ab=1", "a=2"))); /* ... and the other way around */ assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", "b:c"))); assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", ""))); + assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo=aa", "foo=b"))); /* same key, different value */ + assert_se(!machine_tag_list_is_valid(STRV_MAKE("a=1", "b=2", "a=3"))); /* ... also when not adjacent */ + assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo=aa", "bar", "foo=aa", "foo=b"))); + assert_se(!machine_tag_list_is_valid(STRV_MAKE("=aa"))); } TEST(machine_tags_from_string) { @@ -183,6 +210,30 @@ TEST(machine_tags_from_string) { /* Fatal: a single invalid tag fails the whole parse */ ASSERT_ERROR(machine_tags_from_string("foo:in valid:bar", /* graceful= */ false, &l), EINVAL); assert_se(!l); + + /* With assignment */ + ASSERT_OK(machine_tags_from_string("foo=aa:bar=aaa:foo2=x:baz", /* graceful= */ false, &l)); + assert_se(strv_equal(l, STRV_MAKE("bar=aaa", "baz", "foo2=x", "foo=aa"))); + l = strv_free(l); + + /* Graceful: a duplicate key is suppressed, keeping the first (i.e. lexicographically smallest) value */ + ASSERT_OK(machine_tags_from_string("foo=zzz:foo=aaa:foo=mmm", /* graceful= */ true, &l)); + assert_se(strv_equal(l, STRV_MAKE("foo=aaa"))); + l = strv_free(l); + + /* Graceful: a bare key and an assignment for the same name are not considered duplicates */ + ASSERT_OK(machine_tags_from_string("foo:foo=aaa", /* graceful= */ true, &l)); + assert_se(strv_equal(l, STRV_MAKE("foo", "foo=aaa"))); + l = strv_free(l); + + /* Graceful: an invalid value is dropped, conflicting keys that remain are deduplicated */ + ASSERT_OK(machine_tags_from_string("foo=a_b:foo=good:foo=zzz", /* graceful= */ true, &l)); + assert_se(strv_equal(l, STRV_MAKE("foo=good"))); + l = strv_free(l); + + /* Fatal: conflicting values for the same key fail the whole parse */ + ASSERT_ERROR(machine_tags_from_string("foo=a:foo=b", /* graceful= */ false, &l), EINVAL); + assert_se(!l); } DEFINE_TEST_MAIN(LOG_DEBUG); -- 2.47.3