Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(666)

Side by Side Diff: expect_tests/type_definitions.py

Issue 554213004: Refactored types to simplify pickling. (Closed) Base URL: https://chromium.googlesource.com/infra/testing/expect_tests@shebang
Patch Set: Fixed breakpoints computation Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « expect_tests/handle_list.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 self._breakpoints = copy.copy(breakpoints) if breakpoints else []
199 189
190 if self._func_call:
191 if not self._breakpoints or break_funcs:
192 for f in (break_funcs or (func_call.func,)):
193 if hasattr(f, 'im_func'):
194 f = f.im_func
195 self._breakpoints.append((f.func_code.co_filename,
196 f.func_code.co_firstlineno,
197 f.func_code.co_name))
200 198
201 @staticmethod 199 @property
202 def covers_obj(obj): 200 def name(self):
201 return self._name
202
203 @property
204 def func_call(self):
205 return self._func_call
206
207 @property
208 def expect_dir(self):
209 return self._expect_dir
210
211 @property
212 def expect_base(self):
213 return self._expect_base
214
215 @property
216 def ext(self):
217 return self._ext
218
219 @property
220 def covers(self):
221 return self._covers
222
223 @property
224 def breakpoints(self):
225 return self._breakpoints
226
227 def run(self, context=None):
228 return self.func_call(context=context)
229
230 def process(self, func=lambda test: test.run()):
231 """Applies |func| to the test, and yields (self.get_info(), func(self)).
232
233 For duck-typing compatibility with MultiTest.
234
235 Bind(name='context') if used by your test function, is bound to None.
236
237 Used internally by expect_tests, you're not expected to call this yourself.
238 """
239 yield self.get_info(), func(self.bind(context=None))
240
241 def bind(self, *args, **kwargs):
242 return Test(self._name,
243 self._func_call.bind(*args, **kwargs),
244 expect_dir=self.expect_dir,
245 expect_base=self.expect_base,
246 ext=self._ext,
247 covers=self._covers,
248 breakpoints=self._breakpoints)
249
250 def restrict(self, tests):
251 assert tests[0] is self
252 return self
253
254 def get_info(self):
255 """Strips test instance of information required for running test.
256
257 Returns a TestInfo instance.
258 """
259 return Test(self.name,
260 None,
261 expect_dir=self._expect_dir,
262 expect_base=self._expect_base,
263 ext=self._ext,
264 covers=self._covers,
265 breakpoints=self._breakpoints)
266
267 def coverage_includes(self):
268 if self._covers is not None:
269 return self._covers
270 return self.covers_obj(self._func_call.func)
271
272 @classmethod
273 def covers_obj(cls, obj):
203 test_file = inspect.getabsfile(obj) 274 test_file = inspect.getabsfile(obj)
204 covers = [test_file] 275 covers = [test_file]
205 match = Test.TEST_COVERS_MATCH.match(test_file) 276 match = cls.TEST_COVERS_MATCH.match(test_file)
206 if match: 277 if match:
207 covers.append(os.path.join( 278 covers.append(os.path.join(
208 os.path.dirname(os.path.dirname(test_file)), 279 os.path.dirname(os.path.dirname(test_file)),
209 match.group(1) + '.py' 280 match.group(1) + '.py'
210 )) 281 ))
211 return covers 282 return covers
212 283
284 def expect_path(self, ext=None):
285 expect_dir = self.expect_dir
286 if expect_dir is None:
287 expect_dir = self.expect_dir_obj(self._func_call.func)
288 name = self._expect_base or self.name
289 name = ''.join('_' if c in '<>:"\\/|?*\0' else c for c in name)
290 return os.path.join(expect_dir, name + ('.%s' % (ext or self.ext,)))
291
213 @staticmethod 292 @staticmethod
214 def expect_dir_obj(obj): 293 def expect_dir_obj(obj):
215 test_file = inspect.getabsfile(obj) 294 test_file = inspect.getabsfile(obj)
216 return os.path.splitext(test_file)[0] + '.expected' 295 return os.path.splitext(test_file)[0] + '.expected'
217 296
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 297
223 def expect_path(self, ext=None): 298 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. 299 """A wrapper around one or more Test instances.
268 300
269 Allows the entire group to have common pre- and post- actions and an optional 301 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')). 302 shared context between the Test methods (represented by Bind(name='context')).
271 303
272 Args: 304 Args:
273 name - The name of the MultiTest. Each Test's name should be prefixed with 305 name - The name of the MultiTest. Each Test's name should be prefixed with
274 this name, though this is not enforced. 306 this name, though this is not enforced.
275 make_ctx_call - A FuncCall which will be called once before any test in this 307 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 308 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 309 to the name 'context' for both the |destroy_ctx_call| as well as every
278 test in |tests|. 310 test in |tests|.
279 destroy_ctx_call - A FuncCall which will be called once after all tests in 311 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 312 this MultiTest runs. The context object produced by |make_ctx_call| is
281 bound to the name 'context'. 313 bound to the name 'context'.
282 tests - A list of Test instances. The context object produced by 314 tests - A list of Test instances. The context object produced by
283 |make_ctx_call| is bound to the name 'context'. 315 |make_ctx_call| is bound to the name 'context'.
284 atomic - A boolean which indicates that this MultiTest must be executed 316 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). 317 either all at once, or not at all (i.e., subtests may not be filtered).
286 """ 318 """
287 319
288 def restrict(self, tests): 320 def __init__(self, name, make_ctx_call, destroy_ctx_call, tests, atomic):
289 """A helper method to re-cast the MultiTest with fewer subtests. 321 self._name = name
322 self._make_ctx_call = make_ctx_call
323 self._destroy_ctx_call = destroy_ctx_call
324 self._tests = tests
325 self._atomic = atomic
290 326
291 All fields will be identical except for tests. If this MultiTest is atomic, 327 @property
292 then this method returns |self|. 328 def name(self):
329 return self._name
293 330
294 Used interally by expect_tests, you're not expected to call this yourself. 331 @property
295 """ 332 def make_ctx_call(self):
296 if self.atomic: 333 return self._make_ctx_call
297 return self 334
298 assert all(t in self.tests for t in tests) 335 @property
299 return self._replace(tests=tests) 336 def destroy_ctx_call(self):
337 return self._destroy_ctx_call
338
339 @property
340 def tests(self):
341 return self._tests
342
343 @property
344 def atomic(self):
345 return self._atomic
300 346
301 def process(self, func=lambda test: test.run()): 347 def process(self, func=lambda test: test.run()):
302 """Applies |func| to each sub-test, with properly bound context. 348 """Applies |func| to each sub-test, with properly bound context.
303 349
304 make_ctx_call will be called before any test, and its return value becomes 350 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 351 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 352 as well as destroy_ctx_call, which will be invoked after all tests have
307 been yielded. 353 been yielded.
308 354
309 Optionally, you may specify a different function to apply to each test 355 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 356 (by default it is `lambda test: test.run()`). The context will be bound
311 to the test before your function recieves it. 357 to the test before your function recieves it.
312 358
313 Used interally by expect_tests, you're not expected to call this yourself. 359 Used interally by expect_tests, you're not expected to call this yourself.
314 """ 360 """
315 # TODO(iannucci): pass list of test names? 361 # TODO(iannucci): pass list of test names?
316 ctx_object = self.make_ctx_call() 362 ctx_object = self.make_ctx_call()
317 try: 363 try:
318 for test in self.tests: 364 for test in self.tests:
319 yield test, func(test.bind(context=ctx_object)) 365 yield test.get_info(), func(test.bind(context=ctx_object))
320 finally: 366 finally:
321 self.destroy_ctx_call.bind(context=ctx_object)() 367 self.destroy_ctx_call.bind(context=ctx_object)()
322 368
323 @staticmethod 369 def restrict(self, tests):
324 def expect_path(_ext=None): 370 """A helper method to re-cast the MultiTest with fewer subtests.
325 return None 371
372 All fields will be identical except for tests. If this MultiTest is atomic,
373 then this method returns |self|.
374
375 Used internally by expect_tests, you're not expected to call this yourself.
376 """
377 if self.atomic:
378 return self
379 assert all(t in self.tests for t in tests)
380 return MultiTest(self._name, self._make_ctx_call, self._destroy_ctx_call,
381 tests, self._atomic)
326 382
327 def get_info(self): 383 def get_info(self):
328 """Strips MultiTest instance of hard-to-pickle stuff 384 """Strips MultiTest instance of hard-to-pickle stuff
329 385
330 Returns a MultiTestInfo instance. 386 Returns a MultiTestInfo instance.
331 """ 387 """
332 all_tests = [test.get_info() for test in self.tests] 388 all_tests = [test.get_info() for test in self.tests]
333 test = MultiTestInfo(name=self.name, 389 test = MultiTest(self._name, None, None, all_tests, self._atomic)
334 tests=all_tests,
335 atomic=self.atomic
336 )
337 return test 390 return test
338 391
392 @staticmethod
393 def expect_path(_ext=None):
394 return None
395
396
339 397
340 class Handler(object): 398 class Handler(object):
341 """Handler object. 399 """Handler object.
342 400
343 Defines 3 handler methods for each stage of the test pipeline. The pipeline 401 Defines 3 handler methods for each stage of the test pipeline. The pipeline
344 looks like: 402 looks like:
345 403
346 -> -> 404 -> ->
347 -> jobs -> (main) 405 -> jobs -> (main)
348 GenStage -> test_queue -> * -> result_queue -> ResultStage 406 GenStage -> test_queue -> * -> result_queue -> ResultStage
(...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after
451 print 'UNHANDLED:', obj 509 print 'UNHANDLED:', obj
452 return Failure() 510 return Failure()
453 511
454 def finalize(self, aborted): 512 def finalize(self, aborted):
455 """Called after __call__() has been called for all results. 513 """Called after __call__() has been called for all results.
456 514
457 @param aborted: True if the user aborted the run. 515 @param aborted: True if the user aborted the run.
458 @type aborted: bool 516 @type aborted: bool
459 """ 517 """
460 pass 518 pass
OLDNEW
« no previous file with comments | « expect_tests/handle_list.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698