• 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 os
7import selectors
8import subprocess
9import sys
10import tempfile
11import unittest
12from test.support import import_module, unlink, TESTFN
13from test.support.script_helper import assert_python_ok
14
15# Skip tests if there is no readline module
16readline = import_module('readline')
17
18is_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.")
23class 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("entrée 1")
96        except UnicodeEncodeError as err:
97            self.skipTest("Locale cannot encode test data: " + format(err))
98        readline.add_history("entrée 2")
99        readline.replace_history_item(1, "entrée 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), "entrée 1")
109        self.assertEqual(readline.get_history_item(2), "entrée 22")
110
111
112class 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 = """\
125import readline
126readline.set_auto_history({})
127input()
128print("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
147is_editline = readline.__doc__ and "libedit" in readline.__doc__
148inserted = "[\xEFnserted]"
149macro = "|t\xEB[after]"
150set_pre_input_hook = getattr(readline, "set_pre_input_hook", None)
151if 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
156if 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))
160else:
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
168def pre_input_hook():
169    readline.insert_text(inserted)
170    readline.redisplay()
171if set_pre_input_hook:
172    set_pre_input_hook(pre_input_hook)
173
174def 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
186readline.set_completer(completer)
187
188def display(substitution, matches, longest_match_length):
189    print("substitution", ascii(substitution))
190    print("matches", ascii(matches))
191readline.set_completion_display_matches_hook(display)
192
193print("result", ascii(input()))
194print("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
214def 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
264if __name__ == "__main__":
265    unittest.main()
266