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