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