test_pr.py
266 lines
| 9.1 KiB
| text/x-python
|
PythonLexer
/ tools / test_pr.py
Fernando Perez
|
r6745 | #!/usr/bin/env python | ||
Thomas Kluyver
|
r6686 | """ | ||
This is a script for testing pull requests for IPython. It merges the pull | ||||
request with current master, installs and tests on all available versions of | ||||
Python, and posts the results to Gist if any tests fail. | ||||
Usage: | ||||
python test_pr.py 1657 | ||||
""" | ||||
from __future__ import print_function | ||||
import errno | ||||
from glob import glob | ||||
Thomas Kluyver
|
r6711 | import io | ||
Thomas Kluyver
|
r6686 | import json | ||
import os | ||||
Thomas Kluyver
|
r6713 | import pickle | ||
Thomas Kluyver
|
r6692 | import re | ||
Thomas Kluyver
|
r6694 | import requests | ||
Thomas Kluyver
|
r6686 | import shutil | ||
MinRK
|
r6736 | import time | ||
Thomas Kluyver
|
r6686 | from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError | ||
Thomas Kluyver
|
r6711 | import sys | ||
Thomas Kluyver
|
r6686 | |||
Thomas Kluyver
|
r6711 | import gh_api | ||
Thomas Kluyver
|
r7863 | from gh_api import Obj | ||
Thomas Kluyver
|
r6694 | |||
Thomas Kluyver
|
r6686 | basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests") | ||
repodir = os.path.join(basedir, "ipython") | ||||
ipy_repository = 'git://github.com/ipython/ipython.git' | ||||
Matthias BUSSONNIER
|
r6896 | ipy_http_repository = 'http://github.com/ipython/ipython.git' | ||
Thomas Kluyver
|
r6694 | gh_project="ipython/ipython" | ||
Thomas Kluyver
|
r6686 | |||
supported_pythons = ['python2.6', 'python2.7', 'python3.1', 'python3.2'] | ||||
Thomas Kluyver
|
r7863 | |||
Thomas Kluyver
|
r6692 | missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n" | ||
r"\s*(.*?)\n") | ||||
def get_missing_libraries(log): | ||||
m = missing_libs_re.search(log) | ||||
if m: | ||||
return m.group(1) | ||||
Thomas Kluyver
|
r7863 | class TestRun(object): | ||
def __init__(self, pr_num): | ||||
self.unavailable_pythons = [] | ||||
self.venvs = [] | ||||
self.pr_num = pr_num | ||||
self.pr = gh_api.get_pull_request(gh_project, pr_num) | ||||
self.setup() | ||||
self.results = [] | ||||
def available_python_versions(self): | ||||
"""Get the executable names of available versions of Python on the system. | ||||
""" | ||||
for py in supported_pythons: | ||||
try: | ||||
check_call([py, '-c', 'import nose'], stdout=PIPE) | ||||
yield py | ||||
except (OSError, CalledProcessError): | ||||
self.unavailable_pythons.append(py) | ||||
def setup(self): | ||||
"""Prepare the repository and virtualenvs.""" | ||||
try: | ||||
os.mkdir(basedir) | ||||
except OSError as e: | ||||
if e.errno != errno.EEXIST: | ||||
raise | ||||
os.chdir(basedir) | ||||
# Delete virtualenvs and recreate | ||||
for venv in glob('venv-*'): | ||||
shutil.rmtree(venv) | ||||
for py in self.available_python_versions(): | ||||
check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py]) | ||||
self.venvs.append((py, 'venv-%s' % py)) | ||||
# Check out and update the repository | ||||
if not os.path.exists('ipython'): | ||||
try : | ||||
check_call(['git', 'clone', ipy_repository]) | ||||
except CalledProcessError : | ||||
check_call(['git', 'clone', ipy_http_repository]) | ||||
os.chdir(repodir) | ||||
check_call(['git', 'checkout', 'master']) | ||||
try : | ||||
check_call(['git', 'pull', ipy_repository, 'master']) | ||||
except CalledProcessError : | ||||
check_call(['git', 'pull', ipy_http_repository, 'master']) | ||||
os.chdir(basedir) | ||||
def get_branch(self): | ||||
repo = self.pr['head']['repo']['clone_url'] | ||||
branch = self.pr['head']['ref'] | ||||
owner = self.pr['head']['repo']['owner']['login'] | ||||
mergeable = self.pr['mergeable'] | ||||
os.chdir(repodir) | ||||
if mergeable: | ||||
merged_branch = "%s-%s" % (owner, branch) | ||||
# Delete the branch first | ||||
call(['git', 'branch', '-D', merged_branch]) | ||||
check_call(['git', 'checkout', '-b', merged_branch]) | ||||
check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch]) | ||||
check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)]) | ||||
else: | ||||
# Fetch the branch without merging it. | ||||
check_call(['git', 'fetch', repo, branch]) | ||||
check_call(['git', 'checkout', 'FETCH_HEAD']) | ||||
os.chdir(basedir) | ||||
def markdown_format(self): | ||||
def format_result(result): | ||||
s = "* %s: " % result.py | ||||
if result.passed: | ||||
s += "OK" | ||||
else: | ||||
s += "Failed, log at %s" % result.log_url | ||||
if result.missing_libraries: | ||||
s += " (libraries not available: " + result.missing_libraries + ")" | ||||
return s | ||||
if self.pr['mergeable']: | ||||
com = self.pr['head']['sha'][:7] + " merged into master" | ||||
else: | ||||
com = self.pr['head']['sha'][:7] + " (can't merge cleanly)" | ||||
lines = ["**Test results for commit %s**" % com, | ||||
"Platform: " + sys.platform, | ||||
""] + \ | ||||
[format_result(r) for r in self.results] + \ | ||||
["", | ||||
"Not available for testing: " + ", ".join(self.unavailable_pythons)] | ||||
return "\n".join(lines) | ||||
def post_results_comment(self): | ||||
body = self.markdown_format() | ||||
gh_api.post_issue_comment(gh_project, self.pr_num, body) | ||||
def print_results(self): | ||||
pr = self.pr | ||||
print("\n") | ||||
if pr['mergeable']: | ||||
print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7]) | ||||
else: | ||||
print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7]) | ||||
print("Platform:", sys.platform) | ||||
for result in self.results: | ||||
if result.passed: | ||||
print(result.py, ":", "OK") | ||||
else: | ||||
print(result.py, ":", "Failed") | ||||
print(" Test log:", result.get('log_url') or result.log_file) | ||||
if result.missing_libraries: | ||||
print(" Libraries not available:", result.missing_libraries) | ||||
print("Not available for testing:", ", ".join(self.unavailable_pythons)) | ||||
def dump_results(self): | ||||
with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f: | ||||
pickle.dump(self, f) | ||||
@staticmethod | ||||
def load_results(): | ||||
with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f: | ||||
return pickle.load(f) | ||||
def save_logs(self): | ||||
for result in self.results: | ||||
if not result.passed: | ||||
result_locn = os.path.abspath(os.path.join('venv-%s' % result.py, | ||||
self.pr['head']['sha'][:7]+".log")) | ||||
with io.open(result_locn, 'w', encoding='utf-8') as f: | ||||
f.write(result.log) | ||||
result.log_file = result_locn | ||||
def post_logs(self): | ||||
for result in self.results: | ||||
if not result.passed: | ||||
result.log_url = gh_api.post_gist(result.log, | ||||
description='IPython test log', | ||||
filename="results.log", auth=True) | ||||
def run(self): | ||||
for py, venv in self.venvs: | ||||
tic = time.time() | ||||
passed, log = run_tests(venv) | ||||
elapsed = int(time.time() - tic) | ||||
print("Ran tests with %s in %is" % (py, elapsed)) | ||||
missing_libraries = get_missing_libraries(log) | ||||
self.results.append(Obj(py=py, | ||||
passed=passed, | ||||
log=log, | ||||
missing_libraries=missing_libraries | ||||
) | ||||
) | ||||
Thomas Kluyver
|
r6686 | |||
def run_tests(venv): | ||||
py = os.path.join(basedir, venv, 'bin', 'python') | ||||
print(py) | ||||
os.chdir(repodir) | ||||
MinRK
|
r6734 | # cleanup build-dir | ||
if os.path.exists('build'): | ||||
shutil.rmtree('build') | ||||
Thomas Kluyver
|
r6686 | check_call([py, 'setup.py', 'install']) | ||
os.chdir(basedir) | ||||
Thomas Kluyver
|
r6992 | # Environment variables: | ||
orig_path = os.environ["PATH"] | ||||
os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"] | ||||
os.environ.pop("PYTHONPATH", None) | ||||
Thomas Kluyver
|
r6686 | iptest = os.path.join(basedir, venv, 'bin', 'iptest') | ||
if not os.path.exists(iptest): | ||||
iptest = os.path.join(basedir, venv, 'bin', 'iptest3') | ||||
print("\nRunning tests, this typically takes a few minutes...") | ||||
try: | ||||
return True, check_output([iptest], stderr=STDOUT).decode('utf-8') | ||||
except CalledProcessError as e: | ||||
Thomas Kluyver
|
r6694 | return False, e.output.decode('utf-8') | ||
Thomas Kluyver
|
r6992 | finally: | ||
# Restore $PATH | ||||
os.environ["PATH"] = orig_path | ||||
Thomas Kluyver
|
r6686 | |||
Thomas Kluyver
|
r6713 | |||
Thomas Kluyver
|
r6711 | def test_pr(num, post_results=True): | ||
# Get Github authorisation first, so that the user is prompted straight away | ||||
# if their login is needed. | ||||
if post_results: | ||||
gh_api.get_auth_token() | ||||
Thomas Kluyver
|
r7863 | testrun = TestRun(num) | ||
Thomas Kluyver
|
r6686 | |||
Thomas Kluyver
|
r7863 | testrun.get_branch() | ||
Thomas Kluyver
|
r6713 | |||
Thomas Kluyver
|
r7863 | testrun.run() | ||
testrun.dump_results() | ||||
Thomas Kluyver
|
r6713 | |||
Thomas Kluyver
|
r7863 | testrun.save_logs() | ||
testrun.print_results() | ||||
Thomas Kluyver
|
r6686 | |||
Thomas Kluyver
|
r6711 | if post_results: | ||
Thomas Kluyver
|
r7863 | results_urls = testrun.post_logs | ||
testrun.post_results_comment() | ||||
Thomas Kluyver
|
r6711 | print("(Posted to Github)") | ||
Thomas Kluyver
|
r6713 | else: | ||
post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py") | ||||
print("To post the results to Github, run", post_script) | ||||
Thomas Kluyver
|
r6711 | |||
if __name__ == '__main__': | ||||
import argparse | ||||
parser = argparse.ArgumentParser(description="Test an IPython pull request") | ||||
Thomas Kluyver
|
r6713 | parser.add_argument('-p', '--publish', action='store_true', | ||
help="Publish the results to Github") | ||||
Thomas Kluyver
|
r6711 | parser.add_argument('number', type=int, help="The pull request number") | ||
args = parser.parse_args() | ||||
Thomas Kluyver
|
r6713 | test_pr(args.number, post_results=args.publish) | ||