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