1 """
2 Very minimal unittests for parts of the readline module.
3 """
4 import locale
5 import os
6 import sys
7 import tempfile
8 import textwrap
9 import unittest
10 from test.support import verbose
11 from test.support.import_helper import import_module
12 from test.support.os_helper import unlink, temp_dir, TESTFN
13 from test.support.pty_helper import run_pty
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 ESC[4;38;5;81mTestHistoryManipulation (ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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 try:
104 readline.append_history_file(1, hfilename)
105 except FileNotFoundError:
106 pass # Some implementations return this error (libreadline).
107 else:
108 os.unlink(hfilename) # Some create it anyways (libedit).
109 # If the file wasn't created, unlink will fail.
110 # We're just testing that one of the two expected behaviors happens
111 # instead of an incorrect error.
112
113 # write_history_file can create the target
114 readline.write_history_file(hfilename)
115
116 def test_nonascii_history(self):
117 readline.clear_history()
118 try:
119 readline.add_history("entrée 1")
120 except UnicodeEncodeError as err:
121 self.skipTest("Locale cannot encode test data: " + format(err))
122 readline.add_history("entrée 2")
123 readline.replace_history_item(1, "entrée 22")
124 readline.write_history_file(TESTFN)
125 self.addCleanup(os.remove, TESTFN)
126 readline.clear_history()
127 readline.read_history_file(TESTFN)
128 if is_editline:
129 # An add_history() call seems to be required for get_history_
130 # item() to register items from the file
131 readline.add_history("dummy")
132 self.assertEqual(readline.get_history_item(1), "entrée 1")
133 self.assertEqual(readline.get_history_item(2), "entrée 22")
134
135
136 class ESC[4;38;5;81mTestReadline(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
137
138 @unittest.skipIf(readline._READLINE_VERSION < 0x0601 and not is_editline,
139 "not supported in this library version")
140 def test_init(self):
141 # Issue #19884: Ensure that the ANSI sequence "\033[1034h" is not
142 # written into stdout when the readline module is imported and stdout
143 # is redirected to a pipe.
144 rc, stdout, stderr = assert_python_ok('-c', 'import readline',
145 TERM='xterm-256color')
146 self.assertEqual(stdout, b'')
147
148 auto_history_script = """\
149 import readline
150 readline.set_auto_history({})
151 input()
152 print("History length:", readline.get_current_history_length())
153 """
154
155 def test_auto_history_enabled(self):
156 output = run_pty(self.auto_history_script.format(True))
157 # bpo-44949: Sometimes, the newline character is not written at the
158 # end, so don't expect it in the output.
159 self.assertIn(b"History length: 1", output)
160
161 def test_auto_history_disabled(self):
162 output = run_pty(self.auto_history_script.format(False))
163 # bpo-44949: Sometimes, the newline character is not written at the
164 # end, so don't expect it in the output.
165 self.assertIn(b"History length: 0", output)
166
167 def test_set_complete_delims(self):
168 script = textwrap.dedent("""
169 import readline
170 def complete(text, state):
171 if state == 0 and text == "$":
172 return "$complete"
173 return None
174 if "libedit" in getattr(readline, "__doc__", ""):
175 readline.parse_and_bind(r'bind "\\t" rl_complete')
176 else:
177 readline.parse_and_bind(r'"\\t": complete')
178 readline.set_completer_delims(" \\t\\n")
179 readline.set_completer(complete)
180 print(input())
181 """)
182
183 output = run_pty(script, input=b"$\t\n")
184 self.assertIn(b"$complete", output)
185
186 def test_nonascii(self):
187 loc = locale.setlocale(locale.LC_CTYPE, None)
188 if loc in ('C', 'POSIX'):
189 # bpo-29240: On FreeBSD, if the LC_CTYPE locale is C or POSIX,
190 # writing and reading non-ASCII bytes into/from a TTY works, but
191 # readline or ncurses ignores non-ASCII bytes on read.
192 self.skipTest(f"the LC_CTYPE locale is {loc!r}")
193
194 try:
195 readline.add_history("\xEB\xEF")
196 except UnicodeEncodeError as err:
197 self.skipTest("Locale cannot encode test data: " + format(err))
198
199 script = r"""import readline
200
201 is_editline = readline.__doc__ and "libedit" in readline.__doc__
202 inserted = "[\xEFnserted]"
203 macro = "|t\xEB[after]"
204 set_pre_input_hook = getattr(readline, "set_pre_input_hook", None)
205 if is_editline or not set_pre_input_hook:
206 # The insert_line() call via pre_input_hook() does nothing with Editline,
207 # so include the extra text that would have been inserted here
208 macro = inserted + macro
209
210 if is_editline:
211 readline.parse_and_bind(r'bind ^B ed-prev-char')
212 readline.parse_and_bind(r'bind "\t" rl_complete')
213 readline.parse_and_bind(r'bind -s ^A "{}"'.format(macro))
214 else:
215 readline.parse_and_bind(r'Control-b: backward-char')
216 readline.parse_and_bind(r'"\t": complete')
217 readline.parse_and_bind(r'set disable-completion off')
218 readline.parse_and_bind(r'set show-all-if-ambiguous off')
219 readline.parse_and_bind(r'set show-all-if-unmodified off')
220 readline.parse_and_bind(r'Control-a: "{}"'.format(macro))
221
222 def pre_input_hook():
223 readline.insert_text(inserted)
224 readline.redisplay()
225 if set_pre_input_hook:
226 set_pre_input_hook(pre_input_hook)
227
228 def completer(text, state):
229 if text == "t\xEB":
230 if state == 0:
231 print("text", ascii(text))
232 print("line", ascii(readline.get_line_buffer()))
233 print("indexes", readline.get_begidx(), readline.get_endidx())
234 return "t\xEBnt"
235 if state == 1:
236 return "t\xEBxt"
237 if text == "t\xEBx" and state == 0:
238 return "t\xEBxt"
239 return None
240 readline.set_completer(completer)
241
242 def display(substitution, matches, longest_match_length):
243 print("substitution", ascii(substitution))
244 print("matches", ascii(matches))
245 readline.set_completion_display_matches_hook(display)
246
247 print("result", ascii(input()))
248 print("history", ascii(readline.get_history_item(1)))
249 """
250
251 input = b"\x01" # Ctrl-A, expands to "|t\xEB[after]"
252 input += b"\x02" * len("[after]") # Move cursor back
253 input += b"\t\t" # Display possible completions
254 input += b"x\t" # Complete "t\xEBx" -> "t\xEBxt"
255 input += b"\r"
256 output = run_pty(script, input)
257 self.assertIn(b"text 't\\xeb'\r\n", output)
258 self.assertIn(b"line '[\\xefnserted]|t\\xeb[after]'\r\n", output)
259 if sys.platform == "darwin" or not is_editline:
260 self.assertIn(b"indexes 11 13\r\n", output)
261 # Non-macOS libedit does not handle non-ASCII bytes
262 # the same way and generates character indices
263 # rather than byte indices via get_begidx() and
264 # get_endidx(). Ex: libedit2 3.1-20191231-2 on Debian
265 # winds up with "indexes 10 12". Stemming from the
266 # start and end values calls back into readline.c's
267 # rl_attempted_completion_function = flex_complete with:
268 # (11, 13) instead of libreadline's (12, 15).
269
270 if not is_editline and hasattr(readline, "set_pre_input_hook"):
271 self.assertIn(b"substitution 't\\xeb'\r\n", output)
272 self.assertIn(b"matches ['t\\xebnt', 't\\xebxt']\r\n", output)
273 expected = br"'[\xefnserted]|t\xebxt[after]'"
274 self.assertIn(b"result " + expected + b"\r\n", output)
275 # bpo-45195: Sometimes, the newline character is not written at the
276 # end, so don't expect it in the output.
277 self.assertIn(b"history " + expected, output)
278
279 # We have 2 reasons to skip this test:
280 # - readline: history size was added in 6.0
281 # See https://cnswww.cns.cwru.edu/php/chet/readline/CHANGES
282 # - editline: history size is broken on OS X 10.11.6.
283 # Newer versions were not tested yet.
284 @unittest.skipIf(readline._READLINE_VERSION < 0x600,
285 "this readline version does not support history-size")
286 @unittest.skipIf(is_editline,
287 "editline history size configuration is broken")
288 def test_history_size(self):
289 history_size = 10
290 with temp_dir() as test_dir:
291 inputrc = os.path.join(test_dir, "inputrc")
292 with open(inputrc, "wb") as f:
293 f.write(b"set history-size %d\n" % history_size)
294
295 history_file = os.path.join(test_dir, "history")
296 with open(history_file, "wb") as f:
297 # history_size * 2 items crashes readline
298 data = b"".join(b"item %d\n" % i
299 for i in range(history_size * 2))
300 f.write(data)
301
302 script = """
303 import os
304 import readline
305
306 history_file = os.environ["HISTORY_FILE"]
307 readline.read_history_file(history_file)
308 input()
309 readline.write_history_file(history_file)
310 """
311
312 env = dict(os.environ)
313 env["INPUTRC"] = inputrc
314 env["HISTORY_FILE"] = history_file
315
316 run_pty(script, input=b"last input\r", env=env)
317
318 with open(history_file, "rb") as f:
319 lines = f.readlines()
320 self.assertEqual(len(lines), history_size)
321 self.assertEqual(lines[-1].strip(), b"last input")
322
323
324 if __name__ == "__main__":
325 unittest.main()