* Fix handling empty values of NO_COLOR and FORCE_COLOR Fix handling NO_COLOR and FORCE_COLOR environment variables to correctly be ignored when they are set to an empty value, as defined in the specification: > Command-line software which adds ANSI color to its output by default > should check for a NO_COLOR environment variable that, when present > *and not an empty string* (regardless of its value), prevents > the addition of ANSI color. (emphasis mine, https://no-color.org/) The same is true of FORCE_COLOR, https://force-color.org/. * Streamline testing for FORCE_COLOR and NO_COLOR Streamline the tests for FORCE_COLOR and NO_COLOR variables, and cover all possible cases (unset, set to empty, set to "1"). Combine the two assert functions into one taking boolean parameters. Mock file.isatty in all circumstances to ensure that the environment variables take precedence over the fallback value resulting from isatty check (or that the fallback is actually used, in the case of both FORCE_COLOR and NO_COLOR being unset).
309 lines
9.2 KiB
Python
309 lines
9.2 KiB
Python
import io
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Generator
|
|
from typing import Optional
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from _pytest._io import terminalwriter
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
|
|
|
|
# These tests were initially copied from py 1.8.1.
|
|
|
|
|
|
def test_terminal_width_COLUMNS(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setenv("COLUMNS", "42")
|
|
assert terminalwriter.get_terminal_width() == 42
|
|
monkeypatch.delenv("COLUMNS", raising=False)
|
|
|
|
|
|
def test_terminalwriter_width_bogus(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setattr(shutil, "get_terminal_size", mock.Mock(return_value=(10, 10)))
|
|
monkeypatch.delenv("COLUMNS", raising=False)
|
|
tw = terminalwriter.TerminalWriter()
|
|
assert tw.fullwidth == 80
|
|
|
|
|
|
def test_terminalwriter_computes_width(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setattr(terminalwriter, "get_terminal_width", lambda: 42)
|
|
tw = terminalwriter.TerminalWriter()
|
|
assert tw.fullwidth == 42
|
|
|
|
|
|
def test_terminalwriter_dumb_term_no_markup(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setattr(os, "environ", {"TERM": "dumb", "PATH": ""})
|
|
|
|
class MyFile:
|
|
closed = False
|
|
|
|
def isatty(self):
|
|
return True
|
|
|
|
with monkeypatch.context() as m:
|
|
m.setattr(sys, "stdout", MyFile())
|
|
assert sys.stdout.isatty()
|
|
tw = terminalwriter.TerminalWriter()
|
|
assert not tw.hasmarkup
|
|
|
|
|
|
def test_terminalwriter_not_unicode() -> None:
|
|
"""If the file doesn't support Unicode, the string is unicode-escaped (#7475)."""
|
|
buffer = io.BytesIO()
|
|
file = io.TextIOWrapper(buffer, encoding="cp1252")
|
|
tw = terminalwriter.TerminalWriter(file)
|
|
tw.write("hello 🌀 wôrld אבג", flush=True)
|
|
assert buffer.getvalue() == rb"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
|
|
|
|
|
|
win32 = int(sys.platform == "win32")
|
|
|
|
|
|
class TestTerminalWriter:
|
|
@pytest.fixture(params=["path", "stringio"])
|
|
def tw(
|
|
self, request, tmp_path: Path
|
|
) -> Generator[terminalwriter.TerminalWriter, None, None]:
|
|
if request.param == "path":
|
|
p = tmp_path.joinpath("tmpfile")
|
|
f = open(str(p), "w+", encoding="utf8")
|
|
tw = terminalwriter.TerminalWriter(f)
|
|
|
|
def getlines():
|
|
f.flush()
|
|
with open(str(p), encoding="utf8") as fp:
|
|
return fp.readlines()
|
|
|
|
elif request.param == "stringio":
|
|
f = io.StringIO()
|
|
tw = terminalwriter.TerminalWriter(f)
|
|
|
|
def getlines():
|
|
f.seek(0)
|
|
return f.readlines()
|
|
|
|
tw.getlines = getlines # type: ignore
|
|
tw.getvalue = lambda: "".join(getlines()) # type: ignore
|
|
|
|
with f:
|
|
yield tw
|
|
|
|
def test_line(self, tw) -> None:
|
|
tw.line("hello")
|
|
lines = tw.getlines()
|
|
assert len(lines) == 1
|
|
assert lines[0] == "hello\n"
|
|
|
|
def test_line_unicode(self, tw) -> None:
|
|
msg = "b\u00f6y"
|
|
tw.line(msg)
|
|
lines = tw.getlines()
|
|
assert lines[0] == msg + "\n"
|
|
|
|
def test_sep_no_title(self, tw) -> None:
|
|
tw.sep("-", fullwidth=60)
|
|
lines = tw.getlines()
|
|
assert len(lines) == 1
|
|
assert lines[0] == "-" * (60 - win32) + "\n"
|
|
|
|
def test_sep_with_title(self, tw) -> None:
|
|
tw.sep("-", "hello", fullwidth=60)
|
|
lines = tw.getlines()
|
|
assert len(lines) == 1
|
|
assert lines[0] == "-" * 26 + " hello " + "-" * (27 - win32) + "\n"
|
|
|
|
def test_sep_longer_than_width(self, tw) -> None:
|
|
tw.sep("-", "a" * 10, fullwidth=5)
|
|
(line,) = tw.getlines()
|
|
# even though the string is wider than the line, still have a separator
|
|
assert line == "- aaaaaaaaaa -\n"
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi")
|
|
@pytest.mark.parametrize("bold", (True, False))
|
|
@pytest.mark.parametrize("color", ("red", "green"))
|
|
def test_markup(self, tw, bold: bool, color: str) -> None:
|
|
text = tw.markup("hello", **{color: True, "bold": bold})
|
|
assert "hello" in text
|
|
|
|
def test_markup_bad(self, tw) -> None:
|
|
with pytest.raises(ValueError):
|
|
tw.markup("x", wronkw=3)
|
|
with pytest.raises(ValueError):
|
|
tw.markup("x", wronkw=0)
|
|
|
|
def test_line_write_markup(self, tw) -> None:
|
|
tw.hasmarkup = True
|
|
tw.line("x", bold=True)
|
|
tw.write("x\n", red=True)
|
|
lines = tw.getlines()
|
|
if sys.platform != "win32":
|
|
assert len(lines[0]) >= 2, lines
|
|
assert len(lines[1]) >= 2, lines
|
|
|
|
def test_attr_fullwidth(self, tw) -> None:
|
|
tw.sep("-", "hello", fullwidth=70)
|
|
tw.fullwidth = 70
|
|
tw.sep("-", "hello")
|
|
lines = tw.getlines()
|
|
assert len(lines[0]) == len(lines[1])
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi")
|
|
def test_attr_hasmarkup() -> None:
|
|
file = io.StringIO()
|
|
tw = terminalwriter.TerminalWriter(file)
|
|
assert not tw.hasmarkup
|
|
tw.hasmarkup = True
|
|
tw.line("hello", bold=True)
|
|
s = file.getvalue()
|
|
assert len(s) > len("hello\n")
|
|
assert "\x1b[1m" in s
|
|
assert "\x1b[0m" in s
|
|
|
|
|
|
def assert_color(expected: bool, default: Optional[bool] = None) -> None:
|
|
file = io.StringIO()
|
|
if default is None:
|
|
default = not expected
|
|
file.isatty = lambda: default # type: ignore
|
|
tw = terminalwriter.TerminalWriter(file=file)
|
|
assert tw.hasmarkup is expected
|
|
tw.line("hello", bold=True)
|
|
s = file.getvalue()
|
|
if expected:
|
|
assert len(s) > len("hello\n")
|
|
assert "\x1b[1m" in s
|
|
assert "\x1b[0m" in s
|
|
else:
|
|
assert s == "hello\n"
|
|
|
|
|
|
def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setitem(os.environ, "PY_COLORS", "1")
|
|
assert_color(True)
|
|
|
|
|
|
def test_should_not_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setitem(os.environ, "PY_COLORS", "0")
|
|
assert_color(False)
|
|
|
|
|
|
def test_should_not_do_markup_NO_COLOR(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setitem(os.environ, "NO_COLOR", "1")
|
|
assert_color(False)
|
|
|
|
|
|
def test_should_do_markup_FORCE_COLOR(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setitem(os.environ, "FORCE_COLOR", "1")
|
|
assert_color(True)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["NO_COLOR", "FORCE_COLOR", "expected"],
|
|
[
|
|
("1", "1", False),
|
|
("", "1", True),
|
|
("1", "", False),
|
|
],
|
|
)
|
|
def test_NO_COLOR_and_FORCE_COLOR(
|
|
monkeypatch: MonkeyPatch,
|
|
NO_COLOR: str,
|
|
FORCE_COLOR: str,
|
|
expected: bool,
|
|
) -> None:
|
|
monkeypatch.setitem(os.environ, "NO_COLOR", NO_COLOR)
|
|
monkeypatch.setitem(os.environ, "FORCE_COLOR", FORCE_COLOR)
|
|
assert_color(expected)
|
|
|
|
|
|
def test_empty_NO_COLOR_and_FORCE_COLOR_ignored(monkeypatch: MonkeyPatch) -> None:
|
|
monkeypatch.setitem(os.environ, "NO_COLOR", "")
|
|
monkeypatch.setitem(os.environ, "FORCE_COLOR", "")
|
|
assert_color(True, True)
|
|
assert_color(False, False)
|
|
|
|
|
|
class TestTerminalWriterLineWidth:
|
|
def test_init(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
assert tw.width_of_current_line == 0
|
|
|
|
def test_update(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
tw.write("hello world")
|
|
assert tw.width_of_current_line == 11
|
|
|
|
def test_update_with_newline(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
tw.write("hello\nworld")
|
|
assert tw.width_of_current_line == 5
|
|
|
|
def test_update_with_wide_text(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
tw.write("乇乂ㄒ尺卂 ㄒ卄丨匚匚")
|
|
assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2
|
|
|
|
def test_composed(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
text = "café food"
|
|
assert len(text) == 9
|
|
tw.write(text)
|
|
assert tw.width_of_current_line == 9
|
|
|
|
def test_combining(self) -> None:
|
|
tw = terminalwriter.TerminalWriter()
|
|
text = "café food"
|
|
assert len(text) == 10
|
|
tw.write(text)
|
|
assert tw.width_of_current_line == 9
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("has_markup", "code_highlight", "expected"),
|
|
[
|
|
pytest.param(
|
|
True,
|
|
True,
|
|
"{reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
|
|
id="with markup and code_highlight",
|
|
),
|
|
pytest.param(
|
|
True,
|
|
False,
|
|
"assert 0\n",
|
|
id="with markup but no code_highlight",
|
|
),
|
|
pytest.param(
|
|
False,
|
|
True,
|
|
"assert 0\n",
|
|
id="without markup but with code_highlight",
|
|
),
|
|
pytest.param(
|
|
False,
|
|
False,
|
|
"assert 0\n",
|
|
id="neither markup nor code_highlight",
|
|
),
|
|
],
|
|
)
|
|
def test_code_highlight(has_markup, code_highlight, expected, color_mapping):
|
|
f = io.StringIO()
|
|
tw = terminalwriter.TerminalWriter(f)
|
|
tw.hasmarkup = has_markup
|
|
tw.code_highlight = code_highlight
|
|
tw._write_source(["assert 0"])
|
|
|
|
assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected])
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=re.escape("indents size (2) should have same size as lines (1)"),
|
|
):
|
|
tw._write_source(["assert 0"], [" ", " "])
|