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