import io import os import re import shutil import sys from pathlib import Path from typing import Generator 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() == br"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_set(): file = io.StringIO() tw = terminalwriter.TerminalWriter(file) assert tw.hasmarkup 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_not_set(): f = io.StringIO() f.isatty = lambda: True # type: ignore tw = terminalwriter.TerminalWriter(file=f) assert not tw.hasmarkup tw.line("hello", bold=True) s = f.getvalue() 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_set() def test_should_not_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "PY_COLORS", "0") assert_color_not_set() def test_should_not_do_markup_NO_COLOR(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "NO_COLOR", "1") assert_color_not_set() def test_should_do_markup_FORCE_COLOR(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") assert_color_set() def test_should_not_do_markup_NO_COLOR_and_FORCE_COLOR( monkeypatch: MonkeyPatch, ) -> None: monkeypatch.setitem(os.environ, "NO_COLOR", "1") monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") assert_color_not_set() 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, "{kw}assert{hl-reset} {number}0{hl-reset}\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"], [" ", " "])