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