Zack Williams | 28f1e49 | 2019-02-01 10:02:56 -0700 | [diff] [blame^] | 1 | # Copyright 2017-present Open Networking Foundation |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | from __future__ import print_function |
| 16 | import unittest |
| 17 | |
| 18 | import plyxproto.parser as plyxparser |
| 19 | import plyxproto.model as plyxmodel |
| 20 | import plyxproto.helpers as plyxhelpers |
| 21 | |
| 22 | # Example of creating unittests on PLY code: |
| 23 | # https://github.com/dabeaz/ply/tree/master/test |
| 24 | # http://www.dalkescientific.com/writings/NBN/parsing_with_ply.html |
| 25 | |
| 26 | |
| 27 | def get_name(element): |
| 28 | '''returns name of element''' |
| 29 | return element.name.value.pval |
| 30 | |
| 31 | |
| 32 | def get_field_type(ft): |
| 33 | ''' returns field type''' |
| 34 | |
| 35 | # FIXME: these are confusing |
| 36 | if isinstance(ft, plyxmodel.FieldType): |
| 37 | return ft.name.pval |
| 38 | |
| 39 | elif isinstance(ft, plyxmodel.DotName): |
| 40 | return ft.value |
| 41 | |
| 42 | else: |
| 43 | print("unknown field type: %s" % ft) |
| 44 | raise BaseException |
| 45 | |
| 46 | |
| 47 | def get_field_directives(field): |
| 48 | ''' navigate the morass that is fieldDirective ''' |
| 49 | |
| 50 | fds = {} |
| 51 | |
| 52 | for fieldd in field.fieldDirective: |
| 53 | |
| 54 | fd_key = "" |
| 55 | fd_val = "" |
| 56 | |
| 57 | # FIXME: Do these differences in heirarchy have value? They seem |
| 58 | # arbitrary and maybe the parser should handle it instead? |
| 59 | |
| 60 | if hasattr(fieldd, 'name'): |
| 61 | fd_key = fieldd.name.value |
| 62 | elif hasattr(fieldd.pval.name, 'pval'): |
| 63 | fd_key = fieldd.pval.name.pval |
| 64 | elif hasattr(fieldd.pval.name.value, 'pval'): |
| 65 | fd_key = fieldd.pval.name.value.pval |
| 66 | else: |
| 67 | print("problem with key in fieldDirective: ", fieldd) |
| 68 | raise BaseException |
| 69 | |
| 70 | if type(fieldd) is list: |
| 71 | fd_val = [item.pval for item in fieldd.value] |
| 72 | elif hasattr(fieldd, 'value'): |
| 73 | if type(fieldd.value) is list: |
| 74 | fd_val = [item.pval for item in fieldd.value] |
| 75 | elif hasattr(fieldd.value, 'pval'): |
| 76 | fd_val = fieldd.value.pval |
| 77 | elif hasattr(fieldd.value, 'value'): |
| 78 | if hasattr(fieldd.value.value, 'pval'): |
| 79 | fd_val = fieldd.value.value.pval |
| 80 | else: |
| 81 | fd_val = fieldd.value.value |
| 82 | else: |
| 83 | print("problem with value.value in fieldDirective: %s" % fieldd) |
| 84 | raise BaseException |
| 85 | elif hasattr(fieldd.pval.value, 'pval'): |
| 86 | fd_val = fieldd.pval.value.pval |
| 87 | elif hasattr(fieldd.pval.value.value, 'pval'): |
| 88 | fd_val = fieldd.pval.value.value.pval |
| 89 | else: |
| 90 | print("problem with value in fieldDirective: ", fieldd) |
| 91 | raise BaseException |
| 92 | |
| 93 | fds[fd_key] = fd_val |
| 94 | |
| 95 | return fds |
| 96 | |
| 97 | |
| 98 | def msg_dict(msgdef): |
| 99 | ''' |
| 100 | Given a MessageDefinition object, returns a dict describing it |
| 101 | Should probably really be a recursive __dict__ method on that object |
| 102 | ''' |
| 103 | |
| 104 | fd = {} # FieldDefinition |
| 105 | md = {} # sub-ModelDefinition |
| 106 | en = {} # EnumDefinitons |
| 107 | ed = {} # ExtensionsDirective |
| 108 | os = {} # OptionStatement |
| 109 | |
| 110 | assert isinstance(msgdef, plyxmodel.MessageDefinition) |
| 111 | |
| 112 | for field in msgdef.body: |
| 113 | |
| 114 | if isinstance(field, plyxmodel.FieldDefinition): |
| 115 | |
| 116 | fd[field.name.value.pval] = { |
| 117 | 'id': int(field.fieldId.pval), |
| 118 | 'modifier': field.field_modifier.pval, |
| 119 | 'type': get_field_type(field.ftype), |
| 120 | 'policy': field.policy, |
| 121 | 'directives': get_field_directives(field), |
| 122 | } |
| 123 | |
| 124 | elif isinstance(field, plyxmodel.LinkSpec): |
| 125 | |
| 126 | fd[field.field_def.name.value.pval] = { |
| 127 | 'id': int(field.field_def.fieldId.pval), |
| 128 | 'modifier': field.field_def.field_modifier.pval, |
| 129 | 'type': field.field_def.ftype.value, # Why different (not in a LU as in FieldDefinition)? |
| 130 | 'link_type': field.link_def.link_type.pval, |
| 131 | 'link_name': field.link_def.name[0].pval, # Why in a list? |
| 132 | 'link_src_port': field.link_def.src_port.value.pval, # confusingly named - why include "port" ? |
| 133 | 'link_dst_port': field.link_def.dst_port.value.pval, |
| 134 | 'directives': get_field_directives(field.field_def), |
| 135 | } |
| 136 | |
| 137 | elif isinstance(field, plyxmodel.MessageDefinition): |
| 138 | |
| 139 | md[field.name.value.pval] = msg_dict(field) |
| 140 | |
| 141 | elif isinstance(field, plyxmodel.EnumDefinition): |
| 142 | |
| 143 | enumopts = {} |
| 144 | |
| 145 | for enumdef in field.body: |
| 146 | enumopts[int(enumdef.fieldId.pval)] = get_name(enumdef) |
| 147 | |
| 148 | en[field.name.value.pval] = enumopts |
| 149 | |
| 150 | elif isinstance(field, plyxmodel.ExtensionsDirective): |
| 151 | |
| 152 | # FIXME: models.ExtensionsMax() isn't well defined, so |
| 153 | # omit to/from when non-LU is found |
| 154 | |
| 155 | if type(field.fromVal) is plyxhelpers.LU: |
| 156 | ed['from'] = field.fromVal.pval |
| 157 | |
| 158 | if type(field.toVal) is plyxhelpers.LU: |
| 159 | ed['to'] = field.toVal.pval |
| 160 | |
| 161 | elif isinstance(field, plyxmodel.OptionStatement): |
| 162 | |
| 163 | os[field.name.value.pval] = field.value.value.pval |
| 164 | |
| 165 | else: |
| 166 | print("Unknown message type: %s" % type(field)) |
| 167 | raise BaseException |
| 168 | |
| 169 | return fd, md, en, ed, os |
| 170 | |
| 171 | |
| 172 | def options_dict(protofile): |
| 173 | '''returns a dictionary of name:value options from a ProtoFile''' |
| 174 | |
| 175 | options = {} |
| 176 | |
| 177 | for item in protofile.body: |
| 178 | if isinstance(item, plyxmodel.OptionStatement): |
| 179 | options[item.name.value.pval] = item.value.value.pval |
| 180 | |
| 181 | return options |
| 182 | |
| 183 | |
| 184 | class TestParser(unittest.TestCase): |
| 185 | |
| 186 | @classmethod |
| 187 | def setUpClass(self): |
| 188 | self.parser = plyxparser.ProtobufAnalyzer() |
| 189 | |
| 190 | def test_invalid_input(self): |
| 191 | '''invalid input''' |
| 192 | |
| 193 | t_in = """this is invalid""" |
| 194 | |
| 195 | with self.assertRaises(plyxparser.ParsingError): |
| 196 | self.parser.parse_string(t_in) |
| 197 | |
| 198 | def test_package(self): |
| 199 | '''creating a package''' |
| 200 | |
| 201 | t_in = """package tutorial;""" |
| 202 | p_out = self.parser.parse_string(t_in) |
| 203 | |
| 204 | self.assertIsInstance(p_out, plyxmodel.ProtoFile) |
| 205 | self.assertIsInstance(p_out.body[0], plyxmodel.PackageStatement) |
| 206 | self.assertEqual(p_out.body[0].name.value[0].pval, "tutorial") |
| 207 | |
| 208 | def test_def_policy(self): |
| 209 | '''defining a policy''' |
| 210 | |
| 211 | t_in = """policy foo <exists foo: foo.x=foo.y>""" |
| 212 | p_out = self.parser.parse_string(t_in) |
| 213 | |
| 214 | self.assertIsInstance(p_out, plyxmodel.ProtoFile) |
| 215 | self.assertIsInstance(p_out.body[0], plyxmodel.PolicyDefinition) |
| 216 | self.assertEqual(get_name(p_out.body[0]), "foo") |
| 217 | self.assertDictEqual(p_out.body[0].body, {'exists': ['foo', {'=': ('foo.x', 'foo.y')}]}) |
| 218 | |
| 219 | def test_def_message(self): |
| 220 | '''defining a message''' |
| 221 | |
| 222 | t_in = """package tutorial; |
| 223 | |
| 224 | message Person { |
| 225 | required string name = 1; |
| 226 | required int32 id = 2; |
| 227 | optional string email = 3; |
| 228 | } |
| 229 | """ |
| 230 | p_out = self.parser.parse_string(t_in) |
| 231 | |
| 232 | self.assertIsInstance(p_out, plyxmodel.ProtoFile) |
| 233 | # see test_package for testing `package ...` statement |
| 234 | self.assertIsInstance(p_out.body[1], plyxmodel.MessageDefinition) |
| 235 | self.assertEqual(get_name(p_out.body[1]), 'Person') |
| 236 | |
| 237 | fd, md, en, ed, os = msg_dict(p_out.body[1]) |
| 238 | |
| 239 | self.assertDictEqual(fd['name'], |
| 240 | {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}}) |
| 241 | self.assertDictEqual(fd['id'], |
| 242 | {'id': 2, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {}}) |
| 243 | self.assertDictEqual(fd['email'], |
| 244 | {'id': 3, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {}}) |
| 245 | |
| 246 | def test_options(self): |
| 247 | '''setting options''' |
| 248 | |
| 249 | t_in = """package tutorial; |
| 250 | option java_outer_classname = "PushNotifications"; |
| 251 | option optimize_for = SPEED; |
| 252 | """ |
| 253 | p_out = self.parser.parse_string(t_in) |
| 254 | od = options_dict(p_out) |
| 255 | |
| 256 | self.assertDictEqual(od, {'java_outer_classname': '"PushNotifications"', 'optimize_for': 'SPEED'}) |
| 257 | |
| 258 | def test_def_relation(self): |
| 259 | '''test defining related messages''' |
| 260 | |
| 261 | t_in = """package tutorial; |
| 262 | |
| 263 | message Person(core.Actor) { |
| 264 | required string name = 1; |
| 265 | required int32 id = 2; |
| 266 | optional string email = 3; |
| 267 | |
| 268 | required manytoone work_location->Location/types.Company:employees = 4; |
| 269 | |
| 270 | enum PhoneType { |
| 271 | MOBILE = 0; |
| 272 | HOME = 1; |
| 273 | WORK = 2; |
| 274 | } |
| 275 | |
| 276 | message PhoneNumber { |
| 277 | required string number = 1; |
| 278 | optional PhoneType type = 2 [default = HOME]; |
| 279 | } |
| 280 | |
| 281 | repeated PhoneNumber phone = 4; |
| 282 | extensions 500 to 990; |
| 283 | } |
| 284 | |
| 285 | message AddressBook { |
| 286 | repeated Person person = 1; |
| 287 | |
| 288 | // Possible extension numbers. |
| 289 | extensions 500 to max; |
| 290 | } |
| 291 | """ |
| 292 | p_out = self.parser.parse_string(t_in) |
| 293 | |
| 294 | fd, md, en, ed, os = msg_dict(p_out.body[1]) |
| 295 | |
| 296 | self.assertDictEqual(fd, {'name': {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}}, 'id': {'id': 2, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {}}, 'email': {'id': 3, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {}}, 'work_location': {'id': 4, 'modifier': 'required', 'type': 'int32', 'link_type': 'manytoone', 'link_name': 'Location', 'link_src_port': 'work_location', 'link_dst_port': 'employees', 'directives': {'type': 'link', 'model': ['Location'], 'port': 'employees'}}, 'phone': {'id': 4, 'modifier': 'repeated', 'type': 'PhoneNumber', 'policy': None, 'directives': {}}}) |
| 297 | |
| 298 | self.assertDictEqual(md, {'PhoneNumber': ({'number': {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}}, 'type': {'id': 2, 'modifier': 'optional', 'type': 'PhoneType', 'policy': None, 'directives': {'default': 'HOME'}}}, {}, {}, {}, {})}) |
| 299 | self.assertDictEqual(en, {'PhoneType': {0: 'MOBILE', 1: 'HOME', 2: 'WORK'}}) |
| 300 | self.assertDictEqual(ed, {'from': '500', 'to': '990'}) |
| 301 | self.assertDictEqual(os, {}) |
| 302 | |
| 303 | fd, md, en, ed, os = msg_dict(p_out.body[2]) |
| 304 | |
| 305 | self.assertDictEqual(fd, {'person': {'id': 1, 'modifier': 'repeated', 'type': 'Person', 'policy': None, 'directives': {}}}) |
| 306 | self.assertDictEqual(md, {}) |
| 307 | self.assertDictEqual(en, {}) |
| 308 | self.assertDictEqual(ed, {'from': '500'}) |
| 309 | self.assertDictEqual(os, {}) |
| 310 | |
| 311 | def test_xos_core(self): |
| 312 | '''test chunk of xos core xproto''' |
| 313 | |
| 314 | t_in = """ |
| 315 | option app_label = "core"; |
| 316 | option legacy="True"; |
| 317 | |
| 318 | // use thi policy to allow access to admins only |
| 319 | policy admin_policy < ctx.user.is_admin > |
| 320 | |
| 321 | message XOSBase { |
| 322 | option skip_init = True; |
| 323 | option custom_header = "xosbase_header"; |
| 324 | option abstract = True; |
| 325 | |
| 326 | // field 1 is reserved for "id" |
| 327 | required string created = 2 [content_type = "date", auto_now_add = True, help_text = "Time this model was created"]; |
| 328 | required string updated = 3 [default = "now()", content_type = "date", help_text = "Time this model was changed by a non-synchronizer"]; |
| 329 | optional string enacted = 4 [null = True, content_type = "date", blank = True, default = None, help_text = "When synced, set to the timestamp of the data that was synced"]; |
| 330 | optional string policed = 5 [null = True, content_type = "date", blank = True, default = None, help_text = "When policed, set to the timestamp of the data that was policed"]; |
| 331 | optional string backend_register = 6 [default = "{}", max_length = 1024, feedback_state = True]; |
| 332 | required bool backend_need_delete = 7 [default = False, blank = True]; |
| 333 | required bool backend_need_reap = 8 [default = False, blank = True]; |
| 334 | required string backend_status = 9 [default = "Provisioning in progress", max_length = 1024, null = True, feedback_state = True]; |
| 335 | required int32 backend_code = 10 [default = 0, feedback_state = True]; |
| 336 | required bool deleted = 11 [default = False, blank = True]; |
| 337 | required bool write_protect = 12 [default = False, blank = True]; |
| 338 | required bool lazy_blocked = 13 [default = False, blank = True]; |
| 339 | required bool no_sync = 14 [default = False, blank = True]; |
| 340 | required bool no_policy = 15 [default = False, blank = True]; |
| 341 | optional string policy_status = 16 [default = "Policy in process", max_length = 1024, feedback_state = True]; |
| 342 | optional int32 policy_code = 17 [default = 0, feedback_state = True]; |
| 343 | required string leaf_model_name = 18 [null = False, max_length = 1024, help_text = "The most specialized model in this chain of inheritance, often defined by a service developer"]; |
| 344 | required bool backend_need_delete_policy = 19 [default = False, help_text = "True if delete model_policy must be run before object can be reaped", blank = True]; |
| 345 | required bool xos_managed = 20 [default = True, help_text = "True if xos is responsible for creating/deleting this object", blank = True, gui_hidden = True]; |
| 346 | optional string backend_handle = 21 [max_length = 1024, feedback_state = True, blank=True, null=True, help_text = "Handle used by the backend to track this object", gui_hidden = True]; |
| 347 | optional string changed_by_step = 22 [null = True, content_type = "date", blank = True, default = None, gui_hidden = True, help_text = "Time this model was changed by a sync step"]; |
| 348 | optional string changed_by_policy = 23 [null = True, content_type = "date", blank = True, default = None, gui_hidden = True, help_text = "Time this model was changed by a model policy"]; |
| 349 | } |
| 350 | |
| 351 | // A user may give a permission that he has to another user |
| 352 | policy grant_policy < ctx.user.is_admin |
| 353 | | exists Privilege:Privilege.object_type = obj.object_type |
| 354 | & Privilege.object_id = obj.object_id |
| 355 | & Privilege.accessor_type = "User" |
| 356 | & Privilege.accessor_id = ctx.user.id |
| 357 | & Privilege.permission = "role:admin" > |
| 358 | |
| 359 | message Privilege::grant_policy (XOSBase) { |
| 360 | required int32 accessor_id = 1 [null = False, blank=False]; |
| 361 | required string accessor_type = 2 [null = False, max_length=1024, blank = False]; |
| 362 | optional int32 controller_id = 3 [null = True, blank = True]; |
| 363 | required int32 object_id = 4 [null = False, blank=False]; |
| 364 | required string object_type = 5 [null = False, max_length=1024, blank = False]; |
| 365 | required string permission = 6 [null = False, default = "all", max_length=1024, tosca_key=True]; |
| 366 | required string granted = 7 [content_type = "date", auto_now_add = True, max_length=1024]; |
| 367 | required string expires = 8 [content_type = "date", null = True, max_length=1024]; |
| 368 | } |
| 369 | """ |
| 370 | |
| 371 | p_out = self.parser.parse_string(t_in) |
| 372 | |
| 373 | # check options |
| 374 | od = options_dict(p_out) |
| 375 | self.assertDictEqual(od, {'app_label': '"core"', 'legacy': '"True"'}) |
| 376 | |
| 377 | self.assertIsInstance(p_out.body[2], plyxmodel.PolicyDefinition) |
| 378 | self.assertEqual(get_name(p_out.body[2]), "admin_policy") |
| 379 | self.assertEqual(p_out.body[2].body, 'ctx.user.is_admin') |
| 380 | |
| 381 | fd, md, en, ed, os = msg_dict(p_out.body[3]) |
| 382 | |
| 383 | self.assertDictEqual(fd, {'created': {'id': 2, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'auto_now_add': 'True', 'help_text': '"Time this model was created"'}}, 'updated': {'id': 3, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'default': '"now()"', 'content_type': '"date"', 'help_text': '"Time this model was changed by a non-synchronizer"'}}, 'enacted': {'id': 4, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'help_text': '"When synced, set to the timestamp of the data that was synced"'}}, 'policed': {'id': 5, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'help_text': '"When policed, set to the timestamp of the data that was policed"'}}, 'backend_register': {'id': 6, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'default': '"{}"', 'max_length': '1024', 'feedback_state': 'True'}}, 'backend_need_delete': {'id': 7, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'backend_need_reap': {'id': 8, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'backend_status': {'id': 9, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'default': '"Provisioning in progress"', 'max_length': '1024', 'null': 'True', 'feedback_state': 'True'}}, 'backend_code': {'id': 10, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'default': '0', 'feedback_state': 'True'}}, 'deleted': {'id': 11, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'write_protect': {'id': 12, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'lazy_blocked': {'id': 13, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'no_sync': {'id': 14, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'no_policy': {'id': 15, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'policy_status': {'id': 16, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'default': '"Policy in process"', 'max_length': '1024', 'feedback_state': 'True'}}, 'policy_code': {'id': 17, 'modifier': 'optional', 'type': 'int32', 'policy': None, 'directives': {'default': '0', 'feedback_state': 'True'}}, 'leaf_model_name': {'id': 18, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'help_text': '"The most specialized model in this chain of inheritance, often defined by a service developer"'}}, 'backend_need_delete_policy': {'id': 19, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'help_text': '"True if delete model_policy must be run before object can be reaped"', 'blank': 'True'}}, 'xos_managed': {'id': 20, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'True', 'help_text': '"True if xos is responsible for creating/deleting this object"', 'blank': 'True', 'gui_hidden': 'True'}}, 'backend_handle': {'id': 21, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'max_length': '1024', 'feedback_state': 'True', 'blank': 'True', 'null': 'True', 'help_text': '"Handle used by the backend to track this object"', 'gui_hidden': 'True'}}, 'changed_by_step': {'id': 22, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'gui_hidden': 'True', 'help_text': '"Time this model was changed by a sync step"'}}, 'changed_by_policy': {'id': 23, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'gui_hidden': 'True', 'help_text': '"Time this model was changed by a model policy"'}}}) |
| 384 | |
| 385 | self.assertDictEqual(md, {}) |
| 386 | self.assertDictEqual(en, {}) |
| 387 | self.assertDictEqual(ed, {}) |
| 388 | self.assertDictEqual(os, {'skip_init': 'True', 'custom_header': '"xosbase_header"', 'abstract': 'True'}) |
| 389 | |
| 390 | self.assertIsInstance(p_out.body[4], plyxmodel.PolicyDefinition) |
| 391 | |
| 392 | self.assertEqual(get_name(p_out.body[4]), "grant_policy") |
| 393 | self.assertDictEqual(p_out.body[4].body, {'|': |
| 394 | ['ctx.user.is_admin', {'&': |
| 395 | [{'&': [{'&': [{'&': [{'exists': ['Privilege', {'=': ('Privilege.object_type', 'obj.object_type')}]}, |
| 396 | {'=': ('Privilege.object_id', 'obj.object_id')}]}, |
| 397 | {'=': ('Privilege.accessor_type', '"User"')}]}, |
| 398 | {'=': ('Privilege.accessor_id', 'ctx.user.id')}]}, |
| 399 | {'=': ('Privilege.permission', '"role:admin"')}]}]} |
| 400 | ) |
| 401 | fd, md, en, ed, os = msg_dict(p_out.body[5]) |
| 402 | |
| 403 | self.assertDictEqual(fd, {'accessor_id': {'id': 1, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'null': 'False', 'blank': 'False'}}, 'accessor_type': {'id': 2, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'blank': 'False'}}, 'controller_id': {'id': 3, 'modifier': 'optional', 'type': 'int32', 'policy': None, 'directives': {'null': 'True', 'blank': 'True'}}, 'object_id': {'id': 4, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'null': 'False', 'blank': 'False'}}, 'object_type': {'id': 5, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'blank': 'False'}}, 'permission': {'id': 6, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'default': '"all"', 'max_length': '1024', 'tosca_key': 'True'}}, 'granted': {'id': 7, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'auto_now_add': 'True', 'max_length': '1024'}}, 'expires': {'id': 8, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'null': 'True', 'max_length': '1024'}}}) |
| 404 | |
| 405 | self.assertDictEqual(md, {}) |
| 406 | self.assertDictEqual(en, {}) |
| 407 | self.assertDictEqual(ed, {}) |
| 408 | self.assertDictEqual(os, {}) |
| 409 | |
| 410 | # FIXME: these sorts of simple validations should fail but currently don't |
| 411 | # |
| 412 | # def test_invalid_message(self): |
| 413 | # '''an invalid message''' |
| 414 | # |
| 415 | # t_in = """message BadMessage { |
| 416 | # required string name = 0; |
| 417 | # required int32 id = -2; |
| 418 | # }""" |
| 419 | # p_out = self.parser.parse_string(t_in) |
| 420 | # print(p_out) |
| 421 | # |
| 422 | # |
| 423 | # def test_duplicate_id(self): |
| 424 | # '''duplicate id's in a message''' |
| 425 | # |
| 426 | # t_in = """message BadMessage2 { |
| 427 | # required string name = 1; |
| 428 | # required int32 id = 1; |
| 429 | # }""" |
| 430 | # p_out = self.parser.parse_string(t_in) |
| 431 | # print(p_out) |