##// END OF EJS Templates
Fix for test_pr script
Thomas Kluyver -
Show More
@@ -1,266 +1,266 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', ipy_repository, '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 check_call([py, 'setup.py', 'install'])
211 os.chdir(basedir)
211 os.chdir(basedir)
212
212
213 # Environment variables:
213 # Environment variables:
214 orig_path = os.environ["PATH"]
214 orig_path = os.environ["PATH"]
215 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
215 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
216 os.environ.pop("PYTHONPATH", None)
216 os.environ.pop("PYTHONPATH", None)
217
217
218 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
218 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
219 if not os.path.exists(iptest):
219 if not os.path.exists(iptest):
220 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
220 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
221
221
222 print("\nRunning tests, this typically takes a few minutes...")
222 print("\nRunning tests, this typically takes a few minutes...")
223 try:
223 try:
224 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
224 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
225 except CalledProcessError as e:
225 except CalledProcessError as e:
226 return False, e.output.decode('utf-8')
226 return False, e.output.decode('utf-8')
227 finally:
227 finally:
228 # Restore $PATH
228 # Restore $PATH
229 os.environ["PATH"] = orig_path
229 os.environ["PATH"] = orig_path
230
230
231
231
232 def test_pr(num, post_results=True):
232 def test_pr(num, post_results=True):
233 # Get Github authorisation first, so that the user is prompted straight away
233 # Get Github authorisation first, so that the user is prompted straight away
234 # if their login is needed.
234 # if their login is needed.
235 if post_results:
235 if post_results:
236 gh_api.get_auth_token()
236 gh_api.get_auth_token()
237
237
238 testrun = TestRun(num)
238 testrun = TestRun(num)
239
239
240 testrun.get_branch()
240 testrun.get_branch()
241
241
242 testrun.run()
242 testrun.run()
243
243
244 testrun.dump_results()
244 testrun.dump_results()
245
245
246 testrun.save_logs()
246 testrun.save_logs()
247 testrun.print_results()
247 testrun.print_results()
248
248
249 if post_results:
249 if post_results:
250 results_urls = testrun.post_logs
250 testrun.post_logs()
251 testrun.post_results_comment()
251 testrun.post_results_comment()
252 print("(Posted to Github)")
252 print("(Posted to Github)")
253 else:
253 else:
254 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
254 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)
255 print("To post the results to Github, run", post_script)
256
256
257
257
258 if __name__ == '__main__':
258 if __name__ == '__main__':
259 import argparse
259 import argparse
260 parser = argparse.ArgumentParser(description="Test an IPython pull request")
260 parser = argparse.ArgumentParser(description="Test an IPython pull request")
261 parser.add_argument('-p', '--publish', action='store_true',
261 parser.add_argument('-p', '--publish', action='store_true',
262 help="Publish the results to Github")
262 help="Publish the results to Github")
263 parser.add_argument('number', type=int, help="The pull request number")
263 parser.add_argument('number', type=int, help="The pull request number")
264
264
265 args = parser.parse_args()
265 args = parser.parse_args()
266 test_pr(args.number, post_results=args.publish)
266 test_pr(args.number, post_results=args.publish)
General Comments 0
You need to be logged in to leave comments. Login now