##// END OF EJS Templates
Refactor test_pr.py to a more coherent design.
Thomas Kluyver -
Show More
@@ -1,204 +1,215 b''
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 os
9 import os
10
10
11 import requests
11 import requests
12 import getpass
12 import getpass
13 import json
13 import json
14
14
15 # Keyring stores passwords by a 'username', but we're not storing a username and
15 # Keyring stores passwords by a 'username', but we're not storing a username and
16 # password
16 # password
17 fake_username = 'ipython_tools'
17 fake_username = 'ipython_tools'
18
18
19 class Obj(dict):
20 """Dictionary with attribute access to names."""
21 def __getattr__(self, name):
22 try:
23 return self[name]
24 except KeyError:
25 raise AttributeError(name)
26
27 def __setattr__(self, name, val):
28 self[name] = val
29
19 token = None
30 token = None
20 def get_auth_token():
31 def get_auth_token():
21 global token
32 global token
22
33
23 if token is not None:
34 if token is not None:
24 return token
35 return token
25
36
26 import keyring
37 import keyring
27 token = keyring.get_password('github', fake_username)
38 token = keyring.get_password('github', fake_username)
28 if token is not None:
39 if token is not None:
29 return token
40 return token
30
41
31 print("Please enter your github username and password. These are not "
42 print("Please enter your github username and password. These are not "
32 "stored, only used to get an oAuth token. You can revoke this at "
43 "stored, only used to get an oAuth token. You can revoke this at "
33 "any time on Github.")
44 "any time on Github.")
34 user = input("Username: ")
45 user = input("Username: ")
35 pw = getpass.getpass("Password: ")
46 pw = getpass.getpass("Password: ")
36
47
37 auth_request = {
48 auth_request = {
38 "scopes": [
49 "scopes": [
39 "public_repo",
50 "public_repo",
40 "gist"
51 "gist"
41 ],
52 ],
42 "note": "IPython tools",
53 "note": "IPython tools",
43 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
54 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
44 }
55 }
45 response = requests.post('https://api.github.com/authorizations',
56 response = requests.post('https://api.github.com/authorizations',
46 auth=(user, pw), data=json.dumps(auth_request))
57 auth=(user, pw), data=json.dumps(auth_request))
47 response.raise_for_status()
58 response.raise_for_status()
48 token = json.loads(response.text)['token']
59 token = json.loads(response.text)['token']
49 keyring.set_password('github', fake_username, token)
60 keyring.set_password('github', fake_username, token)
50 return token
61 return token
51
62
52 def make_auth_header():
63 def make_auth_header():
53 return {'Authorization': 'token ' + get_auth_token()}
64 return {'Authorization': 'token ' + get_auth_token()}
54
65
55 def post_issue_comment(project, num, body):
66 def post_issue_comment(project, num, body):
56 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
67 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
57 payload = json.dumps({'body': body})
68 payload = json.dumps({'body': body})
58 r = requests.post(url, data=payload, headers=make_auth_header())
69 r = requests.post(url, data=payload, headers=make_auth_header())
59
70
60 def post_gist(content, description='', filename='file', auth=False):
71 def post_gist(content, description='', filename='file', auth=False):
61 """Post some text to a Gist, and return the URL."""
72 """Post some text to a Gist, and return the URL."""
62 post_data = json.dumps({
73 post_data = json.dumps({
63 "description": description,
74 "description": description,
64 "public": True,
75 "public": True,
65 "files": {
76 "files": {
66 filename: {
77 filename: {
67 "content": content
78 "content": content
68 }
79 }
69 }
80 }
70 }).encode('utf-8')
81 }).encode('utf-8')
71
82
72 headers = make_auth_header() if auth else {}
83 headers = make_auth_header() if auth else {}
73 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
84 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
74 response.raise_for_status()
85 response.raise_for_status()
75 response_data = json.loads(response.text)
86 response_data = json.loads(response.text)
76 return response_data['html_url']
87 return response_data['html_url']
77
88
78 def get_pull_request(project, num, github_api=3):
89 def get_pull_request(project, num, github_api=3):
79 """get pull request info by number
90 """get pull request info by number
80
91
81 github_api : version of github api to use
92 github_api : version of github api to use
82 """
93 """
83 if github_api==2 :
94 if github_api==2 :
84 url = "http://github.com/api/v2/json/pulls/{project}/{num}".format(project=project, num=num)
95 url = "http://github.com/api/v2/json/pulls/{project}/{num}".format(project=project, num=num)
85 elif github_api == 3:
96 elif github_api == 3:
86 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
97 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
87 response = requests.get(url)
98 response = requests.get(url)
88 response.raise_for_status()
99 response.raise_for_status()
89 if github_api == 2 :
100 if github_api == 2 :
90 return json.loads(response.text)['pull']
101 return json.loads(response.text)['pull']
91 return json.loads(response.text)
102 return json.loads(response.text, object_hook=Obj)
92
103
93 def get_pulls_list(project, github_api=3):
104 def get_pulls_list(project, github_api=3):
94 """get pull request list
105 """get pull request list
95
106
96 github_api : version of github api to use
107 github_api : version of github api to use
97 """
108 """
98 if github_api == 3 :
109 if github_api == 3 :
99 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
110 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
100 else :
111 else :
101 url = "http://github.com/api/v2/json/pulls/{project}".format(project=project)
112 url = "http://github.com/api/v2/json/pulls/{project}".format(project=project)
102 response = requests.get(url)
113 response = requests.get(url)
103 response.raise_for_status()
114 response.raise_for_status()
104 if github_api == 2 :
115 if github_api == 2 :
105 return json.loads(response.text)['pulls']
116 return json.loads(response.text)['pulls']
106 return json.loads(response.text)
117 return json.loads(response.text)
107
118
108 # encode_multipart_formdata is from urllib3.filepost
119 # encode_multipart_formdata is from urllib3.filepost
109 # The only change is to iter_fields, to enforce S3's required key ordering
120 # The only change is to iter_fields, to enforce S3's required key ordering
110
121
111 def iter_fields(fields):
122 def iter_fields(fields):
112 fields = fields.copy()
123 fields = fields.copy()
113 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
124 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
114 'Policy', 'Signature', 'Content-Type', 'file'):
125 'Policy', 'Signature', 'Content-Type', 'file'):
115 yield (key, fields.pop(key))
126 yield (key, fields.pop(key))
116 for (k,v) in fields.items():
127 for (k,v) in fields.items():
117 yield k,v
128 yield k,v
118
129
119 def encode_multipart_formdata(fields, boundary=None):
130 def encode_multipart_formdata(fields, boundary=None):
120 """
131 """
121 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
132 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
122
133
123 :param fields:
134 :param fields:
124 Dictionary of fields or list of (key, value) field tuples. The key is
135 Dictionary of fields or list of (key, value) field tuples. The key is
125 treated as the field name, and the value as the body of the form-data
136 treated as the field name, and the value as the body of the form-data
126 bytes. If the value is a tuple of two elements, then the first element
137 bytes. If the value is a tuple of two elements, then the first element
127 is treated as the filename of the form-data section.
138 is treated as the filename of the form-data section.
128
139
129 Field names and filenames must be unicode.
140 Field names and filenames must be unicode.
130
141
131 :param boundary:
142 :param boundary:
132 If not specified, then a random boundary will be generated using
143 If not specified, then a random boundary will be generated using
133 :func:`mimetools.choose_boundary`.
144 :func:`mimetools.choose_boundary`.
134 """
145 """
135 # copy requests imports in here:
146 # copy requests imports in here:
136 from io import BytesIO
147 from io import BytesIO
137 from requests.packages.urllib3.filepost import (
148 from requests.packages.urllib3.filepost import (
138 choose_boundary, six, writer, b, get_content_type
149 choose_boundary, six, writer, b, get_content_type
139 )
150 )
140 body = BytesIO()
151 body = BytesIO()
141 if boundary is None:
152 if boundary is None:
142 boundary = choose_boundary()
153 boundary = choose_boundary()
143
154
144 for fieldname, value in iter_fields(fields):
155 for fieldname, value in iter_fields(fields):
145 body.write(b('--%s\r\n' % (boundary)))
156 body.write(b('--%s\r\n' % (boundary)))
146
157
147 if isinstance(value, tuple):
158 if isinstance(value, tuple):
148 filename, data = value
159 filename, data = value
149 writer(body).write('Content-Disposition: form-data; name="%s"; '
160 writer(body).write('Content-Disposition: form-data; name="%s"; '
150 'filename="%s"\r\n' % (fieldname, filename))
161 'filename="%s"\r\n' % (fieldname, filename))
151 body.write(b('Content-Type: %s\r\n\r\n' %
162 body.write(b('Content-Type: %s\r\n\r\n' %
152 (get_content_type(filename))))
163 (get_content_type(filename))))
153 else:
164 else:
154 data = value
165 data = value
155 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
166 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
156 % (fieldname))
167 % (fieldname))
157 body.write(b'Content-Type: text/plain\r\n\r\n')
168 body.write(b'Content-Type: text/plain\r\n\r\n')
158
169
159 if isinstance(data, int):
170 if isinstance(data, int):
160 data = str(data) # Backwards compatibility
171 data = str(data) # Backwards compatibility
161 if isinstance(data, six.text_type):
172 if isinstance(data, six.text_type):
162 writer(body).write(data)
173 writer(body).write(data)
163 else:
174 else:
164 body.write(data)
175 body.write(data)
165
176
166 body.write(b'\r\n')
177 body.write(b'\r\n')
167
178
168 body.write(b('--%s--\r\n' % (boundary)))
179 body.write(b('--%s--\r\n' % (boundary)))
169
180
170 content_type = b('multipart/form-data; boundary=%s' % boundary)
181 content_type = b('multipart/form-data; boundary=%s' % boundary)
171
182
172 return body.getvalue(), content_type
183 return body.getvalue(), content_type
173
184
174
185
175 def post_download(project, filename, name=None, description=""):
186 def post_download(project, filename, name=None, description=""):
176 """Upload a file to the GitHub downloads area"""
187 """Upload a file to the GitHub downloads area"""
177 if name is None:
188 if name is None:
178 name = os.path.basename(filename)
189 name = os.path.basename(filename)
179 with open(filename, 'rb') as f:
190 with open(filename, 'rb') as f:
180 filedata = f.read()
191 filedata = f.read()
181
192
182 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
193 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
183
194
184 payload = json.dumps(dict(name=name, size=len(filedata),
195 payload = json.dumps(dict(name=name, size=len(filedata),
185 description=description))
196 description=description))
186 response = requests.post(url, data=payload, headers=make_auth_header())
197 response = requests.post(url, data=payload, headers=make_auth_header())
187 response.raise_for_status()
198 response.raise_for_status()
188 reply = json.loads(response.content)
199 reply = json.loads(response.content)
189 s3_url = reply['s3_url']
200 s3_url = reply['s3_url']
190
201
191 fields = dict(
202 fields = dict(
192 key=reply['path'],
203 key=reply['path'],
193 acl=reply['acl'],
204 acl=reply['acl'],
194 success_action_status=201,
205 success_action_status=201,
195 Filename=reply['name'],
206 Filename=reply['name'],
196 AWSAccessKeyId=reply['accesskeyid'],
207 AWSAccessKeyId=reply['accesskeyid'],
197 Policy=reply['policy'],
208 Policy=reply['policy'],
198 Signature=reply['signature'],
209 Signature=reply['signature'],
199 file=(reply['name'], filedata),
210 file=(reply['name'], filedata),
200 )
211 )
201 fields['Content-Type'] = reply['mime_type']
212 fields['Content-Type'] = reply['mime_type']
202 data, content_type = encode_multipart_formdata(fields)
213 data, content_type = encode_multipart_formdata(fields)
203 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
214 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
204 return s3r
215 return s3r
@@ -1,13 +1,13 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Post the results of a pull request test to Github.
2 """Post the results of a pull request test to Github.
3 """
3 """
4 from test_pr import load_results, post_logs, post_results_comment, print_results
4 from test_pr import TestRun
5
5
6 num, results, pr, unavailable_pythons = load_results()
6 testrun = TestRun.load_results()
7 results_urls = post_logs(results)
7 testrun.post_logs()
8 print_results(pr, results_urls, unavailable_pythons)
8 testrun.print_results()
9 post_results_comment(pr, results_urls, num, unavailable_pythons)
9 testrun.post_results_comment()
10
10
11 print()
11 print()
12 print("Posted test results to pull request")
12 print("Posted test results to pull request")
13 print(" " + pr['html_url'])
13 print(" " + testrun.pr['html_url'])
@@ -1,259 +1,266 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """
2 """
3 This is a script for testing pull requests for IPython. It merges the pull
3 This is a script for testing pull requests for IPython. It merges the pull
4 request with current master, installs and tests on all available versions of
4 request with current master, installs and tests on all available versions of
5 Python, and posts the results to Gist if any tests fail.
5 Python, and posts the results to Gist if any tests fail.
6
6
7 Usage:
7 Usage:
8 python test_pr.py 1657
8 python test_pr.py 1657
9 """
9 """
10 from __future__ import print_function
10 from __future__ import print_function
11
11
12 import errno
12 import errno
13 from glob import glob
13 from glob import glob
14 import io
14 import io
15 import json
15 import json
16 import os
16 import os
17 import pickle
17 import pickle
18 import re
18 import re
19 import requests
19 import requests
20 import shutil
20 import shutil
21 import time
21 import time
22 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
22 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
23 import sys
23 import sys
24
24
25 import gh_api
25 import gh_api
26 from gh_api import Obj
26
27
27 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
28 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
28 repodir = os.path.join(basedir, "ipython")
29 repodir = os.path.join(basedir, "ipython")
29 ipy_repository = 'git://github.com/ipython/ipython.git'
30 ipy_repository = 'git://github.com/ipython/ipython.git'
30 ipy_http_repository = 'http://github.com/ipython/ipython.git'
31 ipy_http_repository = 'http://github.com/ipython/ipython.git'
31 gh_project="ipython/ipython"
32 gh_project="ipython/ipython"
32
33
33 supported_pythons = ['python2.6', 'python2.7', 'python3.1', 'python3.2']
34 supported_pythons = ['python2.6', 'python2.7', 'python3.1', 'python3.2']
34 unavailable_pythons = []
35
35
36 def available_python_versions():
37 """Get the executable names of available versions of Python on the system.
38 """
39 del unavailable_pythons[:]
40 for py in supported_pythons:
41 try:
42 check_call([py, '-c', 'import nose'], stdout=PIPE)
43 yield py
44 except (OSError, CalledProcessError):
45 unavailable_pythons.append(py)
46
47 venvs = []
48
49 def setup():
50 """Prepare the repository and virtualenvs."""
51 global venvs
52
53 try:
54 os.mkdir(basedir)
55 except OSError as e:
56 if e.errno != errno.EEXIST:
57 raise
58 os.chdir(basedir)
59
60 # Delete virtualenvs and recreate
61 for venv in glob('venv-*'):
62 shutil.rmtree(venv)
63 for py in available_python_versions():
64 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
65 venvs.append((py, 'venv-%s' % py))
66
67 # Check out and update the repository
68 if not os.path.exists('ipython'):
69 try :
70 check_call(['git', 'clone', ipy_repository])
71 except CalledProcessError :
72 check_call(['git', 'clone', ipy_http_repository])
73 os.chdir(repodir)
74 check_call(['git', 'checkout', 'master'])
75 try :
76 check_call(['git', 'pull', ipy_repository, 'master'])
77 except CalledProcessError :
78 check_call(['git', 'pull', ipy_http_repository, 'master'])
79 os.chdir(basedir)
80
81 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
82 r"\s*(.*?)\n")
37 r"\s*(.*?)\n")
83 def get_missing_libraries(log):
38 def get_missing_libraries(log):
84 m = missing_libs_re.search(log)
39 m = missing_libs_re.search(log)
85 if m:
40 if m:
86 return m.group(1)
41 return m.group(1)
87
42
88 def get_branch(repo, branch, owner, mergeable):
43 class TestRun(object):
89 os.chdir(repodir)
44 def __init__(self, pr_num):
90 if mergeable:
45 self.unavailable_pythons = []
91 merged_branch = "%s-%s" % (owner, branch)
46 self.venvs = []
92 # Delete the branch first
47 self.pr_num = pr_num
93 call(['git', 'branch', '-D', merged_branch])
48
94 check_call(['git', 'checkout', '-b', merged_branch])
49 self.pr = gh_api.get_pull_request(gh_project, pr_num)
95 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
50
96 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
51 self.setup()
97 else:
52
98 # Fetch the branch without merging it.
53 self.results = []
99 check_call(['git', 'fetch', repo, branch])
54
100 check_call(['git', 'checkout', 'FETCH_HEAD'])
55 def available_python_versions(self):
101 os.chdir(basedir)
56 """Get the executable names of available versions of Python on the system.
57 """
58 for py in supported_pythons:
59 try:
60 check_call([py, '-c', 'import nose'], stdout=PIPE)
61 yield py
62 except (OSError, CalledProcessError):
63 self.unavailable_pythons.append(py)
64
65 def setup(self):
66 """Prepare the repository and virtualenvs."""
67 try:
68 os.mkdir(basedir)
69 except OSError as e:
70 if e.errno != errno.EEXIST:
71 raise
72 os.chdir(basedir)
73
74 # Delete virtualenvs and recreate
75 for venv in glob('venv-*'):
76 shutil.rmtree(venv)
77 for py in self.available_python_versions():
78 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
79 self.venvs.append((py, 'venv-%s' % py))
80
81 # Check out and update the repository
82 if not os.path.exists('ipython'):
83 try :
84 check_call(['git', 'clone', ipy_repository])
85 except CalledProcessError :
86 check_call(['git', 'clone', ipy_http_repository])
87 os.chdir(repodir)
88 check_call(['git', 'checkout', 'master'])
89 try :
90 check_call(['git', 'pull', ipy_repository, 'master'])
91 except CalledProcessError :
92 check_call(['git', 'pull', ipy_http_repository, 'master'])
93 os.chdir(basedir)
94
95 def get_branch(self):
96 repo = self.pr['head']['repo']['clone_url']
97 branch = self.pr['head']['ref']
98 owner = self.pr['head']['repo']['owner']['login']
99 mergeable = self.pr['mergeable']
100
101 os.chdir(repodir)
102 if mergeable:
103 merged_branch = "%s-%s" % (owner, branch)
104 # Delete the branch first
105 call(['git', 'branch', '-D', merged_branch])
106 check_call(['git', 'checkout', '-b', merged_branch])
107 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
108 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
109 else:
110 # Fetch the branch without merging it.
111 check_call(['git', 'fetch', repo, branch])
112 check_call(['git', 'checkout', 'FETCH_HEAD'])
113 os.chdir(basedir)
114
115 def markdown_format(self):
116 def format_result(result):
117 s = "* %s: " % result.py
118 if result.passed:
119 s += "OK"
120 else:
121 s += "Failed, log at %s" % result.log_url
122 if result.missing_libraries:
123 s += " (libraries not available: " + result.missing_libraries + ")"
124 return s
125
126 if self.pr['mergeable']:
127 com = self.pr['head']['sha'][:7] + " merged into master"
128 else:
129 com = self.pr['head']['sha'][:7] + " (can't merge cleanly)"
130 lines = ["**Test results for commit %s**" % com,
131 "Platform: " + sys.platform,
132 ""] + \
133 [format_result(r) for r in self.results] + \
134 ["",
135 "Not available for testing: " + ", ".join(self.unavailable_pythons)]
136 return "\n".join(lines)
137
138 def post_results_comment(self):
139 body = self.markdown_format()
140 gh_api.post_issue_comment(gh_project, self.pr_num, body)
141
142 def print_results(self):
143 pr = self.pr
144
145 print("\n")
146 if pr['mergeable']:
147 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
148 else:
149 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
150 print("Platform:", sys.platform)
151 for result in self.results:
152 if result.passed:
153 print(result.py, ":", "OK")
154 else:
155 print(result.py, ":", "Failed")
156 print(" Test log:", result.get('log_url') or result.log_file)
157 if result.missing_libraries:
158 print(" Libraries not available:", result.missing_libraries)
159 print("Not available for testing:", ", ".join(self.unavailable_pythons))
160
161 def dump_results(self):
162 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
163 pickle.dump(self, f)
164
165 @staticmethod
166 def load_results():
167 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
168 return pickle.load(f)
169
170 def save_logs(self):
171 for result in self.results:
172 if not result.passed:
173 result_locn = os.path.abspath(os.path.join('venv-%s' % result.py,
174 self.pr['head']['sha'][:7]+".log"))
175 with io.open(result_locn, 'w', encoding='utf-8') as f:
176 f.write(result.log)
177
178 result.log_file = result_locn
179
180 def post_logs(self):
181 for result in self.results:
182 if not result.passed:
183 result.log_url = gh_api.post_gist(result.log,
184 description='IPython test log',
185 filename="results.log", auth=True)
186
187 def run(self):
188 for py, venv in self.venvs:
189 tic = time.time()
190 passed, log = run_tests(venv)
191 elapsed = int(time.time() - tic)
192 print("Ran tests with %s in %is" % (py, elapsed))
193 missing_libraries = get_missing_libraries(log)
194
195 self.results.append(Obj(py=py,
196 passed=passed,
197 log=log,
198 missing_libraries=missing_libraries
199 )
200 )
201
102
202
103 def run_tests(venv):
203 def run_tests(venv):
104 py = os.path.join(basedir, venv, 'bin', 'python')
204 py = os.path.join(basedir, venv, 'bin', 'python')
105 print(py)
205 print(py)
106 os.chdir(repodir)
206 os.chdir(repodir)
107 # cleanup build-dir
207 # cleanup build-dir
108 if os.path.exists('build'):
208 if os.path.exists('build'):
109 shutil.rmtree('build')
209 shutil.rmtree('build')
110 check_call([py, 'setup.py', 'install'])
210 check_call([py, 'setup.py', 'install'])
111 os.chdir(basedir)
211 os.chdir(basedir)
112
212
113 # Environment variables:
213 # Environment variables:
114 orig_path = os.environ["PATH"]
214 orig_path = os.environ["PATH"]
115 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
215 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
116 os.environ.pop("PYTHONPATH", None)
216 os.environ.pop("PYTHONPATH", None)
117
217
118 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
218 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
119 if not os.path.exists(iptest):
219 if not os.path.exists(iptest):
120 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
220 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
121
221
122 print("\nRunning tests, this typically takes a few minutes...")
222 print("\nRunning tests, this typically takes a few minutes...")
123 try:
223 try:
124 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
224 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
125 except CalledProcessError as e:
225 except CalledProcessError as e:
126 return False, e.output.decode('utf-8')
226 return False, e.output.decode('utf-8')
127 finally:
227 finally:
128 # Restore $PATH
228 # Restore $PATH
129 os.environ["PATH"] = orig_path
229 os.environ["PATH"] = orig_path
130
230
131 def markdown_format(pr, results_urls, unavailable_pythons):
132 def format_result(py, passed, gist_url, missing_libraries):
133 s = "* %s: " % py
134 if passed:
135 s += "OK"
136 else:
137 s += "Failed, log at %s" % gist_url
138 if missing_libraries:
139 s += " (libraries not available: " + missing_libraries + ")"
140 return s
141
142 if pr['mergeable']:
143 com = pr['head']['sha'][:7] + " merged into master"
144 else:
145 com = pr['head']['sha'][:7] + " (can't merge cleanly)"
146 lines = ["**Test results for commit %s**" % com,
147 "Platform: " + sys.platform,
148 ""] + \
149 [format_result(*r) for r in results_urls] + \
150 ["",
151 "Not available for testing: " + ", ".join(unavailable_pythons)]
152 return "\n".join(lines)
153
154 def post_results_comment(pr, results, num, unavailable_pythons=unavailable_pythons):
155 body = markdown_format(pr, results, unavailable_pythons)
156 gh_api.post_issue_comment(gh_project, num, body)
157
158 def print_results(pr, results_urls, unavailable_pythons=unavailable_pythons):
159 print("\n")
160 if pr['mergeable']:
161 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
162 else:
163 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
164 print("Platform:", sys.platform)
165 for py, passed, gist_url, missing_libraries in results_urls:
166 if passed:
167 print(py, ":", "OK")
168 else:
169 print(py, ":", "Failed")
170 print(" Test log:", gist_url)
171 if missing_libraries:
172 print(" Libraries not available:", missing_libraries)
173 print("Not available for testing:", ", ".join(unavailable_pythons))
174
175 def dump_results(num, results, pr):
176 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
177 pickle.dump((num, results, pr, unavailable_pythons), f)
178
179 def load_results():
180 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
181 return pickle.load(f)
182
183 def save_logs(results, pr):
184 results_paths = []
185 for py, passed, log, missing_libraries in results:
186 if passed:
187 results_paths.append((py, passed, None, missing_libraries))
188 else:
189
190 result_locn = os.path.abspath(os.path.join('venv-%s' % py,
191 pr['head']['sha'][:7]+".log"))
192 with io.open(result_locn, 'w', encoding='utf-8') as f:
193 f.write(log)
194
195 results_paths.append((py, False, result_locn, missing_libraries))
196
197 return results_paths
198
199 def post_logs(results):
200 results_urls = []
201 for py, passed, log, missing_libraries in results:
202 if passed:
203 results_urls.append((py, passed, None, missing_libraries))
204 else:
205 result_locn = gh_api.post_gist(log, description='IPython test log',
206 filename="results.log", auth=True)
207 results_urls.append((py, False, result_locn, missing_libraries))
208
209 return results_urls
210
231
211 def test_pr(num, post_results=True):
232 def test_pr(num, post_results=True):
212 # Get Github authorisation first, so that the user is prompted straight away
233 # Get Github authorisation first, so that the user is prompted straight away
213 # if their login is needed.
234 # if their login is needed.
214 if post_results:
235 if post_results:
215 gh_api.get_auth_token()
236 gh_api.get_auth_token()
216
237
217 setup()
238 testrun = TestRun(num)
218 pr = gh_api.get_pull_request(gh_project, num)
219 get_branch(repo=pr['head']['repo']['clone_url'],
220 branch=pr['head']['ref'],
221 owner=pr['head']['repo']['owner']['login'],
222 mergeable=pr['mergeable'],
223 )
224
239
225 results = []
240 testrun.get_branch()
226 for py, venv in venvs:
227 tic = time.time()
228 passed, log = run_tests(venv)
229 elapsed = int(time.time() - tic)
230 print("Ran tests with %s in %is" % (py, elapsed))
231 missing_libraries = get_missing_libraries(log)
232 if passed:
233 results.append((py, True, None, missing_libraries))
234 else:
235 results.append((py, False, log, missing_libraries))
236
241
237 dump_results(num, results, pr)
242 testrun.run()
243
244 testrun.dump_results()
238
245
239 results_paths = save_logs(results, pr)
246 testrun.save_logs()
240 print_results(pr, results_paths)
247 testrun.print_results()
241
248
242 if post_results:
249 if post_results:
243 results_urls = post_logs(results)
250 results_urls = testrun.post_logs
244 post_results_comment(pr, results_urls, num)
251 testrun.post_results_comment()
245 print("(Posted to Github)")
252 print("(Posted to Github)")
246 else:
253 else:
247 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
254 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
248 print("To post the results to Github, run", post_script)
255 print("To post the results to Github, run", post_script)
249
256
250
257
251 if __name__ == '__main__':
258 if __name__ == '__main__':
252 import argparse
259 import argparse
253 parser = argparse.ArgumentParser(description="Test an IPython pull request")
260 parser = argparse.ArgumentParser(description="Test an IPython pull request")
254 parser.add_argument('-p', '--publish', action='store_true',
261 parser.add_argument('-p', '--publish', action='store_true',
255 help="Publish the results to Github")
262 help="Publish the results to Github")
256 parser.add_argument('number', type=int, help="The pull request number")
263 parser.add_argument('number', type=int, help="The pull request number")
257
264
258 args = parser.parse_args()
265 args = parser.parse_args()
259 test_pr(args.number, post_results=args.publish)
266 test_pr(args.number, post_results=args.publish)
General Comments 0
You need to be logged in to leave comments. Login now