OILS / spec / stateful / bind.py View on Github | oils.pub

323 lines, 187 significant
1#!/usr/bin/env python3
2"""
3Interactively tests shell bindings.
4
5To invoke this file, run the shell wrapper:
6
7 test/stateful.sh bind-quick
8"""
9from __future__ import print_function
10
11import sys
12import os
13import time
14import pexpect
15import pyte
16
17import harness
18from harness import expect_prompt, register, TerminalDimensionEnvVars
19
20from test.spec_lib import log
21
22
23def add_foo_fn(sh):
24 sh.sendline('function foo() { echo "FOO"; }')
25 time.sleep(0.1)
26
27
28def send_bind(sh, opts, keymap=None):
29 "Helper method to send a bind command and sleep for a moment. W/ optional keymap."
30
31 if keymap:
32 sh.sendline(f"bind -m {keymap} {opts}")
33 else:
34 sh.sendline(f"bind {opts}")
35 time.sleep(0.1)
36
37
38@register(not_impl_shells=['dash', 'mksh'])
39def bind_plain(sh):
40 "test bind (w/out flags) for adding bindings to readline fns"
41 expect_prompt(sh)
42
43 # There aren't many readline fns that will work nicely with pexpect (e.g., cursor-based fns)
44 # Editing input seems like a reasonable choice
45 send_bind(sh, r''' '"\C-x\C-h": backward-delete-char' ''')
46 expect_prompt(sh)
47
48 sh.send("echo FOOM")
49 sh.sendcontrol('x')
50 sh.sendcontrol('h')
51 sh.sendline("P")
52 time.sleep(0.1)
53
54 sh.expect("FOOP")
55
56
57@register(not_impl_shells=['dash', 'mksh'])
58def bind_r_for_bind_x_osh_fn(sh):
59 """test bind -r for removing bindings made with bind -x"""
60
61 # (regular readline fn bind removal is tested in noninteractive builtin-bind.test.sh)
62
63 expect_prompt(sh)
64
65 add_foo_fn(sh)
66 expect_prompt(sh)
67
68 send_bind(sh, r"""-x '"\C-o": foo' """)
69 expect_prompt(sh)
70
71 sh.sendcontrol('o')
72 time.sleep(0.1)
73 sh.expect("FOO")
74
75 send_bind(sh, r'-r "\C-o" ')
76
77 sh.sendcontrol('o')
78 time.sleep(0.1)
79
80 expect_prompt(sh)
81
82
83@register(not_impl_shells=['dash', 'mksh'])
84def bind_x(sh):
85 "test bind -x for setting bindings to custom shell functions"
86 expect_prompt(sh)
87
88 add_foo_fn(sh)
89 expect_prompt(sh)
90
91 send_bind(sh, r"""-x '"\C-o": foo' """)
92 expect_prompt(sh)
93
94 sh.sendcontrol('o')
95 time.sleep(0.1)
96
97 sh.expect("FOO")
98
99
100@register(not_impl_shells=['dash', 'mksh'])
101def bind_x_runtime_envvar_vals(sh):
102 "test bind -x for using env var runtime values (e.g., 'echo $PWD' should change with dir)"
103 expect_prompt(sh)
104
105 sh.sendline("export BIND_X_VAR=foo")
106
107 send_bind(sh, r"""-x '"\C-o": echo $BIND_X_VAR' """)
108 expect_prompt(sh)
109
110 sh.sendline("export BIND_X_VAR=bar")
111 expect_prompt(sh)
112
113 sh.sendcontrol('o')
114 time.sleep(0.1)
115
116 sh.expect("bar")
117
118
119@register(not_impl_shells=['dash', 'mksh'])
120def bind_x_readline_line(sh):
121 "test bind -x for correctly setting $READLINE_LINE for the cmd"
122 expect_prompt(sh)
123
124 send_bind(sh, r"""-x '"\C-o": echo Current line is: $READLINE_LINE' """)
125 expect_prompt(sh)
126
127 sh.send('abcdefghijklmnopqrstuvwxyz')
128
129 sh.sendcontrol('o')
130 time.sleep(0.1)
131
132 # must not match any other output (like debug output or shell names)
133 sh.expect("Current line is: abcdefghijklmnopqrstuvwxyz")
134
135 sh.sendline(
136 '[[ -v READLINE_LINE ]] && echo "READLINE_LINE is set" || echo "READLINE_LINE is unset"'
137 )
138 sh.expect("READLINE_LINE is unset")
139
140
141@register(not_impl_shells=['dash', 'mksh'])
142def bind_x_set_readline_line_to_uppercase(sh):
143 """test bind -x for correctly using $READLINE_LINE changes"""
144 expect_prompt(sh)
145
146 send_bind(sh, r"""-x '"\C-o": READLINE_LINE=${READLINE_LINE^^}' """)
147 expect_prompt(sh)
148
149 sh.send('abcdefghijklmnopqrstuvwxyz')
150
151 sh.sendcontrol('o')
152 time.sleep(0.1)
153
154 sh.sendline('')
155
156 sh.expect("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
157
158
159@register(not_impl_shells=['dash', 'mksh'])
160def bind_x_readline_point(sh):
161 "test bind -x for correctly setting $READLINE_POINT for the cmd"
162 cmd_str = 'abcdefghijklmnop'
163 expected_rl_point = len(cmd_str)
164
165 expect_prompt(sh)
166
167 send_bind(sh, r"""-x '"\C-o": echo Cursor point at: $READLINE_POINT' """)
168 expect_prompt(sh)
169
170 sh.send(cmd_str)
171
172 sh.sendcontrol('o')
173 time.sleep(0.1)
174
175 sh.expect("Cursor point at: " + str(expected_rl_point))
176
177 sh.sendline(
178 '[[ -v READLINE_POINT ]] && echo "READLINE_POINT is set" || echo "READLINE_POINT is unset"'
179 )
180 sh.expect("READLINE_POINT is unset")
181
182
183@register(not_impl_shells=['dash', 'mksh'], needs_dimensions=True)
184def bind_x_set_readline_point_to_insert(sh, test_params):
185 "test bind -x for correctly using $READLINE_POINT to overwrite the cmd"
186
187 failing = "failing"
188 echo_cmd = 'echo "this test is %s"' % failing
189 expected_cmd = 'echo "this test is no longer %s"' % failing
190 new_rl_point = len(echo_cmd) - len(failing) - 1
191
192 bind_cmd = r"""bind -x '"\C-y": READLINE_POINT=%d' """ % new_rl_point
193
194 try:
195 num_lines = test_params['num_lines']
196 num_columns = test_params['num_columns']
197 except KeyError:
198 raise RuntimeError("num_lines and num_columns must be passed in")
199
200 with TerminalDimensionEnvVars(num_lines, num_columns):
201 screen = pyte.Screen(num_columns, num_lines)
202 stream = pyte.Stream(screen)
203
204 # Need to echo, because we don't want just output
205 sh.setecho(True)
206
207 def _emulate_ansi_terminal(raw_output):
208 stream.feed(raw_output)
209
210 lines = screen.display
211 screen.reset()
212
213 return '\n'.join(lines)
214
215 # sh.sendline('stty -icanon')
216 # time.sleep(0.1)
217
218 sh.sendline(bind_cmd)
219 time.sleep(0.1)
220
221 sh.send(echo_cmd)
222 time.sleep(0.1)
223
224 sh.sendcontrol('y')
225 time.sleep(0.2)
226
227 sh.send("no longer ")
228 time.sleep(0.1)
229
230 sh.expect(pexpect.TIMEOUT, timeout=2)
231
232 screen_contents = _emulate_ansi_terminal(sh.before)
233 if expected_cmd not in screen_contents:
234 raise Exception(
235 f"Expected command '{expected_cmd}' not found in screen contents:\n{screen_contents}"
236 )
237
238
239@register(not_impl_shells=['dash', 'mksh'])
240def bind_x_unicode(sh):
241 "test bind -x code for handling unicode"
242 expect_prompt(sh)
243
244 send_bind(sh, r"""-x '"\C-o": echo 🅾️' """)
245 expect_prompt(sh)
246
247 sh.sendcontrol('o')
248 time.sleep(0.1)
249
250 sh.expect("🅾️")
251
252
253@register(not_impl_shells=['dash', 'mksh'])
254def bind_u(sh):
255 "test bind -u for unsetting all bindings to a fn"
256 expect_prompt(sh)
257
258 send_bind(sh, r"'\C-p: yank'")
259 expect_prompt(sh)
260
261 send_bind(sh, "-u yank")
262 expect_prompt(sh)
263
264 send_bind(sh, "-q yank")
265 sh.expect("yank is not bound to any keys")
266
267
268@register(not_impl_shells=['dash', 'mksh'])
269def bind_q(sh):
270 "test bind -q for querying bindings to a fn"
271 expect_prompt(sh)
272
273 # Probably bound, but we're not testing that precisely
274 send_bind(sh, "-q yank")
275 sh.expect(["yank can be invoked via", "yank is not bound to any keys"])
276
277 expect_prompt(sh)
278
279 # Probably NOT bound, but we're not testing that precisely
280 send_bind(sh, "-q dump-functions")
281 sh.expect([
282 "dump-functions can be invoked via",
283 "dump-functions is not bound to any keys"
284 ])
285
286
287@register(not_impl_shells=['dash', 'mksh'])
288def bind_m(sh):
289 "test bind -m for setting bindings in specific keymaps"
290 expect_prompt(sh)
291
292 send_bind(sh, "-u yank", "vi")
293 expect_prompt(sh)
294
295 send_bind(sh, r"'\C-p: yank'", "emacs")
296 expect_prompt(sh)
297
298 send_bind(sh, "-q yank", "vi")
299 sh.expect("yank is not bound to any keys")
300 expect_prompt(sh)
301
302 send_bind(sh, "-q yank", "emacs")
303 sh.expect("yank can be invoked via")
304
305
306@register(not_impl_shells=['dash', 'mksh'])
307def bind_f(sh):
308 "test bind -f for setting bindings from an inputrc init file"
309 expect_prompt(sh)
310
311 send_bind(sh, "-f spec/testdata/bind/bind_f.inputrc")
312 expect_prompt(sh)
313
314 send_bind(sh, "-q downcase-word")
315 sh.expect(r'downcase-word can be invoked via.*"\\C-o\\C-s\\C-h"')
316
317
318if __name__ == '__main__':
319 try:
320 sys.exit(harness.main(sys.argv))
321 except RuntimeError as e:
322 print('FATAL: %s' % e, file=sys.stderr)
323 sys.exit(1)