Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 # that can be found in the LICENSE file. | |
| 4 | |
| 5 """This file contains post process filters for use with the | |
| 6 RecipeTestApi.post_process method in GenTests. | |
| 7 """ | |
| 8 | |
| 9 import re | |
| 10 | |
| 11 from collections import defaultdict, OrderedDict, namedtuple | |
| 12 | |
| 13 | |
| 14 class Filter(object): | |
| 15 """Filter is the interface of the object returned by NewFilter.""" | |
| 16 | |
| 17 def include(self, step_name, *fields): | |
| 18 """Include adds a step to the included steps set. | |
| 19 | |
| 20 Additionally, if any specified fields are provided, they will be the total | |
| 21 set of fields in the filtered step. The 'name' field is always included. If | |
| 22 fields is omitted, the entire step will be included. | |
| 23 | |
| 24 Args: | |
| 25 step_name (str) - The name of the step to include | |
| 26 fields (str) - The field(s) to include. Omit to include all fields. | |
| 27 | |
| 28 Returns the new filter. | |
| 29 """ | |
| 30 raise NotImplementedError() | |
| 31 | |
| 32 def include_re(self, step_name_re, at_least=1, at_most=None, *fields): | |
| 33 """This includes all steps which match the given regular expression. | |
| 34 | |
| 35 If a step matches both an include() directive as well as include_re(), the | |
| 36 include() directive will take precedence. | |
| 37 | |
| 38 Args: | |
| 39 step_name_re (str or regex) - the regular expression of step names to | |
| 40 match. | |
| 41 at_least (int) - the number of steps that this regular expression MUST | |
| 42 match. | |
| 43 at_most (int) - the maximum number of steps that this regular expression | |
| 44 MUST NOT exceed. | |
| 45 fields (str) - the field(s) to include in the matched steps. Omit to | |
| 46 include all fields. | |
| 47 | |
| 48 Returns the new filter. | |
| 49 """ | |
| 50 raise NotImplementedError() | |
| 51 | |
| 52 | |
| 53 class _filterImpl(Filter): | |
| 54 _reEntry = namedtuple('_reEntry', 'at_most at_least fields') | |
| 55 | |
| 56 def __init__(self, data, re_data): | |
| 57 self.data = data # {step_name: frozenset(fields)} | |
| 58 self.re_data = re_data # {regex: _reEntry} | |
| 59 | |
| 60 def __call__(self, check, step_odict): | |
| 61 includes = self.data.copy() | |
| 62 re_data = self.re_data.copy() | |
| 63 | |
| 64 re_usage_count = defaultdict(int) | |
| 65 | |
| 66 to_ret = OrderedDict() | |
| 67 for name, step in step_odict.iteritems(): | |
| 68 field_set = includes.pop(name, None) | |
| 69 if field_set is None: | |
| 70 for exp, (_, _, fset) in re_data.iteritems(): | |
| 71 if exp.match(name): | |
| 72 re_usage_count[exp] += 1 | |
| 73 field_set = fset | |
| 74 break | |
| 75 if field_set is None: | |
| 76 continue | |
| 77 if len(field_set) == 0: | |
| 78 to_ret[name] = step | |
| 79 else: | |
| 80 to_ret[name] = { | |
| 81 k: v for k, v in step.iteritems() | |
| 82 if k in field_set or k == 'name' | |
| 83 } | |
| 84 | |
| 85 check('all includes were used', len(includes) == 0) | |
| 86 | |
| 87 for regex, (at_least, at_most, _) in re_data.iteritems(): | |
| 88 check(re_usage_count[regex] >= at_least) | |
| 89 if at_most is not None: | |
| 90 check(re_usage_count[regex] <= at_most) | |
| 91 | |
| 92 return to_ret | |
| 93 | |
| 94 def include(self, step_name, *fields): | |
| 95 new_data = self.data.copy() | |
|
Michael Achenbach
2016/10/07 16:51:30
Why not mutate self.data as well (by replacing it
Michael Achenbach
2016/10/07 16:54:40
More self thinking:
Agreed that if we mutate we co
| |
| 96 new_data[step_name] = frozenset(fields) | |
| 97 return _filterImpl(new_data, self.re_data) | |
| 98 | |
| 99 def include_re(self, step_name_re, at_least=1, at_most=None, *fields): | |
| 100 new_re_data = self.re_data.copy() | |
| 101 new_re_data[re.compile(step_name_re)] = _filterImpl._reEntry( | |
| 102 (at_least, at_most, frozenset(fields))) | |
| 103 return _filterImpl(self.data, new_re_data) | |
| 104 | |
| 105 | |
| 106 def NewFilter(*steps): | |
| 107 """NewFilter returns a new Filter object. It may be optionally prepopulated by | |
| 108 specifying steps. | |
| 109 | |
| 110 Usage: | |
| 111 f = NewFilter('step_a', 'step_b') | |
| 112 yield TEST + api.post_process(f) | |
| 113 | |
| 114 f = f.include('other_step') | |
| 115 yield TEST + api.post_process(f) | |
| 116 | |
| 117 yield TEST + api.post_process(NewFilter, 'step_a', 'step_b', 'other_step') | |
| 118 """ | |
| 119 return _filterImpl({name: () for name in steps}, {}) | |
| 120 | |
| 121 | |
| 122 def DoesNotRun(check, step_odict, *steps): | |
| 123 """Asserts that the given steps to not run. | |
| 124 | |
| 125 Usage: | |
| 126 yield TEST + api.post_process(DoesNotRun, 'step_a', 'step_b') | |
| 127 | |
| 128 """ | |
| 129 banSet = set(steps) | |
| 130 for step_name in step_odict: | |
| 131 check(step_name not in banSet) | |
| 132 | |
| 133 | |
| 134 def DoesNotRunRE(check, step_odict, *step_regexes): | |
| 135 """Asserts that no steps matching any of the regexes have run. | |
| 136 | |
| 137 Args: | |
| 138 step_regexes (str) - The step name regexes to ban. | |
| 139 | |
| 140 Usage: | |
| 141 yield TEST + api.post_process(DoesNotRunRE, '.*with_patch.*', '.*compile.*') | |
| 142 | |
| 143 """ | |
| 144 step_regexes = [re.compile(r) for r in step_regexes] | |
| 145 for step_name in step_odict: | |
| 146 for r in step_regexes: | |
| 147 check(not r.match(step_name)) | |
|
Michael Achenbach
2016/10/07 16:56:41
nit: Should these canned checks not get some more
| |
| 148 | |
| 149 | |
| 150 def MustRun(check, step_odict, *steps): | |
| 151 """Asserts that steps with the given names are in the expectations. | |
| 152 | |
| 153 Args: | |
| 154 steps (str) - The steps that must have run. | |
| 155 | |
| 156 Usage: | |
| 157 yield TEST + api.post_process(MustRun, 'step_a', 'step_b') | |
| 158 """ | |
| 159 for step_name in steps: | |
| 160 check(step_name in step_odict) | |
| 161 | |
| 162 | |
| 163 def MustRunRE(check, step_odict, step_regex, at_least=1, at_most=None): | |
| 164 """Assert that steps matching the given regex completely are in the | |
| 165 exepectations. | |
| 166 | |
| 167 Args: | |
| 168 step_regex (str, compiled regex) - The regular expression to match. | |
| 169 at_least (int) - Match at least this many steps. Matching fewer than this | |
| 170 is a CHECK failure. | |
| 171 at_most (int) - Optional upper bound on the number of matches. Matching | |
| 172 more than this is a CHECK failure. | |
| 173 | |
| 174 Usage: | |
| 175 yield TEST + api.post_process(MustRunRE, r'.*with_patch.*', at_most=2) | |
| 176 """ | |
| 177 step_regex = re.compile(step_regex) | |
| 178 matches = 0 | |
| 179 for step_name in step_odict: | |
| 180 if step_regex.match(step_name): | |
| 181 matches += 1 | |
| 182 check(matches >= at_least) | |
| 183 if at_most is not None: | |
| 184 check(matches <= at_most) | |
| 185 | |
| 186 | |
| 187 def DropExpectation(_check, _step_odict): | |
| 188 """Using this post-process hook will drop the expectations for this test | |
| 189 completely. | |
| 190 | |
| 191 Usage: | |
| 192 yield TEST + api.post_process(DropExpectation) | |
| 193 | |
| 194 """ | |
| 195 return {} | |
| OLD | NEW |