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