From 9940839621bcdd9a45a35227df189bb1c3a66a46 Mon Sep 17 00:00:00 2001 From: John Vrbanac Date: Sun, 7 Sep 2014 20:45:12 -0500 Subject: [PATCH] Reworking the reporting system a bit Fixes summary report colors #44 Partially addresses #30 --- specter/reporting/console.py | 240 ++++++++++------------------------- specter/reporting/dots.py | 20 ++- specter/reporting/utils.py | 145 +++++++++++++++++++++ tests/test_reporter.py | 36 +++++- tests/test_runner.py | 7 +- 5 files changed, 264 insertions(+), 184 deletions(-) create mode 100644 specter/reporting/utils.py diff --git a/specter/reporting/console.py b/specter/reporting/console.py index a8fe9e8..5fa564d 100644 --- a/specter/reporting/console.py +++ b/specter/reporting/console.py @@ -1,36 +1,12 @@ -import re -import six from specter.spec import TestEvent, DescribeEvent, DataDescribe -from specter import _ from specter.reporting import AbstractConsoleReporter, AbstractSerialReporter - - -class TestStatus(): - PASS = 'passed' - FAIL = 'failed' - SKIP = 'skipped' - INCOMPLETE = 'incomplete' - ERROR = 'error' - - -class ConsoleColors(): - BLACK = 30 - RED = 31 - GREEN = 32 - YELLOW = 33 - BLUE = 34 - MAGENTA = 35 - CYAN = 36 - WHITE = 37 - -# REMOVE THIS GLOBAL WHEN WE REFACTOR THIS -USE_COLOR = True +from specter.reporting.utils import ( + TestStatus, print_expects, print_to_screen, get_color_from_status, + print_test_args, get_item_level, print_indent_msg, ConsoleColors) class ConsoleReporter(AbstractConsoleReporter, AbstractSerialReporter): - """ Temporary console reporter. - At least until I can get something else written. - """ + """ Simple BDD Style console reporter. """ def __init__(self, output_docstrings=False, use_color=True): super(ConsoleReporter, self).__init__() @@ -45,28 +21,17 @@ def __init__(self, output_docstrings=False, use_color=True): self.output_docstrings = output_docstrings def get_name(self): - return 'Temporary Serial console reporter' + return 'Simple BDD Serial console reporter' def process_arguments(self, args): if args.no_color: self.use_color = False - global USE_COLOR - USE_COLOR = False - - def subscribe_to_describe(self, describe): - describe.add_listener(TestEvent.COMPLETE, self.event_received) - describe.add_listener(DescribeEvent.START, self.start_describe) - - def event_received(self, evt): - test_case = evt.payload - level = get_item_level(test_case) - name = test_case.pretty_name - if level > 0: - name = u'\u221F {0}'.format(name) + def get_test_case_status(self, test_case, name): status = TestStatus.FAIL if (test_case.success - and not test_case.skipped and not test_case.incomplete): + and not test_case.skipped + and not test_case.incomplete): status = TestStatus.PASS elif test_case.incomplete: status = TestStatus.INCOMPLETE @@ -78,24 +43,13 @@ def event_received(self, evt): elif test_case.error: status = TestStatus.ERROR - print_test_msg(name, level, status) - - print_test_args(test_case.execute_kwargs, level, status) - - if test_case.doc and self.output_docstrings: - print_indent_msg(test_case.doc, level+1, status) - - # Print error if it exists - if test_case.error: - for line in test_case.error: - print_test_msg(line, level+2, TestStatus.FAIL) - - print_expects(test_case, level) + return status, name - # Add test to totals + def add_to_totals(self, test_case): self.test_total += 1 if (test_case.success - and not test_case.skipped and not test_case.incomplete): + and not test_case.skipped + and not test_case.incomplete): self.passed_tests += 1 elif test_case.skipped: self.skipped_tests += 1 @@ -107,19 +61,67 @@ def event_received(self, evt): self.failed_tests += 1 self.test_expects += len(test_case.expects) - def start_describe(self, evt): + def output_test_case_result(self, test_case, level): + name = test_case.pretty_name + if level > 0: + name = u'\u221F {0}'.format(name) + + status, name = self.get_test_case_status(test_case, name) + + self.output(name, level, status) + print_test_args(test_case.execute_kwargs, level, status, + self.use_color) + + if test_case.doc and self.output_docstrings: + print_indent_msg(test_case.doc, level+1, status) + + # Print error if it exists + if test_case.error: + for line in test_case.error: + self.output(line, level+2, TestStatus.FAIL) + + #if status == TestStatus.FAIL: + print_expects(test_case, level, self.use_color) + + def subscribe_to_describe(self, describe): + describe.add_listener(TestEvent.COMPLETE, self.test_complete) + describe.add_listener(DescribeEvent.START, self.start_spec) + + def test_complete(self, evt): + test_case = evt.payload + level = get_item_level(test_case) + + self.output_test_case_result(test_case, level) + + self.add_to_totals(test_case) + + def start_spec(self, evt): level = get_item_level(evt.payload) name = evt.payload.name if level > 0: name = u'\u221F {0}'.format(name) - print_indent_msg(name, level, color=ConsoleColors.GREEN) + + # Output Spec name + color = ConsoleColors.GREEN if self.use_color else None + print_indent_msg(name, level, color=color) + + # Output Docstrings if enabled if evt.payload.doc and self.output_docstrings: print_indent_msg(evt.payload.doc, level+1) + # Warn of duplicates if isinstance(evt.payload, DataDescribe) and evt.payload.dup_count: + color = ConsoleColors.YELLOW if self.use_color else None print_indent_msg('Warning: Noticed {0} duplicate data ' 'set(s)'.format(evt.payload.dup_count), - level+1, color=ConsoleColors.YELLOW) + level+1, color=color) + + def output(self, msg, indent, status=None): + """ Alias for print_indent_msg with color determined by status.""" + color = None + if self.use_color: + color = get_color_from_status(status) + print_indent_msg(msg, indent, color) def print_summary(self): msg = """------- Summary -------- @@ -137,116 +139,10 @@ def print_summary(self): errored=self.errored_tests) status = TestStatus.FAIL - if self.failed_tests == 0: + if self.failed_tests == 0 and self.errored_tests == 0: status = TestStatus.PASS - print_colored('\n') - print_test_msg('-'*24, 0, status) - print_test_msg(msg, 0, status) - print_test_msg('-'*24, 0, status) - - -def print_test_msg(msg, level, status=TestStatus.PASS): - color = ConsoleColors.RED - if status == TestStatus.PASS: - color = ConsoleColors.GREEN - elif status == TestStatus.SKIP: - color = ConsoleColors.YELLOW - elif status == TestStatus.INCOMPLETE: - color = ConsoleColors.MAGENTA - - print_indent_msg(msg=msg, level=level, color=color) - - -def pretty_print_args(kwargs): - if kwargs is None: - return u'None' - first_seen = False - parts = [] - for k, v in six.iteritems(kwargs): - if not first_seen: - first_seen = True - else: - parts.append(', ') - parts.append('{name} = {value}'.format(name=k, value=v)) - return u''.join(parts) - - -def print_test_args(kwargs, level, status=TestStatus.PASS): - if kwargs and (status == TestStatus.ERROR or - status == TestStatus.FAIL): - msg = u''.join([u' Parameters: ', pretty_print_args(kwargs)]) - print_test_msg(msg, level, status) - - -def print_indent_msg(msg, level=0, color=ConsoleColors.WHITE): - indent = u' ' * 2 - msg = u'{0}{1}'.format(str(indent * level), msg) - print_colored(msg=msg, color=color) - - -def print_msg_list(msg_list, level, color=ConsoleColors.WHITE): - for msg in msg_list: - print_indent_msg(msg=msg, level=level, color=color) - - -def print_colored(msg, color=ConsoleColors.WHITE): - if USE_COLOR: - msg = u'\033[{color}m{msg}\033[0m'.format(color=color, msg=msg) - - print(msg.encode('utf-8')) - - -def get_item_level(item): - levels = 0 - parent_above = item.parent - while parent_above is not None: - levels += 1 - parent_above = parent_above.parent - return levels - - -def print_expects(test_case, level): - # Print expects - for expect in test_case.expects: - mark = u'\u2718' - - status = TestStatus.FAIL - if expect.success: - status = TestStatus.PASS - mark = u'\u2714' - - expect_msg = u'{mark} {msg}'.format(mark=mark, msg=expect) - - print_test_msg(expect_msg, level+1, status=status) - - def hardcoded(param): - result = re.match('^(\'|"|\d)', str(param)) is not None - return result - - def print_param(value, param, indent, prefix): - if not expect.success and not hardcoded(param): - msg_list = str(value).splitlines() or [''] - prefix = _('{0}: {1}').format(param or prefix, msg_list[0]) - print_indent_msg(prefix, indent) - if len(msg_list) > 1: - print_msg_list(msg_list[1:], indent) - - if expect.custom_msg: - print_test_msg(expect.custom_msg, level + 3, status=status) - - # Print the target parameter - try: - print_param(expect.target, expect.target_src_param, - level + 3, 'Target') - except: - print_param('ERROR - Couldn\'t evaluate target value', - expect.target_src_param, level + 3, 'Target') - - # Print the expected parameter - try: - print_param(expect.expected, expect.expected_src_param, - level + 3, 'Expected') - except: - print_param('ERROR - Couldn\'t evaluate expected value', - expect.expected_src_param, level + 3, 'Expected') + print_to_screen('\n') + self.output('-'*24, 0, status) + self.output(msg, 0, status) + self.output('-'*24, 0, status) diff --git a/specter/reporting/dots.py b/specter/reporting/dots.py index cc3ea13..4231aba 100644 --- a/specter/reporting/dots.py +++ b/specter/reporting/dots.py @@ -4,8 +4,8 @@ from specter.spec import TestEvent from specter.reporting import AbstractConsoleReporter from specter.reporting import AbstractParallelReporter -from specter.reporting.console import print_test_msg, print_test_args -from specter.reporting.console import print_expects, TestStatus +from specter.reporting.utils import print_test_msg, print_test_args +from specter.reporting.utils import print_expects, TestStatus class DotsReporter(AbstractConsoleReporter, AbstractParallelReporter): @@ -14,6 +14,7 @@ def __init__(self): super(DotsReporter, self).__init__() self.total = 0 self.failed_tests = [] + self.use_color = True def get_name(self): return _('Dots Reporter') @@ -21,6 +22,10 @@ def get_name(self): def subscribe_to_describe(self, describe): describe.add_listener(TestEvent.COMPLETE, self.test_event) + def process_arguments(self, args): + if args.no_color: + self.use_color = False + def print_error(self, wrapper): """ A crude way of output the errors for now. This needs to be cleaned up into something better. @@ -28,18 +33,19 @@ def print_error(self, wrapper): level = 0 parent = wrapper.parent while parent: - print_test_msg(parent.name, level, TestStatus.FAIL) + print_test_msg(parent.name, level, TestStatus.FAIL, self.use_color) level += 1 parent = parent.parent - print_test_msg(wrapper.name, level, TestStatus.FAIL) - print_test_args(wrapper.execute_kwargs, level, TestStatus.FAIL) + print_test_msg(wrapper.name, level, TestStatus.FAIL, self.use_color) + print_test_args(wrapper.execute_kwargs, level, TestStatus.FAIL, + self.use_color) if wrapper.error: for line in wrapper.error: - print_test_msg(line, level+2, TestStatus.FAIL) + print_test_msg(line, level+2, TestStatus.FAIL, self.use_color) - print_expects(wrapper, level) + print_expects(wrapper, level, use_color=self.use_color) def test_event(self, evt): self.total += 1 diff --git a/specter/reporting/utils.py b/specter/reporting/utils.py new file mode 100644 index 0000000..0877e67 --- /dev/null +++ b/specter/reporting/utils.py @@ -0,0 +1,145 @@ +import re +import six + +from specter import _ + + +class TestStatus(): + PASS = 'passed' + FAIL = 'failed' + SKIP = 'skipped' + INCOMPLETE = 'incomplete' + ERROR = 'error' + + +class ConsoleColors(): + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + WHITE = 37 + + +def get_color_from_status(status): + color = None + if status == TestStatus.PASS: + color = ConsoleColors.GREEN + elif status == TestStatus.SKIP: + color = ConsoleColors.YELLOW + elif status == TestStatus.INCOMPLETE: + color = ConsoleColors.MAGENTA + elif status is not None: + color = ConsoleColors.RED + + return color + + +def print_test_msg(msg, level, status=None, use_color=True): + color = get_color_from_status(status) if use_color else None + print_indent_msg(msg=msg, level=level, color=color) + + +def pretty_print_args(kwargs): + if kwargs is None: + return u'None' + first_seen = False + parts = [] + for k, v in six.iteritems(kwargs): + if not first_seen: + first_seen = True + else: + parts.append(', ') + parts.append('{name} = {value}'.format(name=k, value=v)) + return u''.join(parts) + + +def print_test_args(kwargs, level, status=TestStatus.PASS, use_color=True): + if kwargs and (status == TestStatus.ERROR or + status == TestStatus.FAIL): + msg = u''.join([ + u' Parameters: ', + pretty_print_args(kwargs) + ]) + print_test_msg(msg, level, status, use_color) + + +def print_indent_msg(msg, level=0, color=None): + indent = u' ' * 2 + msg = u'{0}{1}'.format(str(indent * level), msg) + print_to_screen(msg=msg, color=color) + + +def print_msg_list(msg_list, level, color=None): + for msg in msg_list: + print_indent_msg(msg=msg, level=level, color=color) + + +def print_to_screen(msg, color=None): + if color: + msg = u'\033[{color}m{msg}\033[0m'.format(color=color, msg=msg) + + # Re-encode msg to result everything to UTF-8 + encoded_bytes = msg.encode('utf-8') + print(encoded_bytes.decode('utf-8')) + + +def get_item_level(item): + levels = 0 + parent_above = item.parent + while parent_above is not None: + levels += 1 + parent_above = parent_above.parent + return levels + + +def print_expects(test_case, level, use_color=True): + # Print expects + for expect in test_case.expects: + mark = u'\u2718' + + status = TestStatus.FAIL + if expect.success: + status = TestStatus.PASS + mark = u'\u2714' + + # Turn off the status if we're not using color + if not use_color: + status = None + + expect_msg = u'{mark} {msg}'.format(mark=mark, msg=expect) + + print_test_msg(expect_msg, level+1, status=status) + + def hardcoded(param): + result = re.match('^(\'|"|\d)', str(param)) is not None + return result + + def print_param(value, param, indent, prefix): + if not expect.success and not hardcoded(param): + msg_list = str(value).splitlines() or [''] + prefix = _('{0}: {1}').format(param or prefix, msg_list[0]) + print_indent_msg(prefix, indent) + if len(msg_list) > 1: + print_msg_list(msg_list[1:], indent) + + if expect.custom_msg: + print_test_msg(expect.custom_msg, level + 3, status=status) + + # Print the target parameter + try: + print_param(expect.target, expect.target_src_param, + level + 3, 'Target') + except: + print_param('ERROR - Couldn\'t evaluate target value', + expect.target_src_param, level + 3, 'Target') + + # Print the expected parameter + try: + print_param(expect.expected, expect.expected_src_param, + level + 3, 'Expected') + except: + print_param('ERROR - Couldn\'t evaluate expected value', + expect.expected_src_param, level + 3, 'Expected') diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 22b71d5..b9c772f 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -1,15 +1,25 @@ from unittest import TestCase -from specter.reporting.console import ConsoleReporter, print_colored +from nose.plugins.capture import Capture +from specter.reporting.console import ConsoleReporter +from specter.reporting.utils import TestStatus, pretty_print_args class TestConsoleReporter(TestCase): def setUp(self): self.default_reporter = ConsoleReporter() + self.console = Capture() + self.console.begin() + + def tearDown(self): + self.console.end() + + def _get_output(self): + return self.console.buffer def test_get_name(self): self.assertEqual(self.default_reporter.get_name(), - 'Temporary Serial console reporter') + 'Simple BDD Serial console reporter') def test_process_args(self): class dotted_dict(object): @@ -24,5 +34,23 @@ def __getattr__(self, attr): def test_no_color_print(self): self.default_reporter.use_color = False - print_colored('bam') - # When we unify the print to console, add a test here to cover this + self.default_reporter.output('test', 0, TestStatus.PASS) + + self.assertEqual(self._get_output(), 'test\n') + + def test_color_print(self): + self.default_reporter.output('test', 0, TestStatus.PASS) + self.assertEqual(self._get_output(), '\x1b[32mtest\x1b[0m\n') + + +class TestReporterUtils(TestCase): + def setUp(self): + self.console = Capture() + self.console.begin() + + def tearDown(self): + self.console.end() + + def test_pretty_print_args_with_empty_kwargs(self): + result = pretty_print_args(None) + self.assertEqual(result, 'None') diff --git a/tests/test_runner.py b/tests/test_runner.py index 0aaadec..687892b 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,4 +1,5 @@ from unittest import TestCase +from nose.plugins.capture import Capture from specter.runner import SpecterRunner from specter.reporting.console import ConsoleReporter @@ -8,6 +9,11 @@ class TestSpecterRunner(TestCase): def setUp(self): self.runner = SpecterRunner() + self.console = Capture() + self.console.begin() + + def tearDown(self): + self.console.end() def get_console_reporter(self, reporters): for r in reporters: @@ -17,7 +23,6 @@ def get_console_reporter(self, reporters): def test_ascii_art_generation(self): """ We just want to know if it creates something""" art = self.runner.generate_ascii_art() - self.assertGreater(len(art), 0) def test_run(self):