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