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