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

Side by Side Diff: expect_tests/type_definitions.py

Issue 412773002: Copy expect_tests from build (Closed) Base URL: https://chromium.googlesource.com/infra/testing/expect_tests@master
Patch Set: Fancy ast walker Created 6 years, 5 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/serialize.py ('k') | expect_tests/unittest_helper.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 # found in the LICENSE file.
4
5 import inspect
6 import os
7 import re
8
9 from collections import namedtuple
10
11 # 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 #
14 # "no __init__ method" pylint: disable=W0232
15 # "use of super on an old style class" pylint: disable=E1002
16
17 UnknownError = namedtuple('UnknownError', 'message')
18 NoMatchingTestsError = namedtuple('NoMatchingTestsError', '')
19 Result = namedtuple('Result', 'data')
20 MultiResult = namedtuple('MultiResult', 'results')
21
22 class ResultStageAbort(Exception):
23 pass
24
25
26 class Failure(object):
27 pass
28
29
30 class TestError(namedtuple('TestError', 'test message log_lines')):
31 def __new__(cls, test, message, log_lines=()):
32 return super(TestError, cls).__new__(cls, test, message, log_lines)
33
34
35 class Bind(namedtuple('_Bind', 'loc name')):
36 """A placeholder argument for a FuncCall.
37
38 A Bind instance either indicates a 0-based index into the args argument,
39 or a name in kwargs when calling .bind().
40 """
41
42 def __new__(cls, loc=None, name=None):
43 """Either loc or name must be defined."""
44 assert ((loc is None and isinstance(name, str)) or
45 (name is None and 0 <= loc))
46 return super(Bind, cls).__new__(cls, loc, name)
47
48 def bind(self, args=(), kwargs=None):
49 """Return the appropriate value for this Bind when binding against args and
50 kwargs.
51
52 >>> b = Bind(2)
53 >>> # A bind will return itself if a matching arg value isn't present
54 >>> b.bind(['cat'], {'arg': 100}) is b
55 True
56 >>> # Otherwise the matching value is returned
57 >>> v = 'money'
58 >>> b.bind(['happy', 'cool', v]) is v
59 True
60 >>> b2 = Bind(name='cat')
61 >>> b2.bind((), {'cat': 'cool'})
62 'cool'
63 """
64 kwargs = kwargs or {}
65 if self.loc is not None:
66 v = args[self.loc:self.loc+1]
67 return self if not v else v[0]
68 else:
69 return kwargs.get(self.name, self)
70
71 @staticmethod
72 def maybe_bind(value, args, kwargs):
73 """Helper which binds value with (args, kwargs) if value is a Bind."""
74 return value.bind(args, kwargs) if isinstance(value, Bind) else value
75
76
77 class FuncCall(object):
78 def __init__(self, func, *args, **kwargs):
79 """FuncCall is a trivial single-function closure which is pickleable.
80
81 This assumes that func, args and kwargs are all pickleable.
82
83 When constructing the FuncCall, you may also set any positional or named
84 argument to a Bind instance. A FuncCall can then be bound with the
85 .bind(*args, **kwargs) method, and finally called by invoking func_call().
86
87 A FuncCall may also be directly invoked with func_call(*args, **kwargs),
88 which is equivalent to func_call.bind(*args, **kwargs)().
89
90 Invoking a FuncCall with an unbound Bind instance is an error.
91
92 >>> def func(alpha, beta=None, gamma=None):
93 ... return '%s-%s-%s' % (alpha, beta, gamma)
94 >>> f = FuncCall(func, Bind(2), beta=Bind(name='context'), gamma=Bind(2))
95 >>> # the first arg and the named arg 'gamma' are bound to index 2 of args.
96 >>> # the named arg 'beta' is bound to the named kwarg 'context'.
97 >>> #
98 >>> # The FuncCall is equivalent to (py3 pattern syntax):
99 >>> # UNSET = object()
100 >>> # def f(_, _, arg1, *_, context=UNSET, **_):
101 >>> # assert pickle is not UNSET
102 >>> # return func(arg1, beta=context, gamma=arg1)
103 >>> bound = f.bind('foo', 'bar', 'baz', context=100, extra=None)
104 >>> # At this point, bound is a FuncCall with no Bind arguments, and can be
105 >>> # invoked. This would be equivalent to:
106 >>> # func('baz', beta=100, gamma='baz')
107 >>> bound()
108 baz-100-baz
109
110 Unused arguments in the .bind() call are ignored, which allows you to build
111 value-agnostic invocations to FuncCall.bind().
112 """
113 self._func = func
114 self._args = args
115 self._kwargs = kwargs
116 self._fully_bound = None
117
118 # "access to a protected member" pylint: disable=W0212
119 func = property(lambda self: self._func)
120 args = property(lambda self: self._args)
121 kwargs = property(lambda self: self._kwargs)
122
123 @property
124 def fully_bound(self):
125 if self._fully_bound is None:
126 self._fully_bound = not (
127 any(isinstance(v, Bind) for v in self._args) or
128 any(isinstance(v, Bind) for v in self._kwargs.itervalues())
129 )
130 return self._fully_bound
131
132 def bind(self, *args, **kwargs):
133 if self.fully_bound or not (args or kwargs):
134 return self
135
136 new = FuncCall(self._func)
137 new._args = [Bind.maybe_bind(a, args, kwargs) for a in self.args]
138 new._kwargs = {k: Bind.maybe_bind(v, args, kwargs)
139 for k, v in self.kwargs.iteritems()}
140 return new
141
142 def __call__(self, *args, **kwargs):
143 f = self.bind(args, kwargs)
144 assert f.fully_bound
145 return f.func(*f.args, **f.kwargs)
146
147 def __repr__(self):
148 return 'FuncCall(%r, *%r, **%r)' % (self.func, self.args, self.kwargs)
149
150
151 _Test = namedtuple(
152 'Test', 'name func_call expect_dir expect_base ext covers breakpoints')
153
154 class Test(_Test):
155 TEST_COVERS_MATCH = re.compile('.*/test/([^/]*)_test\.py$')
156
157 def __new__(cls, name, func_call, expect_dir=None, expect_base=None,
158 ext='json', covers=None, breakpoints=None, break_funcs=()):
159 """Create a new test.
160
161 @param name: The name of the test. Will be used as the default expect_base
162
163 @param func_call: A FuncCall object
164
165 @param expect_dir: The directory which holds the expectation file for this
166 Test.
167 @param expect_base: The basename (without extension) of the expectation
168 file. Defaults to |name|.
169 @param ext: The extension of the expectation file. Affects the serializer
170 used to write the expectations to disk. Valid values are
171 'json' and 'yaml' (Keys in SERIALIZERS).
172 @param covers: A list of coverage file patterns to include for this Test.
173 By default, a Test covers the file in which its function
174 was defined, as well as the source file matching the test
175 according to TEST_COVERS_MATCH.
176
177 @param breakpoints: A list of (path, lineno, func_name) tuples. These will
178 turn into breakpoints when the tests are run in 'debug'
179 mode. See |break_funcs| for an easier way to set this.
180 @param break_funcs: A list of functions for which to set breakpoints.
181 """
182 breakpoints = breakpoints or []
183 if not breakpoints or break_funcs:
184 for f in (break_funcs or (func_call.func,)):
185 if hasattr(f, 'im_func'):
186 f = f.im_func
187 breakpoints.append((f.func_code.co_filename,
188 f.func_code.co_firstlineno,
189 f.func_code.co_name))
190
191 return super(Test, cls).__new__(cls, name, func_call, expect_dir,
192 expect_base, ext, covers, breakpoints)
193
194 def coverage_includes(self):
195 if self.covers is not None:
196 return self.covers
197
198 test_file = inspect.getabsfile(self.func_call.func)
199 covers = [test_file]
200 match = Test.TEST_COVERS_MATCH.match(test_file)
201 if match:
202 covers.append(os.path.join(
203 os.path.dirname(os.path.dirname(test_file)),
204 match.group(1) + '.py'
205 ))
206
207 return covers
208
209 def expect_path(self, ext=None):
210 expect_dir = self.expect_dir
211 if expect_dir is None:
212 test_file = inspect.getabsfile(self.func_call.func)
213 expect_dir = os.path.splitext(test_file)[0] + '.expected'
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 run(self, context=None):
219 return self.func_call(context=context)
220
221 def process(self, func=lambda test: test.run()):
222 """Applies |func| to the test, and yields (self, func(self)).
223
224 For duck-typing compatibility with MultiTest.
225
226 Bind(name='context') if used by your test function, is bound to None.
227
228 Used interally by expect_tests, you're not expected to call this yourself.
229 """
230 yield self, func(self.bind(context=None))
231
232 def bind(self, *args, **kwargs):
233 return self._replace(func_call=self.func_call.bind(*args, **kwargs))
234
235 def restrict(self, tests):
236 assert tests[0] is self
237 return self
238
239
240 _MultiTest = namedtuple(
241 'MultiTest', 'name make_ctx_call destroy_ctx_call tests atomic')
242
243 class MultiTest(_MultiTest):
244 """A wrapper around one or more Test instances.
245
246 Allows the entire group to have common pre- and post- actions and an optional
247 shared context between the Test methods (represented by Bind(name='context')).
248
249 Args:
250 name - The name of the MultiTest. Each Test's name should be prefixed with
251 this name, though this is not enforced.
252 make_ctx_call - A FuncCall which will be called once before any test in this
253 MultiTest runs. The return value of this FuncCall will become bound
254 to the name 'context' for both the |destroy_ctx_call| as well as every
255 test in |tests|.
256 destroy_ctx_call - A FuncCall which will be called once after all tests in
257 this MultiTest runs. The context object produced by |make_ctx_call| is
258 bound to the name 'context'.
259 tests - A list of Test instances. The context object produced by
260 |make_ctx_call| is bound to the name 'context'.
261 atomic - A boolean which indicates that this MultiTest must be executed
262 either all at once, or not at all (i.e., subtests may not be filtered).
263 """
264
265 def restrict(self, tests):
266 """A helper method to re-cast the MultiTest with fewer subtests.
267
268 All fields will be identical except for tests. If this MultiTest is atomic,
269 then this method returns |self|.
270
271 Used interally by expect_tests, you're not expected to call this yourself.
272 """
273 if self.atomic:
274 return self
275 assert all(t in self.tests for t in tests)
276 return self._replace(tests=tests)
277
278 def process(self, func=lambda test: test.run()):
279 """Applies |func| to each sub-test, with properly bound context.
280
281 make_ctx_call will be called before any test, and its return value becomes
282 bound to the name 'context'. All sub-tests will be bound with this value
283 as well as destroy_ctx_call, which will be invoked after all tests have
284 been yielded.
285
286 Optionally, you may specify a different function to apply to each test
287 (by default it is `lambda test: test.run()`). The context will be bound
288 to the test before your function recieves it.
289
290 Used interally by expect_tests, you're not expected to call this yourself.
291 """
292 # TODO(iannucci): pass list of test names?
293 ctx_object = self.make_ctx_call()
294 try:
295 for test in self.tests:
296 yield test, func(test.bind(context=ctx_object))
297 finally:
298 self.destroy_ctx_call.bind(context=ctx_object)()
299
300 @staticmethod
301 def expect_path(_ext=None):
302 return None
303
304
305 class Handler(object):
306 """Handler object.
307
308 Defines 3 handler methods for each stage of the test pipeline. The pipeline
309 looks like:
310
311 -> ->
312 -> jobs -> (main)
313 GenStage -> test_queue -> * -> result_queue -> ResultStage
314 -> RunStage ->
315 -> ->
316
317 Each process will have an instance of one of the nested handler classes, which
318 will be called on each test / result.
319
320 You can skip the RunStage phase by setting SKIP_RUNLOOP to True on your
321 implementation class.
322
323 Tips:
324 * Only do printing in ResultStage, since it's running on the main process.
325 """
326 SKIP_RUNLOOP = False
327
328 @classmethod
329 def add_options(cls, parser):
330 """
331 @type parser: argparse.ArgumentParser()
332 """
333 pass
334
335 @classmethod
336 def gen_stage_loop(cls, _opts, tests, put_next_stage, _put_result_stage):
337 """Called in the GenStage portion of the pipeline.
338
339 @param opts: Parsed CLI options
340 @param tests:
341 Iteraterable of type_definitions.Test or type_definitions.MultiTest
342 objects.
343 @param put_next_stage:
344 Function to push an object to the next stage of the pipeline (RunStage).
345 Note that you should push the item you got from |tests|, not the
346 subtests, in the case that the item is a MultiTest.
347 @param put_result_stage:
348 Function to push an object to the result stage of the pipeline.
349 """
350 for test in tests:
351 put_next_stage(test)
352
353 @classmethod
354 def run_stage_loop(cls, _opts, tests_results, put_next_stage):
355 """Called in the RunStage portion of the pipeline.
356
357 @param opts: Parsed CLI options
358 @param tests_results: Iteraterable of (type_definitions.Test,
359 type_definitions.Result) objects
360 @param put_next_stage: Function to push an object to the next stage of the
361 pipeline (ResultStage).
362 """
363 for _, result in tests_results:
364 put_next_stage(result)
365
366 @classmethod
367 def result_stage_loop(cls, opts, objects):
368 """Called in the ResultStage portion of the pipeline.
369
370 Consider subclassing ResultStageHandler instead as it provides a more
371 flexible interface for dealing with |objects|.
372
373 @param opts: Parsed CLI options
374 @param objects: Iteraterable of objects from GenStage and RunStage.
375 """
376 error = False
377 aborted = False
378 handler = cls.ResultStageHandler(opts)
379 try:
380 for obj in objects:
381 error |= isinstance(handler(obj), Failure)
382 except ResultStageAbort:
383 aborted = True
384 handler.finalize(aborted)
385 return error
386
387 class ResultStageHandler(object):
388 """SAX-like event handler dispatches to self.handle_{type(obj).__name__}
389
390 So if |obj| is a Test, this would call self.handle_Test(obj).
391
392 self.__unknown is called to handle objects which have no defined handler.
393
394 self.finalize is called after all objects are processed.
395 """
396 def __init__(self, opts):
397 self.opts = opts
398
399 def __call__(self, obj):
400 """Called to handle each object in the ResultStage
401
402 @type obj: Anything passed to put_result in GenStage or RunStage.
403
404 @return: If the handler method returns Failure(), then it will
405 cause the entire test run to ultimately return an error code.
406 """
407 return getattr(self, 'handle_' + type(obj).__name__, self.__unknown)(obj)
408
409 def handle_NoMatchingTestsError(self, _error):
410 print 'No tests found that match the glob: %s' % (
411 ' '.join(self.opts.test_glob),)
412 return Failure()
413
414 def __unknown(self, obj):
415 if self.opts.verbose:
416 print 'UNHANDLED:', obj
417 return Failure()
418
419 def finalize(self, aborted):
420 """Called after __call__() has been called for all results.
421
422 @param aborted: True if the user aborted the run.
423 @type aborted: bool
424 """
425 pass
OLDNEW
« no previous file with comments | « expect_tests/serialize.py ('k') | expect_tests/unittest_helper.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698