Skip to content

Commit 8f5ca11

Browse files
authored
ASCII output for --parallel report lines (#2164)
* 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.
1 parent e1f1826 commit 8f5ca11

4 files changed

Lines changed: 46 additions & 21 deletions

File tree

CONTRIBUTORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Bastien Vallet
2020
Benoit Pierre
2121
Bernat Gabor
2222
Brett Langdon
23+
Brett Smith
2324
Bruno Oliveira
2425
Carl Meyer
2526
Charles Brunet

docs/changelog/1421.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``--parallel`` reports now show ASCII OK/FAIL/SKIP lines when full Unicode output is not available - by :user:`brettcs`

src/tox/util/spinner.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import sys
77
import threading
8-
from collections import OrderedDict
8+
from collections import OrderedDict, namedtuple
99
from datetime import datetime
1010

1111
import py
@@ -19,34 +19,32 @@ class _CursorInfo(ctypes.Structure):
1919
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
2020

2121

22-
def _file_support_encoding(chars, file):
23-
encoding = getattr(file, "encoding", None)
24-
if encoding is not None:
25-
for char in chars:
26-
try:
27-
char.encode(encoding)
28-
except UnicodeEncodeError:
29-
break
22+
_BaseMessage = namedtuple("_BaseMessage", ["unicode_msg", "ascii_msg"])
23+
24+
25+
class SpinnerMessage(_BaseMessage):
26+
def for_file(self, file):
27+
try:
28+
self.unicode_msg.encode(file.encoding)
29+
except (AttributeError, TypeError, UnicodeEncodeError):
30+
return self.ascii_msg
3031
else:
31-
return True
32-
return False
32+
return self.unicode_msg
3333

3434

3535
class Spinner(object):
3636
CLEAR_LINE = "\033[K"
3737
max_width = 120
38-
UNICODE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
39-
ASCII_FRAMES = ["|", "-", "+", "x", "*"]
38+
FRAMES = SpinnerMessage("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", "|-+x*")
39+
OK_FLAG = SpinnerMessage("✔ OK", "[ OK ]")
40+
FAIL_FLAG = SpinnerMessage("✖ FAIL", "[FAIL]")
41+
SKIP_FLAG = SpinnerMessage("⚠ SKIP", "[SKIP]")
4042

4143
def __init__(self, enabled=True, refresh_rate=0.1):
4244
self.refresh_rate = refresh_rate
4345
self.enabled = enabled
4446
self._file = sys.stdout
45-
self.frames = (
46-
self.UNICODE_FRAMES
47-
if _file_support_encoding(self.UNICODE_FRAMES, sys.stdout)
48-
else self.ASCII_FRAMES
49-
)
47+
self.frames = self.FRAMES.for_file(self._file)
5048
self.stream = py.io.TerminalWriter(file=self._file)
5149
self._envs = OrderedDict()
5250
self._frame_index = 0
@@ -105,13 +103,13 @@ def add(self, name):
105103
self._envs[name] = datetime.now()
106104

107105
def succeed(self, key):
108-
self.finalize(key, "✔ OK", green=True)
106+
self.finalize(key, self.OK_FLAG.for_file(self._file), green=True)
109107

110108
def fail(self, key):
111-
self.finalize(key, "✖ FAIL", red=True)
109+
self.finalize(key, self.FAIL_FLAG.for_file(self._file), red=True)
112110

113111
def skip(self, key):
114-
self.finalize(key, "⚠ SKIP", white=True)
112+
self.finalize(key, self.SKIP_FLAG.for_file(self._file), white=True)
115113

116114
def finalize(self, key, status, **kwargs):
117115
start_at = self._envs[key]

tests/unit/util/test_spinner.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,31 @@ def test_spinner_stdout_not_unicode(mocker, capfd):
113113
assert all(f in written for f in spin.frames)
114114

115115

116+
@freeze_time("2012-01-14")
117+
def test_spinner_report_not_unicode(mocker, capfd):
118+
stdout = mocker.patch("tox.util.spinner.sys.stdout")
119+
stdout.encoding = "ascii"
120+
# Disable color to simplify parsing output strings
121+
stdout.isatty = lambda: False
122+
with spinner.Spinner(refresh_rate=100) as spin:
123+
spin.stream.write(os.linesep)
124+
spin.add("ok!")
125+
spin.add("fail!")
126+
spin.add("skip!")
127+
spin.succeed("ok!")
128+
spin.fail("fail!")
129+
spin.skip("skip!")
130+
lines = "".join(args[0] for args, _ in stdout.write.call_args_list).split(os.linesep)
131+
del lines[0]
132+
expected = [
133+
"\r{}[ OK ] ok! in 0.0 seconds".format(spin.CLEAR_LINE),
134+
"\r{}[FAIL] fail! in 0.0 seconds".format(spin.CLEAR_LINE),
135+
"\r{}[SKIP] skip! in 0.0 seconds".format(spin.CLEAR_LINE),
136+
"\r{}".format(spin.CLEAR_LINE),
137+
]
138+
assert lines == expected
139+
140+
116141
@pytest.mark.parametrize(
117142
"seconds, expected",
118143
[

0 commit comments

Comments
 (0)