##// END OF EJS Templates
Refactor test_pr.py to a more coherent design.
Thomas Kluyver -
Show More
@@ -16,6 +16,17 b' import json'
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
@@ -88,7 +99,7 b' def get_pull_request(project, num, github_api=3):'
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
@@ -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'])
@@ -23,6 +23,7 b' from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProce'
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")
@@ -31,25 +32,38 b" 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:
@@ -60,9 +74,9 b' def setup():'
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'):
@@ -78,14 +92,12 b' def setup():'
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)
@@ -100,113 +112,122 b' def get_branch(repo, branch, owner, mergeable):'
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
@@ -214,34 +235,20 b' def test_pr(num, post_results=True):'
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")
General Comments 0
You need to be logged in to leave comments. Login now