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

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: Removed *Info classes 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 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
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
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