diff --git a/avocado/core/dispatcher.py b/avocado/core/dispatcher.py index dbbef6e252785e6f4cb5eeb74b8fc83ef30ccaa7..a196445014d9e90afefc3b4f560075c1b5899ef1 100644 --- a/avocado/core/dispatcher.py +++ b/avocado/core/dispatcher.py @@ -14,6 +14,7 @@ """Extensions/plugins dispatchers.""" +import logging import sys from stevedore import EnabledExtensionManager @@ -30,11 +31,12 @@ class Dispatcher(EnabledExtensionManager): #: Default namespace prefix for Avocado extensions NAMESPACE_PREFIX = 'avocado.plugins.' - def __init__(self, namespace): + def __init__(self, namespace, invoke_kwds={}): self.load_failures = [] super(Dispatcher, self).__init__(namespace=namespace, check_func=self.enabled, invoke_on_load=True, + invoke_kwds=invoke_kwds, on_load_failure_callback=self.store_load_failure, propagate_map_exceptions=True) @@ -171,3 +173,26 @@ class ResultDispatcher(Dispatcher): except: job.log.error('Error running method "%s" of plugin "%s": %s', method_name, ext.name, sys.exc_info()[1]) + + +class ResultEventsDispatcher(Dispatcher): + + def __init__(self, args): + super(ResultEventsDispatcher, self).__init__( + 'avocado.plugins.result_events', + invoke_kwds={'args': args}) + self.log = logging.getLogger("avocado.app") + + def map_method(self, method_name, *args): + for ext in self.extensions: + try: + if hasattr(ext.obj, method_name): + method = getattr(ext.obj, method_name) + method(*args) + except SystemExit: + raise + except KeyboardInterrupt: + raise + except: + self.log.error('Error running method "%s" of plugin "%s": %s', + method_name, ext.name, sys.exc_info()[1]) diff --git a/avocado/core/job.py b/avocado/core/job.py index 25288c7d3c204a83af381c3439a01f2a327f2746..81c630d3d9cbc8493b0a9efed08d3824c291f492 100644 --- a/avocado/core/job.py +++ b/avocado/core/job.py @@ -127,6 +127,13 @@ class Job(object): # A job may not have a dispatcher for pre/post tests execution plugins self._job_pre_post_dispatcher = None + # The result events dispatcher is shared with the test runner. + # Because of our goal to support using the phases of a job + # freely, let's get the result events dispatcher ready early. + # A future optimization may load it on demand. + self._result_events_dispatcher = dispatcher.ResultEventsDispatcher(self.args) + output.log_plugin_failures(self._result_events_dispatcher.load_failures) + def _setup_job_results(self): """ Prepares a job result directory, also known as logdir, for this job @@ -450,6 +457,7 @@ class Job(object): self._job_pre_post_dispatcher = dispatcher.JobPrePostDispatcher() output.log_plugin_failures(self._job_pre_post_dispatcher.load_failures) self._job_pre_post_dispatcher.map_method('pre', self) + self._result_events_dispatcher.map_method('pre_tests', self) def run_tests(self): mux = getattr(self.args, "mux", None) diff --git a/avocado/core/plugin_interfaces.py b/avocado/core/plugin_interfaces.py index d7134440d8e6149f984dbbc7c3e049e4c2c8efa2..2db136255b31f40df50fbb4bca39012a8994d9b2 100644 --- a/avocado/core/plugin_interfaces.py +++ b/avocado/core/plugin_interfaces.py @@ -134,3 +134,70 @@ class Result(Plugin): :param job: the finished job for which a result will be written :type job: :class:`avocado.core.job.Job` """ + + +class JobPreTests(Plugin): + + """ + Base plugin interface for adding actions before a job runs tests + + This interface looks similar to :class:`JobPre`, but it's inteded + to be called at a very specific place, that is, between + :meth:`avocado.core.job.Job.create_test_suite` and + :meth:`avocado.core.job.Job.run_tests`. + """ + + @abc.abstractmethod + def pre_tests(self, job): + """ + Entry point for job running actions before tests execution + """ + + +class JobPostTests(Plugin): + + """ + Base plugin interface for adding actions after a job runs tests + + Plugins using this interface will run at the a time equivalent to + plugins using the :class:`JobPost` interface, that is, at + :meth:`avocado.core.job.Job.post_tests`. This is because + :class:`JobPost` based plugins will eventually be modified to + really run after the job has finished, and not after it has run + tests. + """ + + @abc.abstractmethod + def post_tests(self, job): + """ + Entry point for job running actions after the tests execution + """ + + +class ResultEvents(JobPreTests, JobPostTests): + + """ + Base plugin interface for event based (streameable) results + + Plugins that want to add actions to be run after a job runs, + should use the 'avocado.plugins.result_events' namespace and + implement the defined interface. + """ + + @abc.abstractmethod + def start_test(self, result, state): + """ + Event triggered when a test starts running + """ + + @abc.abstractmethod + def test_progress(self, progress=False): + """ + Interface to notify progress (or not) of the running test + """ + + @abc.abstractmethod + def end_test(self, result, state): + """ + Event triggered when a test finishes running + """ diff --git a/avocado/core/remote/runner.py b/avocado/core/remote/runner.py index 313cff3e5697ee8535c16a7b5b4def1c5f9c412b..936288da70ab65551fd94df003a6f2820980e179 100644 --- a/avocado/core/remote/runner.py +++ b/avocado/core/remote/runner.py @@ -243,8 +243,14 @@ class RemoteTestRunner(TestRunner): state = test.get_state() self.result_proxy.start_test(state) self.result.start_test(state) + self.job._result_events_dispatcher.map_method('start_test', + self.result, + state) self.result_proxy.check_test(state) self.result.check_test(state) + self.job._result_events_dispatcher.map_method('end_test', + self.result, + state) if state['status'] == "INTERRUPTED": summary.add("INTERRUPTED") elif not status.mapping[state['status']]: @@ -258,6 +264,8 @@ class RemoteTestRunner(TestRunner): os.remove(zip_path_filename) self.result_proxy.end_tests() self.result.end_tests() + self.job._result_events_dispatcher.map_method('post_tests', + self.job) finally: try: self.tear_down() diff --git a/avocado/core/runner.py b/avocado/core/runner.py index 8d915465e59dfc6507b86e38d13e824a3a9200fb..5886ece7fa69f24768dd90007605cabf0ec5fe65 100644 --- a/avocado/core/runner.py +++ b/avocado/core/runner.py @@ -176,6 +176,8 @@ class TestStatus(object): elif "paused" in msg: self.status = msg self.job.result_proxy.notify_progress(False) + self.job._result_events_dispatcher.map_method('test_progress', + False) if msg['paused']: reason = msg['paused_msg'] if reason: @@ -315,6 +317,9 @@ class TestRunner(object): self.result_proxy.start_test(early_state) self.result.start_test(early_state) + self.job._result_events_dispatcher.map_method('start_test', + self.result, + early_state) try: instance.run_avocado() finally: @@ -388,6 +393,7 @@ class TestRunner(object): first = 0.01 step = 0.01 abort_reason = None + result_dispatcher = self.job._result_events_dispatcher while True: try: @@ -407,8 +413,11 @@ class TestRunner(object): if (test_status.status.get('running') or self.sigstopped): self.job.result_proxy.notify_progress(False) + result_dispatcher.map_method('test_progress', + False) else: self.job.result_proxy.notify_progress(True) + result_dispatcher.map_method('test_progress', True) else: break except KeyboardInterrupt: @@ -451,6 +460,7 @@ class TestRunner(object): self.result_proxy.check_test(test_state) self.result.check_test(test_state) + result_dispatcher.map_method('end_test', self.result, test_state) if test_state['status'] == "INTERRUPTED": summary.add("INTERRUPTED") elif not mapping[test_state['status']]: @@ -557,6 +567,7 @@ class TestRunner(object): self.job.sysinfo.end_job_hook() self.result_proxy.end_tests() self.result.end_tests() + self.job._result_events_dispatcher.map_method('post_tests', self.job) self.job.funcatexit.run() signal.signal(signal.SIGTSTP, signal.SIG_IGN) return summary diff --git a/selftests/unit/test_remote.py b/selftests/unit/test_remote.py index 0aae5ce8213a82dffcddf65f99b226c92f3e46c3..404a1b3d877d2ed207a0660c9ed40c9b1c269a86 100644 --- a/selftests/unit/test_remote.py +++ b/selftests/unit/test_remote.py @@ -44,10 +44,13 @@ class RemoteTestRunnerTest(unittest.TestCase): env_keep=None) log = flexmock() log.should_receive("info") + result_dispatcher = flexmock() + result_dispatcher.should_receive("map_method") job = flexmock(args=Args, log=log, references=['/tests/sleeptest', '/tests/other/test', 'passtest'], unique_id='1-sleeptest;0', - logdir="/local/path") + logdir="/local/path", + _result_events_dispatcher=result_dispatcher) flexmock(remote.RemoteTestRunner).should_receive('__init__') self.runner = remote.RemoteTestRunner(job, None)