diff --git a/test/meson.build b/test/meson.build index a9ca3fc77..a54e059b9 100644 --- a/test/meson.build +++ b/test/meson.build @@ -88,6 +88,7 @@ test('test-context', executable('test-context', 'test-context.c', 'test-config.c', + 'test-conf-match-rules.c', include_directories: pwtest_inc, dependencies: [spa_dep, spa_support_dep, spa_dbus_dep], link_with: [pwtest_lib, diff --git a/test/test-conf-match-rules.c b/test/test-conf-match-rules.c new file mode 100644 index 000000000..a2c9802e4 --- /dev/null +++ b/test/test-conf-match-rules.c @@ -0,0 +1,565 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "pwtest.h" + +#include +#include + +struct match_result { + int count; + char action[64]; + char value[1024]; +}; + +static int match_callback(void *data, const char *location, const char *action, + const char *str, size_t len) +{ + struct match_result *r = data; + r->count++; + snprintf(r->action, sizeof(r->action), "%s", action); + snprintf(r->value, sizeof(r->value), "%.*s", (int)len, str); + return 0; +} + +static int match_count_callback(void *data, const char *location, const char *action, + const char *str, size_t len) +{ + int *count = data; + (*count)++; + return 0; +} + +static int match_error_callback(void *data, const char *location, const char *action, + const char *str, size_t len) +{ + return -EPERM; +} + +PWTEST(match_rules_basic) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci")); + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci } ]" + " actions = { update-props = { priority = 100 } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 1); + pwtest_str_eq(r.action, "update-props"); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_no_match) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "bluez_sink")); + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci } ]" + " actions = { update-props = { priority = 100 } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_regex) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci-0000_00_1f.3.analog-stereo")); + const char rules[] = + "[ { matches = [ { node.name = \"~alsa_output.*\" } ]" + " actions = { update-props = { priority = 100 } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_negation) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "bluez_sink.XX_XX_XX")); + const char rules[] = + "[ { matches = [ { node.name = \"!alsa_output.pci\" } ]" + " actions = { update-props = { priority = 50 } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_negation_no_match) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci")); + const char rules[] = + "[ { matches = [ { node.name = \"!alsa_output.pci\" } ]" + " actions = { update-props = { priority = 50 } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_negated_regex) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "v4l2_source.camera")); + const char rules[] = + "[ { matches = [ { node.name = \"!~alsa_.*\" } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_multiple_properties) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci"), + SPA_DICT_ITEM_INIT("media.class", "Audio/Sink")); + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci" + " media.class = Audio/Sink } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_multiple_properties_partial_fail) +{ + struct match_result r = { 0 }; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci"), + SPA_DICT_ITEM_INIT("media.class", "Audio/Source")); + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci" + " media.class = Audio/Sink } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_callback, &r), 0); + pwtest_int_eq(r.count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_alternative_matches) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "bluez_sink.XX")); + /* matches array has two objects — OR semantics */ + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci }" + " { node.name = bluez_sink.XX } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_multiple_rules) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "alsa_output.pci"), + SPA_DICT_ITEM_INIT("media.class", "Audio/Sink")); + /* two separate rules, both match */ + const char rules[] = + "[ { matches = [ { node.name = alsa_output.pci } ]" + " actions = { update-props = { } } }" + " { matches = [ { media.class = Audio/Sink } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 2); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_null_property) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test")); + /* match when property is absent: null matches missing prop */ + const char rules[] = + "[ { matches = [ { node.nick = null } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_null_no_match) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test"), + SPA_DICT_ITEM_INIT("node.nick", "present")); + /* null does not match when property exists */ + const char rules[] = + "[ { matches = [ { node.nick = null } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_negated_null) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test"), + SPA_DICT_ITEM_INIT("node.nick", "present")); + /* !null matches when property exists */ + const char rules[] = + "[ { matches = [ { node.nick = \"!null\" } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_empty_props) +{ + int count = 0; + struct spa_dict props = SPA_DICT_INIT(NULL, 0); + const char rules[] = + "[ { matches = [ { node.name = test } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_callback_error) +{ + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test")); + const char rules[] = + "[ { matches = [ { node.name = test } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_error_callback, NULL), -EPERM); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_not_array) +{ + int count = 0; + struct spa_dict props = SPA_DICT_INIT(NULL, 0); + const char rules[] = "{ not = an-array }"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_no_actions) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test")); + /* matches but no actions key */ + const char rules[] = + "[ { matches = [ { node.name = test } ] } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_multiple_actions) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test")); + const char rules[] = + "[ { matches = [ { node.name = test } ]" + " actions = { update-props = { a = b }" + " create-object = { factory = adapter } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 2); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_empty_array) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test")); + const char rules[] = "[ ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_array_property_value) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("node.name", "test"), + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp a2dp ]")); + /* match against one element of an array-valued property */ + const char rules[] = + "[ { matches = [ { device.features = a2dp } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_array_property_either) +{ + int count; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp a2dp ]")); + + pw_init(0, NULL); + + /* first element matches */ + count = 0; + const char rules_first[] = + "[ { matches = [ { device.features = hfp } ]" + " actions = { update-props = { } } } ]"; + pwtest_int_eq(pw_conf_match_rules(rules_first, strlen(rules_first), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + + /* last element matches */ + count = 0; + const char rules_last[] = + "[ { matches = [ { device.features = a2dp } ]" + " actions = { update-props = { } } } ]"; + pwtest_int_eq(pw_conf_match_rules(rules_last, strlen(rules_last), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + + /* neither matches */ + count = 0; + const char rules_none[] = + "[ { matches = [ { device.features = sbc } ]" + " actions = { update-props = { } } } ]"; + pwtest_int_eq(pw_conf_match_rules(rules_none, strlen(rules_none), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_array_property_or) +{ + int count; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp ]")); + /* match when array contains hfp or a2dp */ + const char rules[] = + "[ { matches = [ { device.features = hfp }" + " { device.features = a2dp } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + + /* has hfp but not a2dp — should match */ + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + + /* has a2dp but not hfp — should match */ + struct spa_dict props2 = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ sbc a2dp ]")); + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props2, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + + /* has neither — should not match */ + struct spa_dict props3 = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ sbc aac ]")); + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props3, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_array_property_and) +{ + int count; + /* match when array contains both hfp and a2dp */ + const char rules[] = + "[ { matches = [ { device.features = hfp" + " device.features = a2dp } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + + /* has both — should match */ + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp a2dp ]")); + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + + /* has only hfp — should not match */ + struct spa_dict props2 = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp ]")); + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props2, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + + /* has only a2dp — should not match */ + struct spa_dict props3 = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ sbc a2dp ]")); + count = 0; + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props3, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_array_property_no_match) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.features", "[ hfp hsp ]")); + const char rules[] = + "[ { matches = [ { device.features = a2dp } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 0); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST(match_rules_regex_array_property) +{ + int count = 0; + struct spa_dict props = SPA_DICT_ITEMS( + SPA_DICT_ITEM_INIT("device.profiles", "[ analog-stereo hdmi-stereo ]")); + const char rules[] = + "[ { matches = [ { device.profiles = \"~hdmi-.*\" } ]" + " actions = { update-props = { } } } ]"; + + pw_init(0, NULL); + pwtest_int_eq(pw_conf_match_rules(rules, strlen(rules), NULL, &props, match_count_callback, &count), 0); + pwtest_int_eq(count, 1); + pw_deinit(); + + return PWTEST_PASS; +} + +PWTEST_SUITE(context) +{ + pwtest_add(match_rules_basic, PWTEST_NOARG); + pwtest_add(match_rules_no_match, PWTEST_NOARG); + pwtest_add(match_rules_regex, PWTEST_NOARG); + pwtest_add(match_rules_negation, PWTEST_NOARG); + pwtest_add(match_rules_negation_no_match, PWTEST_NOARG); + pwtest_add(match_rules_negated_regex, PWTEST_NOARG); + pwtest_add(match_rules_multiple_properties, PWTEST_NOARG); + pwtest_add(match_rules_multiple_properties_partial_fail, PWTEST_NOARG); + pwtest_add(match_rules_alternative_matches, PWTEST_NOARG); + pwtest_add(match_rules_multiple_rules, PWTEST_NOARG); + pwtest_add(match_rules_null_property, PWTEST_NOARG); + pwtest_add(match_rules_null_no_match, PWTEST_NOARG); + pwtest_add(match_rules_negated_null, PWTEST_NOARG); + pwtest_add(match_rules_empty_props, PWTEST_NOARG); + pwtest_add(match_rules_callback_error, PWTEST_NOARG); + pwtest_add(match_rules_not_array, PWTEST_NOARG); + pwtest_add(match_rules_no_actions, PWTEST_NOARG); + pwtest_add(match_rules_multiple_actions, PWTEST_NOARG); + pwtest_add(match_rules_empty_array, PWTEST_NOARG); + pwtest_add(match_rules_array_property_value, PWTEST_NOARG); + pwtest_add(match_rules_array_property_either, PWTEST_NOARG); + pwtest_add(match_rules_array_property_or, PWTEST_NOARG); + pwtest_add(match_rules_array_property_and, PWTEST_NOARG); + pwtest_add(match_rules_array_property_no_match, PWTEST_NOARG); + pwtest_add(match_rules_regex_array_property, PWTEST_NOARG); + + return PWTEST_PASS; +}