Home | History | Annotate | Download | only in test
      1 """
      2 Very minimal unittests for parts of the readline module.
      3 """
      4 from contextlib import ExitStack
      5 from errno import EIO
      6 import locale
      7 import os
      8 import selectors
      9 import subprocess
     10 import sys
     11 import tempfile
     12 import unittest
     13 from test.support import import_module, unlink, temp_dir, TESTFN, verbose
     14 from test.support.script_helper import assert_python_ok
     15 
     16 # Skip tests if there is no readline module
     17 readline = import_module('readline')
     18 
     19 if hasattr(readline, "_READLINE_LIBRARY_VERSION"):
     20     is_editline = ("EditLine wrapper" in readline._READLINE_LIBRARY_VERSION)
     21 else:
     22     is_editline = (readline.__doc__ and "libedit" in readline.__doc__)
     23 
     24 
     25 def setUpModule():
     26     if verbose:
     27         # Python implementations other than CPython may not have
     28         # these private attributes
     29         if hasattr(readline, "_READLINE_VERSION"):
     30             print(f"readline version: {readline._READLINE_VERSION:#x}")
     31             print(f"readline runtime version: {readline._READLINE_RUNTIME_VERSION:#x}")
     32         if hasattr(readline, "_READLINE_LIBRARY_VERSION"):
     33             print(f"readline library version: {readline._READLINE_LIBRARY_VERSION!r}")
     34         print(f"use libedit emulation? {is_editline}")
     35 
     36 
     37 @unittest.skipUnless(hasattr(readline, "clear_history"),
     38                      "The history update test cannot be run because the "
     39                      "clear_history method is not available.")
     40 class TestHistoryManipulation (unittest.TestCase):
     41     """
     42     These tests were added to check that the libedit emulation on OSX and the
     43     "real" readline have the same interface for history manipulation. That's
     44     why the tests cover only a small subset of the interface.
     45     """
     46 
     47     def testHistoryUpdates(self):
     48         readline.clear_history()
     49 
     50         readline.add_history("first line")
     51         readline.add_history("second line")
     52 
     53         self.assertEqual(readline.get_history_item(0), None)
     54         self.assertEqual(readline.get_history_item(1), "first line")
     55         self.assertEqual(readline.get_history_item(2), "second line")
     56 
     57         readline.replace_history_item(0, "replaced line")
     58         self.assertEqual(readline.get_history_item(0), None)
     59         self.assertEqual(readline.get_history_item(1), "replaced line")
     60         self.assertEqual(readline.get_history_item(2), "second line")
     61 
     62         self.assertEqual(readline.get_current_history_length(), 2)
     63 
     64         readline.remove_history_item(0)
     65         self.assertEqual(readline.get_history_item(0), None)
     66         self.assertEqual(readline.get_history_item(1), "second line")
     67 
     68         self.assertEqual(readline.get_current_history_length(), 1)
     69 
     70     @unittest.skipUnless(hasattr(readline, "append_history_file"),
     71                          "append_history not available")
     72     def test_write_read_append(self):
     73         hfile = tempfile.NamedTemporaryFile(delete=False)
     74         hfile.close()
     75         hfilename = hfile.name
     76         self.addCleanup(unlink, hfilename)
     77 
     78         # test write-clear-read == nop
     79         readline.clear_history()
     80         readline.add_history("first line")
     81         readline.add_history("second line")
     82         readline.write_history_file(hfilename)
     83 
     84         readline.clear_history()
     85         self.assertEqual(readline.get_current_history_length(), 0)
     86 
     87         readline.read_history_file(hfilename)
     88         self.assertEqual(readline.get_current_history_length(), 2)
     89         self.assertEqual(readline.get_history_item(1), "first line")
     90         self.assertEqual(readline.get_history_item(2), "second line")
     91 
     92         # test append
     93         readline.append_history_file(1, hfilename)
     94         readline.clear_history()
     95         readline.read_history_file(hfilename)
     96         self.assertEqual(readline.get_current_history_length(), 3)
     97         self.assertEqual(readline.get_history_item(1), "first line")
     98         self.assertEqual(readline.get_history_item(2), "second line")
     99         self.assertEqual(readline.get_history_item(3), "second line")
    100 
    101         # test 'no such file' behaviour
    102         os.unlink(hfilename)
    103         with self.assertRaises(FileNotFoundError):
    104             readline.append_history_file(1, hfilename)
    105 
    106         # write_history_file can create the target
    107         readline.write_history_file(hfilename)
    108 
    109     def test_nonascii_history(self):
    110         readline.clear_history()
    111         try:
    112             readline.add_history("entre 1")
    113         except UnicodeEncodeError as err:
    114             self.skipTest("Locale cannot encode test data: " + format(err))
    115         readline.add_history("entre 2")
    116         readline.replace_history_item(1, "entre 22")
    117         readline.write_history_file(TESTFN)
    118         self.addCleanup(os.remove, TESTFN)
    119         readline.clear_history()
    120         readline.read_history_file(TESTFN)
    121         if is_editline:
    122             # An add_history() call seems to be required for get_history_
    123             # item() to register items from the file
    124             readline.add_history("dummy")
    125         self.assertEqual(readline.get_history_item(1), "entre 1")
    126         self.assertEqual(readline.get_history_item(2), "entre 22")
    127 
    128 
    129 class TestReadline(unittest.TestCase):
    130 
    131     @unittest.skipIf(readline._READLINE_VERSION < 0x0601 and not is_editline,
    132                      "not supported in this library version")
    133     def test_init(self):
    134         # Issue #19884: Ensure that the ANSI sequence "\033[1034h" is not
    135         # written into stdout when the readline module is imported and stdout
    136         # is redirected to a pipe.
    137         rc, stdout, stderr = assert_python_ok('-c', 'import readline',
    138                                               TERM='xterm-256color')
    139         self.assertEqual(stdout, b'')
    140 
    141     auto_history_script = """\
    142 import readline
    143 readline.set_auto_history({})
    144 input()
    145 print("History length:", readline.get_current_history_length())
    146 """
    147 
    148     def test_auto_history_enabled(self):
    149         output = run_pty(self.auto_history_script.format(True))
    150         self.assertIn(b"History length: 1\r\n", output)
    151 
    152     def test_auto_history_disabled(self):
    153         output = run_pty(self.auto_history_script.format(False))
    154         self.assertIn(b"History length: 0\r\n", output)
    155 
    156     def test_nonascii(self):
    157         loc = locale.setlocale(locale.LC_CTYPE, None)
    158         if loc in ('C', 'POSIX'):
    159             # bpo-29240: On FreeBSD, if the LC_CTYPE locale is C or POSIX,
    160             # writing and reading non-ASCII bytes into/from a TTY works, but
    161             # readline or ncurses ignores non-ASCII bytes on read.
    162             self.skipTest(f"the LC_CTYPE locale is {loc!r}")
    163 
    164         try:
    165             readline.add_history("\xEB\xEF")
    166         except UnicodeEncodeError as err:
    167             self.skipTest("Locale cannot encode test data: " + format(err))
    168 
    169         script = r"""import readline
    170 
    171 is_editline = readline.__doc__ and "libedit" in readline.__doc__
    172 inserted = "[\xEFnserted]"
    173 macro = "|t\xEB[after]"
    174 set_pre_input_hook = getattr(readline, "set_pre_input_hook", None)
    175 if is_editline or not set_pre_input_hook:
    176     # The insert_line() call via pre_input_hook() does nothing with Editline,
    177     # so include the extra text that would have been inserted here
    178     macro = inserted + macro
    179 
    180 if is_editline:
    181     readline.parse_and_bind(r'bind ^B ed-prev-char')
    182     readline.parse_and_bind(r'bind "\t" rl_complete')
    183     readline.parse_and_bind(r'bind -s ^A "{}"'.format(macro))
    184 else:
    185     readline.parse_and_bind(r'Control-b: backward-char')
    186     readline.parse_and_bind(r'"\t": complete')
    187     readline.parse_and_bind(r'set disable-completion off')
    188     readline.parse_and_bind(r'set show-all-if-ambiguous off')
    189     readline.parse_and_bind(r'set show-all-if-unmodified off')
    190     readline.parse_and_bind(r'Control-a: "{}"'.format(macro))
    191 
    192 def pre_input_hook():
    193     readline.insert_text(inserted)
    194     readline.redisplay()
    195 if set_pre_input_hook:
    196     set_pre_input_hook(pre_input_hook)
    197 
    198 def completer(text, state):
    199     if text == "t\xEB":
    200         if state == 0:
    201             print("text", ascii(text))
    202             print("line", ascii(readline.get_line_buffer()))
    203             print("indexes", readline.get_begidx(), readline.get_endidx())
    204             return "t\xEBnt"
    205         if state == 1:
    206             return "t\xEBxt"
    207     if text == "t\xEBx" and state == 0:
    208         return "t\xEBxt"
    209     return None
    210 readline.set_completer(completer)
    211 
    212 def display(substitution, matches, longest_match_length):
    213     print("substitution", ascii(substitution))
    214     print("matches", ascii(matches))
    215 readline.set_completion_display_matches_hook(display)
    216 
    217 print("result", ascii(input()))
    218 print("history", ascii(readline.get_history_item(1)))
    219 """
    220 
    221         input = b"\x01"  # Ctrl-A, expands to "|t\xEB[after]"
    222         input += b"\x02" * len("[after]")  # Move cursor back
    223         input += b"\t\t"  # Display possible completions
    224         input += b"x\t"  # Complete "t\xEBx" -> "t\xEBxt"
    225         input += b"\r"
    226         output = run_pty(script, input)
    227         self.assertIn(b"text 't\\xeb'\r\n", output)
    228         self.assertIn(b"line '[\\xefnserted]|t\\xeb[after]'\r\n", output)
    229         self.assertIn(b"indexes 11 13\r\n", output)
    230         if not is_editline and hasattr(readline, "set_pre_input_hook"):
    231             self.assertIn(b"substitution 't\\xeb'\r\n", output)
    232             self.assertIn(b"matches ['t\\xebnt', 't\\xebxt']\r\n", output)
    233         expected = br"'[\xefnserted]|t\xebxt[after]'"
    234         self.assertIn(b"result " + expected + b"\r\n", output)
    235         self.assertIn(b"history " + expected + b"\r\n", output)
    236 
    237     # We have 2 reasons to skip this test:
    238     # - readline: history size was added in 6.0
    239     #   See https://cnswww.cns.cwru.edu/php/chet/readline/CHANGES
    240     # - editline: history size is broken on OS X 10.11.6.
    241     #   Newer versions were not tested yet.
    242     @unittest.skipIf(readline._READLINE_VERSION < 0x600,
    243                      "this readline version does not support history-size")
    244     @unittest.skipIf(is_editline,
    245                      "editline history size configuration is broken")
    246     def test_history_size(self):
    247         history_size = 10
    248         with temp_dir() as test_dir:
    249             inputrc = os.path.join(test_dir, "inputrc")
    250             with open(inputrc, "wb") as f:
    251                 f.write(b"set history-size %d\n" % history_size)
    252 
    253             history_file = os.path.join(test_dir, "history")
    254             with open(history_file, "wb") as f:
    255                 # history_size * 2 items crashes readline
    256                 data = b"".join(b"item %d\n" % i
    257                                 for i in range(history_size * 2))
    258                 f.write(data)
    259 
    260             script = """
    261 import os
    262 import readline
    263 
    264 history_file = os.environ["HISTORY_FILE"]
    265 readline.read_history_file(history_file)
    266 input()
    267 readline.write_history_file(history_file)
    268 """
    269 
    270             env = dict(os.environ)
    271             env["INPUTRC"] = inputrc
    272             env["HISTORY_FILE"] = history_file
    273 
    274             run_pty(script, input=b"last input\r", env=env)
    275 
    276             with open(history_file, "rb") as f:
    277                 lines = f.readlines()
    278             self.assertEqual(len(lines), history_size)
    279             self.assertEqual(lines[-1].strip(), b"last input")
    280 
    281 
    282 def run_pty(script, input=b"dummy input\r", env=None):
    283     pty = import_module('pty')
    284     output = bytearray()
    285     [master, slave] = pty.openpty()
    286     args = (sys.executable, '-c', script)
    287     proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env)
    288     os.close(slave)
    289     with ExitStack() as cleanup:
    290         cleanup.enter_context(proc)
    291         def terminate(proc):
    292             try:
    293                 proc.terminate()
    294             except ProcessLookupError:
    295                 # Workaround for Open/Net BSD bug (Issue 16762)
    296                 pass
    297         cleanup.callback(terminate, proc)
    298         cleanup.callback(os.close, master)
    299         # Avoid using DefaultSelector and PollSelector. Kqueue() does not
    300         # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open
    301         # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4
    302         # either (Issue 20472). Hopefully the file descriptor is low enough
    303         # to use with select().
    304         sel = cleanup.enter_context(selectors.SelectSelector())
    305         sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE)
    306         os.set_blocking(master, False)
    307         while True:
    308             for [_, events] in sel.select():
    309                 if events & selectors.EVENT_READ:
    310                     try:
    311                         chunk = os.read(master, 0x10000)
    312                     except OSError as err:
    313                         # Linux raises EIO when slave is closed (Issue 5380)
    314                         if err.errno != EIO:
    315                             raise
    316                         chunk = b""
    317                     if not chunk:
    318                         return output
    319                     output.extend(chunk)
    320                 if events & selectors.EVENT_WRITE:
    321                     try:
    322                         input = input[os.write(master, input):]
    323                     except OSError as err:
    324                         # Apparently EIO means the slave was closed
    325                         if err.errno != EIO:
    326                             raise
    327                         input = b""  # Stop writing
    328                     if not input:
    329                         sel.modify(master, selectors.EVENT_READ)
    330 
    331 
    332 if __name__ == "__main__":
    333     unittest.main()
    334