OLD | NEW |
(Empty) | |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import inspect |
| 6 import os |
| 7 import re |
| 8 |
| 9 from collections import namedtuple |
| 10 |
| 11 # These have to do with deriving classes from namedtuple return values. |
| 12 # Pylint can't tell that namedtuple returns a new-style type() object. |
| 13 # |
| 14 # "no __init__ method" pylint: disable=W0232 |
| 15 # "use of super on an old style class" pylint: disable=E1002 |
| 16 |
| 17 UnknownError = namedtuple('UnknownError', 'message') |
| 18 NoMatchingTestsError = namedtuple('NoMatchingTestsError', '') |
| 19 Result = namedtuple('Result', 'data') |
| 20 MultiResult = namedtuple('MultiResult', 'results') |
| 21 |
| 22 class ResultStageAbort(Exception): |
| 23 pass |
| 24 |
| 25 |
| 26 class Failure(object): |
| 27 pass |
| 28 |
| 29 |
| 30 class TestError(namedtuple('TestError', 'test message log_lines')): |
| 31 def __new__(cls, test, message, log_lines=()): |
| 32 return super(TestError, cls).__new__(cls, test, message, log_lines) |
| 33 |
| 34 |
| 35 class Bind(namedtuple('_Bind', 'loc name')): |
| 36 """A placeholder argument for a FuncCall. |
| 37 |
| 38 A Bind instance either indicates a 0-based index into the args argument, |
| 39 or a name in kwargs when calling .bind(). |
| 40 """ |
| 41 |
| 42 def __new__(cls, loc=None, name=None): |
| 43 """Either loc or name must be defined.""" |
| 44 assert ((loc is None and isinstance(name, str)) or |
| 45 (name is None and 0 <= loc)) |
| 46 return super(Bind, cls).__new__(cls, loc, name) |
| 47 |
| 48 def bind(self, args=(), kwargs=None): |
| 49 """Return the appropriate value for this Bind when binding against args and |
| 50 kwargs. |
| 51 |
| 52 >>> b = Bind(2) |
| 53 >>> # A bind will return itself if a matching arg value isn't present |
| 54 >>> b.bind(['cat'], {'arg': 100}) is b |
| 55 True |
| 56 >>> # Otherwise the matching value is returned |
| 57 >>> v = 'money' |
| 58 >>> b.bind(['happy', 'cool', v]) is v |
| 59 True |
| 60 >>> b2 = Bind(name='cat') |
| 61 >>> b2.bind((), {'cat': 'cool'}) |
| 62 'cool' |
| 63 """ |
| 64 kwargs = kwargs or {} |
| 65 if self.loc is not None: |
| 66 v = args[self.loc:self.loc+1] |
| 67 return self if not v else v[0] |
| 68 else: |
| 69 return kwargs.get(self.name, self) |
| 70 |
| 71 @staticmethod |
| 72 def maybe_bind(value, args, kwargs): |
| 73 """Helper which binds value with (args, kwargs) if value is a Bind.""" |
| 74 return value.bind(args, kwargs) if isinstance(value, Bind) else value |
| 75 |
| 76 |
| 77 class FuncCall(object): |
| 78 def __init__(self, func, *args, **kwargs): |
| 79 """FuncCall is a trivial single-function closure which is pickleable. |
| 80 |
| 81 This assumes that func, args and kwargs are all pickleable. |
| 82 |
| 83 When constructing the FuncCall, you may also set any positional or named |
| 84 argument to a Bind instance. A FuncCall can then be bound with the |
| 85 .bind(*args, **kwargs) method, and finally called by invoking func_call(). |
| 86 |
| 87 A FuncCall may also be directly invoked with func_call(*args, **kwargs), |
| 88 which is equivalent to func_call.bind(*args, **kwargs)(). |
| 89 |
| 90 Invoking a FuncCall with an unbound Bind instance is an error. |
| 91 |
| 92 >>> def func(alpha, beta=None, gamma=None): |
| 93 ... return '%s-%s-%s' % (alpha, beta, gamma) |
| 94 >>> f = FuncCall(func, Bind(2), beta=Bind(name='context'), gamma=Bind(2)) |
| 95 >>> # the first arg and the named arg 'gamma' are bound to index 2 of args. |
| 96 >>> # the named arg 'beta' is bound to the named kwarg 'context'. |
| 97 >>> # |
| 98 >>> # The FuncCall is equivalent to (py3 pattern syntax): |
| 99 >>> # UNSET = object() |
| 100 >>> # def f(_, _, arg1, *_, context=UNSET, **_): |
| 101 >>> # assert pickle is not UNSET |
| 102 >>> # return func(arg1, beta=context, gamma=arg1) |
| 103 >>> bound = f.bind('foo', 'bar', 'baz', context=100, extra=None) |
| 104 >>> # At this point, bound is a FuncCall with no Bind arguments, and can be |
| 105 >>> # invoked. This would be equivalent to: |
| 106 >>> # func('baz', beta=100, gamma='baz') |
| 107 >>> bound() |
| 108 baz-100-baz |
| 109 |
| 110 Unused arguments in the .bind() call are ignored, which allows you to build |
| 111 value-agnostic invocations to FuncCall.bind(). |
| 112 """ |
| 113 self._func = func |
| 114 self._args = args |
| 115 self._kwargs = kwargs |
| 116 self._fully_bound = None |
| 117 |
| 118 # "access to a protected member" pylint: disable=W0212 |
| 119 func = property(lambda self: self._func) |
| 120 args = property(lambda self: self._args) |
| 121 kwargs = property(lambda self: self._kwargs) |
| 122 |
| 123 @property |
| 124 def fully_bound(self): |
| 125 if self._fully_bound is None: |
| 126 self._fully_bound = not ( |
| 127 any(isinstance(v, Bind) for v in self._args) or |
| 128 any(isinstance(v, Bind) for v in self._kwargs.itervalues()) |
| 129 ) |
| 130 return self._fully_bound |
| 131 |
| 132 def bind(self, *args, **kwargs): |
| 133 if self.fully_bound or not (args or kwargs): |
| 134 return self |
| 135 |
| 136 new = FuncCall(self._func) |
| 137 new._args = [Bind.maybe_bind(a, args, kwargs) for a in self.args] |
| 138 new._kwargs = {k: Bind.maybe_bind(v, args, kwargs) |
| 139 for k, v in self.kwargs.iteritems()} |
| 140 return new |
| 141 |
| 142 def __call__(self, *args, **kwargs): |
| 143 f = self.bind(args, kwargs) |
| 144 assert f.fully_bound |
| 145 return f.func(*f.args, **f.kwargs) |
| 146 |
| 147 def __repr__(self): |
| 148 return 'FuncCall(%r, *%r, **%r)' % (self.func, self.args, self.kwargs) |
| 149 |
| 150 |
| 151 _Test = namedtuple( |
| 152 'Test', 'name func_call expect_dir expect_base ext covers breakpoints') |
| 153 |
| 154 class Test(_Test): |
| 155 TEST_COVERS_MATCH = re.compile('.*/test/([^/]*)_test\.py$') |
| 156 |
| 157 def __new__(cls, name, func_call, expect_dir=None, expect_base=None, |
| 158 ext='json', covers=None, breakpoints=None, break_funcs=()): |
| 159 """Create a new test. |
| 160 |
| 161 @param name: The name of the test. Will be used as the default expect_base |
| 162 |
| 163 @param func_call: A FuncCall object |
| 164 |
| 165 @param expect_dir: The directory which holds the expectation file for this |
| 166 Test. |
| 167 @param expect_base: The basename (without extension) of the expectation |
| 168 file. Defaults to |name|. |
| 169 @param ext: The extension of the expectation file. Affects the serializer |
| 170 used to write the expectations to disk. Valid values are |
| 171 'json' and 'yaml' (Keys in SERIALIZERS). |
| 172 @param covers: A list of coverage file patterns to include for this Test. |
| 173 By default, a Test covers the file in which its function |
| 174 was defined, as well as the source file matching the test |
| 175 according to TEST_COVERS_MATCH. |
| 176 |
| 177 @param breakpoints: A list of (path, lineno, func_name) tuples. These will |
| 178 turn into breakpoints when the tests are run in 'debug' |
| 179 mode. See |break_funcs| for an easier way to set this. |
| 180 @param break_funcs: A list of functions for which to set breakpoints. |
| 181 """ |
| 182 breakpoints = breakpoints or [] |
| 183 if not breakpoints or break_funcs: |
| 184 for f in (break_funcs or (func_call.func,)): |
| 185 if hasattr(f, 'im_func'): |
| 186 f = f.im_func |
| 187 breakpoints.append((f.func_code.co_filename, |
| 188 f.func_code.co_firstlineno, |
| 189 f.func_code.co_name)) |
| 190 |
| 191 return super(Test, cls).__new__(cls, name, func_call, expect_dir, |
| 192 expect_base, ext, covers, breakpoints) |
| 193 |
| 194 def coverage_includes(self): |
| 195 if self.covers is not None: |
| 196 return self.covers |
| 197 |
| 198 test_file = inspect.getabsfile(self.func_call.func) |
| 199 covers = [test_file] |
| 200 match = Test.TEST_COVERS_MATCH.match(test_file) |
| 201 if match: |
| 202 covers.append(os.path.join( |
| 203 os.path.dirname(os.path.dirname(test_file)), |
| 204 match.group(1) + '.py' |
| 205 )) |
| 206 |
| 207 return covers |
| 208 |
| 209 def expect_path(self, ext=None): |
| 210 expect_dir = self.expect_dir |
| 211 if expect_dir is None: |
| 212 test_file = inspect.getabsfile(self.func_call.func) |
| 213 expect_dir = os.path.splitext(test_file)[0] + '.expected' |
| 214 name = self.expect_base or self.name |
| 215 name = ''.join('_' if c in '<>:"\\/|?*\0' else c for c in name) |
| 216 return os.path.join(expect_dir, name + ('.%s' % (ext or self.ext))) |
| 217 |
| 218 def run(self, context=None): |
| 219 return self.func_call(context=context) |
| 220 |
| 221 def process(self, func=lambda test: test.run()): |
| 222 """Applies |func| to the test, and yields (self, func(self)). |
| 223 |
| 224 For duck-typing compatibility with MultiTest. |
| 225 |
| 226 Bind(name='context') if used by your test function, is bound to None. |
| 227 |
| 228 Used interally by expect_tests, you're not expected to call this yourself. |
| 229 """ |
| 230 yield self, func(self.bind(context=None)) |
| 231 |
| 232 def bind(self, *args, **kwargs): |
| 233 return self._replace(func_call=self.func_call.bind(*args, **kwargs)) |
| 234 |
| 235 def restrict(self, tests): |
| 236 assert tests[0] is self |
| 237 return self |
| 238 |
| 239 |
| 240 _MultiTest = namedtuple( |
| 241 'MultiTest', 'name make_ctx_call destroy_ctx_call tests atomic') |
| 242 |
| 243 class MultiTest(_MultiTest): |
| 244 """A wrapper around one or more Test instances. |
| 245 |
| 246 Allows the entire group to have common pre- and post- actions and an optional |
| 247 shared context between the Test methods (represented by Bind(name='context')). |
| 248 |
| 249 Args: |
| 250 name - The name of the MultiTest. Each Test's name should be prefixed with |
| 251 this name, though this is not enforced. |
| 252 make_ctx_call - A FuncCall which will be called once before any test in this |
| 253 MultiTest runs. The return value of this FuncCall will become bound |
| 254 to the name 'context' for both the |destroy_ctx_call| as well as every |
| 255 test in |tests|. |
| 256 destroy_ctx_call - A FuncCall which will be called once after all tests in |
| 257 this MultiTest runs. The context object produced by |make_ctx_call| is |
| 258 bound to the name 'context'. |
| 259 tests - A list of Test instances. The context object produced by |
| 260 |make_ctx_call| is bound to the name 'context'. |
| 261 atomic - A boolean which indicates that this MultiTest must be executed |
| 262 either all at once, or not at all (i.e., subtests may not be filtered). |
| 263 """ |
| 264 |
| 265 def restrict(self, tests): |
| 266 """A helper method to re-cast the MultiTest with fewer subtests. |
| 267 |
| 268 All fields will be identical except for tests. If this MultiTest is atomic, |
| 269 then this method returns |self|. |
| 270 |
| 271 Used interally by expect_tests, you're not expected to call this yourself. |
| 272 """ |
| 273 if self.atomic: |
| 274 return self |
| 275 assert all(t in self.tests for t in tests) |
| 276 return self._replace(tests=tests) |
| 277 |
| 278 def process(self, func=lambda test: test.run()): |
| 279 """Applies |func| to each sub-test, with properly bound context. |
| 280 |
| 281 make_ctx_call will be called before any test, and its return value becomes |
| 282 bound to the name 'context'. All sub-tests will be bound with this value |
| 283 as well as destroy_ctx_call, which will be invoked after all tests have |
| 284 been yielded. |
| 285 |
| 286 Optionally, you may specify a different function to apply to each test |
| 287 (by default it is `lambda test: test.run()`). The context will be bound |
| 288 to the test before your function recieves it. |
| 289 |
| 290 Used interally by expect_tests, you're not expected to call this yourself. |
| 291 """ |
| 292 # TODO(iannucci): pass list of test names? |
| 293 ctx_object = self.make_ctx_call() |
| 294 try: |
| 295 for test in self.tests: |
| 296 yield test, func(test.bind(context=ctx_object)) |
| 297 finally: |
| 298 self.destroy_ctx_call.bind(context=ctx_object)() |
| 299 |
| 300 @staticmethod |
| 301 def expect_path(_ext=None): |
| 302 return None |
| 303 |
| 304 |
| 305 class Handler(object): |
| 306 """Handler object. |
| 307 |
| 308 Defines 3 handler methods for each stage of the test pipeline. The pipeline |
| 309 looks like: |
| 310 |
| 311 -> -> |
| 312 -> jobs -> (main) |
| 313 GenStage -> test_queue -> * -> result_queue -> ResultStage |
| 314 -> RunStage -> |
| 315 -> -> |
| 316 |
| 317 Each process will have an instance of one of the nested handler classes, which |
| 318 will be called on each test / result. |
| 319 |
| 320 You can skip the RunStage phase by setting SKIP_RUNLOOP to True on your |
| 321 implementation class. |
| 322 |
| 323 Tips: |
| 324 * Only do printing in ResultStage, since it's running on the main process. |
| 325 """ |
| 326 SKIP_RUNLOOP = False |
| 327 |
| 328 @classmethod |
| 329 def add_options(cls, parser): |
| 330 """ |
| 331 @type parser: argparse.ArgumentParser() |
| 332 """ |
| 333 pass |
| 334 |
| 335 @classmethod |
| 336 def gen_stage_loop(cls, _opts, tests, put_next_stage, _put_result_stage): |
| 337 """Called in the GenStage portion of the pipeline. |
| 338 |
| 339 @param opts: Parsed CLI options |
| 340 @param tests: |
| 341 Iteraterable of type_definitions.Test or type_definitions.MultiTest |
| 342 objects. |
| 343 @param put_next_stage: |
| 344 Function to push an object to the next stage of the pipeline (RunStage). |
| 345 Note that you should push the item you got from |tests|, not the |
| 346 subtests, in the case that the item is a MultiTest. |
| 347 @param put_result_stage: |
| 348 Function to push an object to the result stage of the pipeline. |
| 349 """ |
| 350 for test in tests: |
| 351 put_next_stage(test) |
| 352 |
| 353 @classmethod |
| 354 def run_stage_loop(cls, _opts, tests_results, put_next_stage): |
| 355 """Called in the RunStage portion of the pipeline. |
| 356 |
| 357 @param opts: Parsed CLI options |
| 358 @param tests_results: Iteraterable of (type_definitions.Test, |
| 359 type_definitions.Result) objects |
| 360 @param put_next_stage: Function to push an object to the next stage of the |
| 361 pipeline (ResultStage). |
| 362 """ |
| 363 for _, result in tests_results: |
| 364 put_next_stage(result) |
| 365 |
| 366 @classmethod |
| 367 def result_stage_loop(cls, opts, objects): |
| 368 """Called in the ResultStage portion of the pipeline. |
| 369 |
| 370 Consider subclassing ResultStageHandler instead as it provides a more |
| 371 flexible interface for dealing with |objects|. |
| 372 |
| 373 @param opts: Parsed CLI options |
| 374 @param objects: Iteraterable of objects from GenStage and RunStage. |
| 375 """ |
| 376 error = False |
| 377 aborted = False |
| 378 handler = cls.ResultStageHandler(opts) |
| 379 try: |
| 380 for obj in objects: |
| 381 error |= isinstance(handler(obj), Failure) |
| 382 except ResultStageAbort: |
| 383 aborted = True |
| 384 handler.finalize(aborted) |
| 385 return error |
| 386 |
| 387 class ResultStageHandler(object): |
| 388 """SAX-like event handler dispatches to self.handle_{type(obj).__name__} |
| 389 |
| 390 So if |obj| is a Test, this would call self.handle_Test(obj). |
| 391 |
| 392 self.__unknown is called to handle objects which have no defined handler. |
| 393 |
| 394 self.finalize is called after all objects are processed. |
| 395 """ |
| 396 def __init__(self, opts): |
| 397 self.opts = opts |
| 398 |
| 399 def __call__(self, obj): |
| 400 """Called to handle each object in the ResultStage |
| 401 |
| 402 @type obj: Anything passed to put_result in GenStage or RunStage. |
| 403 |
| 404 @return: If the handler method returns Failure(), then it will |
| 405 cause the entire test run to ultimately return an error code. |
| 406 """ |
| 407 return getattr(self, 'handle_' + type(obj).__name__, self.__unknown)(obj) |
| 408 |
| 409 def handle_NoMatchingTestsError(self, _error): |
| 410 print 'No tests found that match the glob: %s' % ( |
| 411 ' '.join(self.opts.test_glob),) |
| 412 return Failure() |
| 413 |
| 414 def __unknown(self, obj): |
| 415 if self.opts.verbose: |
| 416 print 'UNHANDLED:', obj |
| 417 return Failure() |
| 418 |
| 419 def finalize(self, aborted): |
| 420 """Called after __call__() has been called for all results. |
| 421 |
| 422 @param aborted: True if the user aborted the run. |
| 423 @type aborted: bool |
| 424 """ |
| 425 pass |
OLD | NEW |