##// END OF EJS Templates
Merge pull request #2236 from minrk/test_pr...
Thomas Kluyver -
r8117:4a9e2ada merge
parent child Browse files
Show More
@@ -1,266 +1,282
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 check_call([py, 'setup.py', 'install'])
210 tic = time.time()
211 print ("\nInstalling IPython with %s" % py)
212 logfile = os.path.join(basedir, venv, 'install.log')
213 print ("Install log at %s" % logfile)
214 with open(logfile, 'wb') as f:
215 check_call([py, 'setup.py', 'install'], stdout=f)
216 toc = time.time()
217 print ("Installed IPython in %.1fs" % (toc-tic))
211 os.chdir(basedir)
218 os.chdir(basedir)
212
219
213 # Environment variables:
220 # Environment variables:
214 orig_path = os.environ["PATH"]
221 orig_path = os.environ["PATH"]
215 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"]
216 os.environ.pop("PYTHONPATH", None)
223 os.environ.pop("PYTHONPATH", None)
217
224
225 # check that the right IPython is imported
226 ipython_file = check_output([py, '-c', 'import IPython; print (IPython.__file__)'])
227 ipython_file = ipython_file.strip().decode('utf-8')
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
230 msg += u"\nDo you use setupegg.py develop?"
231 print(msg, file=sys.stderr)
232 return False, msg
233
218 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
234 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
219 if not os.path.exists(iptest):
235 if not os.path.exists(iptest):
220 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
236 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
221
237
222 print("\nRunning tests, this typically takes a few minutes...")
238 print("\nRunning tests, this typically takes a few minutes...")
223 try:
239 try:
224 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
240 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
225 except CalledProcessError as e:
241 except CalledProcessError as e:
226 return False, e.output.decode('utf-8')
242 return False, e.output.decode('utf-8')
227 finally:
243 finally:
228 # Restore $PATH
244 # Restore $PATH
229 os.environ["PATH"] = orig_path
245 os.environ["PATH"] = orig_path
230
246
231
247
232 def test_pr(num, post_results=True):
248 def test_pr(num, post_results=True):
233 # 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
234 # if their login is needed.
250 # if their login is needed.
235 if post_results:
251 if post_results:
236 gh_api.get_auth_token()
252 gh_api.get_auth_token()
237
253
238 testrun = TestRun(num)
254 testrun = TestRun(num)
239
255
240 testrun.get_branch()
256 testrun.get_branch()
241
257
242 testrun.run()
258 testrun.run()
243
259
244 testrun.dump_results()
260 testrun.dump_results()
245
261
246 testrun.save_logs()
262 testrun.save_logs()
247 testrun.print_results()
263 testrun.print_results()
248
264
249 if post_results:
265 if post_results:
250 testrun.post_logs()
266 testrun.post_logs()
251 testrun.post_results_comment()
267 testrun.post_results_comment()
252 print("(Posted to Github)")
268 print("(Posted to Github)")
253 else:
269 else:
254 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")
255 print("To post the results to Github, run", post_script)
271 print("To post the results to Github, run", post_script)
256
272
257
273
258 if __name__ == '__main__':
274 if __name__ == '__main__':
259 import argparse
275 import argparse
260 parser = argparse.ArgumentParser(description="Test an IPython pull request")
276 parser = argparse.ArgumentParser(description="Test an IPython pull request")
261 parser.add_argument('-p', '--publish', action='store_true',
277 parser.add_argument('-p', '--publish', action='store_true',
262 help="Publish the results to Github")
278 help="Publish the results to Github")
263 parser.add_argument('number', type=int, help="The pull request number")
279 parser.add_argument('number', type=int, help="The pull request number")
264
280
265 args = parser.parse_args()
281 args = parser.parse_args()
266 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