diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 14d9bbadf..d91bef347 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -20,6 +20,7 @@ Bastien Vallet Benoit Pierre Bernat Gabor Brett Langdon +Brett Smith Bruno Oliveira Carl Meyer Charles Brunet diff --git a/docs/changelog/1421.bugfix.rst b/docs/changelog/1421.bugfix.rst new file mode 100644 index 000000000..8e0a5ff54 --- /dev/null +++ b/docs/changelog/1421.bugfix.rst @@ -0,0 +1 @@ +``--parallel`` reports now show ASCII OK/FAIL/SKIP lines when full Unicode output is not available - by :user:`brettcs` diff --git a/src/tox/util/spinner.py b/src/tox/util/spinner.py index 523a0c496..ee2258958 100644 --- a/src/tox/util/spinner.py +++ b/src/tox/util/spinner.py @@ -5,7 +5,7 @@ import os import sys import threading -from collections import OrderedDict +from collections import OrderedDict, namedtuple from datetime import datetime import py @@ -19,34 +19,32 @@ class _CursorInfo(ctypes.Structure): _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] -def _file_support_encoding(chars, file): - encoding = getattr(file, "encoding", None) - if encoding is not None: - for char in chars: - try: - char.encode(encoding) - except UnicodeEncodeError: - break +_BaseMessage = namedtuple("_BaseMessage", ["unicode_msg", "ascii_msg"]) + + +class SpinnerMessage(_BaseMessage): + def for_file(self, file): + try: + self.unicode_msg.encode(file.encoding) + except (AttributeError, TypeError, UnicodeEncodeError): + return self.ascii_msg else: - return True - return False + return self.unicode_msg class Spinner(object): CLEAR_LINE = "\033[K" max_width = 120 - UNICODE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] - ASCII_FRAMES = ["|", "-", "+", "x", "*"] + FRAMES = SpinnerMessage("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", "|-+x*") + OK_FLAG = SpinnerMessage("✔ OK", "[ OK ]") + FAIL_FLAG = SpinnerMessage("✖ FAIL", "[FAIL]") + SKIP_FLAG = SpinnerMessage("⚠ SKIP", "[SKIP]") def __init__(self, enabled=True, refresh_rate=0.1): self.refresh_rate = refresh_rate self.enabled = enabled self._file = sys.stdout - self.frames = ( - self.UNICODE_FRAMES - if _file_support_encoding(self.UNICODE_FRAMES, sys.stdout) - else self.ASCII_FRAMES - ) + self.frames = self.FRAMES.for_file(self._file) self.stream = py.io.TerminalWriter(file=self._file) self._envs = OrderedDict() self._frame_index = 0 @@ -105,13 +103,13 @@ def add(self, name): self._envs[name] = datetime.now() def succeed(self, key): - self.finalize(key, "✔ OK", green=True) + self.finalize(key, self.OK_FLAG.for_file(self._file), green=True) def fail(self, key): - self.finalize(key, "✖ FAIL", red=True) + self.finalize(key, self.FAIL_FLAG.for_file(self._file), red=True) def skip(self, key): - self.finalize(key, "⚠ SKIP", white=True) + self.finalize(key, self.SKIP_FLAG.for_file(self._file), white=True) def finalize(self, key, status, **kwargs): start_at = self._envs[key] diff --git a/tests/unit/util/test_spinner.py b/tests/unit/util/test_spinner.py index 7c64ac453..2511d5848 100644 --- a/tests/unit/util/test_spinner.py +++ b/tests/unit/util/test_spinner.py @@ -113,6 +113,31 @@ def test_spinner_stdout_not_unicode(mocker, capfd): assert all(f in written for f in spin.frames) +@freeze_time("2012-01-14") +def test_spinner_report_not_unicode(mocker, capfd): + stdout = mocker.patch("tox.util.spinner.sys.stdout") + stdout.encoding = "ascii" + # Disable color to simplify parsing output strings + stdout.isatty = lambda: False + with spinner.Spinner(refresh_rate=100) as spin: + spin.stream.write(os.linesep) + spin.add("ok!") + spin.add("fail!") + spin.add("skip!") + spin.succeed("ok!") + spin.fail("fail!") + spin.skip("skip!") + lines = "".join(args[0] for args, _ in stdout.write.call_args_list).split(os.linesep) + del lines[0] + expected = [ + "\r{}[ OK ] ok! in 0.0 seconds".format(spin.CLEAR_LINE), + "\r{}[FAIL] fail! in 0.0 seconds".format(spin.CLEAR_LINE), + "\r{}[SKIP] skip! in 0.0 seconds".format(spin.CLEAR_LINE), + "\r{}".format(spin.CLEAR_LINE), + ] + assert lines == expected + + @pytest.mark.parametrize( "seconds, expected", [