Merge ~emitorino/review-tools:multi_value_attrs_in_decl into review-tools:master

Proposed by Emilia Torino
Status: Merged
Merged at revision: 8f1d06564faf23c9ddfef696007bc871715ec79a
Proposed branch: ~emitorino/review-tools:multi_value_attrs_in_decl
Merge into: review-tools:master
Diff against target: 568 lines (+448/-66)
2 files modified
reviewtools/sr_declaration.py (+49/-63)
reviewtools/tests/test_sr_declaration.py (+399/-3)
Reviewer Review Type Date Requested Status
Samuele Pedroni (community) Approve
Alex Murray Approve
Review via email: mp+427054@code.launchpad.net

Commit message

- sr_declaration.py: add support for multi value attrs declarations
- test_sr_declaration.py: test cases to support the new functionality

Description of the change

The current code considers if the attribute in the constraint is a list, just one must match (it is
considered a list of alternatives, aka, a list of OR constraints).

As per https://bugs.launchpad.net/review-tools/+bug/1981637 review-tools needs to support a list of constraints that could be applied to a list of iface attr values. This MP fixes this issue.

Whats pending to do is to confirm if nested list attrs should be allowed. We can create a follow up MP to include it.

I added several test scenarios including the cases I could think of that should/should not be allowed after this change. Please suggest more that I could be missing so we make sure we dont break this important functionality.

The current test_sr_declaration.py already contains lots of test cases that keep passing with this change. If some should not anymore, please let me know.

To post a comment you must log in.
Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM - thanks for the detailed test cases.

review: Approve
Revision history for this message
Samuele Pedroni (pedronis) wrote :

question about more deep/recursive situations

Revision history for this message
Emilia Torino (emitorino) wrote :

Adding support for nested lists in declaration attrs

Revision history for this message
Samuele Pedroni (pedronis) wrote :

lgtm, wondering if the code can be simplified now though, as the code for lists _chec_attrib and the top loop do similar things ?

review: Approve
Revision history for this message
Emilia Torino (emitorino) wrote :

> lgtm, wondering if the code can be simplified now though, as the code for
> lists _chec_attrib and the top loop do similar things ?

