From 5f5327b8315be62b107e2e5eea7d73c56c2d6f7c Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 28 Jun 2017 21:45:38 +0200 Subject: [PATCH] Use EDM for Travis CI builds Shamelessly copied from the TraitsUI project... --- .travis.yml | 65 ++-- build_pyside_wheel.sh | 27 -- ci/edmtool.py | 321 ++++++++++++++++++ ci/install-edm.sh | 29 ++ travis-ci-requirements => ci/requirements.txt | 13 +- 5 files changed, 388 insertions(+), 67 deletions(-) delete mode 100755 build_pyside_wheel.sh create mode 100644 ci/edmtool.py create mode 100755 ci/install-edm.sh rename travis-ci-requirements => ci/requirements.txt (88%) diff --git a/.travis.yml b/.travis.yml index 148e60495..df18d630e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,12 @@ -language: python +language: generic sudo: false -python: - - '2.7_with_system_site_packages' - - 3.4 + addons: apt: packages: - python-qt4 - python-qt4-dev - python-qt4-gl - - python-pip - - python-numpy - python-wxtools - ccache - cmake @@ -18,40 +14,49 @@ addons: - zlib1g-dev - libpng-dev - libfreetype6-dev - - python-cairo + - libcairo2-dev + env: - - ETS_TOOLKIT=wx PILLOW='pillow' - - ETS_TOOLKIT=qt4 PILLOW='pillow' - - ETS_TOOLKIT=null.image PILLOW='pillow<3.0.0' - - ETS_TOOLKIT=null.image PILLOW='pillow' + global: + - INSTALL_EDM_VERSION=1.5.2 + PYTHONUNBUFFERED="1" + matrix: - exclude: - - python: 3.4 - env: ETS_TOOLKIT=wx PILLOW='pillow' + include: + - env: RUNTIME=2.7 TOOLKIT=wx PILLOW='pillow' + - env: RUNTIME=2.7 TOOLKIT=pyqt PILLOW='pillow' + - env: RUNTIME=3.5 TOOLKIT=pyqt PILLOW='pillow' + - env: RUNTIME=2.7 TOOLKIT=null PILLOW='pillow' + - env: RUNTIME=3.5 TOOLKIT=null PILLOW='pillow' + - env: RUNTIME=2.7 TOOLKIT=null PILLOW='pillow<3.0.0' + - env: RUNTIME=3.5 TOOLKIT=null PILLOW='pillow<3.0.0' + allow_failures: + - env: RUNTIME=2.7 TOOLKIT=wx PILLOW='pillow' + fast_finish: true + +branches: + only: + - master + cache: directories: - $HOME/.cache - $HOME/.ccache + before_install: - ccache -s - - export PATH=/usr/lib/ccache:${PATH} - - pip install --upgrade pip - - if [[ ${TRAVIS_PYTHON_VERSION} == "3.4" && ${ETS_TOOLKIT} == "qt4" ]]; then ./build_pyside_wheel.sh; fi - # setup X11 for the tests + - mkdir -p "${HOME}/.cache/download" + - ci/install-edm.sh + - export PATH="${HOME}/edm/bin:/usr/lib/ccache:${PATH}" + - edm install -y wheel click coverage - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start install: - # Install pillow separately to control the version - - pip install $PILLOW - - pip install -r travis-ci-requirements - - python setup.py develop -before_script: - - mkdir testrunner - - cp .coveragerc testrunner - - cd testrunner + - edm run -- python ci/edmtool.py install --runtime=${RUNTIME} --toolkit=${TOOLKIT} --pillow=${PILLOW} script: - - coverage run -m nose.core enable -v - - coverage run -a -m nose.core kiva -v + - edm run -- python ci/edmtool.py test --runtime=${RUNTIME} --toolkit=${TOOLKIT} --pillow=${PILLOW} + after_success: - - pip install codecov - - codecov + - edm run -- coverage combine + - edm run -- pip install codecov + - edm run -- codecov diff --git a/build_pyside_wheel.sh b/build_pyside_wheel.sh deleted file mode 100755 index 2372d60a0..000000000 --- a/build_pyside_wheel.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -set -e - -if [ -f "${HOME}/.cache/PySide-1.2.2-cp34-cp34m-linux_x86_64.whl" ]; then - echo "PySide wheel found" -else - echo "Building PySide" - - git clone https://github.com/PySide/pyside-setup.git - cd pyside-setup - - # The normal pyside repos only have the right tags upto 1.1.1 - # So we need to replace the repos with the newer ones - git submodule deinit . - git rm sources/pyside - git rm sources/shiboken - git submodule add --name sources/shiboken -- https://github.com/PySide/shiboken2.git sources/shiboken - git submodule add --name sources/pyside -- https://github.com/PySide/pyside2.git sources/pyside - git submodule sync - - # now it is time to build the pyside wheels - python setup.py bdist_wheel --qmake=/usr/bin/qmake-qt4 --version=1.2.2 --jobs=2 - ls dist/ - cp dist/PySide-1.2.2-cp34-cp34m-linux_x86_64.whl $HOME/.cache/ -fi - -pip install "${HOME}/.cache/PySide-1.2.2-cp34-cp34m-linux_x86_64.whl" diff --git a/ci/edmtool.py b/ci/edmtool.py new file mode 100644 index 000000000..045fd6c8f --- /dev/null +++ b/ci/edmtool.py @@ -0,0 +1,321 @@ +# +# Copyright (c) 2017, Enthought, Inc. +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in enthought/LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +# +""" +Tasks for Test Runs +=================== + +This file is intended to be used with a python environment with the +click library to automate the process of setting up test environments +and running the test within them. This improves repeatability and +reliability of tests be removing many of the variables around the +developer's particular Python environment. Test environment setup and +package management is performed using `EDM +http://docs.enthought.com/edm/`_ + +To use this to run you tests, you will need to install EDM and click +into your working environment. You will also need to have git +installed to access required source code from github repositories. +You can then do:: + + python edmtool.py install --runtime=... --toolkit=... + +to create a test environment from the current codebase and:: + + python edmtool.py test --runtime=... --toolkit=... + +to run tests in that environment. You can remove the environment with:: + + python edmtool.py cleanup --runtime=... --toolkit=... + +If you make changes you will either need to remove and re-install the +environment or manually update the environment using ``edm``, as +the install performs a ``python setup.py install`` rather than a ``develop``, +so changes in your code will not be automatically mirrored in the test +environment. You can update with a command like:: + + edm run --environment ... -- python setup.py install + +You can run all three tasks at once with:: + + python edmtool.py test_clean --runtime=... --toolkit=... + +which will create, install, run tests, and then clean-up the environment. And +you can run tests in all supported runtimes and toolkits (with cleanup) +using:: + + python edmtool.py test_all + +Currently supported runtime values are ``2.7`` and ``3.5``, and currently +supported toolkits are ``null``, ``pyqt``, and ``wx``. Not all +combinations of toolkits and runtimes will work, but the tasks will fail with +a clear error if that is the case. + +Tests can still be run via the usual means in other environments if that suits +a developer's purpose. + +Changing This File +------------------ + +To change the packages installed during a test run, change the dependencies +variable below. To install a package from github, or one which is not yet +available via EDM, add it to the `ci/requirements.txt` file (these will be +installed by `pip`). + +Other changes to commands should be a straightforward change to the listed +commands for each task. See the EDM documentation for more information about +how to run commands within an EDM enviornment. + +""" + +from __future__ import print_function + +import glob +import os +import subprocess +import sys +from shutil import rmtree, copy as copyfile +from tempfile import mkdtemp +from contextlib import contextmanager + +import click + +supported_combinations = { + '2.7': {'pyqt', 'wx', 'null'}, + '3.5': {'pyqt', 'null'}, +} + +dependencies = { + "six", + "nose", + "mock", + "coverage", + "numpy", + "pygments", + "pyparsing", + "unittest2", + "pypdf2", +} + +extra_dependencies = { + 'pyqt': {'pyqt'}, + 'wx': {'wxpython'}, + 'null': set() +} + +environment_vars = { + 'pyqt': {'ETS_TOOLKIT': 'qt4', 'QT_API': 'pyqt'}, + 'wx': {'ETS_TOOLKIT': 'wx'}, + 'null': {'ETS_TOOLKIT': 'null.image'}, +} + +if sys.version_info < (3, 0): + import string + pillow_trans = string.maketrans('<=>', '___') +else: + pillow_trans = ''.maketrans({'<': '_', '=': '_', '>': '_'}) + +if sys.platform == 'darwin': + dependencies.add('Cython') + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--pillow', default='pillow') +@click.option('--environment', default=None) +def install(runtime, toolkit, pillow, environment): + """ Install project and dependencies into a clean EDM environment. + + """ + parameters = get_parameters(runtime, toolkit, pillow, environment) + parameters['packages'] = ' '.join( + dependencies | extra_dependencies.get(toolkit, set())) + # edm commands to setup the development environment + commands = [ + "edm environments create {environment} --force --version={runtime}", + "edm install -y -e {environment} {packages}", + "edm run -e {environment} -- pip install {pillow}", + ("edm run -e {environment} -- pip install -r ci/requirements.txt" + " --no-dependencies"), + "edm run -e {environment} -- python setup.py install", + ] + click.echo("Creating environment '{environment}'".format(**parameters)) + execute(commands, parameters) + click.echo('Done install') + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--pillow', default='pillow') +@click.option('--environment', default=None) +def test(runtime, toolkit, pillow, environment): + """ Run the test suite in a given environment with the specified toolkit. + + """ + parameters = get_parameters(runtime, toolkit, pillow, environment) + environ = environment_vars.get(toolkit, {}).copy() + environ['PYTHONUNBUFFERED'] = "1" + commands = [ + "edm run -e {environment} -- coverage run -m nose.core enable -v", + "edm run -e {environment} -- coverage run -a -m nose.core kiva -v", + ] + + # We run in a tempdir to avoid accidentally picking up wrong traitsui + # code from a local dir. We need to ensure a good .coveragerc is in + # that directory, plus coverage has a bug that means a non-local coverage + # file doesn't get populated correctly. + click.echo("Running tests in '{environment}'".format(**parameters)) + with do_in_tempdir(files=['.coveragerc'], capture_files=['./.coverage*']): + os.environ.update(environ) + execute(commands, parameters) + click.echo('Done test') + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--pillow', default='pillow') +@click.option('--environment', default=None) +def cleanup(runtime, toolkit, pillow, environment): + """ Remove a development environment. + + """ + parameters = get_parameters(runtime, toolkit, pillow, environment) + commands = [ + "edm run -e {environment} -- python setup.py clean", + "edm environments remove {environment} --purge -y", + ] + click.echo("Cleaning up environment '{environment}'".format(**parameters)) + execute(commands, parameters) + click.echo('Done cleanup') + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--pillow', default='pillow') +def test_clean(runtime, toolkit, pillow): + """ Run tests in a clean environment, cleaning up afterwards + + """ + args = ['--toolkit={}'.format(toolkit), + '--runtime={}'.format(runtime), + '--pillow={}'.format(pillow)] + try: + install(args=args, standalone_mode=False) + test(args=args, standalone_mode=False) + finally: + cleanup(args=args, standalone_mode=False) + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--pillow', default='pillow') +@click.option('--environment', default=None) +def update(runtime, toolkit, pillow, environment): + """ Update/Reinstall package into environment. + + """ + parameters = get_parameters(runtime, toolkit, pillow, environment) + commands = [ + "edm run -e {environment} -- python setup.py install"] + click.echo("Re-installing in '{environment}'".format(**parameters)) + execute(commands, parameters) + click.echo('Done update') + + +@cli.command() +def test_all(): + """ Run test_clean across all supported environment combinations. + + """ + for runtime, toolkits in supported_combinations.items(): + for toolkit in toolkits: + args = ['--toolkit={}'.format(toolkit), + '--runtime={}'.format(runtime)] + test_clean(args, standalone_mode=True) + + +# ---------------------------------------------------------------------------- +# Utility routines +# ---------------------------------------------------------------------------- + +def get_parameters(runtime, toolkit, pillow, environment): + """Set up parameters dictionary for format() substitution + """ + parameters = {'runtime': runtime, 'toolkit': toolkit, 'pillow': pillow, + 'environment': environment} + if toolkit not in supported_combinations[runtime]: + msg = ("Python {runtime}, toolkit {toolkit}, and pillow {pillow} " + "not supported by test environments") + raise RuntimeError(msg.format(**parameters)) + if environment is None: + tmpl = 'enable-test-{runtime}-{toolkit}' + environment = tmpl.format(**parameters) + environment += '-{}'.format(str(pillow).translate(pillow_trans)) + parameters['environment'] = environment + return parameters + + +@contextmanager +def do_in_tempdir(files=(), capture_files=()): + """ Create a temporary directory, cleaning up after done. + + Creates the temporary directory, and changes into it. On exit returns to + original directory and removes temporary dir. + + Parameters + ---------- + files : sequence of filenames + Files to be copied across to temporary directory. + capture_files : sequence of filenames + Files to be copied back from temporary directory. + """ + path = mkdtemp() + old_path = os.getcwd() + + # send across any files we need + for filepath in files: + click.echo('copying file to tempdir: {}'.format(filepath)) + copyfile(filepath, path) + + os.chdir(path) + try: + yield path + # retrieve any result files we want + for pattern in capture_files: + for filepath in glob.iglob(pattern): + click.echo('copying file back: {}'.format(filepath)) + copyfile(filepath, old_path) + finally: + os.chdir(old_path) + rmtree(path) + + +def execute(commands, parameters): + for command in commands: + print("[EXECUTING]", command.format(**parameters)) + try: + subprocess.check_call(command.format(**parameters).split()) + except subprocess.CalledProcessError: + sys.exit(1) + + +if __name__ == '__main__': + cli() diff --git a/ci/install-edm.sh b/ci/install-edm.sh new file mode 100755 index 000000000..9efeec668 --- /dev/null +++ b/ci/install-edm.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +install_edm() { + local EDM_MAJOR_MINOR="$(echo "$INSTALL_EDM_VERSION" | sed -E -e 's/([[:digit:]]+\.[[:digit:]]+)\..*/\1/')" + local EDM_PACKAGE="edm_${INSTALL_EDM_VERSION}_linux_x86_64.sh" + local EDM_INSTALLER_DIR="${HOME}/.cache/download" + local EDM_INSTALLER_PATH="${EDM_INSTALLER_DIR}/${EDM_PACKAGE}" + local DOWNLOAD_URL="https://package-data.enthought.com/edm/rh5_x86_64/${EDM_MAJOR_MINOR}/${EDM_PACKAGE}" + + if [ ! -e "$EDM_INSTALLER_PATH" ]; then + mkdir -p ${EDM_INSTALLER_DIR} + curl -o "$EDM_INSTALLER_PATH" -L "$DOWNLOAD_URL" + if [ $? -ne 0 ]; then + echo "Failed to download $DOWNLOAD_URL" + exit 1 + fi + fi + + bash "$EDM_INSTALLER_PATH" -b -p "${HOME}/edm" +} + +if [ -z $INSTALL_EDM_VERSION ]; then + echo "The desired EDM version must be set in the INSTALL_EDM_VERSION environment variable before running this script!" + exit 1 +fi + +install_edm diff --git a/travis-ci-requirements b/ci/requirements.txt similarity index 88% rename from travis-ci-requirements rename to ci/requirements.txt index 408a1ae47..31791d09c 100644 --- a/travis-ci-requirements +++ b/ci/requirements.txt @@ -1,17 +1,10 @@ -six -numpy -coverage -pillow -pyparsing -fonttools==3.10.0 -PyPDF2 -https://bitbucket.org/pyglet/pyglet/get/pyglet-1.1.4.zip ; python_version <= "2.7" pygarrayimage +hypothesis<3.0.0 +fonttools==3.10.0 reportlab<=3.1 kiwisolver ; python_version <= "2.7" +https://bitbucket.org/pyglet/pyglet/get/pyglet-1.1.4.zip ; python_version <= "2.7" git+http://github.com/enthought/traits.git#egg=traits git+http://github.com/enthought/pyface.git#egg=pyface git+http://github.com/enthought/traitsui.git#egg=traitsui git+http://github.com/enthought/apptools.git#egg=apptools -unittest2 -hypothesis<3.0.0