mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			651 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			651 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)
 |