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