diff --git a/.travis.yml b/.travis.yml index f6647211a..3faff246a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,61 +1,51 @@ -language: python +language: generic sudo: false -python: - - 2.7_with_system_site_packages - - 3.4 - - 3.5 -env: - - ETS_TOOLKIT='qt4' - - ETS_TOOLKIT='wx' - - ETS_TOOLKIT='null' -matrix: - exclude: - - python: 3.4 - env: ETS_TOOLKIT='wx' - - python: 3.5 - env: ETS_TOOLKIT='wx' - - python: 3.5 - env: ETS_TOOLKIT='qt4' -cache: - - pip - - ccache + addons: apt: packages: - - python-qt4 - - python-qt4-gl - - python-qt4-dev - - python-wxtools - - python-numpy - - libjpeg8-dev - - zlib1g-dev - - libpng-dev - - libfreetype6-dev - - python-cairo + - ccache - cmake - swig - - ccache + +env: + global: + - INSTALL_EDM_VERSION=1.5.2 + PYTHONUNBUFFERED="1" + +matrix: + include: + - env: RUNTIME=2.7 TOOLKIT=wx + - env: RUNTIME=2.7 TOOLKIT=pyqt + - env: RUNTIME=2.7 TOOLKIT=null + - env: RUNTIME=2.7 TOOLKIT=pyside + - env: RUNTIME=3.5 TOOLKIT=pyqt + - env: RUNTIME=3.5 TOOLKIT=null + fast_finish: true + +branches: + only: + - master + +cache: + directories: + - $HOME/.cache + - $HOME/.ccache + before_install: - ccache -s - - pip install --upgrade pip - - if [[ ${TRAVIS_PYTHON_VERSION} == "3.4" && ${ETS_TOOLKIT} == "qt4" ]]; then ./build_pyside_wheel.sh; fi - - export PATH=/usr/lib/ccache:${PATH} + - 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: - - pip install cython - - pip install -r travis-ci-requirements.txt - - git clone https://github.com/enthought/enable.git --depth 1 - - cd enable - - pip install -r travis-ci-requirements - - python setup.py develop - - cd .. - - python setup.py develop - - mkdir testing_dir - - cd testing_dir + - edm run -- python ci/edmtool.py install --runtime=${RUNTIME} --toolkit=${TOOLKIT} script: - - coverage run -m nose.core -v chaco --exclude-dir=../chaco/tests_with_backend - - if [[ $ETS_TOOLKIT != 'null' ]]; then coverage run -a -m nose.core -v ../chaco/tests_with_backend; fi + - edm run -- python ci/edmtool.py test --runtime=${RUNTIME} --toolkit=${TOOLKIT} + 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..b787e8323 --- /dev/null +++ b/ci/edmtool.py @@ -0,0 +1,293 @@ +# +# 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``, ``pyside``, 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', 'pyside', 'wx', 'null'}, + '3.5': {'pyqt', 'null'}, +} + +dependencies = { + "six", + "nose", + "mock", + "numpy", + "pygments", + "pyparsing", + "cython" +} + +extra_dependencies = { + 'pyside': {'pyside'}, + 'pyqt': {'pyqt'}, + 'wx': {'wxpython'}, + 'null': set() +} + +environment_vars = { + 'pyside': {'ETS_TOOLKIT': 'qt4', 'QT_API': 'pyside'}, + 'pyqt': {'ETS_TOOLKIT': 'qt4', 'QT_API': 'pyqt'}, + 'wx': {'ETS_TOOLKIT': 'wx'}, + 'null': {'ETS_TOOLKIT': 'null.image'}, +} + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--environment', default=None) +def install(runtime, toolkit, environment): + """ Install project and dependencies into a clean EDM environment. + """ + parameters = get_parameters(runtime, toolkit, 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 -r ci/requirements.txt" + " --no-dependencies"), + ("edm run -e {environment} -- " + "pip install git+https://git@github.com/enthought/enable.git"), + "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('--environment', default=None) +def test(runtime, toolkit, environment): + """ Run the test suite in a given environment with the specified toolkit. + """ + parameters = get_parameters(runtime, toolkit, environment) + environ = environment_vars.get(toolkit, {}).copy() + environ['PYTHONUNBUFFERED'] = "1" + commands_nobackend = [ + "edm run -e {environment} -- coverage run -m nose.core chaco -v " + ] + + cwd = os.getcwd() + + # 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_nobackend, parameters) + + if toolkit != 'null': + backend_tests = os.path.join(cwd, 'chaco/tests_with_backend') + commands_backend = [ + ("edm run -e {{environment}} -- coverage run -a " + "-m nose.core -v {}").format(backend_tests) + ] + execute(commands_backend, parameters) + + click.echo('Done test') + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--toolkit', default='null') +@click.option('--environment', default=None) +def cleanup(runtime, toolkit, environment): + """ Remove a development environment. + """ + parameters = get_parameters(runtime, toolkit, 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') +def test_clean(runtime, toolkit): + """ Run tests in a clean environment, cleaning up afterwards + """ + args = ['--toolkit={}'.format(toolkit), + '--runtime={}'.format(runtime)] + 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('--environment', default=None) +def update(runtime, toolkit, environment): + """ Update/Reinstall package into environment. + """ + parameters = get_parameters(runtime, toolkit, 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, environment): + """Set up parameters dictionary for format() substitution + """ + parameters = {'runtime': runtime, 'toolkit': toolkit, + 'environment': environment} + if toolkit not in supported_combinations[runtime]: + msg = ("Python {runtime}, toolkit {toolkit}, " + "not supported by test environments") + raise RuntimeError(msg.format(**parameters)) + if environment is None: + tmpl = 'chaco-test-{runtime}-{toolkit}' + environment = tmpl.format(**parameters) + 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..91498b46f --- /dev/null +++ b/ci/install-edm.sh @@ -0,0 +1,17 @@ +#!/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_PATH="${HOME}/.cache/download/${EDM_PACKAGE}" + + if [ ! -e "$EDM_INSTALLER_PATH" ]; then + curl -o "$EDM_INSTALLER_PATH" -L "https://package-data.enthought.com/edm/rh5_x86_64/${EDM_MAJOR_MINOR}/${EDM_PACKAGE}" + fi + + bash "$EDM_INSTALLER_PATH" -b -p "${HOME}/edm" +} + +install_edm \ No newline at end of file diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 000000000..90d13b176 --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1,3 @@ +nose-exclude +coverage +