runtest.py: fix cleanup to use kill.
[jackhill/mal.git] / runtest.py
1 #!/usr/bin/env python
2
3 import os, sys, re
4 import argparse, time
5
6 import pty
7 from subprocess import Popen, STDOUT, PIPE
8 from select import select
9
10 # TODO: do we need to support '\n' too
11 sep = "\r\n"
12 #sep = "\n"
13 rundir = None
14
15 parser = argparse.ArgumentParser(
16 description="Run a test file against a Mal implementation")
17 parser.add_argument('--rundir',
18 help="change to the directory before running tests")
19 parser.add_argument('--start-timeout', default=10, type=int,
20 help="default timeout for initial prompt")
21 parser.add_argument('--test-timeout', default=20, type=int,
22 help="default timeout for each individual test action")
23 parser.add_argument('--pre-eval', default=None, type=str,
24 help="Mal code to evaluate prior to running the test")
25 parser.add_argument('--redirect', action='store_true',
26 help="Run implementation in bash and redirect output to /dev/null")
27
28 parser.add_argument('test_file', type=argparse.FileType('r'),
29 help="a test file formatted as with mal test data")
30 parser.add_argument('mal_cmd', nargs="*",
31 help="Mal implementation command line. Use '--' to "
32 "specify a Mal command line with dashed options.")
33
34 class Runner():
35 def __init__(self, args, redirect=False):
36 print "args: %s" % repr(args)
37 if redirect:
38 #cmd = '/bin/bash -c "' + " ".join(args) + ' |tee /dev/null"'
39 cmd = " ".join(args) + ' |tee /dev/null'
40 print "using redirect cmd: '%s'" % cmd
41 self.p = Popen(cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True)
42 self.stdin = self.p.stdin
43 self.stdout = self.p.stdout
44 else:
45 # provide tty to get 'interactive' readline to work
46 master, slave = pty.openpty()
47 self.p = Popen(args, bufsize=0, stdin=slave, stdout=slave, stderr=STDOUT)
48 self.stdin = os.fdopen(master, 'r+b', 0)
49 self.stdout = self.stdin
50
51 #print "started"
52 self.buf = ""
53 self.last_prompt = ""
54
55 def read_to_prompt(self, prompts, timeout):
56 end_time = time.time() + timeout
57 while time.time() < end_time:
58 [outs,_,_] = select([self.stdin], [], [], 1)
59 if self.stdin in outs:
60 new_data = self.stdin.read(1)
61 #print "new_data: '%s'" % new_data
62 self.buf += new_data
63 for prompt in prompts:
64 regexp = re.compile(prompt)
65 match = regexp.search(self.buf)
66 if match:
67 end = match.end()
68 buf = self.buf[0:end-len(prompt)]
69 self.buf = self.buf[end:]
70 self.last_prompt = prompt
71 return buf
72 return None
73
74 def write(self, str):
75 self.stdout.write(str)
76
77 def cleanup(self):
78 if self.p:
79 self.p.kill()
80 self.p = None
81
82
83 args = parser.parse_args(sys.argv[1:])
84 test_data = args.test_file.read().split('\n')
85
86 if args.rundir: os.chdir(args.rundir)
87
88 r = Runner(args.mal_cmd, redirect=args.redirect)
89
90
91 test_idx = 0
92 def read_test(data):
93 global test_idx
94 form, output, ret = None, "", None
95 while data:
96 test_idx += 1
97 line = data.pop(0)
98 if re.match(r"^\s*$", line): # blank line
99 continue
100 elif line[0:3] == ";;;": # ignore comment
101 continue
102 elif line[0:2] == ";;": # output comment
103 print line[3:]
104 continue
105 elif line[0:2] == ";": # unexpected comment
106 print "Test data error at line %d:\n%s" % (test_idx, line)
107 return None, None, None, test_idx
108 form = line # the line is a form to send
109
110 # Now find the output and return value
111 while data:
112 line = data[0]
113 if line[0:3] == ";=>":
114 ret = line[3:].replace('\\r', '\r').replace('\\n', '\n')
115 test_idx += 1
116 data.pop(0)
117 break
118 elif line[0:2] == "; ":
119 output = output + line[2:] + sep
120 test_idx += 1
121 data.pop(0)
122 else:
123 ret = "*"
124 break
125 if ret: break
126
127 return form, output, ret, test_idx
128
129 def assert_prompt(timeout):
130 # Wait for the initial prompt
131 header = r.read_to_prompt(['user> ', 'mal-user> '], timeout=timeout)
132 if not header == None:
133 if header:
134 print "Started with:\n%s" % header
135 else:
136 print "Did not get 'user> ' or 'mal-user> ' prompt"
137 print " Got : %s" % repr(r.buf)
138 r.cleanup()
139 sys.exit(1)
140
141
142 # Wait for the initial prompt
143 assert_prompt(args.start_timeout)
144
145 # Send the pre-eval code if any
146 if args.pre_eval:
147 sys.stdout.write("RUNNING pre-eval: %s" % args.pre_eval)
148 p.write(args.pre_eval)
149 assert_prompt(args.test_timeout)
150
151 fail_cnt = 0
152
153 while test_data:
154 form, out, ret, line_num = read_test(test_data)
155 if form == None:
156 break
157 sys.stdout.write("TEST: %s -> [%s,%s]" % (form, repr(out), repr(ret)))
158 sys.stdout.flush()
159 expected = "%s%s%s%s" % (form, sep, out, ret)
160
161 r.write(form + "\n")
162 try:
163 res = r.read_to_prompt(['\r\nuser> ', '\nuser> ',
164 '\r\nmal-user> ', '\nmal-user> '],
165 timeout=args.test_timeout)
166 #print "%s,%s,%s" % (idx, repr(p.before), repr(p.after))
167 if ret == "*" or res == expected:
168 print " -> SUCCESS"
169 else:
170 print " -> FAIL (line %d):" % line_num
171 print " Expected : %s" % repr(expected)
172 print " Got : %s" % repr(res)
173 fail_cnt += 1
174 except:
175 print "Got Exception"
176 r.cleanup()
177 sys.exit(1)
178 r.cleanup()
179
180 if fail_cnt > 0:
181 print "FAILURES: %d" % fail_cnt
182 sys.exit(2)
183 sys.exit(0)