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