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