##// END OF EJS Templates
swallow stdout and report time for build/install step in test_pr...
MinRK -
Show More
@@ -1,266 +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 90 check_call(['git', 'pull', ipy_repository, '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 check_call([py, 'setup.py', 'install'])
210 tic = time.time()
211 print ("\nInstalling IPython with %s" % py)
212 logfile = os.path.join(basedir, venv, 'install.log')
213 print ("Install log at %s" % logfile)
214 with open(logfile, 'wb') as f:
215 check_call([py, 'setup.py', 'install'], stdout=f)
216 toc = time.time()
217 print ("Installed IPython in %.1fs" % (toc-tic))
211 218 os.chdir(basedir)
212 219
213 220 # Environment variables:
214 221 orig_path = os.environ["PATH"]
215 222 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
216 223 os.environ.pop("PYTHONPATH", None)
217 224
225 # check that the right IPython is imported
226 ipython_file = check_output([py, '-c', 'import IPython; print (IPython.__file__)'])
227 ipython_file = ipython_file.strip().decode('utf-8')
228 if not ipython_file.startswith(os.path.join(basedir, venv)):
229 msg = u"IPython does not appear to be in the venv: %s" % ipython_file
230 msg += u"\nDo you use setupegg.py develop?"
231 print(msg, file=sys.stderr)
232 return False, msg
233
218 234 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
219 235 if not os.path.exists(iptest):
220 236 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
221 237
222 238 print("\nRunning tests, this typically takes a few minutes...")
223 239 try:
224 240 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
225 241 except CalledProcessError as e:
226 242 return False, e.output.decode('utf-8')
227 243 finally:
228 244 # Restore $PATH
229 245 os.environ["PATH"] = orig_path
230 246
231 247
232 248 def test_pr(num, post_results=True):
233 249 # Get Github authorisation first, so that the user is prompted straight away
234 250 # if their login is needed.
235 251 if post_results:
236 252 gh_api.get_auth_token()
237 253
238 254 testrun = TestRun(num)
239 255
240 256 testrun.get_branch()
241 257
242 258 testrun.run()
243 259
244 260 testrun.dump_results()
245 261
246 262 testrun.save_logs()
247 263 testrun.print_results()
248 264
249 265 if post_results:
250 266 testrun.post_logs()
251 267 testrun.post_results_comment()
252 268 print("(Posted to Github)")
253 269 else:
254 270 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
255 271 print("To post the results to Github, run", post_script)
256 272
257 273
258 274 if __name__ == '__main__':
259 275 import argparse
260 276 parser = argparse.ArgumentParser(description="Test an IPython pull request")
261 277 parser.add_argument('-p', '--publish', action='store_true',
262 278 help="Publish the results to Github")
263 279 parser.add_argument('number', type=int, help="The pull request number")
264 280
265 281 args = parser.parse_args()
266 282 test_pr(args.number, post_results=args.publish)
General Comments 0
You need to be logged in to leave comments. Login now