Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import copy | |
| 5 import inspect | 6 import inspect |
| 6 import os | 7 import os |
| 7 import re | 8 import re |
| 8 | 9 |
| 9 from collections import namedtuple | 10 from collections import namedtuple |
| 10 | 11 |
| 11 # These have to do with deriving classes from namedtuple return values. | 12 # 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 # Pylint can't tell that namedtuple returns a new-style type() object. |
| 13 # | 14 # |
| 14 # "no __init__ method" pylint: disable=W0232 | 15 # "no __init__ method" pylint: disable=W0232 |
| (...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 141 | 142 |
| 142 def __call__(self, *args, **kwargs): | 143 def __call__(self, *args, **kwargs): |
| 143 f = self.bind(args, kwargs) | 144 f = self.bind(args, kwargs) |
| 144 assert f.fully_bound | 145 assert f.fully_bound |
| 145 return f.func(*f.args, **f.kwargs) | 146 return f.func(*f.args, **f.kwargs) |
| 146 | 147 |
| 147 def __repr__(self): | 148 def __repr__(self): |
| 148 return 'FuncCall(%r, *%r, **%r)' % (self.func, self.args, self.kwargs) | 149 return 'FuncCall(%r, *%r, **%r)' % (self.func, self.args, self.kwargs) |
| 149 | 150 |
| 150 | 151 |
| 151 _Test = namedtuple( | 152 class TestInfo(object): |
| 152 'Test', 'name func_call expect_dir expect_base ext covers breakpoints') | 153 TEST_COVERS_MATCH = re.compile('.*/test/([^/]*)_test\.py$') |
|
dnj
2014/09/16 23:13:26
I know this is kind of outside of the immediate sc
| |
| 153 | 154 |
| 154 TestInfo = namedtuple( | 155 def __init__(self, name, expect_dir=None, expect_base=None, |
| 155 'TestInfo', 'name expect_dir expect_base ext') | 156 ext='json', covers=None, breakpoints=None): |
| 157 self._name = name | |
| 158 self._expect_dir = expect_dir | |
| 159 self._expect_base = expect_base | |
| 160 self._ext = ext | |
| 161 self._covers = covers | |
| 162 self._breakpoints = breakpoints | |
| 163 | |
| 164 @property | |
| 165 def name(self): | |
| 166 return self._name | |
| 167 | |
| 168 @property | |
| 169 def expect_dir(self): | |
| 170 return self._expect_dir | |
| 171 | |
| 172 @property | |
| 173 def expect_base(self): | |
| 174 return self._expect_base | |
| 175 | |
| 176 @property | |
| 177 def ext(self): | |
| 178 return self._ext | |
| 179 | |
| 180 @property | |
| 181 def covers(self): | |
| 182 return self._covers | |
| 183 | |
| 184 @property | |
| 185 def breakpoints(self): | |
| 186 return self._breakpoints | |
| 187 | |
| 188 @staticmethod | |
| 189 def covers_obj(obj): | |
| 190 test_file = inspect.getabsfile(obj) | |
| 191 covers = [test_file] | |
| 192 match = Test.TEST_COVERS_MATCH.match(test_file) | |
|
dnj
2014/09/16 23:13:26
Make this a "classmethod" and use "cls.TEST_COVERS
| |
| 193 if match: | |
| 194 covers.append(os.path.join( | |
| 195 os.path.dirname(os.path.dirname(test_file)), | |
| 196 match.group(1) + '.py' | |
| 197 )) | |
| 198 return covers | |
| 199 | |
| 200 @staticmethod | |
| 201 def expect_dir_obj(obj): | |
| 202 test_file = inspect.getabsfile(obj) | |
| 203 return os.path.splitext(test_file)[0] + '.expected' | |
| 204 | |
| 205 def coverage_includes(self): | |
| 206 if self.covers is not None: | |
| 207 return self.covers | |
| 208 return self.covers_obj(self.func_call.func) | |
| 209 | |
| 210 def expect_path(self, ext=None): | |
| 211 expect_dir = self.expect_dir | |
| 212 if expect_dir is None: | |
| 213 expect_dir = self.expect_dir_obj(self.func_call.func) | |
| 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 restrict(self, tests): | |
| 219 assert tests[0] is self | |
| 220 return self | |
| 221 | |
| 222 def get_info(self): | |
| 223 return self | |
| 156 | 224 |
| 157 | 225 |
| 158 class Test(_Test): | 226 class Test(TestInfo): |
| 159 TEST_COVERS_MATCH = re.compile('.*/test/([^/]*)_test\.py$') | 227 def __init__(self, name, func_call, expect_dir=None, expect_base=None, |
| 160 | |
| 161 def __new__(cls, name, func_call, expect_dir=None, expect_base=None, | |
| 162 ext='json', covers=None, breakpoints=None, break_funcs=()): | 228 ext='json', covers=None, breakpoints=None, break_funcs=()): |
| 163 """Create a new test. | 229 """Create a new test. |
| 164 | 230 |
| 165 @param name: The name of the test. Will be used as the default expect_base | 231 @param name: The name of the test. Will be used as the default expect_base |
| 166 | 232 |
| 167 @param func_call: A FuncCall object | 233 @param func_call: A FuncCall object |
| 168 | 234 |
| 169 @param expect_dir: The directory which holds the expectation file for this | 235 @param expect_dir: The directory which holds the expectation file for this |
| 170 Test. | 236 Test. |
| 171 @param expect_base: The basename (without extension) of the expectation | 237 @param expect_base: The basename (without extension) of the expectation |
| 172 file. Defaults to |name|. | 238 file. Defaults to |name|. |
| 173 @param ext: The extension of the expectation file. Affects the serializer | 239 @param ext: The extension of the expectation file. Affects the serializer |
| 174 used to write the expectations to disk. Valid values are | 240 used to write the expectations to disk. Valid values are |
| 175 'json' and 'yaml' (Keys in SERIALIZERS). | 241 'json' and 'yaml' (Keys in SERIALIZERS). |
| 176 @param covers: A list of coverage file patterns to include for this Test. | 242 @param covers: A list of coverage file patterns to include for this Test. |
| 177 By default, a Test covers the file in which its function | 243 By default, a Test covers the file in which its function |
| 178 was defined, as well as the source file matching the test | 244 was defined, as well as the source file matching the test |
| 179 according to TEST_COVERS_MATCH. | 245 according to TEST_COVERS_MATCH. |
| 180 | 246 |
| 181 @param breakpoints: A list of (path, lineno, func_name) tuples. These will | 247 @param breakpoints: A list of (path, lineno, func_name) tuples. These will |
| 182 turn into breakpoints when the tests are run in 'debug' | 248 turn into breakpoints when the tests are run in 'debug' |
| 183 mode. See |break_funcs| for an easier way to set this. | 249 mode. See |break_funcs| for an easier way to set this. |
| 184 @param break_funcs: A list of functions for which to set breakpoints. | 250 @param break_funcs: A list of functions for which to set breakpoints. |
| 185 """ | 251 """ |
| 186 breakpoints = breakpoints or [] | 252 breakpoints = breakpoints or [] |
| 253 self._func_call = func_call | |
| 254 | |
| 187 if not breakpoints or break_funcs: | 255 if not breakpoints or break_funcs: |
| 256 breakpoints = copy.copy(breakpoints) | |
| 188 for f in (break_funcs or (func_call.func,)): | 257 for f in (break_funcs or (func_call.func,)): |
| 189 if hasattr(f, 'im_func'): | 258 if hasattr(f, 'im_func'): |
| 190 f = f.im_func | 259 f = f.im_func |
| 191 breakpoints.append((f.func_code.co_filename, | 260 breakpoints.append((f.func_code.co_filename, |
| 192 f.func_code.co_firstlineno, | 261 f.func_code.co_firstlineno, |
| 193 f.func_code.co_name)) | 262 f.func_code.co_name)) |
| 194 | 263 |
| 195 if expect_dir: | 264 if expect_dir: |
| 196 expect_dir = expect_dir.rstrip('/') | 265 expect_dir = expect_dir.rstrip('/') |
| 197 return super(Test, cls).__new__(cls, name, func_call, expect_dir, | 266 super(Test, self).__init__(name, expect_dir, expect_base, |
| 198 expect_base, ext, covers, breakpoints) | 267 ext, covers, breakpoints) |
| 199 | 268 |
| 200 | 269 @property |
| 201 @staticmethod | 270 def func_call(self): |
| 202 def covers_obj(obj): | 271 return self._func_call |
| 203 test_file = inspect.getabsfile(obj) | |
| 204 covers = [test_file] | |
| 205 match = Test.TEST_COVERS_MATCH.match(test_file) | |
| 206 if match: | |
| 207 covers.append(os.path.join( | |
| 208 os.path.dirname(os.path.dirname(test_file)), | |
| 209 match.group(1) + '.py' | |
| 210 )) | |
| 211 return covers | |
| 212 | |
| 213 @staticmethod | |
| 214 def expect_dir_obj(obj): | |
| 215 test_file = inspect.getabsfile(obj) | |
| 216 return os.path.splitext(test_file)[0] + '.expected' | |
| 217 | |
| 218 def coverage_includes(self): | |
| 219 if self.covers is not None: | |
| 220 return self.covers | |
| 221 return self.covers_obj(self.func_call.func) | |
| 222 | |
| 223 def expect_path(self, ext=None): | |
| 224 expect_dir = self.expect_dir | |
| 225 if expect_dir is None: | |
| 226 expect_dir = self.expect_dir_obj(self.func_call.func) | |
| 227 name = self.expect_base or self.name | |
| 228 name = ''.join('_' if c in '<>:"\\/|?*\0' else c for c in name) | |
| 229 return os.path.join(expect_dir, name + ('.%s' % (ext or self.ext))) | |
| 230 | 272 |
| 231 def run(self, context=None): | 273 def run(self, context=None): |
| 232 return self.func_call(context=context) | 274 return self.func_call(context=context) |
| 233 | 275 |
| 234 def process(self, func=lambda test: test.run()): | 276 def process(self, func=lambda test: test.run()): |
| 235 """Applies |func| to the test, and yields (self, func(self)). | 277 """Applies |func| to the test, and yields (self.get_info(), func(self)). |
| 236 | 278 |
| 237 For duck-typing compatibility with MultiTest. | 279 For duck-typing compatibility with MultiTest. |
| 238 | 280 |
| 239 Bind(name='context') if used by your test function, is bound to None. | 281 Bind(name='context') if used by your test function, is bound to None. |
| 240 | 282 |
| 241 Used interally by expect_tests, you're not expected to call this yourself. | 283 Used internally by expect_tests, you're not expected to call this yourself. |
| 242 """ | 284 """ |
| 243 yield self, func(self.bind(context=None)) | 285 yield self.get_info(), func(self.bind(context=None)) |
| 244 | 286 |
| 245 def bind(self, *args, **kwargs): | 287 def bind(self, *args, **kwargs): |
| 246 return self._replace(func_call=self.func_call.bind(*args, **kwargs)) | 288 return Test(self._name, |
| 247 | 289 self._func_call.bind(*args, **kwargs), |
| 248 def restrict(self, tests): | 290 expect_dir=self.expect_dir, |
| 249 assert tests[0] is self | 291 expect_base=self.expect_base, |
| 250 return self | 292 ext=self._ext, |
| 293 covers=self._covers, | |
| 294 breakpoints=self._breakpoints) | |
| 251 | 295 |
| 252 def get_info(self): | 296 def get_info(self): |
| 253 """Strips test instance of hard-to-pickle stuff | 297 """Strips test instance of information required for running test. |
| 254 | 298 |
| 255 Returns a TestInfo instance. | 299 Returns a TestInfo instance. |
| 256 """ | 300 """ |
| 257 return TestInfo(self.name, self.expect_dir, self.expect_base, self.ext) | 301 return TestInfo(self.name, |
| 302 expect_dir=self._expect_dir, | |
| 303 expect_base=self._expect_base, | |
| 304 ext=self._ext, | |
| 305 covers=self._covers, | |
| 306 breakpoints=self._breakpoints) | |
| 258 | 307 |
| 259 | 308 |
| 260 _MultiTest = namedtuple( | 309 class MultiTestInfo(object): |
| 261 'MultiTest', 'name make_ctx_call destroy_ctx_call tests atomic') | 310 def __init__(self, name, tests, atomic): |
| 311 self._name = name | |
| 312 self._tests = tests | |
| 313 self._atomic = atomic | |
| 262 | 314 |
| 263 MultiTestInfo = namedtuple('MultiTestInfo', 'name tests atomic') | 315 @property |
| 316 def name(self): | |
| 317 return self._name | |
| 318 | |
| 319 @property | |
| 320 def tests(self): | |
| 321 return self._tests | |
| 322 | |
| 323 @property | |
| 324 def atomic(self): | |
| 325 return self._atomic | |
| 326 | |
| 327 @staticmethod | |
| 328 def expect_path(_ext=None): | |
| 329 return None | |
| 330 | |
| 331 def restrict(self, tests): | |
| 332 """A helper method to re-cast the MultiTest with fewer subtests. | |
| 333 | |
| 334 All fields will be identical except for tests. If this MultiTest is atomic, | |
| 335 then this method returns |self|. | |
| 336 | |
| 337 Used interally by expect_tests, you're not expected to call this yourself. | |
| 338 """ | |
| 339 if self.atomic: | |
| 340 return self | |
| 341 assert all(t in self.tests for t in tests) | |
| 342 return MultiTestInfo(self._name, tests, self._atomic) | |
| 343 | |
| 344 def get_info(self): | |
| 345 return self | |
| 264 | 346 |
| 265 | 347 |
| 266 class MultiTest(_MultiTest): | 348 class MultiTest(MultiTestInfo): |
| 267 """A wrapper around one or more Test instances. | 349 """A wrapper around one or more Test instances. |
| 268 | 350 |
| 269 Allows the entire group to have common pre- and post- actions and an optional | 351 Allows the entire group to have common pre- and post- actions and an optional |
| 270 shared context between the Test methods (represented by Bind(name='context')). | 352 shared context between the Test methods (represented by Bind(name='context')). |
| 271 | 353 |
| 272 Args: | 354 Args: |
| 273 name - The name of the MultiTest. Each Test's name should be prefixed with | 355 name - The name of the MultiTest. Each Test's name should be prefixed with |
| 274 this name, though this is not enforced. | 356 this name, though this is not enforced. |
| 275 make_ctx_call - A FuncCall which will be called once before any test in this | 357 make_ctx_call - A FuncCall which will be called once before any test in this |
| 276 MultiTest runs. The return value of this FuncCall will become bound | 358 MultiTest runs. The return value of this FuncCall will become bound |
| 277 to the name 'context' for both the |destroy_ctx_call| as well as every | 359 to the name 'context' for both the |destroy_ctx_call| as well as every |
| 278 test in |tests|. | 360 test in |tests|. |
| 279 destroy_ctx_call - A FuncCall which will be called once after all tests in | 361 destroy_ctx_call - A FuncCall which will be called once after all tests in |
| 280 this MultiTest runs. The context object produced by |make_ctx_call| is | 362 this MultiTest runs. The context object produced by |make_ctx_call| is |
| 281 bound to the name 'context'. | 363 bound to the name 'context'. |
| 282 tests - A list of Test instances. The context object produced by | 364 tests - A list of Test instances. The context object produced by |
| 283 |make_ctx_call| is bound to the name 'context'. | 365 |make_ctx_call| is bound to the name 'context'. |
| 284 atomic - A boolean which indicates that this MultiTest must be executed | 366 atomic - A boolean which indicates that this MultiTest must be executed |
| 285 either all at once, or not at all (i.e., subtests may not be filtered). | 367 either all at once, or not at all (i.e., subtests may not be filtered). |
| 286 """ | 368 """ |
| 287 | 369 |
| 288 def restrict(self, tests): | 370 def __init__(self, name, make_ctx_call, destroy_ctx_call, tests, atomic): |
| 289 """A helper method to re-cast the MultiTest with fewer subtests. | 371 self._make_ctx_call = make_ctx_call |
| 372 self._destroy_ctx_call = destroy_ctx_call | |
| 373 super(MultiTest, self).__init__(name, tests, atomic) | |
| 290 | 374 |
| 291 All fields will be identical except for tests. If this MultiTest is atomic, | 375 @property |
| 292 then this method returns |self|. | 376 def make_ctx_call(self): |
| 377 return self._make_ctx_call | |
| 293 | 378 |
| 294 Used interally by expect_tests, you're not expected to call this yourself. | 379 @property |
| 295 """ | 380 def destroy_ctx_call(self): |
| 296 if self.atomic: | 381 return self._destroy_ctx_call |
| 297 return self | |
| 298 assert all(t in self.tests for t in tests) | |
| 299 return self._replace(tests=tests) | |
| 300 | 382 |
| 301 def process(self, func=lambda test: test.run()): | 383 def process(self, func=lambda test: test.run()): |
| 302 """Applies |func| to each sub-test, with properly bound context. | 384 """Applies |func| to each sub-test, with properly bound context. |
| 303 | 385 |
| 304 make_ctx_call will be called before any test, and its return value becomes | 386 make_ctx_call will be called before any test, and its return value becomes |
| 305 bound to the name 'context'. All sub-tests will be bound with this value | 387 bound to the name 'context'. All sub-tests will be bound with this value |
| 306 as well as destroy_ctx_call, which will be invoked after all tests have | 388 as well as destroy_ctx_call, which will be invoked after all tests have |
| 307 been yielded. | 389 been yielded. |
| 308 | 390 |
| 309 Optionally, you may specify a different function to apply to each test | 391 Optionally, you may specify a different function to apply to each test |
| 310 (by default it is `lambda test: test.run()`). The context will be bound | 392 (by default it is `lambda test: test.run()`). The context will be bound |
| 311 to the test before your function recieves it. | 393 to the test before your function recieves it. |
| 312 | 394 |
| 313 Used interally by expect_tests, you're not expected to call this yourself. | 395 Used interally by expect_tests, you're not expected to call this yourself. |
| 314 """ | 396 """ |
| 315 # TODO(iannucci): pass list of test names? | 397 # TODO(iannucci): pass list of test names? |
| 316 ctx_object = self.make_ctx_call() | 398 ctx_object = self.make_ctx_call() |
| 317 try: | 399 try: |
| 318 for test in self.tests: | 400 for test in self.tests: |
| 319 yield test, func(test.bind(context=ctx_object)) | 401 yield test.get_info(), func(test.bind(context=ctx_object)) |
| 320 finally: | 402 finally: |
| 321 self.destroy_ctx_call.bind(context=ctx_object)() | 403 self.destroy_ctx_call.bind(context=ctx_object)() |
| 322 | 404 |
| 323 @staticmethod | 405 def restrict(self, tests): |
| 324 def expect_path(_ext=None): | 406 """A helper method to re-cast the MultiTest with fewer subtests. |
| 325 return None | 407 |
| 408 All fields will be identical except for tests. If this MultiTest is atomic, | |
| 409 then this method returns |self|. | |
| 410 | |
| 411 Used internally by expect_tests, you're not expected to call this yourself. | |
| 412 """ | |
| 413 if self.atomic: | |
| 414 return self | |
| 415 assert all(t in self.tests for t in tests) | |
| 416 return MultiTest(self._name, self._make_ctx_call, self._destroy_ctx_call, | |
| 417 tests, self._atomic) | |
| 326 | 418 |
| 327 def get_info(self): | 419 def get_info(self): |
| 328 """Strips MultiTest instance of hard-to-pickle stuff | 420 """Strips MultiTest instance of hard-to-pickle stuff |
| 329 | 421 |
| 330 Returns a MultiTestInfo instance. | 422 Returns a MultiTestInfo instance. |
| 331 """ | 423 """ |
| 332 all_tests = [test.get_info() for test in self.tests] | 424 all_tests = [test.get_info() for test in self.tests] |
| 333 test = MultiTestInfo(name=self.name, | 425 test = MultiTestInfo(name=self.name, |
| 334 tests=all_tests, | 426 tests=all_tests, |
| 335 atomic=self.atomic | 427 atomic=self.atomic |
| (...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 451 print 'UNHANDLED:', obj | 543 print 'UNHANDLED:', obj |
| 452 return Failure() | 544 return Failure() |
| 453 | 545 |
| 454 def finalize(self, aborted): | 546 def finalize(self, aborted): |
| 455 """Called after __call__() has been called for all results. | 547 """Called after __call__() has been called for all results. |
| 456 | 548 |
| 457 @param aborted: True if the user aborted the run. | 549 @param aborted: True if the user aborted the run. |
| 458 @type aborted: bool | 550 @type aborted: bool |
| 459 """ | 551 """ |
| 460 pass | 552 pass |
| OLD | NEW |