blob: 913d26f363ed57490baceaea204af90d045e85f7 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
from collections import namedtuple
UnknownError = namedtuple('UnknownError', 'message')
TestError = namedtuple('TestError', 'test message')
NoMatchingTestsError = namedtuple('NoMatchingTestsError', '')
Result = namedtuple('Result', 'data')
class ResultStageAbort(Exception):
pass
class Failure(object):
pass
_Test = namedtuple(
'Test', 'name func args kwargs expect_dir expect_base ext breakpoints')
class Test(_Test): # pylint: disable=W0232
def __new__(cls, name, func, args=(), kwargs=None, expect_dir=None,
expect_base=None, ext='json', breakpoints=None, break_funcs=()):
"""Create a new test.
@param name: The name of the test. Will be used as the default expect_base
@param func: The function to execute to run this test. Must be pickleable.
@param args: *args for |func|
@param kwargs: **kwargs for |func|
@param expect_dir: The directory which holds the expectation file for this
Test.
@param expect_base: The basename (without extension) of the expectation
file. Defaults to |name|.
@param ext: The extension of the expectation file. Affects the serializer
used to write the expectations to disk. Valid values are
'json' and 'yaml' (Keys in SERIALIZERS).
@param breakpoints: A list of (path, lineno, func_name) tuples. These will
turn into breakpoints when the tests are run in 'debug'
mode. See |break_funcs| for an easier way to set this.
@param break_funcs: A list of functions for which to set breakpoints.
"""
# pylint: disable=E1002
kwargs = kwargs or {}
breakpoints = breakpoints or []
if not breakpoints or break_funcs:
for f in (break_funcs or (func,)):
if hasattr(f, 'im_func'):
f = f.im_func
breakpoints.append((f.func_code.co_filename,
f.func_code.co_firstlineno,
f.func_code.co_name))
return super(Test, cls).__new__(cls, name, func, args, kwargs, expect_dir,
expect_base, ext, breakpoints)
def expect_path(self, ext=None):
name = self.expect_base or self.name
name = ''.join('_' if c in '<>:"\\/|?*\0' else c for c in name)
return os.path.join(self.expect_dir, name + ('.%s' % (ext or self.ext)))
def run(self):
return self.func(*self.args, **self.kwargs)
class Handler(object):
"""Handler object.
Defines 3 handler methods for each stage of the test pipeline. The pipeline
looks like:
-> ->
-> jobs -> (main)
GenStage -> test_queue -> * -> result_queue -> ResultStage
-> RunStage ->
-> ->
Each process will have an instance of one of the nested handler classes, which
will be called on each test / result.
You can skip the RunStage phase by setting SKIP_RUNLOOP to True on your
implementation class.
Tips:
* Only do printing in ResultStage, since it's running on the main process.
"""
SKIP_RUNLOOP = False
@classmethod
def add_options(cls, parser):
"""
@type parser: argparse.ArgumentParser()
"""
pass
@classmethod
def gen_stage_loop(cls, _opts, tests, put_next_stage, _put_result_stage):
"""Called in the GenStage portion of the pipeline.
@param opts: Parsed CLI options
@param tests: Iteraterable of type_definitions.Test objects
@param put_next_stage: Function to push an object to the next stage of the
pipeline (RunStage).
@param put_result_stage: Function to push an object to the result stage of
the pipeline.
"""
for test in tests:
put_next_stage(test)
@classmethod
def run_stage_loop(cls, _opts, tests_results, put_next_stage):
"""Called in the RunStage portion of the pipeline.
@param opts: Parsed CLI options
@param tests_results: Iteraterable of (type_definitions.Test,
type_definitions.Result) objects
@param put_next_stage: Function to push an object to the next stage of the
pipeline (ResultStage).
"""
for _, result in tests_results:
put_next_stage(result)
@classmethod
def result_stage_loop(cls, opts, objects):
"""Called in the ResultStage portion of the pipeline.
Consider subclassing ResultStageHandler instead as it provides a more
flexible interface for dealing with |objects|.
@param opts: Parsed CLI options
@param objects: Iteraterable of objects from GenStage and RunStage.
"""
error = False
aborted = False
handler = cls.ResultStageHandler(opts)
try:
for obj in objects:
error |= isinstance(handler(obj), Failure)
except ResultStageAbort:
aborted = True
handler.finalize(aborted)
return error
class ResultStageHandler(object):
"""SAX-like event handler dispatches to self.handle_{type(obj).__name__}
So if |obj| is a Test, this would call self.handle_Test(obj).
self.__unknown is called to handle objects which have no defined handler.
self.finalize is called after all objects are processed.
"""
def __init__(self, opts):
self.opts = opts
def __call__(self, obj):
"""Called to handle each object in the ResultStage
@type obj: Anything passed to put_result in GenStage or RunStage.
@return: If the handler method returns Failure(), then it will
cause the entire test run to ultimately return an error code.
"""
return getattr(self, 'handle_' + type(obj).__name__, self.__unknown)(obj)
def handle_NoMatchingTestsError(self, _error):
print 'No tests found that match the glob: %s' % (
' '.join(self.opts.test_glob),)
return Failure()
def __unknown(self, obj):
if self.opts.verbose:
print 'UNHANDLED:', obj
return Failure()
def finalize(self, aborted):
"""Called after __call__() has been called for all results.
@param aborted: True if the user aborted the run.
@type aborted: bool
"""
pass