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