From 780961a6433830a5928e1785460a2755e498639d Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 May 2013 12:17:51 +0200 Subject: Add Nose plugin for BeakerLib integration The plugin hooks into the Nose runner and IPA's logging infrastructure and calls the appropriate BeakerLib functions (rl*). IPA's log_manager is extended to accept custom Handler classes. The ipa-run-tests helper now loads the plugin. Patr of the work for: https://fedorahosted.org/freeipa/ticket/3621 --- ipapython/log_manager.py | 19 +++-- ipatests/beakerlib_plugin.py | 168 +++++++++++++++++++++++++++++++++++++++++++ ipatests/ipa-run-tests | 35 ++++++--- 3 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 ipatests/beakerlib_plugin.py diff --git a/ipapython/log_manager.py b/ipapython/log_manager.py index 21e41060a..9625bdfb7 100644 --- a/ipapython/log_manager.py +++ b/ipapython/log_manager.py @@ -1006,6 +1006,9 @@ class LogManager(object): Specifies that a FileHandler be created, using the specified filename. + log_handler + Specifies a custom logging.Handler to use + Common keys: ------------ @@ -1140,8 +1143,10 @@ class LogManager(object): # Iterate over handler configurations. for cfg in configs: - # File or stream handler? + # Type of handler? filename = cfg.get('filename') + stream = cfg.get("stream") + log_handler = cfg.get("log_handler") if filename: if cfg.has_key("stream"): raise ValueError("both filename and stream are specified, must be one or the other, config: %s" % cfg) @@ -1188,11 +1193,7 @@ class LogManager(object): permission = cfg.get('permission') if permission is not None: os.chmod(path, permission) - else: - stream = cfg.get("stream") - if stream is None: - raise ValueError("neither file nor stream specified in config: %s" % cfg) - + elif stream: handler = logging.StreamHandler(stream) # Set the handler name @@ -1200,6 +1201,12 @@ class LogManager(object): if name is None: name = 'stream:%s' % (stream) handler.name = name + elif log_handler: + handler = log_handler + else: + raise ValueError( + "neither file nor stream nor log_handler specified in " + "config: %s" % cfg) # Add the handler handlers.append(handler) diff --git a/ipatests/beakerlib_plugin.py b/ipatests/beakerlib_plugin.py new file mode 100644 index 000000000..2ad2674eb --- /dev/null +++ b/ipatests/beakerlib_plugin.py @@ -0,0 +1,168 @@ +# Authors: +# Petr Viktorin +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""A Nose plugin that integrates with BeakerLib""" + +import os +import sys +import subprocess +import traceback +import logging + +import nose +from nose.plugins import Plugin + +from ipapython import ipautil +from ipapython.ipa_log_manager import log_mgr + + +def shell_quote(string): + """Quote a string for the shell + + Adapted from Python3's shlex.quote + """ + return "'" + str(string).replace("'", "'\"'\"'") + "'" + + +class BeakerLibLogHandler(logging.Handler): + def __init__(self, beakerlib_command): + super(BeakerLibLogHandler, self).__init__() + self.beakerlib_command = beakerlib_command + + def emit(self, record): + command = { + 'DEBUG': 'rlLogDebug', + 'INFO': 'rlLogInfo', + 'WARNING': 'rlLogWarning', + 'ERROR': 'rlLogError', + 'CRITICAL': 'rlLogFatal', + }.get(record.levelname, 'rlLog') + self.beakerlib_command([command, self.format(record)]) + + +class BeakerLibPlugin(Plugin): + """A Nose plugin that integrates with BeakerLib""" + # Since BeakerLib is a Bash library, we need to run it in Bash. + # The plugin maintains a Bash process and feeds it with commands + # on events like test start/end, logging, etc. + # See nose.plugins.base.IPluginInterface for Nose plugin interface docs + name = 'beakerlib' + + def options(self, parser, env=os.environ): + super(BeakerLibPlugin, self).options(parser, env=env) + self.env = env + self.parser = parser + + def configure(self, options, conf): + super(BeakerLibPlugin, self).configure(options, conf) + if not self.enabled: + return + + if 'BEAKERLIB' not in self.env: + self.parser.error( + 'BeakerLib not active, cannot use --with-beakerlib') + + # Set up the Bash process + self.bash = subprocess.Popen(['bash'], + stdin=subprocess.PIPE) + source_path = os.path.join(self.env['BEAKERLIB'], 'beakerlib.sh') + self.run_beakerlib_command(['.', source_path]) + + # _in_class is set when we are in setup_class, so its rlPhaseEnd can + # be called when the first test starts + self._in_class = False + + # Redirect logging to our own handlers + self.setup_log_handler(BeakerLibLogHandler(self.run_beakerlib_command)) + + def setup_log_handler(self, handler): + log_mgr.configure( + { + 'default_level': 'DEBUG', + 'handlers': [{'log_handler': handler, + 'format': '[%(name)s] %(message)s', + 'level': 'debug'}]}, + configure_state='beakerlib_plugin') + + def run_beakerlib_command(self, cmd): + """Given a command as a Popen-style list, run it in the Bash process""" + for word in cmd: + self.bash.stdin.write(shell_quote(word)) + self.bash.stdin.write(' ') + self.bash.stdin.write('\n') + self.bash.stdin.flush() + assert self.bash.returncode is None, "BeakerLib Bash process exited" + + def report(self, stream): + """End the Bash process""" + self.run_beakerlib_command(['exit']) + self.bash.communicate() + + def startContext(self, context): + """Start a test context (module, class) + + For test classes, this starts a BeakerLib phase + """ + if not isinstance(context, type): + return + message = 'Class setup: %s' % context.__name__ + self.run_beakerlib_command(['rlPhaseStart', 'FAIL', message]) + self._in_class = True + + def stopContext(self, context): + """End a test context""" + if self._in_class: + self.run_beakerlib_command(['rlPhaseEnd']) + + def startTest(self, test): + """Start a test phase""" + if self._in_class: + self.run_beakerlib_command(['rlPhaseEnd']) + self.run_beakerlib_command(['rlPhaseStart', 'FAIL', + 'Nose test: %s' % test]) + + def stopTest(self, test): + """End a test phase""" + self.run_beakerlib_command(['rlPhaseEnd']) + + def addSuccess(self, test): + self.run_beakerlib_command(['rlPass', 'Test succeeded']) + + def log_exception(self, err): + """Log an exception + + err is a 3-tuple as returned from sys.exc_info() + """ + message = ''.join(traceback.format_exception(*err)).rstrip() + self.run_beakerlib_command(['rlLogError', message]) + + def addError(self, test, err): + if issubclass(err[0], nose.SkipTest): + # Log skipped test. + # Unfortunately we only get to see this if the built-in skip + # plugin is disabled (--no-skip) + self.run_beakerlib_command(['rlPass', 'Test skipped: %s' % err[1]]) + else: + self.log_exception(err) + self.run_beakerlib_command( + ['rlFail', 'Test failed: unhandled exception']) + + def addFailure(self, test, err): + self.log_exception(err) + self.run_beakerlib_command(['rlFail', 'Test failed']) diff --git a/ipatests/ipa-run-tests b/ipatests/ipa-run-tests index 872b15e9c..e788df30b 100755 --- a/ipatests/ipa-run-tests +++ b/ipatests/ipa-run-tests @@ -1,5 +1,25 @@ #!/usr/bin/python +# Authors: +# Petr Viktorin +# Jason Gerard DeRose +# +# Copyright (C) 2008-2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + """Nose wrapper for running an installed (not in-tree) IPA test suite Any command-line arguments are passed directly to Nose. @@ -10,12 +30,13 @@ import sys import os from os import path -import ipatests +import nose -nose = '/usr/bin/nosetests' +import ipatests +from ipatests.beakerlib_plugin import BeakerLibPlugin cmd = [ - nose, + sys.argv[0], '-v', '--with-doctest', '--doctest-tests', @@ -28,10 +49,4 @@ cmd += sys.argv[1:] # This must be set so ipalib.api gets initialized property for tests: os.environ['IPA_UNIT_TEST_MODE'] = 'cli_test' - -if not path.isfile(nose): - print 'ERROR: need %r' % nose - sys.exit(100) - -print ' '.join(cmd) -sys.exit(call(cmd)) +nose.main(argv=cmd, addplugins=[BeakerLibPlugin()]) -- cgit