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