Skip to content

Commit 3f9c1da

Browse files
author
Andy C
committed
[test/interactive] Run interactive tests against bash too.
They are now closer to the style of spec tests. This addresses #1077, and paves the way for testing many other bug fixes (all the SIGWINCH, wait, read, trap stuff). I factored common code out of test/sh_spec.py into test/spec_lib.py. We should probably reuse the report table too. This will show what tests aren't running in bash and OSH.
1 parent 6b6f7be commit 3f9c1da

File tree

5 files changed

+339
-213
lines changed

5 files changed

+339
-213
lines changed

test/__init__.py

Whitespace-only changes.

test/interactive.py

+190-91
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"""
33
Test OSH in interactive mode.
44
5-
Usage (run from project root):
5+
To invoke this file, run the shell wrapper:
66
7-
test/interactive.py
7+
test/interactive.sh all
88
99
Env Vars:
1010
- OSH_TEST_INTERACTIVE_SHELL: override default shell path (default, bin/osh)
@@ -26,6 +26,8 @@
2626
import sys
2727
import time
2828

29+
from test import spec_lib # Using this for a common interface
30+
2931

3032
SHELL = os.environ.get("OSH_TEST_INTERACTIVE_SHELL", "bin/osh")
3133
TIMEOUT = int(os.environ.get("OSH_TEST_INTERACTIVE_TIMEOUT", 3))
@@ -42,22 +44,22 @@ def get_pid_by_name(name):
4244
# XXX: make sure this is restricted to subprocesses under us.
4345
# This could be problematic on the continuous build if many tests are running
4446
# in parallel.
45-
output = pexpect.run('pgrep --exact --newest cat')
47+
output = pexpect.run('pgrep --exact --newest %s' % name)
4648
return int(output.split()[-1])
4749

4850

51+
def send_signal(name, sig_num):
52+
"""Kill the most recent process matching `name`."""
53+
os.kill(get_pid_by_name(name), sig_num)
54+
55+
4956
# XXX: osh.sendcontrol("z") does not suspend the foreground process :(
5057
#
5158
# why does osh.sendcontrol("c") generate SIGINT, while osh.sendcontrol("z")
5259
# appears to do nothing?
5360
def stop_process__hack(name):
5461
"""Send sigstop to the most recent process matching `name`"""
55-
os.kill(get_pid_by_name(name), signal.SIGSTOP)
56-
57-
58-
def kill_process(name):
59-
"""Kill the most recent process matching `name`."""
60-
os.kill(get_pid_by_name(name), signal.SIGINT)
62+
send_signal(name, signal.SIGSTOP)
6163

6264

6365
class InteractiveTest(object):
@@ -78,18 +80,27 @@ def __init__(self, description, program=SHELL):
7880
self.description = description
7981

8082
def __enter__(self):
81-
if DEBUG:
82-
print(self.description)
83-
else:
84-
print(self.description, end='')
83+
if 0:
84+
if DEBUG:
85+
print(self.description)
86+
else:
87+
print(self.description, end='')
8588

8689
#env = dict(os.environ)
8790
#env['PS1'] = 'test$ '
8891
env = None
8992

93+
sh_argv = ['--rcfile', '/dev/null']
94+
95+
# Why the heck is --norc different from --rcfile /dev/null in bash??? This
96+
# makes it so the prompt of the parent shell doesn't leak. Very annoying.
97+
if self.program == 'bash':
98+
sh_argv.append('--norc')
99+
#print(sh_argv)
100+
90101
# Python 3: encoding required
91102
self.shell = pexpect.spawn(
92-
self.program, ['--rcfile', '/dev/null'], env=env, encoding='utf-8',
103+
self.program, sh_argv, env=env, encoding='utf-8',
93104
timeout=TIMEOUT)
94105

95106
# suppress output when DEBUG is not set.
@@ -118,111 +129,199 @@ def __exit__(self, t, v, tb):
118129
pass
119130

120131

121-
def main(argv):
122-
with InteractiveTest('Ctrl-C during external command') as osh:
123-
osh.sendline('sleep 5')
124132

125-
time.sleep(0.1)
126-
osh.sendintr() # SIGINT
133+
CASES = []
134+
135+
def register(skip_shells=None):
136+
if skip_shells is None:
137+
skip_shells = []
138+
139+
def decorator(func):
140+
CASES.append((func.__doc__, func, skip_shells))
141+
return func
142+
return decorator
143+
144+
145+
# TODO: Make this pass in OSH
146+
@register(skip_shells=['osh'])
147+
def a(sh):
148+
'wait builtin then SIGWINCH (issue 1067)'
149+
150+
sh.sendline('sleep 1 &')
151+
sh.sendline('wait')
152+
153+
time.sleep(0.1)
154+
155+
# simulate window size change
156+
sh.kill(signal.SIGWINCH)
157+
158+
sh.expect(r'.*\$') # expect prompt
159+
160+
sh.sendline('echo status=$?')
161+
sh.expect('status=0')
162+
127163

128-
osh.expect(r'.*\$') # expect prompt
164+
@register()
165+
def b(sh):
166+
'Ctrl-C during external command'
129167

130-
osh.sendline('echo status=$?')
131-
osh.expect('status=130')
168+
sh.sendline('sleep 5')
132169

133-
with InteractiveTest('Ctrl-C during read builtin') as osh:
134-
osh.sendline('read')
170+
time.sleep(0.1)
171+
sh.sendintr() # SIGINT
135172

136-
time.sleep(0.1)
137-
osh.sendintr() # SIGINT
173+
sh.expect(r'.*\$') # expect prompt
138174

139-
osh.expect(r'.*\$') # expect prompt
175+
sh.sendline('echo status=$?')
176+
sh.expect('status=130')
140177

141-
osh.sendline('echo status=$?')
142-
osh.expect('status=130')
143178

144-
with InteractiveTest('Ctrl-C during wait builtin') as osh:
145-
osh.sendline('sleep 5 &')
146-
osh.sendline('wait')
179+
@register()
180+
def c(sh):
181+
'Ctrl-C during read builtin'
147182

148-
time.sleep(0.1)
149-
osh.sendintr() # SIGINT
183+
sh.sendline('read')
150184

151-
osh.expect(r'.*\$') # expect prompt
185+
time.sleep(0.1)
186+
sh.sendintr() # SIGINT
152187

153-
osh.sendline('echo status=$?')
154-
# TODO: Should be exit code 130 like bash
155-
osh.expect('status=0')
188+
sh.expect(r'.*\$') # expect prompt
156189

157-
with InteractiveTest('Ctrl-C during pipeline') as osh:
158-
osh.sendline('sleep 5 | cat')
190+
sh.sendline('echo status=$?')
191+
sh.expect('status=130')
159192

160-
time.sleep(0.1)
161-
osh.sendintr() # SIGINT
162193

163-
osh.expect(r'.*\$') # expect prompt
194+
# TODO: make it work on bash
195+
@register(skip_shells=['bash'])
196+
def d(sh):
197+
'Ctrl-C during wait builtin'
164198

165-
osh.sendline('echo status=$?')
166-
osh.expect('status=130')
199+
sh.sendline('sleep 5 &')
200+
sh.sendline('wait')
167201

168-
with InteractiveTest("Ctrl-C during Command Sub (issue 467)") as osh:
169-
osh.sendline('`sleep 5`')
202+
time.sleep(0.1)
203+
sh.sendintr() # SIGINT
170204

171-
time.sleep(0.1)
172-
osh.sendintr() # SIGINT
205+
sh.expect(r'.*\$') # expect prompt
173206

174-
osh.expect(r'.*\$') # expect prompt
207+
sh.sendline('echo status=$?')
208+
# TODO: Should be exit code 130 like bash
209+
sh.expect('status=0')
175210

176-
osh.sendline('echo status=$?')
177-
# TODO: This should be status 130 like bash
178-
osh.expect('status=0')
179211

180-
with InteractiveTest("fg twice should not result in fatal error (issue 1004)") as osh:
181-
osh.expect(r'.*\$ ')
182-
osh.sendline("cat")
183-
stop_process__hack("cat")
184-
osh.expect("\r\n\\[PID \\d+\\] Stopped")
185-
osh.expect(r".*\$")
186-
osh.sendline("fg")
187-
osh.expect(r"Continue PID \d+")
212+
@register()
213+
def e(sh):
214+
'Ctrl-C during pipeline'
215+
sh.sendline('sleep 5 | cat')
188216

189-
#osh.sendcontrol("c")
190-
osh.sendintr() # SIGINT
217+
time.sleep(0.1)
218+
sh.sendintr() # SIGINT
219+
220+
sh.expect(r'.*\$') # expect prompt
221+
222+
sh.sendline('echo status=$?')
223+
sh.expect('status=130')
224+
225+
226+
# TODO: make it work on bash
227+
@register(skip_shells=['bash'])
228+
def f(sh):
229+
'Ctrl-C during Command Sub (issue 467)'
230+
sh.sendline('`sleep 5`')
231+
232+
time.sleep(0.1)
233+
sh.sendintr() # SIGINT
234+
235+
sh.expect(r'.*\$') # expect prompt
236+
237+
sh.sendline('echo status=$?')
238+
# TODO: This should be status 130 like bash
239+
sh.expect('status=0')
240+
241+
242+
@register(skip_shells=['bash'])
243+
def g(sh):
244+
'fg twice should not result in fatal error (issue 1004)'
245+
sh.expect(r'.*\$ ')
246+
sh.sendline("cat")
247+
stop_process__hack("cat")
248+
sh.expect("\r\n\\[PID \\d+\\] Stopped")
249+
sh.expect(r".*\$")
250+
sh.sendline("fg")
251+
sh.expect(r"Continue PID \d+")
252+
253+
#sh.sendcontrol("c")
254+
sh.sendintr() # SIGINT
255+
256+
sh.expect(r".*\$")
257+
sh.sendline("fg")
258+
sh.expect("No job to put in the foreground")
259+
260+
261+
@register(skip_shells=['bash'])
262+
def h(sh):
263+
'Test resuming a killed process'
264+
sh.expect(r'.*\$ ')
265+
sh.sendline("cat")
266+
stop_process__hack("cat")
267+
sh.expect("\r\n\\[PID \\d+\\] Stopped")
268+
sh.expect(r".*\$")
269+
sh.sendline("fg")
270+
sh.expect(r"Continue PID \d+")
271+
send_signal("cat", signal.SIGINT)
272+
sh.expect(r".*\$")
273+
sh.sendline("fg")
274+
sh.expect("No job to put in the foreground")
275+
276+
277+
@register(skip_shells=['bash'])
278+
def j(sh):
279+
'Call fg after process exits (issue 721)'
280+
281+
sh.expect(r".*\$")
282+
sh.sendline("cat")
283+
284+
#osh.sendcontrol("c")
285+
sh.sendintr() # SIGINT
286+
287+
sh.expect(r".*\$")
288+
sh.sendline("fg")
289+
sh.expect("No job to put in the foreground")
290+
sh.expect(r".*\$")
291+
sh.sendline("fg")
292+
sh.expect("No job to put in the foreground")
293+
sh.expect(r".*\$")
294+
295+
296+
297+
def main(argv):
298+
# NOTE: Some options are ignored
299+
o = spec_lib.Options()
300+
opts, argv = o.parse_args(argv)
191301

192-
osh.expect(r".*\$")
193-
osh.sendline("fg")
194-
osh.expect("No job to put in the foreground")
302+
shells = argv[1:]
303+
shell_pairs = spec_lib.MakeShellPairs(shells)
195304

196-
with InteractiveTest('Test resuming a killed process') as osh:
197-
osh.expect(r'.*\$ ')
198-
osh.sendline("cat")
199-
stop_process__hack("cat")
200-
osh.expect("\r\n\\[PID \\d+\\] Stopped")
201-
osh.expect(r".*\$")
202-
osh.sendline("fg")
203-
osh.expect(r"Continue PID \d+")
204-
kill_process("cat")
205-
osh.expect(r".*\$")
206-
osh.sendline("fg")
207-
osh.expect("No job to put in the foreground")
305+
if 0:
306+
print(shell_pairs)
307+
print(CASES)
208308

309+
for i, (desc, func, skip_shells) in enumerate(CASES):
310+
for shell_label, shell_path in shell_pairs:
311+
skip = shell_label in skip_shells
312+
skip_str = 'SKIP' if skip else ''
209313

210-
with InteractiveTest('Call fg after process exits (issue 721)') as osh:
211-
osh.expect(r".*\$")
212-
osh.sendline("cat")
314+
print()
315+
print('%s\t%d\t%s\t%s' % (skip_str, i, shell_label, desc))
316+
print()
213317

214-
#osh.sendcontrol("c")
215-
osh.sendintr() # SIGINT
318+
if skip:
319+
continue
216320

217-
osh.expect(r".*\$")
218-
osh.sendline("fg")
219-
osh.expect("No job to put in the foreground")
220-
osh.expect(r".*\$")
221-
osh.sendline("fg")
222-
osh.expect("No job to put in the foreground")
223-
osh.expect(r".*\$")
321+
with InteractiveTest(desc, program=shell_path) as sh:
322+
func(sh)
224323

225-
return 1 if g_failures else 0
324+
return 0
226325

227326

228327
if __name__ == '__main__':

test/interactive.sh

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,24 @@ set -o nounset
99
set -o pipefail
1010
set -o errexit
1111

12+
export PYTHONPATH=.
13+
14+
run() {
15+
### Wrapper for PYTHONPATH
16+
17+
test/interactive.py "$@"
18+
}
19+
20+
all() {
21+
### What we want to pass
22+
23+
# TODO: source build/dev-shell.sh to get this?
24+
25+
run bin/osh ../oil_DEPS/spec-bin/bash
26+
}
27+
1228
soil-run() {
13-
# Run it a few times to test flakiness
29+
### Run it a few times to work around flakiness
1430

1531
local n=5
1632
echo "Running $n times"

0 commit comments

Comments
 (0)