mirror of
https://github.com/python/cpython.git
synced 2025-10-20 08:23:47 +00:00
652 lines
22 KiB
Python
652 lines
22 KiB
Python
![]() |
"""Tests comparing PyREPL's pure Python curses implementation with the standard curses module."""
|
||
|
|
||
|
import json
|
||
|
import os
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import unittest
|
||
|
from test.support import requires, has_subprocess_support
|
||
|
from textwrap import dedent
|
||
|
|
||
|
# Only run these tests if curses is available
|
||
|
requires("curses")
|
||
|
|
||
|
try:
|
||
|
import _curses
|
||
|
except ImportError:
|
||
|
try:
|
||
|
import curses as _curses
|
||
|
except ImportError:
|
||
|
_curses = None
|
||
|
|
||
|
from _pyrepl import terminfo
|
||
|
|
||
|
|
||
|
ABSENT_STRING = terminfo.ABSENT_STRING
|
||
|
CANCELLED_STRING = terminfo.CANCELLED_STRING
|
||
|
|
||
|
|
||
|
class TestCursesCompatibility(unittest.TestCase):
|
||
|
"""Test that PyREPL's curses implementation matches the standard curses behavior.
|
||
|
|
||
|
Python's `curses` doesn't allow calling `setupterm()` again with a different
|
||
|
$TERM in the same process, so we subprocess all `curses` tests to get correctly
|
||
|
set up terminfo."""
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
if _curses is None:
|
||
|
raise unittest.SkipTest(
|
||
|
"`curses` capability provided to regrtest but `_curses` not importable"
|
||
|
)
|
||
|
|
||
|
if not has_subprocess_support:
|
||
|
raise unittest.SkipTest("test module requires subprocess")
|
||
|
|
||
|
# we need to ensure there's a terminfo database on the system and that
|
||
|
# `infocmp` works
|
||
|
cls.infocmp("dumb")
|
||
|
|
||
|
def setUp(self):
|
||
|
self.original_term = os.environ.get("TERM", None)
|
||
|
|
||
|
def tearDown(self):
|
||
|
if self.original_term is not None:
|
||
|
os.environ["TERM"] = self.original_term
|
||
|
elif "TERM" in os.environ:
|
||
|
del os.environ["TERM"]
|
||
|
|
||
|
@classmethod
|
||
|
def infocmp(cls, term) -> list[str]:
|
||
|
all_caps = []
|
||
|
try:
|
||
|
result = subprocess.run(
|
||
|
["infocmp", "-l1", term],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
check=True,
|
||
|
)
|
||
|
except Exception:
|
||
|
raise unittest.SkipTest("calling `infocmp` failed on the system")
|
||
|
|
||
|
for line in result.stdout.splitlines():
|
||
|
line = line.strip()
|
||
|
if line.startswith("#"):
|
||
|
if "terminfo" not in line and "termcap" in line:
|
||
|
# PyREPL terminfo doesn't parse termcap databases
|
||
|
raise unittest.SkipTest(
|
||
|
"curses using termcap.db: no terminfo database on"
|
||
|
" the system"
|
||
|
)
|
||
|
elif "=" in line:
|
||
|
cap_name = line.split("=")[0]
|
||
|
all_caps.append(cap_name)
|
||
|
|
||
|
return all_caps
|
||
|
|
||
|
def test_setupterm_basic(self):
|
||
|
"""Test basic setupterm functionality."""
|
||
|
# Test with explicit terminal type
|
||
|
test_terms = ["xterm", "xterm-256color", "vt100", "ansi"]
|
||
|
|
||
|
for term in test_terms:
|
||
|
with self.subTest(term=term):
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
try:
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
print(json.dumps({{"success": True}}))
|
||
|
except Exception as e:
|
||
|
print(json.dumps({{"success": False, "error": str(e)}}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
std_success = ncurses_data["success"]
|
||
|
|
||
|
# Set up with PyREPL curses
|
||
|
try:
|
||
|
terminfo.TermInfo(term, fallback=False)
|
||
|
pyrepl_success = True
|
||
|
except Exception as e:
|
||
|
pyrepl_success = False
|
||
|
pyrepl_error = e
|
||
|
|
||
|
# Both should succeed or both should fail
|
||
|
if std_success:
|
||
|
self.assertTrue(
|
||
|
pyrepl_success,
|
||
|
f"Standard curses succeeded but PyREPL failed for {term}",
|
||
|
)
|
||
|
else:
|
||
|
# If standard curses failed, PyREPL might still succeed with fallback
|
||
|
# This is acceptable as PyREPL has hardcoded fallbacks
|
||
|
pass
|
||
|
|
||
|
def test_setupterm_none(self):
|
||
|
"""Test setupterm with None (uses TERM from environment)."""
|
||
|
# Test with current TERM
|
||
|
ncurses_code = dedent(
|
||
|
"""
|
||
|
import _curses
|
||
|
import json
|
||
|
try:
|
||
|
_curses.setupterm(None, 1)
|
||
|
print(json.dumps({"success": True}))
|
||
|
except Exception as e:
|
||
|
print(json.dumps({"success": False, "error": str(e)}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
std_success = ncurses_data["success"]
|
||
|
|
||
|
try:
|
||
|
terminfo.TermInfo(None, fallback=False)
|
||
|
pyrepl_success = True
|
||
|
except Exception:
|
||
|
pyrepl_success = False
|
||
|
|
||
|
# Both should have same result
|
||
|
if std_success:
|
||
|
self.assertTrue(
|
||
|
pyrepl_success,
|
||
|
"Standard curses succeeded but PyREPL failed for None",
|
||
|
)
|
||
|
|
||
|
def test_tigetstr_common_capabilities(self):
|
||
|
"""Test tigetstr for common terminal capabilities."""
|
||
|
# Test with a known terminal type
|
||
|
term = "xterm"
|
||
|
|
||
|
# Get ALL capabilities from infocmp
|
||
|
all_caps = self.infocmp(term)
|
||
|
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
results = {{}}
|
||
|
for cap in {repr(all_caps)}:
|
||
|
try:
|
||
|
val = _curses.tigetstr(cap)
|
||
|
if val is None:
|
||
|
results[cap] = None
|
||
|
elif val == -1:
|
||
|
results[cap] = -1
|
||
|
else:
|
||
|
results[cap] = list(val)
|
||
|
except BaseException:
|
||
|
results[cap] = "error"
|
||
|
print(json.dumps(results))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
self.assertEqual(
|
||
|
result.returncode, 0, f"Failed to run ncurses: {result.stderr}"
|
||
|
)
|
||
|
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# Test every single capability
|
||
|
for cap in all_caps:
|
||
|
if cap not in ncurses_data or ncurses_data[cap] == "error":
|
||
|
continue
|
||
|
|
||
|
with self.subTest(capability=cap):
|
||
|
ncurses_val = ncurses_data[cap]
|
||
|
if isinstance(ncurses_val, list):
|
||
|
ncurses_val = bytes(ncurses_val)
|
||
|
|
||
|
pyrepl_val = ti.get(cap)
|
||
|
|
||
|
self.assertEqual(
|
||
|
pyrepl_val,
|
||
|
ncurses_val,
|
||
|
f"Capability {cap}: ncurses={repr(ncurses_val)}, "
|
||
|
f"pyrepl={repr(pyrepl_val)}",
|
||
|
)
|
||
|
|
||
|
def test_tigetstr_input_types(self):
|
||
|
"""Test tigetstr with different input types."""
|
||
|
term = "xterm"
|
||
|
cap = "cup"
|
||
|
|
||
|
# Test standard curses behavior with string in subprocess
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
|
||
|
# Test with string input
|
||
|
try:
|
||
|
std_str_result = _curses.tigetstr({repr(cap)})
|
||
|
std_accepts_str = True
|
||
|
if std_str_result is None:
|
||
|
std_str_val = None
|
||
|
elif std_str_result == -1:
|
||
|
std_str_val = -1
|
||
|
else:
|
||
|
std_str_val = list(std_str_result)
|
||
|
except TypeError:
|
||
|
std_accepts_str = False
|
||
|
std_str_val = None
|
||
|
|
||
|
print(json.dumps({{
|
||
|
"accepts_str": std_accepts_str,
|
||
|
"str_result": std_str_val
|
||
|
}}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
# PyREPL setup
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# PyREPL behavior with string
|
||
|
try:
|
||
|
pyrepl_str_result = ti.get(cap)
|
||
|
pyrepl_accepts_str = True
|
||
|
except TypeError:
|
||
|
pyrepl_accepts_str = False
|
||
|
|
||
|
# PyREPL should also only accept strings for compatibility
|
||
|
with self.assertRaises(TypeError):
|
||
|
ti.get(cap.encode("ascii"))
|
||
|
|
||
|
# Both should accept string input
|
||
|
self.assertEqual(
|
||
|
pyrepl_accepts_str,
|
||
|
ncurses_data["accepts_str"],
|
||
|
"PyREPL and standard curses should have same string handling",
|
||
|
)
|
||
|
self.assertTrue(
|
||
|
pyrepl_accepts_str, "PyREPL should accept string input"
|
||
|
)
|
||
|
|
||
|
def test_tparm_basic(self):
|
||
|
"""Test basic tparm functionality."""
|
||
|
term = "xterm"
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# Test cursor positioning (cup)
|
||
|
cup = ti.get("cup")
|
||
|
if cup and cup not in {ABSENT_STRING, CANCELLED_STRING}:
|
||
|
# Test various parameter combinations
|
||
|
test_cases = [
|
||
|
(0, 0), # Top-left
|
||
|
(5, 10), # Arbitrary position
|
||
|
(23, 79), # Bottom-right of standard terminal
|
||
|
(999, 999), # Large values
|
||
|
]
|
||
|
|
||
|
# Get ncurses results in subprocess
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
|
||
|
# Get cup capability
|
||
|
cup = _curses.tigetstr('cup')
|
||
|
results = {{}}
|
||
|
|
||
|
for row, col in {repr(test_cases)}:
|
||
|
try:
|
||
|
result = _curses.tparm(cup, row, col)
|
||
|
results[f"{{row}},{{col}}"] = list(result)
|
||
|
except Exception as e:
|
||
|
results[f"{{row}},{{col}}"] = {{"error": str(e)}}
|
||
|
|
||
|
print(json.dumps(results))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
self.assertEqual(
|
||
|
result.returncode, 0, f"Failed to run ncurses: {result.stderr}"
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
for row, col in test_cases:
|
||
|
with self.subTest(row=row, col=col):
|
||
|
# Standard curses tparm from subprocess
|
||
|
key = f"{row},{col}"
|
||
|
if (
|
||
|
isinstance(ncurses_data[key], dict)
|
||
|
and "error" in ncurses_data[key]
|
||
|
):
|
||
|
self.fail(
|
||
|
f"ncurses tparm failed: {ncurses_data[key]['error']}"
|
||
|
)
|
||
|
std_result = bytes(ncurses_data[key])
|
||
|
|
||
|
# PyREPL curses tparm
|
||
|
pyrepl_result = terminfo.tparm(cup, row, col)
|
||
|
|
||
|
# Results should be identical
|
||
|
self.assertEqual(
|
||
|
pyrepl_result,
|
||
|
std_result,
|
||
|
f"tparm(cup, {row}, {col}): "
|
||
|
f"std={repr(std_result)}, pyrepl={repr(pyrepl_result)}",
|
||
|
)
|
||
|
else:
|
||
|
raise unittest.SkipTest(
|
||
|
"test_tparm_basic() requires the `cup` capability"
|
||
|
)
|
||
|
|
||
|
def test_tparm_multiple_params(self):
|
||
|
"""Test tparm with capabilities using multiple parameters."""
|
||
|
term = "xterm"
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# Test capabilities that take parameters
|
||
|
param_caps = {
|
||
|
"cub": 1, # cursor_left with count
|
||
|
"cuf": 1, # cursor_right with count
|
||
|
"cuu": 1, # cursor_up with count
|
||
|
"cud": 1, # cursor_down with count
|
||
|
"dch": 1, # delete_character with count
|
||
|
"ich": 1, # insert_character with count
|
||
|
}
|
||
|
|
||
|
# Get all capabilities from PyREPL first
|
||
|
pyrepl_caps = {}
|
||
|
for cap in param_caps:
|
||
|
cap_value = ti.get(cap)
|
||
|
if cap_value and cap_value not in {
|
||
|
ABSENT_STRING,
|
||
|
CANCELLED_STRING,
|
||
|
}:
|
||
|
pyrepl_caps[cap] = cap_value
|
||
|
|
||
|
if not pyrepl_caps:
|
||
|
self.skipTest("No parametrized capabilities found")
|
||
|
|
||
|
# Get ncurses results in subprocess
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
|
||
|
param_caps = {repr(param_caps)}
|
||
|
test_values = [1, 5, 10, 99]
|
||
|
results = {{}}
|
||
|
|
||
|
for cap in param_caps:
|
||
|
cap_value = _curses.tigetstr(cap)
|
||
|
if cap_value and cap_value != -1:
|
||
|
for value in test_values:
|
||
|
try:
|
||
|
result = _curses.tparm(cap_value, value)
|
||
|
results[f"{{cap}},{{value}}"] = list(result)
|
||
|
except Exception as e:
|
||
|
results[f"{{cap}},{{value}}"] = {{"error": str(e)}}
|
||
|
|
||
|
print(json.dumps(results))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
self.assertEqual(
|
||
|
result.returncode, 0, f"Failed to run ncurses: {result.stderr}"
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
for cap, cap_value in pyrepl_caps.items():
|
||
|
with self.subTest(capability=cap):
|
||
|
# Test with different parameter values
|
||
|
for value in [1, 5, 10, 99]:
|
||
|
key = f"{cap},{value}"
|
||
|
if key in ncurses_data:
|
||
|
if (
|
||
|
isinstance(ncurses_data[key], dict)
|
||
|
and "error" in ncurses_data[key]
|
||
|
):
|
||
|
self.fail(
|
||
|
f"ncurses tparm failed: {ncurses_data[key]['error']}"
|
||
|
)
|
||
|
std_result = bytes(ncurses_data[key])
|
||
|
|
||
|
pyrepl_result = terminfo.tparm(cap_value, value)
|
||
|
self.assertEqual(
|
||
|
pyrepl_result,
|
||
|
std_result,
|
||
|
f"tparm({cap}, {value}): "
|
||
|
f"std={repr(std_result)}, pyrepl={repr(pyrepl_result)}",
|
||
|
)
|
||
|
|
||
|
def test_tparm_null_handling(self):
|
||
|
"""Test tparm with None/null input."""
|
||
|
term = "xterm"
|
||
|
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
|
||
|
# Test with None
|
||
|
try:
|
||
|
_curses.tparm(None)
|
||
|
raises_typeerror = False
|
||
|
except TypeError:
|
||
|
raises_typeerror = True
|
||
|
except Exception as e:
|
||
|
raises_typeerror = False
|
||
|
error_type = type(e).__name__
|
||
|
|
||
|
print(json.dumps({{"raises_typeerror": raises_typeerror}}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
# PyREPL setup
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# Test with None - both should raise TypeError
|
||
|
if ncurses_data["raises_typeerror"]:
|
||
|
with self.assertRaises(TypeError):
|
||
|
terminfo.tparm(None)
|
||
|
else:
|
||
|
# If ncurses doesn't raise TypeError, PyREPL shouldn't either
|
||
|
try:
|
||
|
terminfo.tparm(None)
|
||
|
except TypeError:
|
||
|
self.fail("PyREPL raised TypeError but ncurses did not")
|
||
|
|
||
|
def test_special_terminals(self):
|
||
|
"""Test with special terminal types."""
|
||
|
special_terms = [
|
||
|
"dumb", # Minimal terminal
|
||
|
"unknown", # Should fall back to defaults
|
||
|
"linux", # Linux console
|
||
|
"screen", # GNU Screen
|
||
|
"tmux", # tmux
|
||
|
]
|
||
|
|
||
|
# Get all string capabilities from ncurses
|
||
|
for term in special_terms:
|
||
|
with self.subTest(term=term):
|
||
|
all_caps = self.infocmp(term)
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
import sys
|
||
|
|
||
|
try:
|
||
|
_curses.setupterm({repr(term)}, 1)
|
||
|
results = {{}}
|
||
|
for cap in {repr(all_caps)}:
|
||
|
try:
|
||
|
val = _curses.tigetstr(cap)
|
||
|
if val is None:
|
||
|
results[cap] = None
|
||
|
elif val == -1:
|
||
|
results[cap] = -1
|
||
|
else:
|
||
|
# Convert bytes to list of ints for JSON
|
||
|
results[cap] = list(val)
|
||
|
except BaseException:
|
||
|
results[cap] = "error"
|
||
|
print(json.dumps(results))
|
||
|
except Exception as e:
|
||
|
print(json.dumps({{"error": str(e)}}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
# Get ncurses results
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
if result.returncode != 0:
|
||
|
self.fail(
|
||
|
f"Failed to get ncurses data for {term}: {result.stderr}"
|
||
|
)
|
||
|
|
||
|
try:
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
except json.JSONDecodeError:
|
||
|
self.fail(
|
||
|
f"Failed to parse ncurses output for {term}: {result.stdout}"
|
||
|
)
|
||
|
|
||
|
if "error" in ncurses_data and len(ncurses_data) == 1:
|
||
|
# ncurses failed to setup this terminal
|
||
|
# PyREPL should still work with fallback
|
||
|
ti = terminfo.TermInfo(term, fallback=True)
|
||
|
continue
|
||
|
|
||
|
ti = terminfo.TermInfo(term, fallback=False)
|
||
|
|
||
|
# Compare all capabilities
|
||
|
for cap in all_caps:
|
||
|
if cap not in ncurses_data:
|
||
|
continue
|
||
|
|
||
|
with self.subTest(term=term, capability=cap):
|
||
|
ncurses_val = ncurses_data[cap]
|
||
|
if isinstance(ncurses_val, list):
|
||
|
# Convert back to bytes
|
||
|
ncurses_val = bytes(ncurses_val)
|
||
|
|
||
|
pyrepl_val = ti.get(cap)
|
||
|
|
||
|
# Both should return the same value
|
||
|
self.assertEqual(
|
||
|
pyrepl_val,
|
||
|
ncurses_val,
|
||
|
f"Capability {cap} for {term}: "
|
||
|
f"ncurses={repr(ncurses_val)}, "
|
||
|
f"pyrepl={repr(pyrepl_val)}",
|
||
|
)
|
||
|
|
||
|
def test_terminfo_fallback(self):
|
||
|
"""Test that PyREPL falls back gracefully when terminfo is not found."""
|
||
|
# Use a non-existent terminal type
|
||
|
fake_term = "nonexistent-terminal-type-12345"
|
||
|
|
||
|
# Check if standard curses can setup this terminal in subprocess
|
||
|
ncurses_code = dedent(
|
||
|
f"""
|
||
|
import _curses
|
||
|
import json
|
||
|
try:
|
||
|
_curses.setupterm({repr(fake_term)}, 1)
|
||
|
print(json.dumps({{"success": True}}))
|
||
|
except _curses.error:
|
||
|
print(json.dumps({{"success": False, "error": "curses.error"}}))
|
||
|
except Exception as e:
|
||
|
print(json.dumps({{"success": False, "error": str(e)}}))
|
||
|
"""
|
||
|
)
|
||
|
|
||
|
result = subprocess.run(
|
||
|
[sys.executable, "-c", ncurses_code],
|
||
|
capture_output=True,
|
||
|
text=True,
|
||
|
)
|
||
|
ncurses_data = json.loads(result.stdout)
|
||
|
|
||
|
if ncurses_data["success"]:
|
||
|
# If it succeeded, skip this test as we can't test fallback
|
||
|
self.skipTest(
|
||
|
f"System unexpectedly has terminfo for '{fake_term}'"
|
||
|
)
|
||
|
|
||
|
# PyREPL should succeed with fallback
|
||
|
try:
|
||
|
ti = terminfo.TermInfo(fake_term, fallback=True)
|
||
|
pyrepl_ok = True
|
||
|
except Exception:
|
||
|
pyrepl_ok = False
|
||
|
|
||
|
self.assertTrue(
|
||
|
pyrepl_ok, "PyREPL should fall back for unknown terminals"
|
||
|
)
|
||
|
|
||
|
# Should still be able to get basic capabilities
|
||
|
bel = ti.get("bel")
|
||
|
self.assertIsNotNone(
|
||
|
bel, "PyREPL should provide basic capabilities after fallback"
|
||
|
)
|
||
|
|
||
|
def test_invalid_terminal_names(self):
|
||
|
cases = [
|
||
|
(42, TypeError),
|
||
|
("", ValueError),
|
||
|
("w\x00t", ValueError),
|
||
|
(f"..{os.sep}name", ValueError),
|
||
|
]
|
||
|
|
||
|
for term, exc in cases:
|
||
|
with self.subTest(term=term):
|
||
|
with self.assertRaises(exc):
|
||
|
terminfo._validate_terminal_name_or_raise(term)
|