diff --git a/.travis.yml b/.travis.yml index 2bd356f4e..e3dcc99be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,45 @@ -language: python -sudo: false -notifications: - email: false -python: - - 2.7 - - 3.5 - - 3.6 -addons: - apt: - packages: - - ccache - - swig - - libhdf5-serial-dev +language: generic +dist: xenial +services: + - xvfb + +env: + global: + - INSTALL_EDM_VERSION=1.10.0 + PYTHONUNBUFFERED="1" + +matrix: + include: + - env: RUNTIME=2.7 + - env: RUNTIME=3.5 + - env: RUNTIME=3.6 + - os: osx + env: RUNTIME=2.7 + - os: osx + env: RUNTIME=3.5 + - os: osx + env: RUNTIME=3.6 + fast_finish: true + cache: - apt: true directories: - - $HOME/.cache/pip - - $HOME/.ccache + - "~/.cache" + before_install: - - ccache -s - - pip install --upgrade pip - - export PATH=/usr/lib/ccache:${PATH} + - mkdir -p "${HOME}/.cache/download" + - if [[ ${TRAVIS_OS_NAME} == 'linux' ]]; then ./install-edm-linux.sh; export PATH="${HOME}/edm/bin:${PATH}"; fi + - if [[ ${TRAVIS_OS_NAME} == 'osx' ]]; then ./install-edm-osx.sh; export PATH="${PATH}:/usr/local/bin"; fi + - edm install -y wheel click coverage install: - - pip install -r travis-ci-requirements.txt - - python setup.py develop -before_script: - - mkdir testrun - - cp .coveragerc testrun - - cd testrun + - edm run -- python etstool.py install --runtime=${RUNTIME} || exit script: - - coverage run -m nose.core apptools -v + - edm run -- python etstool.py test --runtime=${RUNTIME} || exit + after_success: - - pip install codecov - - codecov + - edm run -- coverage combine + - edm run -- pip install codecov + - edm run -- codecov + +notifications: + email: + - travis-ci@enthought.com diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..941c323ad --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,36 @@ +build: false +environment: + + global: + PYTHONUNBUFFERED: "1" + INSTALL_EDM_VERSION: "1.9.2" + # We're having trouble setting up EDM 1.10.0. + # See PR #420 for details. + + matrix: + + - RUNTIME: '2.7' + - RUNTIME: '3.5' + - RUNTIME: '3.6' + +matrix: + fast_finish: true + +cache: + - C:\Users\appveyor\.cache -> appveyor-clean-cache.txt + - C:\Users\appveyor\AppData\Local\pip\Cache -> appveyor-clean-cache.txt + +init: + - ps: $Env:path = "C:/Enthought/edm;" + $Env:path + - ps: md C:/Users/appveyor/.cache -Force + +install: + - install-edm-windows.cmd + - edm install -y wheel click coverage + - edm run -- python etstool.py install --runtime=%runtime% +test_script: + - edm run -- python etstool.py test --runtime=%runtime% +on_success: + - edm run -- coverage combine + - edm run -- pip install codecov + - edm run -- codecov diff --git a/ci-src-requirements.txt b/ci-src-requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/etstool.py b/etstool.py new file mode 100644 index 000000000..e0ab9b1b9 --- /dev/null +++ b/etstool.py @@ -0,0 +1,277 @@ +# +# 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 +`_ + +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 etstool.py install --runtime=... + +to create a test environment from the current codebase and:: + + python etstool.py test --runtime=... + +to run tests in that environment. You can remove the environment with:: + + python etstool.py cleanup --runtime=... + +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 etstool.py test_clean --runtime=... + +which will create, install, run tests, and then clean-up the environment. And +you can run tests in all supported runtimes:: + + python etstool.py test_all + +Currently supported runtime values are ``2.7``, ``3.5``, ``3.6``. Not all +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-src-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. + +""" + +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_runtimes = [ + '2.7', + '3.5', + '3.6', +] + +dependencies = { + "traitsui", + "configobj", + "coverage", + "pytables", + "pandas", + "pyface", + "nose", + "mock", +} + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--environment', default=None) +def install(runtime, environment): + """ Install project and dependencies into a clean EDM environment. + + """ + parameters = get_parameters(runtime, environment) + packages = ' '.join(dependencies) + # 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-src-requirements.txt --no-dependencies", + "edm run -e {environment} -- python setup.py clean --all", + "edm run -e {environment} -- python setup.py develop" + ] + # pip install pyqt5 and pyside2, because we don't have them in EDM yet + + click.echo("Creating environment '{environment}'".format(**parameters)) + execute(commands, parameters) + click.echo('Done install') + + +@cli.command() +@click.option('--runtime', default='3.5') +@click.option('--environment', default=None) +def test(runtime, environment): + """ Run the test suite in a given environment. + + """ + parameters = get_parameters(runtime, environment) + environ = {} + environ['PYTHONUNBUFFERED'] = "1" + commands = [ + "edm run -e {environment} -- coverage run -p -m nose.core -v apptools --nologcapture"] + + # We run in a tempdir to avoid accidentally picking up wrong apptools + # 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('--environment', default=None) +def cleanup(runtime, environment): + """ Remove a development environment. + + """ + parameters = get_parameters(runtime, 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(name='test-clean') +@click.option('--runtime', default='3.5') +def test_clean(runtime): + """ Run tests in a clean environment, cleaning up afterwards + + """ + args = ['--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('--environment', default=None) +def update(runtime, environment): + """ Update/Reinstall package into environment. + + """ + parameters = get_parameters(runtime, 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(name='test-all') +def test_all(): + """ Run test_clean across all supported runtimes. + + """ + failed_command = False + for runtime in supported_runtimes: + args = [ + '--runtime={}'.format(runtime) + ] + try: + test_clean(args, standalone_mode=True) + except SystemExit: + failed_command = True + if failed_command: + sys.exit(1) + +# ---------------------------------------------------------------------------- +# Utility routines +# ---------------------------------------------------------------------------- + +def get_parameters(runtime, environment): + """ Set up parameters dictionary for format() substitution """ + parameters = {'runtime': runtime, 'environment': environment} + if environment is None: + parameters['environment'] = 'apptools-test-{runtime}'.format(**parameters) + 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: + click.echo("[EXECUTING] {}".format(command.format(**parameters))) + try: + subprocess.check_call([arg.format(**parameters) + for arg in command.split()]) + except subprocess.CalledProcessError as exc: + click.echo(str(exc)) + sys.exit(1) + + +if __name__ == '__main__': + cli() diff --git a/install-edm-linux.sh b/install-edm-linux.sh new file mode 100755 index 000000000..4715066fc --- /dev/null +++ b/install-edm-linux.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 diff --git a/install-edm-osx.sh b/install-edm-osx.sh new file mode 100755 index 000000000..b6d18b63e --- /dev/null +++ b/install-edm-osx.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}.pkg" + 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/osx_x86_64/${EDM_MAJOR_MINOR}/${EDM_PACKAGE}" + fi + + sudo installer -pkg "$EDM_INSTALLER_PATH" -target / +} + +install_edm diff --git a/install-edm-windows.cmd b/install-edm-windows.cmd new file mode 100644 index 000000000..594443516 --- /dev/null +++ b/install-edm-windows.cmd @@ -0,0 +1,26 @@ +@ECHO OFF + +SETLOCAL EnableDelayedExpansion + +FOR /F "tokens=1,2,3 delims=." %%a in ("%INSTALL_EDM_VERSION%") do ( + SET MAJOR=%%a + SET MINOR=%%b + SET REVISION=%%c +) + +SET EDM_MAJOR_MINOR=%MAJOR%.%MINOR% +SET EDM_PACKAGE=edm_%INSTALL_EDM_VERSION%_x86_64.msi +SET EDM_INSTALLER_PATH=%HOMEDRIVE%%HOMEPATH%\.cache\%EDM_PACKAGE% +SET COMMAND="(new-object net.webclient).DownloadFile('https://package-data.enthought.com/edm/win_x86_64/%EDM_MAJOR_MINOR%/%EDM_PACKAGE%', '%EDM_INSTALLER_PATH%')" + +IF NOT EXIST %EDM_INSTALLER_PATH% CALL powershell.exe -Command %COMMAND% || GOTO error +CALL msiexec /qn /a %EDM_INSTALLER_PATH% TARGETDIR=c:\ || GOTO error + +ENDLOCAL +@ECHO.DONE +EXIT + +:error: +ENDLOCAL +@ECHO.ERROR +EXIT /b %ERRORLEVEL%