mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +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)
							 |