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 os
      7 import selectors
      8 import subprocess
      9 import sys
     10 import tempfile
     11 import unittest
     12 from test.support import import_module, unlink, TESTFN
     13 from test.support.script_helper import assert_python_ok
     14 
     15 # Skip tests if there is no readline module
     16 readline = import_module('readline')
     17 
     18 is_editline = readline.__doc__ and "libedit" in readline.__doc__
     19 
     20 @unittest.skipUnless(hasattr(readline, "clear_history"),
     21                      "The history update test cannot be run because the "
     22                      "clear_history method is not available.")
     23 class TestHistoryManipulation (unittest.TestCase):
     24     """
     25     These tests were added to check that the libedit emulation on OSX and the
     26     "real" readline have the same interface for history manipulation. That's
     27     why the tests cover only a small subset of the interface.
     28     """
     29 
     30     def testHistoryUpdates(self):
     31         readline.clear_history()
     32 
     33         readline.add_history("first line")
     34         readline.add_history("second line")
     35 
     36         self.assertEqual(readline.get_history_item(0), None)
     37         self.assertEqual(readline.get_history_item(1), "first line")
     38         self.assertEqual(readline.get_history_item(2), "second line")
     39 
     40         readline.replace_history_item(0, "replaced line")
     41         self.assertEqual(readline.get_history_item(0), None)
     42         self.assertEqual(readline.get_history_item(1), "replaced line")
     43         self.assertEqual(readline.get_history_item(2), "second line")
     44 
     45         self.assertEqual(readline.get_current_history_length(), 2)
     46 
     47         readline.remove_history_item(0)
     48         self.assertEqual(readline.get_history_item(0), None)
     49         self.assertEqual(readline.get_history_item(1), "second line")
     50 
     51         self.assertEqual(readline.get_current_history_length(), 1)
     52 
     53     @unittest.skipUnless(hasattr(readline, "append_history_file"),
     54                          "append_history not available")
     55     def test_write_read_append(self):
     56         hfile = tempfile.NamedTemporaryFile(delete=False)
     57         hfile.close()
     58         hfilename = hfile.name
     59         self.addCleanup(unlink, hfilename)
     60 
     61         # test write-clear-read == nop
     62         readline.clear_history()
     63         readline.add_history("first line")
     64         readline.add_history("second line")
     65         readline.write_history_file(hfilename)
     66 
     67         readline.clear_history()
     68         self.assertEqual(readline.get_current_history_length(), 0)
     69 
     70         readline.read_history_file(hfilename)
     71         self.assertEqual(readline.get_current_history_length(), 2)
     72         self.assertEqual(readline.get_history_item(1), "first line")
     73         self.assertEqual(readline.get_history_item(2), "second line")
     74 
     75         # test append
     76         readline.append_history_file(1, hfilename)
     77         readline.clear_history()
     78         readline.read_history_file(hfilename)
     79         self.assertEqual(readline.get_current_history_length(), 3)
     80         self.assertEqual(readline.get_history_item(1), "first line")
     81         self.assertEqual(readline.get_history_item(2), "second line")
     82         self.assertEqual(readline.get_history_item(3), "second line")
     83 
     84         # test 'no such file' behaviour
     85         os.unlink(hfilename)
     86         with self.assertRaises(FileNotFoundError):
     87             readline.append_history_file(1, hfilename)
     88 
     89         # write_history_file can create the target
     90         readline.write_history_file(hfilename)
     91 
     92     def test_nonascii_history(self):
     93         readline.clear_history()
     94         try:
     95             readline.add_history("entre 1")
     96         except UnicodeEncodeError as err:
     97             self.skipTest("Locale cannot encode test data: " + format(err))
     98         readline.add_history("entre 2")
     99         readline.replace_history_item(1, "entre 22")
    100         readline.write_history_file(TESTFN)
    101         self.addCleanup(os.remove, TESTFN)
    102         readline.clear_history()
    103         readline.read_history_file(TESTFN)
    104         if is_editline:
    105             # An add_history() call seems to be required for get_history_
    106             # item() to register items from the file
    107             readline.add_history("dummy")
    108         self.assertEqual(readline.get_history_item(1), "entre 1")
    109         self.assertEqual(readline.get_history_item(2), "entre 22")
    110 
    111 
    112 class TestReadline(unittest.TestCase):
    113 
    114     @unittest.skipIf(readline._READLINE_VERSION < 0x0601 and not is_editline,
    115                      "not supported in this library version")
    116     def test_init(self):
    117         # Issue #19884: Ensure that the ANSI sequence "\033[1034h" is not
    118         # written into stdout when the readline module is imported and stdout
    119         # is redirected to a pipe.
    120         rc, stdout, stderr = assert_python_ok('-c', 'import readline',
    121                                               TERM='xterm-256color')
    122         self.assertEqual(stdout, b'')
    123 
    124     auto_history_script = """\
    125 import readline
    126 readline.set_auto_history({})
    127 input()
    128 print("History length:", readline.get_current_history_length())
    129 """
    130 
    131     def test_auto_history_enabled(self):
    132         output = run_pty(self.auto_history_script.format(True))
    133         self.assertIn(b"History length: 1\r\n", output)
    134 
    135     def test_auto_history_disabled(self):
    136         output = run_pty(self.auto_history_script.format(False))
    137         self.assertIn(b"History length: 0\r\n", output)
    138 
    139     def test_nonascii(self):
    140         try:
    141             readline.add_history("\xEB\xEF")
    142         except UnicodeEncodeError as err:
    143             self.skipTest("Locale cannot encode test data: " + format(err))
    144 
    145         script = r"""import readline
    146 
    147 is_editline = readline.__doc__ and "libedit" in readline.__doc__
    148 inserted = "[\xEFnserted]"
    149 macro = "|t\xEB[after]"
    150 set_pre_input_hook = getattr(readline, "set_pre_input_hook", None)
    151 if is_editline or not set_pre_input_hook:
    152     # The insert_line() call via pre_input_hook() does nothing with Editline,
    153     # so include the extra text that would have been inserted here
    154     macro = inserted + macro
    155 
    156 if is_editline:
    157     readline.parse_and_bind(r'bind ^B ed-prev-char')
    158     readline.parse_and_bind(r'bind "\t" rl_complete')
    159     readline.parse_and_bind(r'bind -s ^A "{}"'.format(macro))
    160 else:
    161     readline.parse_and_bind(r'Control-b: backward-char')
    162     readline.parse_and_bind(r'"\t": complete')
    163     readline.parse_and_bind(r'set disable-completion off')
    164     readline.parse_and_bind(r'set show-all-if-ambiguous off')
    165     readline.parse_and_bind(r'set show-all-if-unmodified off')
    166     readline.parse_and_bind(r'Control-a: "{}"'.format(macro))
    167 
    168 def pre_input_hook():
    169     readline.insert_text(inserted)
    170     readline.redisplay()
    171 if set_pre_input_hook:
    172     set_pre_input_hook(pre_input_hook)
    173 
    174 def completer(text, state):
    175     if text == "t\xEB":
    176         if state == 0:
    177             print("text", ascii(text))
    178             print("line", ascii(readline.get_line_buffer()))
    179             print("indexes", readline.get_begidx(), readline.get_endidx())
    180             return "t\xEBnt"
    181         if state == 1:
    182             return "t\xEBxt"
    183     if text == "t\xEBx" and state == 0:
    184         return "t\xEBxt"
    185     return None
    186 readline.set_completer(completer)
    187 
    188 def display(substitution, matches, longest_match_length):
    189     print("substitution", ascii(substitution))
    190     print("matches", ascii(matches))
    191 readline.set_completion_display_matches_hook(display)
    192 
    193 print("result", ascii(input()))
    194 print("history", ascii(readline.get_history_item(1)))
    195 """
    196 
    197         input = b"\x01"  # Ctrl-A, expands to "|t\xEB[after]"
    198         input += b"\x02" * len("[after]")  # Move cursor back
    199         input += b"\t\t"  # Display possible completions
    200         input += b"x\t"  # Complete "t\xEBx" -> "t\xEBxt"
    201         input += b"\r"
    202         output = run_pty(script, input)
    203         self.assertIn(b"text 't\\xeb'\r\n", output)
    204         self.assertIn(b"line '[\\xefnserted]|t\\xeb[after]'\r\n", output)
    205         self.assertIn(b"indexes 11 13\r\n", output)
    206         if not is_editline and hasattr(readline, "set_pre_input_hook"):
    207             self.assertIn(b"substitution 't\\xeb'\r\n", output)
    208             self.assertIn(b"matches ['t\\xebnt', 't\\xebxt']\r\n", output)
    209         expected = br"'[\xefnserted]|t\xebxt[after]'"
    210         self.assertIn(b"result " + expected + b"\r\n", output)
    211         self.assertIn(b"history " + expected + b"\r\n", output)
    212 
    213 
    214 def run_pty(script, input=b"dummy input\r"):
    215     pty = import_module('pty')
    216     output = bytearray()
    217     [master, slave] = pty.openpty()
    218     args = (sys.executable, '-c', script)
    219     proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave)
    220     os.close(slave)
    221     with ExitStack() as cleanup:
    222         cleanup.enter_context(proc)
    223         def terminate(proc):
    224             try:
    225                 proc.terminate()
    226             except ProcessLookupError:
    227                 # Workaround for Open/Net BSD bug (Issue 16762)
    228                 pass
    229         cleanup.callback(terminate, proc)
    230         cleanup.callback(os.close, master)
    231         # Avoid using DefaultSelector and PollSelector. Kqueue() does not
    232         # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open
    233         # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4
    234         # either (Issue 20472). Hopefully the file descriptor is low enough
    235         # to use with select().
    236         sel = cleanup.enter_context(selectors.SelectSelector())
    237         sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE)
    238         os.set_blocking(master, False)
    239         while True:
    240             for [_, events] in sel.select():
    241                 if events & selectors.EVENT_READ:
    242                     try:
    243                         chunk = os.read(master, 0x10000)
    244                     except OSError as err:
    245                         # Linux raises EIO when slave is closed (Issue 5380)
    246                         if err.errno != EIO:
    247                             raise
    248                         chunk = b""
    249                     if not chunk:
    250                         return output
    251                     output.extend(chunk)
    252                 if events & selectors.EVENT_WRITE:
    253                     try:
    254                         input = input[os.write(master, input):]
    255                     except OSError as err:
    256                         # Apparently EIO means the slave was closed
    257                         if err.errno != EIO:
    258                             raise
    259                         input = b""  # Stop writing
    260                     if not input:
    261                         sel.modify(master, selectors.EVENT_READ)
    262 
    263 
    264 if __name__ == "__main__":
    265     unittest.main()
    266