1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16# See "Type Validation" in master/docs/developer/tests.rst 17 18import datetime 19import json 20import re 21 22from buildbot.util import UTC 23from buildbot.util import bytes2unicode 24 25# Base class 26 27validatorsByName = {} 28 29 30class Validator: 31 32 name = None 33 hasArgs = False 34 35 def validate(self, name, object): 36 raise NotImplementedError 37 38 class __metaclass__(type): 39 40 def __new__(mcs, name, bases, attrs): 41 cls = type.__new__(mcs, name, bases, attrs) 42 if 'name' in attrs and attrs['name']: 43 assert attrs['name'] not in validatorsByName 44 validatorsByName[attrs['name']] = cls 45 return cls 46 47 48# Basic types 49 50class InstanceValidator(Validator): 51 types = () 52 53 def validate(self, name, object): 54 if not isinstance(object, self.types): 55 yield "{} ({!r}) is not a {}".format( 56 name, object, self.name or repr(self.types)) 57 58 59class IntValidator(InstanceValidator): 60 types = (int,) 61 name = 'integer' 62 63 64class BooleanValidator(InstanceValidator): 65 types = (bool,) 66 name = 'boolean' 67 68 69class StringValidator(InstanceValidator): 70 # strings must be unicode 71 types = (str,) 72 name = 'string' 73 74 75class BinaryValidator(InstanceValidator): 76 types = (bytes,) 77 name = 'bytestring' 78 79 80class StrValidator(InstanceValidator): 81 types = (str,) 82 name = 'str' 83 84 85class DateTimeValidator(Validator): 86 types = (datetime.datetime,) 87 name = 'datetime' 88 89 def validate(self, name, object): 90 if not isinstance(object, datetime.datetime): 91 yield "{} - {!r} - is not a datetime".format(name, object) 92 elif object.tzinfo != UTC: 93 yield "{} is not a UTC datetime".format(name) 94 95 96class IdentifierValidator(Validator): 97 types = (str,) 98 name = 'identifier' 99 hasArgs = True 100 101 ident_re = re.compile('^[a-zA-Z\u00a0-\U0010ffff_-][a-zA-Z0-9\u00a0-\U0010ffff_-]*$', 102 flags=re.UNICODE) 103 104 def __init__(self, len): 105 self.len = len 106 107 def validate(self, name, object): 108 if not isinstance(object, str): 109 yield "{} - {!r} - is not a unicode string".format(name, object) 110 elif not self.ident_re.match(object): 111 yield "{} - {!r} - is not an identifier".format(name, object) 112 elif not object: 113 yield "{} - identifiers cannot be an empty string".format(name) 114 elif len(object) > self.len: 115 yield "{} - {!r} - is longer than {} characters".format( 116 name, object, self.len) 117 118# Miscellaneous 119 120 121class NoneOk: 122 123 def __init__(self, original): 124 self.original = original 125 126 def validate(self, name, object): 127 if object is None: 128 return 129 else: 130 for msg in self.original.validate(name, object): 131 yield msg 132 133 134class Any: 135 136 def validate(self, name, object): 137 return 138 139# Compound Types 140 141 142class DictValidator(Validator): 143 144 name = 'dict' 145 146 def __init__(self, optionalNames=None, **keys): 147 if optionalNames is None: 148 optionalNames = [] 149 self.optionalNames = set(optionalNames) 150 self.keys = keys 151 self.expectedNames = set(keys.keys()) 152 153 def validate(self, name, object): 154 # this uses isinstance, allowing dict subclasses as used by the DB API 155 if not isinstance(object, dict): 156 yield "{} ({!r}) is not a dictionary (got type {})".format( 157 name, object, type(object)) 158 return 159 160 gotNames = set(object.keys()) 161 162 unexpected = gotNames - self.expectedNames 163 if unexpected: 164 yield "{} has unexpected keys {}".format(name, 165 ", ".join([repr(n) for n in unexpected])) 166 167 missing = self.expectedNames - self.optionalNames - gotNames 168 if missing: 169 yield "{} is missing keys {}".format(name, 170 ", ".join([repr(n) for n in missing])) 171 172 for k in gotNames & self.expectedNames: 173 for msg in self.keys[k].validate("{}[{!r}]".format(name, k), object[k]): 174 yield msg 175 176 177class SequenceValidator(Validator): 178 type = None 179 180 def __init__(self, elementValidator): 181 self.elementValidator = elementValidator 182 183 def validate(self, name, object): 184 if not isinstance(object, self.type): # noqa pylint: disable=isinstance-second-argument-not-valid-type 185 yield "{} ({!r}) is not a {}".format(name, object, self.name) 186 return 187 188 for idx, elt in enumerate(object): 189 for msg in self.elementValidator.validate("{}[{}]".format(name, idx), 190 elt): 191 yield msg 192 193 194class ListValidator(SequenceValidator): 195 type = list 196 name = 'list' 197 198 199class TupleValidator(SequenceValidator): 200 type = tuple 201 name = 'tuple' 202 203 204class StringListValidator(ListValidator): 205 name = 'string-list' 206 207 def __init__(self): 208 super().__init__(StringValidator()) 209 210 211class SourcedPropertiesValidator(Validator): 212 213 name = 'sourced-properties' 214 215 def validate(self, name, object): 216 if not isinstance(object, dict): 217 yield "{} is not sourced properties (not a dict)".format(name) 218 return 219 for k, v in object.items(): 220 if not isinstance(k, str): 221 yield "{} property name {!r} is not unicode".format(name, k) 222 if not isinstance(v, tuple) or len(v) != 2: 223 yield "{} property value for '{}' is not a 2-tuple".format(name, k) 224 return 225 propval, propsrc = v 226 if not isinstance(propsrc, str): 227 yield "{}[{}] source {!r} is not unicode".format(name, k, propsrc) 228 try: 229 json.dumps(propval) 230 except (TypeError, ValueError): 231 yield "{}[{!r}] value is not JSON-able".format(name, k) 232 233 234class JsonValidator(Validator): 235 236 name = 'json' 237 238 def validate(self, name, object): 239 try: 240 json.dumps(object) 241 except (TypeError, ValueError): 242 yield "{}[{!r}] value is not JSON-able".format(name, object) 243 244 245class PatchValidator(Validator): 246 247 name = 'patch' 248 249 validator = DictValidator( 250 body=NoneOk(BinaryValidator()), 251 level=NoneOk(IntValidator()), 252 subdir=NoneOk(StringValidator()), 253 author=NoneOk(StringValidator()), 254 comment=NoneOk(StringValidator()), 255 ) 256 257 def validate(self, name, object): 258 for msg in self.validator.validate(name, object): 259 yield msg 260 261 262class MessageValidator(Validator): 263 264 routingKeyValidator = TupleValidator(StrValidator()) 265 266 def __init__(self, events, messageValidator): 267 self.events = [bytes2unicode(e) for e in set(events)] 268 self.messageValidator = messageValidator 269 270 def validate(self, name, routingKey_message): 271 try: 272 routingKey, message = routingKey_message 273 except (TypeError, ValueError) as e: 274 yield "{!r}: not a routing key and message: {}".format(routingKey_message, e) 275 routingKeyBad = False 276 for msg in self.routingKeyValidator.validate("routingKey", routingKey): 277 yield msg 278 routingKeyBad = True 279 280 if not routingKeyBad: 281 event = routingKey[-1] 282 if event not in self.events: 283 yield "routing key event {!r} is not valid".format(event) 284 285 for msg in self.messageValidator.validate("{} message".format(routingKey[0]), 286 message): 287 yield msg 288 289 290class Selector(Validator): 291 292 def __init__(self): 293 self.selectors = [] 294 295 def add(self, selector, validator): 296 self.selectors.append((selector, validator)) 297 298 def validate(self, name, arg_object): 299 try: 300 arg, object = arg_object 301 except (TypeError, ValueError) as e: 302 yield "{!r}: not a not data options and data dict: {}".format(arg_object, e) 303 for selector, validator in self.selectors: 304 if selector is None or selector(arg): 305 for msg in validator.validate(name, object): 306 yield msg 307 return 308 yield "no match for selector argument {!r}".format(arg) 309 310 311# Type definitions 312 313message = {} 314dbdict = {} 315 316# parse and use a ResourceType class's dataFields into a validator 317 318# masters 319 320message['masters'] = Selector() 321message['masters'].add(None, 322 MessageValidator( 323 events=[b'started', b'stopped'], 324 messageValidator=DictValidator( 325 masterid=IntValidator(), 326 name=StringValidator(), 327 active=BooleanValidator(), 328 # last_active is not included 329 ))) 330 331dbdict['masterdict'] = DictValidator( 332 id=IntValidator(), 333 name=StringValidator(), 334 active=BooleanValidator(), 335 last_active=DateTimeValidator(), 336) 337 338# sourcestamp 339 340_sourcestamp = dict( 341 ssid=IntValidator(), 342 branch=NoneOk(StringValidator()), 343 revision=NoneOk(StringValidator()), 344 repository=StringValidator(), 345 project=StringValidator(), 346 codebase=StringValidator(), 347 created_at=DateTimeValidator(), 348 patch=NoneOk(DictValidator( 349 body=NoneOk(BinaryValidator()), 350 level=NoneOk(IntValidator()), 351 subdir=NoneOk(StringValidator()), 352 author=NoneOk(StringValidator()), 353 comment=NoneOk(StringValidator()))), 354) 355 356message['sourcestamps'] = Selector() 357message['sourcestamps'].add(None, 358 DictValidator( 359 **_sourcestamp 360 )) 361 362dbdict['ssdict'] = DictValidator( 363 ssid=IntValidator(), 364 branch=NoneOk(StringValidator()), 365 revision=NoneOk(StringValidator()), 366 patchid=NoneOk(IntValidator()), 367 patch_body=NoneOk(BinaryValidator()), 368 patch_level=NoneOk(IntValidator()), 369 patch_subdir=NoneOk(StringValidator()), 370 patch_author=NoneOk(StringValidator()), 371 patch_comment=NoneOk(StringValidator()), 372 codebase=StringValidator(), 373 repository=StringValidator(), 374 project=StringValidator(), 375 created_at=DateTimeValidator(), 376) 377 378# builder 379 380message['builders'] = Selector() 381message['builders'].add(None, 382 MessageValidator( 383 events=[b'started', b'stopped'], 384 messageValidator=DictValidator( 385 builderid=IntValidator(), 386 masterid=IntValidator(), 387 name=StringValidator(), 388 ))) 389 390dbdict['builderdict'] = DictValidator( 391 id=IntValidator(), 392 masterids=ListValidator(IntValidator()), 393 name=StringValidator(), 394 description=NoneOk(StringValidator()), 395 tags=ListValidator(StringValidator()), 396) 397 398# worker 399 400dbdict['workerdict'] = DictValidator( 401 id=IntValidator(), 402 name=StringValidator(), 403 configured_on=ListValidator( 404 DictValidator( 405 masterid=IntValidator(), 406 builderid=IntValidator(), 407 ) 408 ), 409 paused=BooleanValidator(), 410 graceful=BooleanValidator(), 411 connected_to=ListValidator(IntValidator()), 412 workerinfo=JsonValidator(), 413) 414 415# buildset 416 417_buildset = dict( 418 bsid=IntValidator(), 419 external_idstring=NoneOk(StringValidator()), 420 reason=StringValidator(), 421 submitted_at=IntValidator(), 422 complete=BooleanValidator(), 423 complete_at=NoneOk(IntValidator()), 424 results=NoneOk(IntValidator()), 425 parent_buildid=NoneOk(IntValidator()), 426 parent_relationship=NoneOk(StringValidator()), 427) 428_buildsetEvents = [b'new', b'complete'] 429 430message['buildsets'] = Selector() 431message['buildsets'].add(lambda k: k[-1] == 'new', 432 MessageValidator( 433 events=_buildsetEvents, 434 messageValidator=DictValidator( 435 scheduler=StringValidator(), # only for 'new' 436 sourcestamps=ListValidator( 437 DictValidator( 438 **_sourcestamp 439 )), 440 **_buildset 441 ))) 442message['buildsets'].add(None, 443 MessageValidator( 444 events=_buildsetEvents, 445 messageValidator=DictValidator( 446 sourcestamps=ListValidator( 447 DictValidator( 448 **_sourcestamp 449 )), 450 **_buildset 451 ))) 452 453dbdict['bsdict'] = DictValidator( 454 bsid=IntValidator(), 455 external_idstring=NoneOk(StringValidator()), 456 reason=StringValidator(), 457 sourcestamps=ListValidator(IntValidator()), 458 submitted_at=DateTimeValidator(), 459 complete=BooleanValidator(), 460 complete_at=NoneOk(DateTimeValidator()), 461 results=NoneOk(IntValidator()), 462 parent_buildid=NoneOk(IntValidator()), 463 parent_relationship=NoneOk(StringValidator()), 464) 465 466# buildrequest 467 468message['buildrequests'] = Selector() 469message['buildrequests'].add(None, 470 MessageValidator( 471 events=[b'new', b'claimed', b'unclaimed'], 472 messageValidator=DictValidator( 473 # TODO: probably wrong! 474 brid=IntValidator(), 475 builderid=IntValidator(), 476 bsid=IntValidator(), 477 buildername=StringValidator(), 478 ))) 479 480# change 481 482message['changes'] = Selector() 483message['changes'].add(None, 484 MessageValidator( 485 events=[b'new'], 486 messageValidator=DictValidator( 487 changeid=IntValidator(), 488 parent_changeids=ListValidator(IntValidator()), 489 author=StringValidator(), 490 committer=StringValidator(), 491 files=ListValidator(StringValidator()), 492 comments=StringValidator(), 493 revision=NoneOk(StringValidator()), 494 when_timestamp=IntValidator(), 495 branch=NoneOk(StringValidator()), 496 category=NoneOk(StringValidator()), 497 revlink=NoneOk(StringValidator()), 498 properties=SourcedPropertiesValidator(), 499 repository=StringValidator(), 500 project=StringValidator(), 501 codebase=StringValidator(), 502 sourcestamp=DictValidator( 503 **_sourcestamp 504 ), 505 ))) 506 507dbdict['chdict'] = DictValidator( 508 changeid=IntValidator(), 509 author=StringValidator(), 510 committer=StringValidator(), 511 files=ListValidator(StringValidator()), 512 comments=StringValidator(), 513 revision=NoneOk(StringValidator()), 514 when_timestamp=DateTimeValidator(), 515 branch=NoneOk(StringValidator()), 516 category=NoneOk(StringValidator()), 517 revlink=NoneOk(StringValidator()), 518 properties=SourcedPropertiesValidator(), 519 repository=StringValidator(), 520 project=StringValidator(), 521 codebase=StringValidator(), 522 sourcestampid=IntValidator(), 523 parent_changeids=ListValidator(IntValidator()), 524) 525 526# changesources 527 528dbdict['changesourcedict'] = DictValidator( 529 id=IntValidator(), 530 name=StringValidator(), 531 masterid=NoneOk(IntValidator()), 532) 533 534# schedulers 535 536dbdict['schedulerdict'] = DictValidator( 537 id=IntValidator(), 538 name=StringValidator(), 539 masterid=NoneOk(IntValidator()), 540 enabled=BooleanValidator(), 541) 542 543# builds 544 545_build = dict( 546 buildid=IntValidator(), 547 number=IntValidator(), 548 builderid=IntValidator(), 549 buildrequestid=IntValidator(), 550 workerid=IntValidator(), 551 masterid=IntValidator(), 552 started_at=IntValidator(), 553 complete=BooleanValidator(), 554 complete_at=NoneOk(IntValidator()), 555 state_string=StringValidator(), 556 results=NoneOk(IntValidator()), 557) 558_buildEvents = [b'new', b'complete'] 559 560message['builds'] = Selector() 561message['builds'].add(None, 562 MessageValidator( 563 events=_buildEvents, 564 messageValidator=DictValidator( 565 **_build 566 ))) 567 568# As build's properties are fetched at DATA API level, 569# a distinction shall be made as both are not equal. 570# Validates DB layer 571dbdict['dbbuilddict'] = buildbase = DictValidator( 572 id=IntValidator(), 573 number=IntValidator(), 574 builderid=IntValidator(), 575 buildrequestid=IntValidator(), 576 workerid=IntValidator(), 577 masterid=IntValidator(), 578 started_at=DateTimeValidator(), 579 complete_at=NoneOk(DateTimeValidator()), 580 state_string=StringValidator(), 581 results=NoneOk(IntValidator()), 582) 583 584# Validates DATA API layer 585dbdict['builddict'] = DictValidator( 586 properties=NoneOk(SourcedPropertiesValidator()), **buildbase.keys) 587 588# build data 589 590_build_data_msgdict = DictValidator( 591 buildid=IntValidator(), 592 name=StringValidator(), 593 value=NoneOk(BinaryValidator()), 594 length=IntValidator(), 595 source=StringValidator(), 596) 597 598message['build_data'] = Selector() 599message['build_data'].add(None, 600 MessageValidator(events=[], 601 messageValidator=_build_data_msgdict)) 602 603dbdict['build_datadict'] = DictValidator( 604 buildid=IntValidator(), 605 name=StringValidator(), 606 value=NoneOk(BinaryValidator()), 607 length=IntValidator(), 608 source=StringValidator(), 609) 610 611# steps 612 613_step = dict( 614 stepid=IntValidator(), 615 number=IntValidator(), 616 name=IdentifierValidator(50), 617 buildid=IntValidator(), 618 started_at=IntValidator(), 619 complete=BooleanValidator(), 620 complete_at=NoneOk(IntValidator()), 621 state_string=StringValidator(), 622 results=NoneOk(IntValidator()), 623 urls=ListValidator(StringValidator()), 624 hidden=BooleanValidator(), 625) 626_stepEvents = [b'new', b'complete'] 627 628message['steps'] = Selector() 629message['steps'].add(None, 630 MessageValidator( 631 events=_stepEvents, 632 messageValidator=DictValidator( 633 **_step 634 ))) 635 636dbdict['stepdict'] = DictValidator( 637 id=IntValidator(), 638 number=IntValidator(), 639 name=IdentifierValidator(50), 640 buildid=IntValidator(), 641 started_at=DateTimeValidator(), 642 complete_at=NoneOk(DateTimeValidator()), 643 state_string=StringValidator(), 644 results=NoneOk(IntValidator()), 645 urls=ListValidator(StringValidator()), 646 hidden=BooleanValidator(), 647) 648 649# logs 650 651_log = dict( 652 logid=IntValidator(), 653 name=IdentifierValidator(50), 654 stepid=IntValidator(), 655 complete=BooleanValidator(), 656 num_lines=IntValidator(), 657 type=IdentifierValidator(1)) 658_logEvents = ['new', 'complete', 'appended'] 659 660# message['log'] 661 662dbdict['logdict'] = DictValidator( 663 id=IntValidator(), 664 stepid=IntValidator(), 665 name=StringValidator(), 666 slug=IdentifierValidator(50), 667 complete=BooleanValidator(), 668 num_lines=IntValidator(), 669 type=IdentifierValidator(1)) 670 671# test results sets 672 673_test_result_set_msgdict = DictValidator( 674 builderid=IntValidator(), 675 buildid=IntValidator(), 676 stepid=IntValidator(), 677 description=NoneOk(StringValidator()), 678 category=StringValidator(), 679 value_unit=StringValidator(), 680 tests_passed=NoneOk(IntValidator()), 681 tests_failed=NoneOk(IntValidator()), 682 complete=BooleanValidator() 683) 684 685message['test_result_sets'] = Selector() 686message['test_result_sets'].add(None, 687 MessageValidator(events=[b'new', b'completed'], 688 messageValidator=_test_result_set_msgdict)) 689 690dbdict['test_result_setdict'] = DictValidator( 691 id=IntValidator(), 692 builderid=IntValidator(), 693 buildid=IntValidator(), 694 stepid=IntValidator(), 695 description=NoneOk(StringValidator()), 696 category=StringValidator(), 697 value_unit=StringValidator(), 698 tests_passed=NoneOk(IntValidator()), 699 tests_failed=NoneOk(IntValidator()), 700 complete=BooleanValidator() 701) 702 703# test results 704 705_test_results_msgdict = DictValidator( 706 builderid=IntValidator(), 707 test_result_setid=IntValidator(), 708 test_name=NoneOk(StringValidator()), 709 test_code_path=NoneOk(StringValidator()), 710 line=NoneOk(IntValidator()), 711 duration_ns=NoneOk(IntValidator()), 712 value=StringValidator(), 713) 714 715message['test_results'] = Selector() 716message['test_results'].add(None, 717 MessageValidator(events=[b'new'], 718 messageValidator=_test_results_msgdict)) 719 720dbdict['test_resultdict'] = DictValidator( 721 id=IntValidator(), 722 builderid=IntValidator(), 723 test_result_setid=IntValidator(), 724 test_name=NoneOk(StringValidator()), 725 test_code_path=NoneOk(StringValidator()), 726 line=NoneOk(IntValidator()), 727 duration_ns=NoneOk(IntValidator()), 728 value=StringValidator(), 729) 730 731 732# external functions 733 734def _verify(testcase, validator, name, object): 735 msgs = list(validator.validate(name, object)) 736 if msgs: 737 msg = "; ".join(msgs) 738 if testcase: 739 testcase.fail(msg) 740 else: 741 raise AssertionError(msg) 742 743 744def verifyMessage(testcase, routingKey, message_): 745 # the validator is a Selector wrapping a MessageValidator, so we need to 746 # pass (arg, (routingKey, message)), where the routing key is the arg 747 # the "type" of the message is identified by last path name 748 # -1 being the event, and -2 the id. 749 750 validator = message[bytes2unicode(routingKey[-3])] 751 _verify(testcase, validator, '', 752 (routingKey, (routingKey, message_))) 753 754 755def verifyDbDict(testcase, type, value): 756 _verify(testcase, dbdict[type], type, value) 757 758 759def verifyData(testcase, entityType, options, value): 760 _verify(testcase, entityType, entityType.name, value) 761 762 763def verifyType(testcase, name, value, validator): 764 _verify(testcase, validator, name, value) 765