##// 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():
36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
37 r"\s*(.*?)\n")
38 def get_missing_libraries(log):
39 m = missing_libs_re.search(log)
40 if m:
41 return m.group(1)
42
43 class TestRun(object):
44 def __init__(self, pr_num):
45 self.unavailable_pythons = []
46 self.venvs = []
47 self.pr_num = pr_num
48
49 self.pr = gh_api.get_pull_request(gh_project, pr_num)
50
51 self.setup()
52
53 self.results = []
54
55 def available_python_versions(self):
37 """Get the executable names of available versions of Python on the system.
56 """Get the executable names of available versions of Python on the system.
38 """
57 """
39 del unavailable_pythons[:]
40 for py in supported_pythons:
58 for py in supported_pythons:
41 try:
59 try:
42 check_call([py, '-c', 'import nose'], stdout=PIPE)
60 check_call([py, '-c', 'import nose'], stdout=PIPE)
43 yield py
61 yield py
44 except (OSError, CalledProcessError):
62 except (OSError, CalledProcessError):
45 unavailable_pythons.append(py)
63 self.unavailable_pythons.append(py)
46
47 venvs = []
48
64
49 def setup():
65 def setup(self):
50 """Prepare the repository and virtualenvs."""
66 """Prepare the repository and virtualenvs."""
51 global venvs
52
53 try:
67 try:
54 os.mkdir(basedir)
68 os.mkdir(basedir)
55 except OSError as e:
69 except OSError as e:
56 if e.errno != errno.EEXIST:
70 if e.errno != errno.EEXIST:
57 raise
71 raise
58 os.chdir(basedir)
72 os.chdir(basedir)
59
73
60 # Delete virtualenvs and recreate
74 # Delete virtualenvs and recreate
61 for venv in glob('venv-*'):
75 for venv in glob('venv-*'):
62 shutil.rmtree(venv)
76 shutil.rmtree(venv)
63 for py in available_python_versions():
77 for py in self.available_python_versions():
64 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
78 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
65 venvs.append((py, 'venv-%s' % py))
79 self.venvs.append((py, 'venv-%s' % py))
66
80
67 # Check out and update the repository
81 # Check out and update the repository
68 if not os.path.exists('ipython'):
82 if not os.path.exists('ipython'):
69 try :
83 try :
70 check_call(['git', 'clone', ipy_repository])
84 check_call(['git', 'clone', ipy_repository])
71 except CalledProcessError :
85 except CalledProcessError :
72 check_call(['git', 'clone', ipy_http_repository])
86 check_call(['git', 'clone', ipy_http_repository])
73 os.chdir(repodir)
87 os.chdir(repodir)
74 check_call(['git', 'checkout', 'master'])
88 check_call(['git', 'checkout', 'master'])
75 try :
89 try :
76 check_call(['git', 'pull', ipy_repository, 'master'])
90 check_call(['git', 'pull', ipy_repository, 'master'])
77 except CalledProcessError :
91 except CalledProcessError :
78 check_call(['git', 'pull', ipy_http_repository, 'master'])
92 check_call(['git', 'pull', ipy_http_repository, 'master'])
79 os.chdir(basedir)
93 os.chdir(basedir)
80
94
81 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
95 def get_branch(self):
82 r"\s*(.*?)\n")
96 repo = self.pr['head']['repo']['clone_url']
83 def get_missing_libraries(log):
97 branch = self.pr['head']['ref']
84 m = missing_libs_re.search(log)
98 owner = self.pr['head']['repo']['owner']['login']
85 if m:
99 mergeable = self.pr['mergeable']
86 return m.group(1)
87
100
88 def get_branch(repo, branch, owner, mergeable):
89 os.chdir(repodir)
101 os.chdir(repodir)
90 if mergeable:
102 if mergeable:
91 merged_branch = "%s-%s" % (owner, branch)
103 merged_branch = "%s-%s" % (owner, branch)
92 # Delete the branch first
104 # Delete the branch first
93 call(['git', 'branch', '-D', merged_branch])
105 call(['git', 'branch', '-D', merged_branch])
94 check_call(['git', 'checkout', '-b', merged_branch])
106 check_call(['git', 'checkout', '-b', merged_branch])
95 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
107 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
96 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
108 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
97 else:
109 else:
98 # Fetch the branch without merging it.
110 # Fetch the branch without merging it.
99 check_call(['git', 'fetch', repo, branch])
111 check_call(['git', 'fetch', repo, branch])
100 check_call(['git', 'checkout', 'FETCH_HEAD'])
112 check_call(['git', 'checkout', 'FETCH_HEAD'])
101 os.chdir(basedir)
113 os.chdir(basedir)
102
114
103 def run_tests(venv):
115 def markdown_format(self):
104 py = os.path.join(basedir, venv, 'bin', 'python')
116 def format_result(result):
105 print(py)
117 s = "* %s: " % result.py
106 os.chdir(repodir)
118 if result.passed:
107 # cleanup build-dir
108 if os.path.exists('build'):
109 shutil.rmtree('build')
110 check_call([py, 'setup.py', 'install'])
111 os.chdir(basedir)
112
113 # Environment variables:
114 orig_path = os.environ["PATH"]
115 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
116 os.environ.pop("PYTHONPATH", None)
117
118 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
119 if not os.path.exists(iptest):
120 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
121
122 print("\nRunning tests, this typically takes a few minutes...")
123 try:
124 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
125 except CalledProcessError as e:
126 return False, e.output.decode('utf-8')
127 finally:
128 # Restore $PATH
129 os.environ["PATH"] = orig_path
130
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"
119 s += "OK"
136 else:
120 else:
137 s += "Failed, log at %s" % gist_url
121 s += "Failed, log at %s" % result.log_url
138 if missing_libraries:
122 if result.missing_libraries:
139 s += " (libraries not available: " + missing_libraries + ")"
123 s += " (libraries not available: " + result.missing_libraries + ")"
140 return s
124 return s
141
125
142 if pr['mergeable']:
126 if self.pr['mergeable']:
143 com = pr['head']['sha'][:7] + " merged into master"
127 com = self.pr['head']['sha'][:7] + " merged into master"
144 else:
128 else:
145 com = pr['head']['sha'][:7] + " (can't merge cleanly)"
129 com = self.pr['head']['sha'][:7] + " (can't merge cleanly)"
146 lines = ["**Test results for commit %s**" % com,
130 lines = ["**Test results for commit %s**" % com,
147 "Platform: " + sys.platform,
131 "Platform: " + sys.platform,
148 ""] + \
132 ""] + \
149 [format_result(*r) for r in results_urls] + \
133 [format_result(r) for r in self.results] + \
150 ["",
134 ["",
151 "Not available for testing: " + ", ".join(unavailable_pythons)]
135 "Not available for testing: " + ", ".join(self.unavailable_pythons)]
152 return "\n".join(lines)
136 return "\n".join(lines)
153
137
154 def post_results_comment(pr, results, num, unavailable_pythons=unavailable_pythons):
138 def post_results_comment(self):
155 body = markdown_format(pr, results, unavailable_pythons)
139 body = self.markdown_format()
156 gh_api.post_issue_comment(gh_project, num, body)
140 gh_api.post_issue_comment(gh_project, self.pr_num, body)
141
142 def print_results(self):
143 pr = self.pr
157
144
158 def print_results(pr, results_urls, unavailable_pythons=unavailable_pythons):
159 print("\n")
145 print("\n")
160 if pr['mergeable']:
146 if pr['mergeable']:
161 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
147 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
162 else:
148 else:
163 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
149 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
164 print("Platform:", sys.platform)
150 print("Platform:", sys.platform)
165 for py, passed, gist_url, missing_libraries in results_urls:
151 for result in self.results:
166 if passed:
152 if result.passed:
167 print(py, ":", "OK")
153 print(result.py, ":", "OK")
168 else:
154 else:
169 print(py, ":", "Failed")
155 print(result.py, ":", "Failed")
170 print(" Test log:", gist_url)
156 print(" Test log:", result.get('log_url') or result.log_file)
171 if missing_libraries:
157 if result.missing_libraries:
172 print(" Libraries not available:", missing_libraries)
158 print(" Libraries not available:", result.missing_libraries)
173 print("Not available for testing:", ", ".join(unavailable_pythons))
159 print("Not available for testing:", ", ".join(self.unavailable_pythons))
174
160
175 def dump_results(num, results, pr):
161 def dump_results(self):
176 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
162 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
177 pickle.dump((num, results, pr, unavailable_pythons), f)
163 pickle.dump(self, f)
178
164
165 @staticmethod
179 def load_results():
166 def load_results():
180 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
167 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
181 return pickle.load(f)
168 return pickle.load(f)
182
169
183 def save_logs(results, pr):
170 def save_logs(self):
184 results_paths = []
171 for result in self.results:
185 for py, passed, log, missing_libraries in results:
172 if not result.passed:
186 if passed:
173 result_locn = os.path.abspath(os.path.join('venv-%s' % result.py,
187 results_paths.append((py, passed, None, missing_libraries))
174 self.pr['head']['sha'][:7]+".log"))
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:
175 with io.open(result_locn, 'w', encoding='utf-8') as f:
193 f.write(log)
176 f.write(result.log)
194
195 results_paths.append((py, False, result_locn, missing_libraries))
196
177
197 return results_paths
178 result.log_file = result_locn
198
179
199 def post_logs(results):
180 def post_logs(self):
200 results_urls = []
181 for result in self.results:
201 for py, passed, log, missing_libraries in results:
182 if not result.passed:
202 if passed:
183 result.log_url = gh_api.post_gist(result.log,
203 results_urls.append((py, passed, None, missing_libraries))
184 description='IPython test log',
204 else:
205 result_locn = gh_api.post_gist(log, description='IPython test log',
206 filename="results.log", auth=True)
185 filename="results.log", auth=True)
207 results_urls.append((py, False, result_locn, missing_libraries))
208
186
209 return results_urls
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
202
203 def run_tests(venv):
204 py = os.path.join(basedir, venv, 'bin', 'python')
205 print(py)
206 os.chdir(repodir)
207 # cleanup build-dir
208 if os.path.exists('build'):
209 shutil.rmtree('build')
210 check_call([py, 'setup.py', 'install'])
211 os.chdir(basedir)
212
213 # Environment variables:
214 orig_path = os.environ["PATH"]
215 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
216 os.environ.pop("PYTHONPATH", None)
217
218 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
219 if not os.path.exists(iptest):
220 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
221
222 print("\nRunning tests, this typically takes a few minutes...")
223 try:
224 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
225 except CalledProcessError as e:
226 return False, e.output.decode('utf-8')
227 finally:
228 # Restore $PATH
229 os.environ["PATH"] = orig_path
230
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:
241
227 tic = time.time()
242 testrun.run()
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
243
237 dump_results(num, results, pr)
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