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