diff --git a/tools/test_pr.py b/tools/test_pr.py new file mode 100755 index 0000000..d3f31a5 --- /dev/null +++ b/tools/test_pr.py @@ -0,0 +1,141 @@ +""" +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 +import json +import os +import shutil +from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests") +repodir = os.path.join(basedir, "ipython") +ipy_repository = 'git://github.com/ipython/ipython.git' + +supported_pythons = ['python2.6', 'python2.7', 'python3.1', 'python3.2'] +unavailable_pythons = [] + +def available_python_versions(): + """Get the executable names of available versions of Python on the system. + """ + del unavailable_pythons[:] + for py in supported_pythons: + try: + check_call([py, '-c', 'import nose'], stdout=PIPE) + yield py + except (OSError, CalledProcessError): + unavailable_pythons.append(py) + +venvs = [] + +def setup(): + """Prepare the repository and virtualenvs.""" + global venvs + + 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 available_python_versions(): + check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py]) + venvs.append((py, 'venv-%s' % py)) + + # Check out and update the repository + if not os.path.exists('ipython'): + check_call(['git', 'clone', ipy_repository]) + os.chdir(repodir) + check_call(['git', 'checkout', 'master']) + check_call(['git', 'pull', ipy_repository, 'master']) + os.chdir(basedir) + +def get_pull_request(num, project="ipython/ipython"): + url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num) + response = urlopen(url).read().decode('utf-8') + return json.loads(response) + +def merge_branch(repo, branch, owner): + merged_branch = "%s-%s" % (owner, branch) + os.chdir(repodir) + # Delete the branch first + call(['git', 'branch', '-D', merged_branch]) + check_call(['git', 'checkout', '-b', merged_branch]) + check_call(['git', 'pull', repo, branch]) + os.chdir(basedir) + +def run_tests(venv): + py = os.path.join(basedir, venv, 'bin', 'python') + print(py) + os.chdir(repodir) + check_call([py, 'setup.py', 'install']) + os.chdir(basedir) + + 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: + return False, e.output + +def post_gist(content, description='IPython test log', filename="results.log"): + """Post some text to a Gist, and return the URL.""" + post_data = json.dumps({ + "description": description, + "public": True, + "files": { + filename: { + "content": content + } + } + }).encode('utf-8') + + response = urlopen("https://api.github.com/gists", post_data) + response_data = json.loads(response.read().decode('utf-8')) + return response_data['html_url'] + +if __name__ == '__main__': + import sys + num = sys.argv[1] + setup() + pr = get_pull_request(num) + merge_branch(repo=pr['head']['repo']['clone_url'], + branch=pr['head']['ref'], + owner=pr['head']['repo']['owner']['login']) + + results = [] + for py, venv in venvs: + passed, log = run_tests(venv) + if passed: + results.append((py, True, None)) + else: + gist_url = post_gist(log) + results.append((py, False, gist_url)) + + print("\n") + print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7]) + for py, passed, gist_url in results: + if passed: + print(py, ":", "OK") + else: + print(py, ":", "Failed") + print(" Test log:", gist_url) + print("Not available for testing:", ", ".join(unavailable_pythons))