• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Very minimal unittests for parts of the readline module.
3"""
4from contextlib import ExitStack
5from errno import EIO
6import locale
7import os
8import selectors
9import subprocess
10import sys
11import tempfile
12import unittest
13from test.support import import_module, unlink, temp_dir, TESTFN, verbose
14from test.support.script_helper import assert_python_ok
15
16# Skip tests if there is no readline module
17readline = import_module('readline')
18
19if hasattr(readline, "_READLINE_LIBRARY_VERSION"):
20    is_editline = ("EditLine wrapper" in readline._READLINE_LIBRARY_VERSION)
21else:
22    is_editline = (readline.__doc__ and "libedit" in readline.__doc__)
23
24
25def 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.")
40class 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("entrée 1")
113        except UnicodeEncodeError as err:
114            self.skipTest("Locale cannot encode test data: " + format(err))
115        readline.add_history("entrée 2")
116        readline.replace_history_item(1, "entrée 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), "entrée 1")
126        self.assertEqual(readline.get_history_item(2), "entrée 22")
127
128
129class 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 = """\
142import readline
143readline.set_auto_history({})
144input()
145print("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
171is_editline = readline.__doc__ and "libedit" in readline.__doc__
172inserted = "[\xEFnserted]"
173macro = "|t\xEB[after]"
174set_pre_input_hook = getattr(readline, "set_pre_input_hook", None)
175if 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
180if 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))
184else:
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
192def pre_input_hook():
193    readline.insert_text(inserted)
194    readline.redisplay()
195if set_pre_input_hook:
196    set_pre_input_hook(pre_input_hook)
197
198def 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
210readline.set_completer(completer)
211
212def display(substitution, matches, longest_match_length):
213    print("substitution", ascii(substitution))
214    print("matches", ascii(matches))
215readline.set_completion_display_matches_hook(display)
216
217print("result", ascii(input()))
218print("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 = """
261import os
262import readline
263
264history_file = os.environ["HISTORY_FILE"]
265readline.read_history_file(history_file)
266input()
267readline.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
282def 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
332if __name__ == "__main__":
333    unittest.main()
334