diff --git a/os_testr/subunit_trace.py b/os_testr/subunit_trace.py index 315d850..48f2416 100755 --- a/os_testr/subunit_trace.py +++ b/os_testr/subunit_trace.py @@ -17,6 +17,7 @@ # under the License. """Trace a subunit stream in reasonable detail and high accuracy.""" +from __future__ import absolute_import import argparse import datetime @@ -28,6 +29,8 @@ import sys import subunit import testtools +from os_testr.utils import colorizer + # NOTE(mtreinish) on python3 anydbm was renamed dbm and the python2 dbm module # was renamed to dbm.ndbm, this block takes that into account try: @@ -148,7 +151,8 @@ def find_test_run_time_diff(test_id, run_time): def show_outcome(stream, test, print_failures=False, failonly=False, - enable_diff=False, threshold='0', abbreviate=False): + enable_diff=False, threshold='0', abbreviate=False, + enable_color=False): global RESULTS status = test['status'] # TODO(sdague): ask lifeless why on this? @@ -167,19 +171,29 @@ def show_outcome(stream, test, print_failures=False, failonly=False, if name == 'process-returncode': return + for color in [colorizer.AnsiColorizer, colorizer.NullColorizer]: + if not enable_color: + color = colorizer.NullColorizer(stream) + break + if color.supported(): + color = color(stream) + break + if status == 'fail': FAILS.append(test) if abbreviate: - stream.write('F') + color.write('F', 'red') else: - stream.write('{%s} %s [%s] ... FAILED\n' % ( + stream.write('{%s} %s [%s] ... ' % ( worker, name, duration)) + color.write('FAILED', 'red') + stream.write('\n') if not print_failures: print_attachments(stream, test, all_channels=True) elif not failonly: if status == 'success': if abbreviate: - stream.write('.') + color.write('.', 'green') else: out_string = '{%s} %s [%s' % (worker, name, duration) perc_diff = find_test_run_time_diff(test['id'], duration) @@ -189,17 +203,22 @@ def show_outcome(stream, test, print_failures=False, failonly=False, out_string = out_string + ' +%.2f%%' % perc_diff else: out_string = out_string + ' %.2f%%' % perc_diff - stream.write(out_string + '] ... ok\n') + stream.write(out_string + '] ... ') + color.write('ok', 'green') + stream.write('\n') print_attachments(stream, test) elif status == 'skip': if abbreviate: - stream.write('S') + color.write('S', 'blue') else: reason = test['details'].get('reason', '') if reason: reason = ': ' + reason.as_text() - stream.write('{%s} %s ... SKIPPED%s\n' % ( - worker, name, reason)) + stream.write('{%s} %s ... ' % ( + worker, name)) + color.write('SKIPPED', 'blue') + stream.write('%s' % (reason)) + stream.write('\n') else: if abbreviate: stream.write('%s' % test['status'][0]) @@ -320,6 +339,8 @@ def parse_args(): parser.add_argument('--no-summary', action='store_true', help="Don't print the summary of the test run after " " completes") + parser.add_argument('--color', action='store_true', + help="Print results with colors") return parser.parse_args() @@ -332,7 +353,8 @@ def main(): print_failures=args.print_failures, failonly=args.failonly, enable_diff=args.enable_diff, - abbreviate=args.abbreviate)) + abbreviate=args.abbreviate, + enable_color=args.color)) summary = testtools.StreamSummary() result = testtools.CopyStreamResult([outcomes, summary]) result = testtools.StreamResultRouter(result) diff --git a/os_testr/utils/__init__.py b/os_testr/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_testr/utils/colorizer.py b/os_testr/utils/colorizer.py new file mode 100644 index 0000000..8ecec35 --- /dev/null +++ b/os_testr/utils/colorizer.py @@ -0,0 +1,98 @@ +# Copyright 2015 NEC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import sys + + +class AnsiColorizer(object): + """A colorizer is an object that loosely wraps around a stream + + allowing callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + @classmethod + def supported(cls, stream=sys.stdout): + """Check the current platform supports coloring terminal output + + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except Exception: + # guess false in case of error + return False + + def write(self, text, color): + """Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class NullColorizer(object): + """See _AnsiColorizer docstring.""" + def __init__(self, stream): + self.stream = stream + + @classmethod + def supported(cls, stream=sys.stdout): + return True + + def write(self, text, color): + self.stream.write(text)