##// END OF EJS Templates
Update test_pr to allow passing args to iptest
Thomas Kluyver -
Show More
@@ -1,282 +1,288
1 1 #!/usr/bin/env python
2 2 """
3 3 This is a script for testing pull requests for IPython. It merges the pull
4 4 request with current master, installs and tests on all available versions of
5 5 Python, and posts the results to Gist if any tests fail.
6 6
7 7 Usage:
8 8 python test_pr.py 1657
9 9 """
10 10 from __future__ import print_function
11 11
12 12 import errno
13 13 from glob import glob
14 14 import io
15 15 import json
16 16 import os
17 17 import pickle
18 18 import re
19 19 import requests
20 20 import shutil
21 21 import time
22 22 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
23 23 import sys
24 24
25 25 import gh_api
26 26 from gh_api import Obj
27 27
28 28 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
29 29 repodir = os.path.join(basedir, "ipython")
30 30 ipy_repository = 'git://github.com/ipython/ipython.git'
31 31 ipy_http_repository = 'http://github.com/ipython/ipython.git'
32 32 gh_project="ipython/ipython"
33 33
34 34 supported_pythons = ['python2.6', 'python2.7', 'python3.2']
35 35
36 36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
37 37 r"\s*(.*?)\n")
38 38 def get_missing_libraries(log):
39 39 m = missing_libs_re.search(log)
40 40 if m:
41 41 return m.group(1)
42 42
43 43 class TestRun(object):
44 def __init__(self, pr_num):
44 def __init__(self, pr_num, extra_args):
45 45 self.unavailable_pythons = []
46 46 self.venvs = []
47 47 self.pr_num = pr_num
48 self.extra_args = extra_args
48 49
49 50 self.pr = gh_api.get_pull_request(gh_project, pr_num)
50 51
51 52 self.setup()
52 53
53 54 self.results = []
54 55
55 56 def available_python_versions(self):
56 57 """Get the executable names of available versions of Python on the system.
57 58 """
58 59 for py in supported_pythons:
59 60 try:
60 61 check_call([py, '-c', 'import nose'], stdout=PIPE)
61 62 yield py
62 63 except (OSError, CalledProcessError):
63 64 self.unavailable_pythons.append(py)
64 65
65 66 def setup(self):
66 67 """Prepare the repository and virtualenvs."""
67 68 try:
68 69 os.mkdir(basedir)
69 70 except OSError as e:
70 71 if e.errno != errno.EEXIST:
71 72 raise
72 73 os.chdir(basedir)
73 74
74 75 # Delete virtualenvs and recreate
75 76 for venv in glob('venv-*'):
76 77 shutil.rmtree(venv)
77 78 for py in self.available_python_versions():
78 79 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
79 80 self.venvs.append((py, 'venv-%s' % py))
80 81
81 82 # Check out and update the repository
82 83 if not os.path.exists('ipython'):
83 84 try :
84 85 check_call(['git', 'clone', ipy_repository])
85 86 except CalledProcessError :
86 87 check_call(['git', 'clone', ipy_http_repository])
87 88 os.chdir(repodir)
88 89 check_call(['git', 'checkout', 'master'])
89 90 try :
90 91 check_call(['git', 'pull', 'origin', 'master'])
91 92 except CalledProcessError :
92 93 check_call(['git', 'pull', ipy_http_repository, 'master'])
93 94 os.chdir(basedir)
94 95
95 96 def get_branch(self):
96 97 repo = self.pr['head']['repo']['clone_url']
97 98 branch = self.pr['head']['ref']
98 99 owner = self.pr['head']['repo']['owner']['login']
99 100 mergeable = self.pr['mergeable']
100 101
101 102 os.chdir(repodir)
102 103 if mergeable:
103 104 merged_branch = "%s-%s" % (owner, branch)
104 105 # Delete the branch first
105 106 call(['git', 'branch', '-D', merged_branch])
106 107 check_call(['git', 'checkout', '-b', merged_branch])
107 108 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
108 109 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
109 110 else:
110 111 # Fetch the branch without merging it.
111 112 check_call(['git', 'fetch', repo, branch])
112 113 check_call(['git', 'checkout', 'FETCH_HEAD'])
113 114 os.chdir(basedir)
114 115
115 116 def markdown_format(self):
116 117 def format_result(result):
117 118 s = "* %s: " % result.py
118 119 if result.passed:
119 120 s += "OK"
120 121 else:
121 122 s += "Failed, log at %s" % result.log_url
122 123 if result.missing_libraries:
123 124 s += " (libraries not available: " + result.missing_libraries + ")"
124 125 return s
125 126
126 127 if self.pr['mergeable']:
127 128 com = self.pr['head']['sha'][:7] + " merged into master"
128 129 else:
129 130 com = self.pr['head']['sha'][:7] + " (can't merge cleanly)"
130 131 lines = ["**Test results for commit %s**" % com,
131 132 "Platform: " + sys.platform,
132 133 ""] + \
133 134 [format_result(r) for r in self.results] + \
134 ["",
135 "Not available for testing: " + ", ".join(self.unavailable_pythons)]
135 [""]
136 if self.extra_args:
137 lines.append("Extra args: %r" % self.extra_args),
138 lines.append("Not available for testing: " + ", ".join(self.unavailable_pythons))
136 139 return "\n".join(lines)
137 140
138 141 def post_results_comment(self):
139 142 body = self.markdown_format()
140 143 gh_api.post_issue_comment(gh_project, self.pr_num, body)
141 144
142 145 def print_results(self):
143 146 pr = self.pr
144 147
145 148 print("\n")
146 149 if pr['mergeable']:
147 150 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
148 151 else:
149 152 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
150 153 print("Platform:", sys.platform)
151 154 for result in self.results:
152 155 if result.passed:
153 156 print(result.py, ":", "OK")
154 157 else:
155 158 print(result.py, ":", "Failed")
156 159 print(" Test log:", result.get('log_url') or result.log_file)
157 160 if result.missing_libraries:
158 161 print(" Libraries not available:", result.missing_libraries)
162
163 if self.extra_args:
164 print("Extra args:", self.extra_args)
159 165 print("Not available for testing:", ", ".join(self.unavailable_pythons))
160 166
161 167 def dump_results(self):
162 168 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
163 169 pickle.dump(self, f)
164 170
165 171 @staticmethod
166 172 def load_results():
167 173 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
168 174 return pickle.load(f)
169 175
170 176 def save_logs(self):
171 177 for result in self.results:
172 178 if not result.passed:
173 179 result_locn = os.path.abspath(os.path.join('venv-%s' % result.py,
174 180 self.pr['head']['sha'][:7]+".log"))
175 181 with io.open(result_locn, 'w', encoding='utf-8') as f:
176 182 f.write(result.log)
177 183
178 184 result.log_file = result_locn
179 185
180 186 def post_logs(self):
181 187 for result in self.results:
182 188 if not result.passed:
183 189 result.log_url = gh_api.post_gist(result.log,
184 190 description='IPython test log',
185 191 filename="results.log", auth=True)
186 192
187 193 def run(self):
188 194 for py, venv in self.venvs:
189 195 tic = time.time()
190 passed, log = run_tests(venv)
196 passed, log = run_tests(venv, self.extra_args)
191 197 elapsed = int(time.time() - tic)
192 198 print("Ran tests with %s in %is" % (py, elapsed))
193 199 missing_libraries = get_missing_libraries(log)
194 200
195 201 self.results.append(Obj(py=py,
196 202 passed=passed,
197 203 log=log,
198 204 missing_libraries=missing_libraries
199 205 )
200 206 )
201 207
202 208
203 def run_tests(venv):
209 def run_tests(venv, extra_args):
204 210 py = os.path.join(basedir, venv, 'bin', 'python')
205 211 print(py)
206 212 os.chdir(repodir)
207 213 # cleanup build-dir
208 214 if os.path.exists('build'):
209 215 shutil.rmtree('build')
210 216 tic = time.time()
211 217 print ("\nInstalling IPython with %s" % py)
212 218 logfile = os.path.join(basedir, venv, 'install.log')
213 219 print ("Install log at %s" % logfile)
214 220 with open(logfile, 'wb') as f:
215 221 check_call([py, 'setup.py', 'install'], stdout=f)
216 222 toc = time.time()
217 223 print ("Installed IPython in %.1fs" % (toc-tic))
218 224 os.chdir(basedir)
219 225
220 226 # Environment variables:
221 227 orig_path = os.environ["PATH"]
222 228 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
223 229 os.environ.pop("PYTHONPATH", None)
224 230
225 231 # check that the right IPython is imported
226 232 ipython_file = check_output([py, '-c', 'import IPython; print (IPython.__file__)'])
227 233 ipython_file = ipython_file.strip().decode('utf-8')
228 234 if not ipython_file.startswith(os.path.join(basedir, venv)):
229 msg = u"IPython does not appear to be in the venv: %s" % ipython_file
230 msg += u"\nDo you use setupegg.py develop?"
235 msg = "IPython does not appear to be in the venv: %s" % ipython_file
236 msg += "\nDo you use setupegg.py develop?"
231 237 print(msg, file=sys.stderr)
232 238 return False, msg
233 239
234 240 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
235 241 if not os.path.exists(iptest):
236 242 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
237 243
238 244 print("\nRunning tests, this typically takes a few minutes...")
239 245 try:
240 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
246 return True, check_output([iptest] + extra_args, stderr=STDOUT).decode('utf-8')
241 247 except CalledProcessError as e:
242 248 return False, e.output.decode('utf-8')
243 249 finally:
244 250 # Restore $PATH
245 251 os.environ["PATH"] = orig_path
246 252
247 253
248 def test_pr(num, post_results=True):
254 def test_pr(num, post_results=True, extra_args=None):
249 255 # Get Github authorisation first, so that the user is prompted straight away
250 256 # if their login is needed.
251 257 if post_results:
252 258 gh_api.get_auth_token()
253 259
254 testrun = TestRun(num)
260 testrun = TestRun(num, extra_args or [])
255 261
256 262 testrun.get_branch()
257 263
258 264 testrun.run()
259 265
260 266 testrun.dump_results()
261 267
262 268 testrun.save_logs()
263 269 testrun.print_results()
264 270
265 271 if post_results:
266 272 testrun.post_logs()
267 273 testrun.post_results_comment()
268 274 print("(Posted to Github)")
269 275 else:
270 276 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
271 277 print("To post the results to Github, run", post_script)
272 278
273 279
274 280 if __name__ == '__main__':
275 281 import argparse
276 282 parser = argparse.ArgumentParser(description="Test an IPython pull request")
277 283 parser.add_argument('-p', '--publish', action='store_true',
278 284 help="Publish the results to Github")
279 285 parser.add_argument('number', type=int, help="The pull request number")
280 286
281 args = parser.parse_args()
282 test_pr(args.number, post_results=args.publish)
287 args, extra_args = parser.parse_known_args()
288 test_pr(args.number, post_results=args.publish, extra_args=extra_args)
General Comments 0
You need to be logged in to leave comments. Login now