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