From 53660761bcc305e38438e9696a9ec6aa5baacd2a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 6 May 2013 22:00:57 -0700 Subject: [PATCH] Adding ipython_nose for testing in the notebook. --- python/sw_engineering/ipython_nose.py | 398 ++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 python/sw_engineering/ipython_nose.py diff --git a/python/sw_engineering/ipython_nose.py b/python/sw_engineering/ipython_nose.py new file mode 100644 index 0000000..91d6819 --- /dev/null +++ b/python/sw_engineering/ipython_nose.py @@ -0,0 +1,398 @@ +import cgi +import os +import traceback +import re +import shlex +import string +import sys +import types +import uuid + +from nose import core as nose_core +from nose import loader as nose_loader +from nose.config import Config, all_config_files +from nose.plugins.base import Plugin +from nose.plugins.skip import SkipTest +from nose.plugins.manager import DefaultPluginManager +from IPython.core import displaypub, magic + + +class Template(string.Formatter): + def __init__(self, template): + self._template = template + + def format(self, **context): + return self.vformat(self._template, (), context) + + def convert_field(self, value, conversion): + if conversion == 'e': + return cgi.escape(value) + else: + return super(Template, self).convert_field(value, conversion) + + +class DummyUnittestStream: + def write(self, *arg): + pass + + def writeln(self, *arg): + pass + + def flush(self, *arg): + pass + + +class NotebookLiveOutput(object): + def __init__(self): + self.output_id = 'ipython_nose_%s' % uuid.uuid4().hex + displaypub.publish_html( + '
' % self.output_id) + displaypub.publish_javascript( + 'document.%s = $("#%s");' % (self.output_id, self.output_id)) + + def finalize(self): + displaypub.publish_javascript('delete document.%s;' % self.output_id) + + def write_chars(self, chars): + displaypub.publish_javascript( + 'document.%s.append($("%s"));' % ( + self.output_id, cgi.escape(chars))) + + def write_line(self, line): + displaypub.publish_javascript( + 'document.%s.append($("
%s
"));' % ( + self.output_id, cgi.escape(line))) + + +class ConsoleLiveOutput(object): + def __init__(self, stream_obj): + self.stream_obj = stream_obj + + def finalize(self): + self.stream_obj.stream.write('\n') + + def write_chars(self, chars): + self.stream_obj.stream.write(chars) + + def write_line(self, line): + self.stream_obj.stream.write(line + '\n') + + +def html_escape(s): + return cgi.escape(str(s)) + + +class IPythonDisplay(Plugin): + """Do something nice in IPython.""" + + name = 'ipython-html' + enabled = True + score = 2 + + def __init__(self, verbose=False): + super(IPythonDisplay, self).__init__() + self.verbose = verbose + self.html = [] + self.num_tests = 0 + self.failures = [] + self.skipped = 0 + + _nose_css = '''\ + + ''' + + _show_hide_js = ''' + + ''' + + _summary_template_html = Template(''' +
+
+   +
+ +
+   +
+ {text!e} +
+ ''') + + _summary_template_text = Template('''{text}\n''') + + def _summary(self, numtests, numfailed, numskipped, template): + text = "%d/%d tests passed" % (numtests - numfailed, numtests) + if numfailed > 0: + text += "; %d failed" % numfailed + if numskipped > 0: + text += "; %d skipped" % numskipped + + failpercent = int(float(numfailed) / numtests * 100) + if numfailed > 0 and failpercent < 5: + # Ensure the red bar is visible + failpercent = 5 + + skippercent = int(float(numskipped) / numtests * 100) + if numskipped > 0 and skippercent < 5: + # Ditto for the yellow bar + skippercent = 5 + + passpercent = 100 - failpercent - skippercent + + return template.format( + text=text, failpercent=failpercent, skippercent=skippercent, + passpercent=passpercent) + + _tracebacks_template_html = Template(''' +
+
+ failed: {name!e} + [toggle traceback] +
+
{formatted_traceback!e}
+
+ ''') + + _tracebacks_template_text = Template( + '''========\n{name}\n========\n{formatted_traceback}\n''') + + def _tracebacks(self, failures, template): + output = [] + for test, exc in failures: + name = test.shortDescription() or str(test) + formatted_traceback = ''.join(traceback.format_exception(*exc)) + output.append(template.format( + name=name, formatted_traceback=formatted_traceback + )) + return ''.join(output) + + def addSuccess(self, test): + if self.verbose: + self.live_output.write_line(str(test) + " ... pass") + else: + self.live_output.write_chars('.') + + def addError(self, test, err): + if issubclass(err[0], SkipTest): + return self.addSkip(test) + if self.verbose: + self.live_output.write_line(str(test) + " ... error") + else: + self.live_output.write_chars('E') + self.failures.append((test, err)) + + def addFailure(self, test, err): + if self.verbose: + self.live_output.write_line(str(test) + " ... fail") + else: + self.live_output.write_chars('F') + self.failures.append((test, err)) + + # Deprecated in newer versions of nose; skipped tests are handled in + # addError in newer versions + def addSkip(self, test): + if self.verbose: + self.live_output.write_line(str(test) + " ... SKIP") + else: + self.live_output.write_chars('S') + self.skipped += 1 + + def begin(self): + # This feels really hacky + try: # >= ipython 1.0 + from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook + except ImportError: + from IPython.zmq.displayhook import ZMQShellDisplayHook + if isinstance(sys.displayhook, ZMQShellDisplayHook): + self.live_output = NotebookLiveOutput() + else: + self.live_output = ConsoleLiveOutput(self) + + def finalize(self, result): + self.result = result + self.live_output.finalize() + + def setOutputStream(self, stream): + # grab for own use + self.stream = stream + return DummyUnittestStream() + + def startContext(self, ctx): + pass + + def stopContext(self, ctx): + pass + + def startTest(self, test): + self.num_tests += 1 + + def stopTest(self, test): + pass + + @staticmethod + def make_link(matches): + target = matches.group(0) + input_id = matches.group(1) + link = '{target}'.format(target=target) + make_anchor_js = ''''''.format(input_id=input_id, target=target) + return link + make_anchor_js + + def linkify_html_traceback(self, html): + return re.sub( + r'ipython-input-(\d+)-[0-9a-f]{12}', + self.make_link, + html) + + def _repr_html_(self): + if self.num_tests <= 0: + return 'No tests found.' + + output = [self._nose_css, self._show_hide_js] + + output.append(self._summary( + self.num_tests, len(self.failures), self.skipped, + self._summary_template_html)) + output.append(self.linkify_html_traceback(self._tracebacks( + self.failures, self._tracebacks_template_html))) + return ''.join(output) + + def _repr_pretty_(self, p, cycle): + if self.num_tests <= 0: + p.text('No tests found.') + return + p.text(self._summary( + self.num_tests, len(self.failures), self.skipped, + self._summary_template_text)) + p.text(self._tracebacks(self.failures, self._tracebacks_template_text)) + + +def get_ipython_user_ns_as_a_module(): + test_module = types.ModuleType('test_module') + test_module.__dict__.update(get_ipython().user_ns) + return test_module + + +def makeNoseConfig(env): + """Load a Config, pre-filled with user config files if any are + found. + """ + cfg_files = all_config_files() + manager = DefaultPluginManager() + return Config(env=env, files=cfg_files, plugins=manager) + + +def nose(line, test_module=get_ipython_user_ns_as_a_module): + if callable(test_module): + test_module = test_module() + config = makeNoseConfig(os.environ) + loader = nose_loader.TestLoader(config=config) + tests = loader.loadTestsFromModule(test_module) + extra_args = shlex.split(str(line)) + argv = ['ipython-nose', '--with-ipython-html', '--no-skip'] + extra_args + verbose = '-v' in extra_args + plug = IPythonDisplay(verbose=verbose) + + nose_core.TestProgram( + argv=argv, suite=tests, addplugins=[plug], exit=False, config=config) + + return plug + + +def load_ipython_extension(ipython): + magic.register_line_magic(nose) -- 2.26.2