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