1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
r"""Tests for SoftwareVersion class and Terminal.get_software_version().
XTVERSION Query Format
======================
The XTVERSION query (CSI > q or ESC [ > q) requests the terminal software
name and version. Supported by modern terminal emulators including xterm,
mintty, iTerm2, tmux, kitty, WezTerm, foot, and VTE-based terminals.
Terminal response: DCS > | text ST (ESC P > | text ESC \)
Text format varies by terminal:
- XTerm(367)
- kitty(0.24.2)
- tmux 3.2a
- WezTerm 20220207-230252-0826fb06
- X.Org 7.7.0(370)
"""
# std
import time
# 3rd party
import pytest
# local
from .conftest import TEST_KEYBOARD, IS_WINDOWS
from .accessories import (
TestTerminal,
pty_test,
as_subprocess,
)
from blessed.keyboard import SoftwareVersion
pytestmark = pytest.mark.skipif(
not TEST_KEYBOARD or IS_WINDOWS,
reason="Timing-sensitive tests please do not run on build farms.")
@pytest.mark.parametrize("response,expected_name,expected_version", [
('\x1bP>|kitty(0.24.2)\x1b\\', 'kitty', '0.24.2'),
('\x1bP>|tmux 3.2a\x1b\\', 'tmux', '3.2a'),
('\x1bP>|foot\x1b\\', 'foot', ''),
('\x1bP>|WezTerm 20220207-230252-0826fb06\x1b\\', 'WezTerm', '20220207-230252-0826fb06'),
('\x1bP>|XTerm(367)\x1b\\', 'XTerm', '367'),
('\x1bP>|X.Org 7.7.0(370)\x1b\\', 'X.Org', '7.7.0(370)'),
])
def test_software_version_from_match(response, expected_name, expected_version):
"""Test SoftwareVersion.from_match() with various response formats."""
match = SoftwareVersion.RE_RESPONSE.match(response)
sv = SoftwareVersion.from_match(match)
assert sv is not None
assert sv.name == expected_name
assert sv.version == expected_version
assert sv.raw == response
@pytest.mark.parametrize("invalid_input", ['invalid', ''])
def test_software_version_from_match_invalid(invalid_input):
"""Test SoftwareVersion.from_match() with invalid input."""
match = SoftwareVersion.RE_RESPONSE.match(invalid_input)
assert match is None
def test_software_version_repr():
"""Test SoftwareVersion.__repr__()."""
sv = SoftwareVersion('\x1bP>|kitty(0.24.2)\x1b\\', 'kitty', '0.24.2')
repr_str = repr(sv)
assert 'SoftwareVersion' in repr_str
assert "name='kitty'" in repr_str
assert "version='0.24.2'" in repr_str
@pytest.mark.parametrize("text,expected_name,expected_version", [
('kitty(0.24.2)', 'kitty', '0.24.2'),
('tmux 3.2a', 'tmux', '3.2a'),
('foot', 'foot', ''),
('WezTerm 20220207-230252-0826fb06', 'WezTerm', '20220207-230252-0826fb06'),
])
def test_software_version_parse_text(text, expected_name, expected_version):
"""Test SoftwareVersion._parse_text() with various formats."""
name, version = SoftwareVersion._parse_text(text)
assert name == expected_name
assert version == expected_version
@pytest.mark.parametrize("response,expected_name,expected_version,test_suffix", [
('\x1bP>|kitty(0.24.2)\x1b\\', 'kitty', '0.24.2', 'OK'),
('\x1bP>|XTerm(367)\x1b\\', 'XTerm', '367', 'XTERM'),
('\x1bP>|tmux 3.2a\x1b\\', 'tmux', '3.2a', 'TMUX'),
('\x1bP>|WezTerm 20220207-230252-0826fb06\x1b\\',
'WezTerm', '20220207-230252-0826fb06', 'WEZTERM'),
])
def test_get_software_version_via_ungetch(response, expected_name, expected_version, test_suffix):
"""Test get_software_version() with various terminal responses via ungetch."""
def child(term):
term.ungetch(response)
sv = term.get_software_version(timeout=0.01)
assert sv is not None
assert sv.name == expected_name
assert sv.version == expected_version
return test_suffix.encode('ascii')
output = pty_test(child, parent_func=None,
test_name=f'test_get_software_version_{test_suffix.lower()}')
assert output == f'\x1b[>q{test_suffix}'
def test_get_software_version_timeout():
"""Test get_software_version() timeout without response."""
def child(term):
stime = time.time()
sv = term.get_software_version(timeout=0.1)
elapsed = time.time() - stime
assert sv is None
assert 0.08 <= elapsed <= 0.15
return b'TIMEOUT'
output = pty_test(child, parent_func=None, test_name='test_get_software_version_timeout')
assert output == '\x1b[>qTIMEOUT'
def test_get_software_version_force_bypass_cache():
"""Test get_software_version() with force=True bypasses cache."""
def child(term):
# First response: kitty 0.24.2
term.ungetch('\x1bP>|kitty(0.24.2)\x1b\\')
sv1 = term.get_software_version(timeout=0.01)
# Second response: XTerm 367 with force=True
term.ungetch('\x1bP>|XTerm(367)\x1b\\')
sv2 = term.get_software_version(timeout=0.01, force=True)
assert sv1 is not None
assert sv2 is not None
assert sv1.name == 'kitty'
assert sv2.name == 'XTerm'
assert sv1 is not sv2
return b'FORCED'
output = pty_test(child, parent_func=None,
test_name='test_get_software_version_force_bypass_cache')
assert output == '\x1b[>q\x1b[>qFORCED'
def test_get_software_version_no_force_uses_cache():
"""Test get_software_version() without force uses cached result."""
def child(term):
# First response: kitty 0.24.2
term.ungetch('\x1bP>|kitty(0.24.2)\x1b\\')
sv1 = term.get_software_version(timeout=0.01)
# Second query without force should use cache even with different ungetch data
# Response: XTerm 367 - but this is ignored due to cache
term.ungetch('\x1bP>|XTerm(367)\x1b\\')
sv2 = term.get_software_version(timeout=0.01, force=False)
assert sv1 is not None
assert sv2 is not None
assert sv1 is sv2
assert sv1.name == 'kitty'
assert sv2.name == 'kitty'
return b'NO_FORCE'
output = pty_test(child, parent_func=None,
test_name='test_get_software_version_no_force_uses_cache')
assert output == '\x1b[>qNO_FORCE'
def test_get_software_version_retry_after_timeout():
"""Test get_software_version() can retry after timeout."""
def child(term):
# First query fails (timeout)
sv1 = term.get_software_version(timeout=0.01)
# Second query succeeds: kitty 0.24.2
term.ungetch('\x1bP>|kitty(0.24.2)\x1b\\')
sv2 = term.get_software_version(timeout=0.01)
assert sv1 is None
assert sv2 is not None
assert sv2.name == 'kitty'
assert sv2.version == '0.24.2'
return b'RETRY'
output = pty_test(child, parent_func=None,
test_name='test_get_software_version_retry_after_timeout')
assert output == '\x1b[>q\x1b[>qRETRY'
def test_get_software_version_raw_stored():
"""Test SoftwareVersion stores raw response string."""
raw = '\x1bP>|kitty(0.24.2)\x1b\\'
match = SoftwareVersion.RE_RESPONSE.match(raw)
sv = SoftwareVersion.from_match(match)
assert sv is not None
assert sv.raw == raw
def test_get_software_version_not_a_tty():
"""Test get_software_version() returns None when not a TTY."""
@as_subprocess
def child():
import io
term = TestTerminal(stream=io.StringIO(), force_styling=True)
term._is_a_tty = False
sv = term.get_software_version(timeout=0.01)
assert sv is None
child()
def test_software_version_init():
"""Test SoftwareVersion.__init__() stores all parameters."""
sv = SoftwareVersion('\x1bP>|kitty(0.24.2)\x1b\\', 'kitty', '0.24.2')
assert sv.raw == '\x1bP>|kitty(0.24.2)\x1b\\'
assert sv.name == 'kitty'
assert sv.version == '0.24.2'
|