Thanks for this comment. I indeed simplified the rules processing code but had to extend a bit more the _chec_attrib to support considering lists in "against" (rules) for non-lists in val (snap decl).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/reviewtools/sr_declaration.py b/reviewtools/sr_declaration.py
index 58f5f3b..61bd313 100644
--- a/reviewtools/sr_declaration.py
+++ b/reviewtools/sr_declaration.py
@@ -811,61 +811,49 @@ class SnapReviewDeclaration(SnapReview):
811 if type(val) not in [str, list, dict, bool]:811 if type(val) not in [str, list, dict, bool]:
812 raise SnapDeclarationException("unknown type '%s'" % val)812 raise SnapDeclarationException("unknown type '%s'" % val)
813813
814 # Keep in mind by this point each OR constraint is being iterated
815 # through such that 'against' as a list is a nested list.
816 # For now, punt on nested lists since they are impractical in use
817 # since they are a list of OR options where one option must match
818 # all of val, but if you have that you may as well just use a
819 # string. Eg for this snap.yaml:
820 # snap.yaml:
821 # plugs:
822 # foo:
823 # bar: [ baz, norf ]
824 #
825 # a snap decl that uses a list for 'bar' might be:
826 # snap decl:
827 # foo:
828 # plug-attributes:
829 # bar:
830 # - baz|norf
831 # - something|else
832 #
833 # but, 'something|else' is pointless so it will never match, so the
834 # decl should be rewritten more simply as:
835 # snap decl:
836 # foo:
837 # plug-attributes:
838 # bar: baz|norf
839 # Importantly, the type of the attribute in the snap decl means
840 # something different than the type in snap.yaml
841 if isinstance(against, list):
842 raise SnapDeclarationException(
843 "attribute lists in the declaration not supported"
844 )
845
846 matched = False814 matched = False
847 if isinstance(val, str) and isinstance(against, str):815 if isinstance(val, str):
848 if against.startswith("$"):816 if isinstance(against, str):
849 if against == "$MISSING":817 if against.startswith("$"):
850 matched = False # value must not be set818 if against == "$MISSING":
851 elif re.search(r"^\$PLUG\(%s\)$" % rules_attrib, against):819 matched = False # value must not be set
852 matched = True820 elif re.search(r"^\$PLUG\(%s\)$" % rules_attrib, against):
853 elif re.search(r"^\$SLOT\(%s\)$" % rules_attrib, against):821 matched = True
822 elif re.search(r"^\$SLOT\(%s\)$" % rules_attrib, against):
823 matched = True
824 else:
825 raise SnapDeclarationException(
826 "unknown special attrib '%s'" % against
827 )
828 elif re.search(r"^(%s)$" % against, val):
854 matched = True829 matched = True
855 else:830 elif isinstance(against, list):
856 raise SnapDeclarationException(831 for declaration_value in against:
857 "unknown special attrib '%s'" % against832 # if the attribute in the snap (val) is a string and the
858 )833 # declaration value (against) is a list, then to match,
859 elif re.search(r"^(%s)$" % against, val):834 # val must match some entry in against
860 matched = True835 if _check_attrib(val, declaration_value, side, rules_attrib):
836 matched = True
837 break
861 elif isinstance(val, list):838 elif isinstance(val, list):
862 # if the attribute in the snap (val) is a list and the
863 # declaration value (against) is a string, then to match,
864 # against must be a regex that matches all entries in val
865 num_matched = 0839 num_matched = 0
866 for i in val:840 for snap_attribute in val:
867 if _check_attrib(i, against, side, rules_attrib):841 if isinstance(against, str):
868 num_matched += 1842 # if the attribute in the snap (val) is a list and the
843 # declaration value (against) is a string, then to match,
844 # against must be a regex that matches all entries in val
845 if _check_attrib(snap_attribute, against, side, rules_attrib):
846 num_matched += 1
847 elif isinstance(against, list):
848 for declaration_value in against:
849 # if the attribute in the snap (val) is a list and the
850 # declaration value (against) is a list, then to match,
851 # every item in against must match some entry in val
852 if _check_attrib(
853 snap_attribute, declaration_value, side, rules_attrib
854 ):
855 num_matched += 1
856 break
869 if num_matched == len(val):857 if num_matched == len(val):
870 matched = True858 matched = True
871 elif isinstance(val, dict):859 elif isinstance(val, dict):
@@ -880,6 +868,14 @@ class SnapReviewDeclaration(SnapReview):
880 )868 )
881 if not matched:869 if not matched:
882 break870 break
871 elif isinstance(against, list):
872 for declaration_value in against:
873 # if the attribute in the snap (val) is a dict and the
874 # declaration value (against) is a list, then to match,
875 # val must match some entry in against
876 if _check_attrib(val, declaration_value, side, rules_attrib):
877 matched = True
878 break
883 else:879 else:
884 matched = False880 matched = False
885 else: # bools881 else: # bools
@@ -973,18 +969,8 @@ class SnapReviewDeclaration(SnapReview):
973 val = iface[rules_attrib]969 val = iface[rules_attrib]
974 against = rules[rules_key][rules_attrib]970 against = rules[rules_key][rules_attrib]
975971
976 if isinstance(against, list):972 if _check_attrib(val, against, side, rules_attrib):
977 # As a practical matter, if the attribute in the973 attributes_matched[rules_key]["matched"] += 1
978 # constraint is a list, just one must match (it is
979 # considered a list of alternatives, aka, a list of
980 # OR constraints).
981 for i in against:
982 if _check_attrib(val, i, side, rules_attrib):
983 attributes_matched[rules_key]["matched"] += 1
984 break
985 else:
986 if _check_attrib(val, against, side, rules_attrib):
987 attributes_matched[rules_key]["matched"] += 1
988 else:974 else:
989 # when the attribute is missing from the interface don't975 # when the attribute is missing from the interface don't
990 # mark as checked (missing attributes are checked976 # mark as checked (missing attributes are checked
diff --git a/reviewtools/tests/test_sr_declaration.py b/reviewtools/tests/test_sr_declaration.py
index 1b36992..38154f0 100644
--- a/reviewtools/tests/test_sr_declaration.py
+++ b/reviewtools/tests/test_sr_declaration.py
@@ -3892,6 +3892,403 @@ slots:
3892 }3892 }
3893 self.check_results(r, expected=expected)3893 self.check_results(r, expected=expected)
38943894
3895 def test_check_declaration_slots_multi_value_attrs_issue(self,):
3896 review_ok_output = {
3897 "error": dict(),
3898 "warn": dict(),
3899 "info": {
3900 "declaration-snap-v2:slots:fram-devices:custom-device": {"text": "OK"}
3901 },
3902 }
3903
3904 review_err_output = {
3905 "error": {
3906 "declaration-snap-v2:slots_installation:fram-devices:custom-device": {
3907 "text": "human review required due to 'allow-installation' constraint (interface attributes)"
3908 }
3909 },
3910 "warn": dict(),
3911 "info": dict(),
3912 }
3913
3914 multi_value_attrs_test_data = {
3915 "iface_with_multiple_multi_value_attrs_and_multi_value_decl_matches": (
3916 {
3917 "read-devices": [
3918 "/dev/chunkfs/geom",
3919 "/dev/chunkfs/stats",
3920 "/dev/chunkfs/sizes",
3921 ],
3922 "devices": ["/dev/mtd[0-9]", "/dev/chunkfs/chunkfs[0-2]"],
3923 },
3924 {
3925 "read-devices": [
3926 "/dev/chunkfs/geom",
3927 "/dev/chunkfs/stats",
3928 "/dev/chunkfs/sizes",
3929 ],
3930 "devices": ["/dev/mtd\\[0-9\\]", "/dev/chunkfs/chunkfs\\[0-2\\]"],
3931 },
3932 {"info": 1, "warn": 0, "error": 0},
3933 review_ok_output,
3934 ),
3935 "iface_with_multiple_multi_value_attrs_and_multi_value_decl_different_order_matches": (
3936 {
3937 "read-devices": [
3938 "/dev/chunkfs/stats",
3939 "/dev/chunkfs/sizes",
3940 "/dev/chunkfs/geom",
3941 ],
3942 "devices": ["/dev/chunkfs/chunkfs[0-2]", "/dev/mtd[0-9]"],
3943 },
3944 {
3945 "read-devices": [
3946 "/dev/chunkfs/geom",
3947 "/dev/chunkfs/stats",
3948 "/dev/chunkfs/sizes",
3949 ],
3950 "devices": ["/dev/mtd\\[0-9\\]", "/dev/chunkfs/chunkfs\\[0-2\\]"],
3951 },
3952 {"info": 1, "warn": 0, "error": 0},
3953 review_ok_output,
3954 ),
3955 "iface_with_multiple_multi_value_attrs_and_single_value_list_decl_matches": (
3956 {"devices": ["/dev/mtd1", "/dev/mtd2"]},
3957 {"devices": ["/dev/mtd[0-9]"]},
3958 {"info": 1, "warn": 0, "error": 0},
3959 review_ok_output,
3960 ),
3961 "iface_with_single_value_attrs_and_single_value_list_decl_matches": (
3962 {"devices": "/dev/mtd1"},
3963 {"devices": ["/dev/mtd[0-9]"]},
3964 {"info": 1, "warn": 0, "error": 0},
3965 review_ok_output,
3966 ),
3967 "iface_with_single_value_attrs_and_single_value_decl_matches": (
3968 {"devices": "/dev/mtd1"},
3969 {"devices": "/dev/mtd[0-9]"},
3970 {"info": 1, "warn": 0, "error": 0},
3971 review_ok_output,
3972 ),
3973 "iface_with_one_multi_value_attrs_all_matching": (
3974 {"devices": ["/dev/mtd[0-9]", "/dev/chunkfs/chunkfs[0-2]"]},
3975 {"devices": ["/dev/mtd\\[0-9\\]", "/dev/chunkfs/chunkfs\\[0-2\\]"]},
3976 {"info": 1, "warn": 0, "error": 0},
3977 review_ok_output,
3978 ),
3979 "regex_in_list_of_single_attr_matches": ( # This case represents the current workaround
3980 {
3981 "read-devices": [
3982 "/dev/chunkfs/geom",
3983 "/dev/chunkfs/stats",
3984 "/dev/chunkfs/sizes",
3985 ],
3986 "devices": ["/dev/mtd[0-9]", "/dev/chunkfs/chunkfs[0-2]"],
3987 },
3988 {
3989 "devices": ["(/dev/mtd\\[0-9\\]|/dev/chunkfs/chunkfs\\[0-2\\])"],
3990 "read-devices": [
3991 "(/dev/chunkfs/geom|/dev/chunkfs/stats|/dev/chunkfs/sizes)"
3992 ],
3993 },
3994 {"info": 1, "warn": 0, "error": 0},
3995 review_ok_output,
3996 ),
3997 "extra_iface_attr_matches": ( # Even though this test passes here,
3998 # unknown attrs are detected as part _verify_declaration check
3999 {
4000 "read-devices": [
4001 "/dev/chunkfs/geom",
4002 "/dev/chunkfs/stats",
4003 "/dev/chunkfs/sizes",
4004 ],
4005 "devices": ["/dev/mtd[0-9]", "/dev/chunkfs/chunkfs[0-2]"],
4006 },
4007 {
4008 "read-devices": [
4009 "/dev/chunkfs/geom",
4010 "/dev/chunkfs/stats",
4011 "/dev/chunkfs/sizes",
4012 ],
4013 },
4014 {"info": 1, "warn": 0, "error": 0},
4015 review_ok_output,
4016 ),
4017 "less_items_in_iface_attr_list_but_all_matching_matches": (
4018 {"read-devices": ["/dev/chunkfs/geom", "/dev/chunkfs/stats"]},
4019 {
4020 "read-devices": [
4021 "/dev/chunkfs/geom",
4022 "/dev/chunkfs/stats",
4023 "/dev/chunkfs/sizes",
4024 ],
4025 },
4026 {"info": 1, "warn": 0, "error": 0},
4027 review_ok_output,
4028 ),
4029 "missing_attr_in_iface_fails": (
4030 {
4031 "read-devices": [
4032 "/dev/chunkfs/geom",
4033 "/dev/chunkfs/stats",
4034 "/dev/chunkfs/sizes",
4035 ],
4036 },
4037 {
4038 "read-devices": [
4039 "/dev/chunkfs/geom",
4040 "/dev/chunkfs/stats",
4041 "/dev/chunkfs/sizes",
4042 ],
4043 "devices": ["/dev/mtd\\[0-9\\]", "/dev/chunkfs/chunkfs\\[0-2\\]"],
4044 },
4045 {"info": 0, "warn": 0, "error": 1},
4046 review_err_output,
4047 ),
4048 "no_matching_item_in_iface_attr_list_fails": (
4049 {
4050 "read-devices": [
4051 "/dev/chunkfs/geom",
4052 "/dev/chunkfs/stats",
4053 "/dev/chunkfs/sizes",
4054 ]
4055 },
4056 {"read-devices": ["/dev/chunkfs/geom", "/dev/chunkfs/stats"]},
4057 {"info": 0, "warn": 0, "error": 1},
4058 review_err_output,
4059 ),
4060 "iface_with_single_value_attrs_and_multiple_value_decl_matches": (
4061 {"devices": "/dev/mtd1"},
4062 {"devices": ["/dev/mtd[0-9]", "/dev/chunkfs/chunkfs[0-2]"]},
4063 {"info": 1, "warn": 0, "error": 0},
4064 review_ok_output,
4065 ),
4066 }
4067
4068 for test, test_data in multi_value_attrs_test_data.items():
4069 (
4070 iface_attr_value,
4071 rule_value,
4072 expected_counts,
4073 expected_output_data,
4074 ) = test_data
4075 with self.subTest(
4076 iface_attr_value=iface_attr_value,
4077 rule_value=iface_attr_value,
4078 expected_counts=expected_counts,
4079 expected_output_data=expected_output_data,
4080 ):
4081 iface_data = {"interface": "custom-device"}
4082 iface_data.update(iface_attr_value)
4083 slots = {"fram-devices": iface_data}
4084 self.set_test_snap_yaml("slots", slots)
4085 c = SnapReviewDeclaration(self.test_name)
4086 base = {
4087 "slots": {
4088 "custom-device": {
4089 "allow-installation": {
4090 "slot-attributes": rule_value,
4091 "slot-names": ["fram-devices"],
4092 }
4093 }
4094 }
4095 }
4096 self._set_base_declaration(c, base)
4097 c.check_declaration()
4098 r = c.review_report
4099 self.check_results(r, expected_counts)
4100 self.check_results(r, expected=expected_output_data)
4101
4102 def test_check_declaration_slots_multi_value_attrs_issue_support_nested(self,):
4103 review_ok_output = {
4104 "error": dict(),
4105 "warn": dict(),
4106 "info": {"declaration-snap-v2:plugs:mnt:mount-control": {"text": "OK"}},
4107 }
4108
4109 review_err_output = {
4110 "error": {
4111 "declaration-snap-v2:plugs_installation:mnt:mount-control": {
4112 "text": "human review required due to 'allow-installation' constraint (interface attributes)"
4113 }
4114 },
4115 "warn": dict(),
4116 "info": dict(),
4117 }
4118
4119 multi_value_attrs_test_data = {
4120 "iface_with_list_attrs_and_single_value_decl_list_matches": (
4121 {
4122 "mnt": [
4123 {
4124 "what": "/dev/x*",
4125 "where": "/foo/*",
4126 "options": ["rw", "nodev"],
4127 },
4128 {
4129 "what": "/bar/*",
4130 "where": "/baz/*",
4131 "options": ["rw", "bind"],
4132 },
4133 ]
4134 },
4135 {
4136 "mnt": [
4137 {
4138 "what": r"/(bar/|dev/x)\*",
4139 "where": r"/(foo|baz)/\*",
4140 "options": "rw|bind|nodev",
4141 }
4142 ]
4143 },
4144 {"info": 1, "warn": 0, "error": 0},
4145 review_ok_output,
4146 ),
4147 "iface_with_single_list_attrs_and_single_value_decl_list_matches": (
4148 {
4149 "mnt": [
4150 {
4151 "what": "/dev/x*",
4152 "where": "/foo/*",
4153 "options": ["rw", "nodev"],
4154 }
4155 ]
4156 },
4157 {
4158 "mnt": [
4159 {
4160 "what": r"/(bar/|dev/x)\*",
4161 "where": r"/(foo|baz)/\*",
4162 "options": "rw|bind|nodev",
4163 }
4164 ]
4165 },
4166 {"info": 1, "warn": 0, "error": 0},
4167 review_ok_output,
4168 ),
4169 "iface_with_list_attrs_and_multiple_value_decl_list_matches": (
4170 {
4171 "mnt": [
4172 {
4173 "what": "/dev/x*",
4174 "where": "/foo/*",
4175 "options": ["rw", "nodev"],
4176 },
4177 {
4178 "what": "/bar/*",
4179 "where": "/baz/*",
4180 "options": ["rw", "bind"],
4181 },
4182 ]
4183 },
4184 {
4185 "mnt": [
4186 {
4187 "what": r"/dev/x\*",
4188 "where": r"/foo/\*",
4189 "options": ["nodev", "rw"],
4190 },
4191 {
4192 "what": r"/bar/\*",
4193 "where": r"/baz/\*",
4194 "options": ["rw", "bind"],
4195 },
4196 ]
4197 },
4198 {"info": 1, "warn": 0, "error": 0},
4199 review_ok_output,
4200 ),
4201 "iface_with_list_attrs_and_multiple_value_decl_list_with_no_alternatives_fails": (
4202 {
4203 "mnt": [
4204 {
4205 "what": "/dev/x*",
4206 "where": "/foo/*",
4207 "options": ["rw", "nodev"],
4208 },
4209 {
4210 "what": "/bar/*",
4211 "where": "/baz/*",
4212 "options": ["rw", "bind"],
4213 },
4214 ]
4215 },
4216 {
4217 "mnt": [
4218 {"what": r"/dev/x\*", "where": r"/foo/\*", "options": ["rw"]},
4219 {
4220 "what": r"/bar/\*",
4221 "where": r"/baz/\*",
4222 "options": ["rw", "bind"],
4223 },
4224 ]
4225 },
4226 {"info": 0, "warn": 0, "error": 1},
4227 review_err_output,
4228 ),
4229 "iface_with_extra_list_attrs_and_multiple_value_decl_list_fails": (
4230 {
4231 "mnt": [
4232 {
4233 "what": "/dev/x*",
4234 "where": "/foo/*",
4235 "options": ["rw", "nodev"],
4236 },
4237 {
4238 "what": "/bar/*",
4239 "where": "/baz/*",
4240 "foo_options": ["rw", "bind"],
4241 },
4242 ]
4243 },
4244 {
4245 "mnt": [
4246 {
4247 "what": r"/(bar/|dev/x)\*",
4248 "where": r"/(foo|baz)/\*",
4249 "options": "rw|bind|nodev",
4250 }
4251 ]
4252 },
4253 {"info": 0, "warn": 0, "error": 1},
4254 review_err_output,
4255 ),
4256 }
4257
4258 for test, test_data in multi_value_attrs_test_data.items():
4259 (
4260 iface_attr_value,
4261 rule_value,
4262 expected_counts,
4263 expected_output_data,
4264 ) = test_data
4265 with self.subTest(
4266 iface_attr_value=iface_attr_value,
4267 rule_value=iface_attr_value,
4268 expected_counts=expected_counts,
4269 expected_output_data=expected_output_data,
4270 ):
4271 iface_data = {"interface": "mount-control"}
4272 iface_data.update(iface_attr_value)
4273 plugs = {"mnt": iface_data}
4274 self.set_test_snap_yaml("plugs", plugs)
4275 c = SnapReviewDeclaration(self.test_name)
4276 base = {
4277 "plugs": {
4278 "mount-control": {
4279 "allow-installation": {
4280 "plug-attributes": rule_value,
4281 "plug-names": ["mnt"],
4282 }
4283 }
4284 }
4285 }
4286 self._set_base_declaration(c, base)
4287 c.check_declaration()
4288 r = c.review_report
4289 self.check_results(r, expected_counts)
4290 self.check_results(r, expected=expected_output_data)
4291
3895 def test_check_declaration_slots_deny_connection_attrib_list_match_unmatching_plug_names(4292 def test_check_declaration_slots_deny_connection_attrib_list_match_unmatching_plug_names(
3896 self,4293 self,
3897 ):4294 ):
@@ -3973,7 +4370,7 @@ slots:
3973 expected["info"][name] = {"text": "OK"}4370 expected["info"][name] = {"text": "OK"}
3974 self.check_results(r, expected=expected)4371 self.check_results(r, expected=expected)
39754372
3976 def test_check_declaration_slots_allow_connection_attrib_lists_exception(self):4373 def test_check_declaration_slots_allow_connection_attrib_lists_supported(self):
3977 """Test check_declaration - slots/allow-connection/attrib - lists match snap attribute list in decl list of lists"""4374 """Test check_declaration - slots/allow-connection/attrib - lists match snap attribute list in decl list of lists"""
3978 slots = {"iface-foo": {"interface": "foo", "attrib1": ["b", "a"]}}4375 slots = {"iface-foo": {"interface": "foo", "attrib1": ["b", "a"]}}
3979 self.set_test_snap_yaml("slots", slots)4376 self.set_test_snap_yaml("slots", slots)
@@ -3991,8 +4388,7 @@ slots:
3991 try:4388 try:
3992 c.check_declaration()4389 c.check_declaration()
3993 except SnapDeclarationException:4390 except SnapDeclarationException:
3994 return4391 raise Exception("base declaration should not be invalid") # pragma: nocover
3995 raise Exception("base declaration should be invalid") # pragma: nocover
39964392
3997 def test_check_declaration_slots_allow_connection_attrib_list_nomatch(self):4393 def test_check_declaration_slots_allow_connection_attrib_list_nomatch(self):
3998 """Test check_declaration - slots/allow-connection/attrib - list nomatch snap attribute str not in decl list"""4394 """Test check_declaration - slots/allow-connection/attrib - list nomatch snap attribute str not in decl list"""

Subscribers

People subscribed via source and target branches