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