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