##// 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 16 # password
17 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 30 token = None
20 31 def get_auth_token():
21 32 global token
@@ -88,7 +99,7 b' def get_pull_request(project, num, github_api=3):'
88 99 response.raise_for_status()
89 100 if github_api == 2 :
90 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 104 def get_pulls_list(project, github_api=3):
94 105 """get pull request list
@@ -1,13 +1,13 b''
1 1 #!/usr/bin/env python
2 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()
7 results_urls = post_logs(results)
8 print_results(pr, results_urls, unavailable_pythons)
9 post_results_comment(pr, results_urls, num, unavailable_pythons)
6 testrun = TestRun.load_results()
7 testrun.post_logs()
8 testrun.print_results()
9 testrun.post_results_comment()
10 10
11 11 print()
12 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 23 import sys
24 24
25 25 import gh_api
26 from gh_api import Obj
26 27
27 28 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
28 29 repodir = os.path.join(basedir, "ipython")
@@ -31,53 +32,7 b" ipy_http_repository = 'http://github.com/ipython/ipython.git'"
31 32 gh_project="ipython/ipython"
32 33
33 34 supported_pythons = ['python2.6', 'python2.7', 'python3.1', 'python3.2']
34 unavailable_pythons = []
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
35
81 36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
82 37 r"\s*(.*?)\n")
83 38 def get_missing_libraries(log):
@@ -85,20 +40,165 b' def get_missing_libraries(log):'
85 40 if m:
86 41 return m.group(1)
87 42
88 def get_branch(repo, branch, owner, mergeable):
89 os.chdir(repodir)
90 if mergeable:
91 merged_branch = "%s-%s" % (owner, branch)
92 # Delete the branch first
93 call(['git', 'branch', '-D', merged_branch])
94 check_call(['git', 'checkout', '-b', merged_branch])
95 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
96 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
97 else:
98 # Fetch the branch without merging it.
99 check_call(['git', 'fetch', repo, branch])
100 check_call(['git', 'checkout', 'FETCH_HEAD'])
101 os.chdir(basedir)
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):
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 203 def run_tests(venv):
104 204 py = os.path.join(basedir, venv, 'bin', 'python')
@@ -128,85 +228,6 b' def run_tests(venv):'
128 228 # Restore $PATH
129 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 232 def test_pr(num, post_results=True):
212 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 235 if post_results:
215 236 gh_api.get_auth_token()
216 237
217 setup()
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 )
238 testrun = TestRun(num)
224 239
225 results = []
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))
240 testrun.get_branch()
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)
240 print_results(pr, results_paths)
246 testrun.save_logs()
247 testrun.print_results()
241 248
242 249 if post_results:
243 results_urls = post_logs(results)
244 post_results_comment(pr, results_urls, num)
250 results_urls = testrun.post_logs
251 testrun.post_results_comment()
245 252 print("(Posted to Github)")
246 253 else:
247 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