Skip to content

Commit

Permalink
ASCII output for --parallel report lines (#2164)
Browse files Browse the repository at this point in the history
* Add SpinnerMessage helper class

This class provides a single interface for spinner strings where we
ideally want to report Unicode but want to fall back to ASCII when the
terminal doesn't support it.

* Add ASCII messages for OK/FAIL/SKIP lines

Fixes #1421.
  • Loading branch information
brettcs authored Aug 21, 2021
1 parent e1f1826 commit 8f5ca11
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 21 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Bastien Vallet
Benoit Pierre
Bernat Gabor
Brett Langdon
Brett Smith
Bruno Oliveira
Carl Meyer
Charles Brunet
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/1421.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``--parallel`` reports now show ASCII OK/FAIL/SKIP lines when full Unicode output is not available - by :user:`brettcs`
40 changes: 19 additions & 21 deletions src/tox/util/spinner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/util/test_spinner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down

0 comments on commit 8f5ca11

Please sign in to comment.