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

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: 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 | « no previous file | 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 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
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
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698