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