├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── setup.cfg ├── setup.py ├── src └── tox_venv │ ├── __init__.py │ └── hooks.py ├── tests ├── conftest.py ├── test_venv.py └── test_z_cmdline.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | language: python 9 | python: 10 | - "2.7" 11 | - "3.5" 12 | - "3.6" 13 | - "3.7" 14 | - "3.8" 15 | - "pypy3.5" 16 | 17 | jobs: 18 | fast_finish: true 19 | allow_failures: 20 | - python: "pypy3.5" 21 | 22 | include: 23 | - os: osx 24 | language: generic 25 | env: TOXENV=py37 26 | 27 | - python: "3.8" 28 | env: TOXENV="isort,lint,coverage" 29 | script: tox 30 | after_success: 31 | - pip install codecov && codecov 32 | - coverage report 33 | 34 | 35 | # We don't need to get the latest version of Python 3 on macOS. While it 36 | # would be nice, the 5-10 minutes it takes to update Homebrew and upgrade 37 | # the various dependent packages, means that it's is not worth it. 38 | # Note that we also print the python version in case of macOS updates 39 | before_install: 40 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 41 | python3 --version; 42 | python3 -m venv venv; 43 | source venv/bin/activate; 44 | fi 45 | install: 46 | - pip install -U six setuptools wheel 47 | - pip install -U tox tox-travis 48 | - python setup.py bdist_wheel 49 | script: tox --installpkg ./dist/tox_venv-*.whl 50 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Unreleased 2 | ========== 3 | 4 | - Make plugin compatible with tox 3.14 5 | - Add Python 3.8 support 6 | - Drop Python 3.4 support 7 | 8 | 0.4.0 (2019-03-28) 9 | ================== 10 | 11 | - Make plugin compatible with tox 3.8.1 12 | - Add Python 3.7 support 13 | 14 | 0.3.1 (2018-06-24) 15 | ================== 16 | 17 | - Fix stderr handling. 18 | 19 | 0.3.0 (2018-06-01) 20 | ================== 21 | 22 | - Improve executable path determination. 23 | - Update test suite from tox 3. 24 | - Add code quality checks to test suite. 25 | - Add installation & usage docs. 26 | 27 | 0.2.0 (2018-01-28) 28 | ================== 29 | 30 | - Fix compatibility for Windows. 31 | - Improve Python 3 version detection. 32 | - Move to tox-dev organization. 33 | 34 | 0.1.0 (2017-11-19) 35 | ================== 36 | 37 | - Add initial implementation. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Ryan P Kilby 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | 3 | include tox.ini 4 | recursive-include tests *.py 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | tox-venv 2 | ======== 3 | 4 | Note the project has been deprecated as of 2020 May the 1st. ``virtualenv`` now creates venv style environments out of box, so this plugin is no longer needed. 5 | 6 | .. image:: https://travis-ci.org/tox-dev/tox-venv.svg?branch=master 7 | :target: https://travis-ci.org/tox-dev/tox-venv 8 | .. image:: https://ci.appveyor.com/api/projects/status/fak35ur9yibmn0ly?svg=true 9 | :target: https://ci.appveyor.com/project/rpkilby/tox-venv 10 | .. image:: https://codecov.io/gh/tox-dev/tox-venv/branch/master/graph/badge.svg 11 | :target: https://codecov.io/gh/tox-dev/tox-venv 12 | .. image:: https://img.shields.io/pypi/v/tox-venv.svg 13 | :target: https://pypi.python.org/pypi/tox-venv 14 | .. image:: https://img.shields.io/pypi/pyversions/tox-venv.svg 15 | :target: https://pypi.org/project/tox-venv/ 16 | .. image:: https://img.shields.io/pypi/l/tox-venv.svg 17 | :target: https://pypi.python.org/pypi/tox-venv 18 | 19 | 20 | What is tox-venv? 21 | ----------------- 22 | 23 | tox-venv is a plugin that uses Python 3's builtin ``venv`` module for creating test environments instead of creating 24 | them with the ``virtualenv`` package. For Python versions that do not include ``venv`` (namely 3.2 and earlier), this 25 | package does nothing and reverts to tox's default implementation. 26 | 27 | 28 | Why use tox-venv? 29 | ----------------- 30 | 31 | tox-venv was originally created because of compatibility issues between modern versions of Python and an aging 32 | ``virtualenv``. Since then, ``virtualenv`` has undergone a major rewrite, and tox-venv has largely been made 33 | unnecessary. However, there may be cases where it's preferable to create test environments directly with the 34 | ``venv`` module, in which case you should use tox-venv. 35 | 36 | 37 | Installation & Usage 38 | -------------------- 39 | 40 | To use tox-venv, install it alongside tox in your environment. Then, run ``tox`` as normal - no configuration necessary. 41 | 42 | .. code-block:: 43 | 44 | $ pip install tox tox-venv 45 | $ tox 46 | 47 | If you have already ran tox, it's necessary to recreate the test envs. Either run ``tox --recreate``, or delete the 48 | ``.tox`` directory. 49 | 50 | 51 | Compatibility 52 | ------------- 53 | 54 | tox-venv is compatible with both Python 2 and 3, however it only creates test environments in Python 3.3 and later. 55 | Python 3.3 environments are only partially compatible, as not all options (such as ``--copies``/``--symlinks``) were 56 | supported. Environments for Python 3.4 and later are fully compatible. 57 | 58 | 59 | Release process 60 | --------------- 61 | 62 | * Update changelog 63 | * Update package version in setup.py 64 | * Create git tag for version 65 | * Upload release to PyPI 66 | 67 | .. code-block:: 68 | 69 | $ pip install -U twine setuptools wheel 70 | $ rm -rf dist/ build/ 71 | # python setup.py sdist bdist_wheel 72 | $ twine upload dist/* 73 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false # Not a C# project 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | environment: 8 | matrix: 9 | - TOXENV: py35 10 | - TOXENV: py36 11 | - TOXENV: py37 12 | - TOXENV: py38 13 | 14 | matrix: 15 | fast_finish: true 16 | 17 | install: 18 | - C:\Python36\python -m pip install -U six setuptools wheel 19 | - C:\Python36\python -m pip install -U tox 20 | - C:\Python36\python setup.py bdist_wheel 21 | 22 | test_script: 23 | - C:\Python36\scripts\tox 24 | 25 | cache: 26 | - '%LOCALAPPDATA%\pip\cache' 27 | - '%USERPROFILE%\.cache\pre-commit' 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max_line_length = 120 6 | max_complexity = 10 7 | 8 | [isort] 9 | skip = .tox 10 | atomic = true 11 | line_length = 120 12 | multi_line_output = 3 13 | include_trailing_comma = true 14 | known_third_party = tox 15 | known_first_party = tox_venv 16 | 17 | [coverage:run] 18 | branch = true 19 | source = src 20 | 21 | [coverage:report] 22 | show_missing = true 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def get_long_description(): 7 | with io.open('README.rst', encoding='utf-8') as f: 8 | return f.read() 9 | 10 | 11 | setup( 12 | name='tox-venv', 13 | description='Use Python 3 venvs for Python 3 tox testenvs', 14 | long_description=get_long_description(), 15 | author='Ryan P Kilby', 16 | author_email='kilbyr@gmail.com', 17 | url='https://github.com/tox-dev/tox-venv', 18 | license='BSD', 19 | version='0.4.0', 20 | package_dir={'': 'src'}, 21 | packages=find_packages('src'), 22 | entry_points={'tox': ['venv = tox_venv.hooks']}, 23 | install_requires=['tox>=3.8.1'], 24 | classifiers=[ 25 | 'Development Status :: 3 - Alpha', 26 | 'Framework :: tox', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | 'Programming Language :: Python :: Implementation :: CPython', 38 | 'Programming Language :: Python :: Implementation :: PyPy', 39 | 'Topic :: Software Development :: Testing', 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /src/tox_venv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tox-dev/tox-venv/dab94edb9612897e95fa330d187b9e87fcab1350/src/tox_venv/__init__.py -------------------------------------------------------------------------------- /src/tox_venv/hooks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import tox 5 | from tox.venv import cleanup_for_venv 6 | 7 | 8 | def real_python3(python, version_dict): 9 | """ 10 | Determine the path of the real python executable, which is then used for 11 | venv creation. This is necessary, because an active virtualenv environment 12 | will cause venv creation to malfunction. By getting the path of the real 13 | executable, this issue is bypassed. 14 | 15 | The provided `python` path may be either: 16 | - A real python executable 17 | - A virtual python executable (with venv) 18 | - A virtual python executable (with virtualenv) 19 | 20 | If the virtual environment was created with virtualenv, the `sys` module 21 | will have a `real_prefix` attribute, which points to the directory where 22 | the real python files are installed. 23 | 24 | If `real_prefix` is not present, the environment was not created with 25 | virtualenv, and the python executable is safe to use. 26 | 27 | The `version_dict` is used for attempting to derive the real executable 28 | path. This is necessary when the name of the virtual python executable 29 | does not exist in the Python installation's directory. For example, if 30 | the `basepython` is explicitly set to `python`, tox will use this name 31 | instead of attempting `pythonX.Y`. In many cases, Python 3 installations 32 | do not contain an executable named `python`, so we attempt to derive this 33 | from the version info. e.g., `python3.6.5`, `python3.6`, then `python3`. 34 | """ 35 | args = [python, '-c', 'import sys; print(sys.real_prefix)'] 36 | 37 | # get python prefix 38 | try: 39 | output = subprocess.check_output(args, stderr=subprocess.STDOUT) 40 | prefix = output.decode('UTF-8').strip() 41 | except subprocess.CalledProcessError: 42 | # process fails, implies *not* in active virtualenv 43 | return python 44 | 45 | # determine absolute binary path 46 | if os.name == 'nt': # pragma: no cover 47 | paths = [os.path.join(prefix, os.path.basename(python))] 48 | else: 49 | paths = [os.path.join(prefix, 'bin', python) for python in [ 50 | os.path.basename(python), 51 | 'python%(major)d.%(minor)d.%(micro)d' % version_dict, 52 | 'python%(major)d.%(minor)d' % version_dict, 53 | 'python%(major)d' % version_dict, 54 | 'python', 55 | ]] 56 | 57 | for path in paths: 58 | if os.path.isfile(path): 59 | break 60 | else: 61 | path = None 62 | 63 | # the executable path must exist 64 | assert path, '\n- '.join(['Could not find interpreter. Attempted:'] + paths) 65 | v1 = subprocess.check_output([python, '--version']) 66 | v2 = subprocess.check_output([path, '--version']) 67 | assert v1 == v2, 'Expected versions to match (%s != %s).' % (v1, v2) 68 | 69 | return path 70 | 71 | 72 | def use_builtin_venv(venv): 73 | """ 74 | Determine if the builtin venv module should be used to create the testenv's 75 | virtual environment. The venv module was added in python 3.3, although some 76 | options are not supported until 3.4 and later. 77 | """ 78 | version = venv.envconfig.python_info.version_info 79 | return version is not None and version >= (3, 3) 80 | 81 | 82 | @tox.hookimpl 83 | def tox_testenv_create(venv, action): 84 | # Bypass hook when venv is not available for the target python version 85 | if not use_builtin_venv(venv): 86 | return 87 | 88 | v = venv.envconfig.python_info.version_info 89 | version_dict = {'major': v[0], 'minor': v[1], 'micro': v[2]} 90 | 91 | config_interpreter = str(venv.getsupportedinterpreter()) 92 | real_executable = real_python3(config_interpreter, version_dict) 93 | 94 | args = [real_executable, '-m', 'venv'] 95 | if venv.envconfig.sitepackages: 96 | args.append('--system-site-packages') 97 | if venv.envconfig.alwayscopy: 98 | args.append('--copies') 99 | 100 | # Handles making the empty dir for the `venv.path`. 101 | cleanup_for_venv(venv) 102 | 103 | basepath = venv.path.dirpath() 104 | basepath.ensure(dir=1) 105 | args.append(venv.path.basename) 106 | 107 | if not os.environ.get('_TOX_SKIP_ENV_CREATION_TEST', False) == '1': 108 | try: 109 | venv._pcall(args, venv=False, action=action, cwd=basepath) 110 | except KeyboardInterrupt: 111 | venv.status = 'keyboardinterrupt' 112 | raise 113 | 114 | # Return non-None to indicate the plugin has completed 115 | return True 116 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from tox._pytestplugin import * # noqa 2 | -------------------------------------------------------------------------------- /tests/test_venv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import py 5 | import pytest 6 | 7 | import tox 8 | from tox.interpreters import NoInterpreterInfo 9 | from tox.session.commands.run.sequential import installpkg, runtestenv 10 | from tox.venv import ( 11 | CreationConfig, 12 | VirtualEnv, 13 | getdigest, 14 | prepend_shebang_interpreter, 15 | tox_testenv_install_deps, 16 | ) 17 | 18 | from tox_venv.hooks import use_builtin_venv 19 | 20 | 21 | def tox_testenv_create(action, venv): 22 | return venv.hook.tox_testenv_create(action=action, venv=venv) 23 | 24 | 25 | def test_getdigest(tmpdir): 26 | assert getdigest(tmpdir) == "0" * 32 27 | 28 | 29 | def test_getsupportedinterpreter(monkeypatch, newconfig, mocksession): 30 | config = newconfig( 31 | [], 32 | """\ 33 | [testenv:python] 34 | basepython={} 35 | """.format( 36 | sys.executable 37 | ), 38 | ) 39 | mocksession.new_config(config) 40 | venv = mocksession.getvenv("python") 41 | interp = venv.getsupportedinterpreter() 42 | # realpath needed for debian symlinks 43 | assert py.path.local(interp).realpath() == py.path.local(sys.executable).realpath() 44 | monkeypatch.setattr(tox.INFO, "IS_WIN", True) 45 | monkeypatch.setattr(venv.envconfig, "basepython", "jython") 46 | with pytest.raises(tox.exception.UnsupportedInterpreter): 47 | venv.getsupportedinterpreter() 48 | monkeypatch.undo() 49 | monkeypatch.setattr(venv.envconfig, "envname", "py1") 50 | monkeypatch.setattr(venv.envconfig, "basepython", "notexisting") 51 | with pytest.raises(tox.exception.InterpreterNotFound): 52 | venv.getsupportedinterpreter() 53 | monkeypatch.undo() 54 | # check that we properly report when no version_info is present 55 | info = NoInterpreterInfo(name=venv.name) 56 | info.executable = "something" 57 | monkeypatch.setattr(config.interpreters, "get_info", lambda *args, **kw: info) 58 | with pytest.raises(tox.exception.InvocationError): 59 | venv.getsupportedinterpreter() 60 | 61 | 62 | def test_create(mocksession, newconfig): 63 | config = newconfig( 64 | [], 65 | """\ 66 | [testenv:py123] 67 | """, 68 | ) 69 | envconfig = config.envconfigs["py123"] 70 | mocksession.new_config(config) 71 | venv = mocksession.getvenv("py123") 72 | assert venv.path == envconfig.envdir 73 | assert not venv.path.check() 74 | with mocksession.newaction(venv.name, "getenv") as action: 75 | tox_testenv_create(action=action, venv=venv) 76 | pcalls = mocksession._pcalls 77 | assert len(pcalls) >= 1 78 | args = pcalls[0].args 79 | module = "venv" if use_builtin_venv(venv) else "virtualenv" 80 | assert module == str(args[2]) 81 | if not tox.INFO.IS_WIN: 82 | executable = sys.executable 83 | if use_builtin_venv(venv) and hasattr(sys, "real_prefix"): 84 | # workaround virtualenv prefixing issue w/ venv on python3 85 | executable = "python{}.{}".format(*sys.version_info) 86 | executable = os.path.join(sys.real_prefix, "bin", executable) 87 | # realpath is needed for stuff like the debian symlinks 88 | our_sys_path = py.path.local(executable).realpath() 89 | assert our_sys_path == py.path.local(args[0]).realpath() 90 | # assert Envconfig.toxworkdir in args 91 | assert venv.getcommandpath("easy_install", cwd=py.path.local()) 92 | interp = venv._getliveconfig().base_resolved_python_path 93 | assert interp == venv.envconfig.python_info.executable 94 | assert venv.path_config.check(exists=False) 95 | 96 | 97 | def test_create_KeyboardInterrupt(mocksession, newconfig, mocker): 98 | config = newconfig( 99 | [], 100 | """\ 101 | [testenv:py123] 102 | """, 103 | ) 104 | mocksession.new_config(config) 105 | venv = mocksession.getvenv("py123") 106 | mocker.patch.object(venv, "_pcall", side_effect=KeyboardInterrupt) 107 | with pytest.raises(KeyboardInterrupt): 108 | venv.setupenv() 109 | 110 | assert venv.status == "keyboardinterrupt" 111 | 112 | 113 | def test_commandpath_venv_precedence(tmpdir, monkeypatch, mocksession, newconfig): 114 | config = newconfig( 115 | [], 116 | """\ 117 | [testenv:py123] 118 | """, 119 | ) 120 | mocksession.new_config(config) 121 | venv = mocksession.getvenv("py123") 122 | envconfig = venv.envconfig 123 | tmpdir.ensure("easy_install") 124 | monkeypatch.setenv("PATH", str(tmpdir), prepend=os.pathsep) 125 | envconfig.envbindir.ensure("easy_install") 126 | p = venv.getcommandpath("easy_install") 127 | assert py.path.local(p).relto(envconfig.envbindir), p 128 | 129 | 130 | def test_create_sitepackages(mocksession, newconfig): 131 | config = newconfig( 132 | [], 133 | """\ 134 | [testenv:site] 135 | sitepackages=True 136 | 137 | [testenv:nosite] 138 | sitepackages=False 139 | """, 140 | ) 141 | mocksession.new_config(config) 142 | venv = mocksession.getvenv("site") 143 | with mocksession.newaction(venv.name, "getenv") as action: 144 | tox_testenv_create(action=action, venv=venv) 145 | pcalls = mocksession._pcalls 146 | assert len(pcalls) >= 1 147 | args = pcalls[0].args 148 | assert "--system-site-packages" in map(str, args) 149 | mocksession._clearmocks() 150 | 151 | venv = mocksession.getvenv("nosite") 152 | with mocksession.newaction(venv.name, "getenv") as action: 153 | tox_testenv_create(action=action, venv=venv) 154 | pcalls = mocksession._pcalls 155 | assert len(pcalls) >= 1 156 | args = pcalls[0].args 157 | assert "--system-site-packages" not in map(str, args) 158 | assert "--no-site-packages" not in map(str, args) 159 | 160 | 161 | def test_install_deps_wildcard(newmocksession): 162 | mocksession = newmocksession( 163 | [], 164 | """\ 165 | [tox] 166 | distshare = {toxworkdir}/distshare 167 | [testenv:py123] 168 | deps= 169 | {distshare}/dep1-* 170 | """, 171 | ) 172 | venv = mocksession.getvenv("py123") 173 | with mocksession.newaction(venv.name, "getenv") as action: 174 | tox_testenv_create(action=action, venv=venv) 175 | pcalls = mocksession._pcalls 176 | assert len(pcalls) == 1 177 | distshare = venv.envconfig.config.distshare 178 | distshare.ensure("dep1-1.0.zip") 179 | distshare.ensure("dep1-1.1.zip") 180 | 181 | tox_testenv_install_deps(action=action, venv=venv) 182 | assert len(pcalls) == 2 183 | args = pcalls[-1].args 184 | assert pcalls[-1].cwd == venv.envconfig.config.toxinidir 185 | 186 | assert py.path.local.sysfind("python") == args[0] 187 | assert ["-m", "pip"] == args[1:3] 188 | assert args[3] == "install" 189 | args = [arg for arg in args if str(arg).endswith("dep1-1.1.zip")] 190 | assert len(args) == 1 191 | 192 | 193 | def test_install_deps_indexserver(newmocksession): 194 | mocksession = newmocksession( 195 | [], 196 | """\ 197 | [tox] 198 | indexserver = 199 | abc = ABC 200 | abc2 = ABC 201 | [testenv:py123] 202 | deps= 203 | dep1 204 | :abc:dep2 205 | :abc2:dep3 206 | """, 207 | ) 208 | venv = mocksession.getvenv("py123") 209 | with mocksession.newaction(venv.name, "getenv") as action: 210 | tox_testenv_create(action=action, venv=venv) 211 | pcalls = mocksession._pcalls 212 | assert len(pcalls) == 1 213 | pcalls[:] = [] 214 | 215 | tox_testenv_install_deps(action=action, venv=venv) 216 | # two different index servers, two calls 217 | assert len(pcalls) == 3 218 | args = " ".join(pcalls[0].args) 219 | assert "-i " not in args 220 | assert "dep1" in args 221 | 222 | args = " ".join(pcalls[1].args) 223 | assert "-i ABC" in args 224 | assert "dep2" in args 225 | args = " ".join(pcalls[2].args) 226 | assert "-i ABC" in args 227 | assert "dep3" in args 228 | 229 | 230 | def test_install_deps_pre(newmocksession): 231 | mocksession = newmocksession( 232 | [], 233 | """\ 234 | [testenv] 235 | pip_pre=true 236 | deps= 237 | dep1 238 | """, 239 | ) 240 | venv = mocksession.getvenv("python") 241 | with mocksession.newaction(venv.name, "getenv") as action: 242 | tox_testenv_create(action=action, venv=venv) 243 | pcalls = mocksession._pcalls 244 | assert len(pcalls) == 1 245 | pcalls[:] = [] 246 | 247 | tox_testenv_install_deps(action=action, venv=venv) 248 | assert len(pcalls) == 1 249 | args = " ".join(pcalls[0].args) 250 | assert "--pre " in args 251 | assert "dep1" in args 252 | 253 | 254 | def test_installpkg_indexserver(newmocksession, tmpdir): 255 | mocksession = newmocksession( 256 | [], 257 | """\ 258 | [tox] 259 | indexserver = 260 | default = ABC 261 | """, 262 | ) 263 | venv = mocksession.getvenv("python") 264 | pcalls = mocksession._pcalls 265 | p = tmpdir.ensure("distfile.tar.gz") 266 | installpkg(venv, p) 267 | # two different index servers, two calls 268 | assert len(pcalls) == 1 269 | args = " ".join(pcalls[0].args) 270 | assert "-i ABC" in args 271 | 272 | 273 | def test_install_recreate(newmocksession, tmpdir): 274 | pkg = tmpdir.ensure("package.tar.gz") 275 | mocksession = newmocksession( 276 | ["--recreate"], 277 | """\ 278 | [testenv] 279 | deps=xyz 280 | """, 281 | ) 282 | venv = mocksession.getvenv("python") 283 | 284 | with mocksession.newaction(venv.name, "update") as action: 285 | venv.update(action) 286 | installpkg(venv, pkg) 287 | mocksession.report.expect("verbosity0", "*create*") 288 | venv.update(action) 289 | mocksession.report.expect("verbosity0", "*recreate*") 290 | 291 | 292 | def test_install_sdist_extras(newmocksession): 293 | mocksession = newmocksession( 294 | [], 295 | """\ 296 | [testenv] 297 | extras = testing 298 | development 299 | """, 300 | ) 301 | venv = mocksession.getvenv("python") 302 | with mocksession.newaction(venv.name, "getenv") as action: 303 | tox_testenv_create(action=action, venv=venv) 304 | pcalls = mocksession._pcalls 305 | assert len(pcalls) == 1 306 | pcalls[:] = [] 307 | 308 | venv.installpkg("distfile.tar.gz", action=action) 309 | assert "distfile.tar.gz[testing,development]" in pcalls[-1].args 310 | 311 | 312 | def test_develop_extras(newmocksession, tmpdir): 313 | mocksession = newmocksession( 314 | [], 315 | """\ 316 | [testenv] 317 | extras = testing 318 | development 319 | """, 320 | ) 321 | venv = mocksession.getvenv("python") 322 | with mocksession.newaction(venv.name, "getenv") as action: 323 | tox_testenv_create(action=action, venv=venv) 324 | pcalls = mocksession._pcalls 325 | assert len(pcalls) == 1 326 | pcalls[:] = [] 327 | 328 | venv.developpkg(tmpdir, action=action) 329 | expected = "{}[testing,development]".format(tmpdir.strpath) 330 | assert expected in pcalls[-1].args 331 | 332 | 333 | def test_env_variables_added_to_needs_reinstall(tmpdir, mocksession, newconfig, monkeypatch): 334 | tmpdir.ensure("setup.py") 335 | monkeypatch.setenv("TEMP_PASS_VAR", "123") 336 | monkeypatch.setenv("TEMP_NOPASS_VAR", "456") 337 | config = newconfig( 338 | [], 339 | """\ 340 | [testenv:python] 341 | passenv = temp_pass_var 342 | setenv = 343 | CUSTOM_VAR = 789 344 | """, 345 | ) 346 | mocksession.new_config(config) 347 | venv = mocksession.getvenv("python") 348 | with mocksession.newaction(venv.name, "hello") as action: 349 | venv._needs_reinstall(tmpdir, action) 350 | 351 | pcalls = mocksession._pcalls 352 | assert len(pcalls) == 2 353 | env = pcalls[0].env 354 | 355 | # should have access to setenv vars 356 | assert "CUSTOM_VAR" in env 357 | assert env["CUSTOM_VAR"] == "789" 358 | 359 | # should have access to passenv vars 360 | assert "TEMP_PASS_VAR" in env 361 | assert env["TEMP_PASS_VAR"] == "123" 362 | 363 | # should also have access to full invocation environment, 364 | # for backward compatibility, and to match behavior of venv.run_install_command() 365 | assert "TEMP_NOPASS_VAR" in env 366 | assert env["TEMP_NOPASS_VAR"] == "456" 367 | 368 | 369 | def test_test_hashseed_is_in_output(newmocksession, monkeypatch): 370 | seed = "123456789" 371 | monkeypatch.setattr("tox.config.make_hashseed", lambda: seed) 372 | mocksession = newmocksession([], "") 373 | venv = mocksession.getvenv("python") 374 | with mocksession.newaction(venv.name, "update") as action: 375 | venv.update(action) 376 | tox.venv.tox_runtest_pre(venv) 377 | mocksession.report.expect("verbosity0", "run-test-pre: PYTHONHASHSEED='{}'".format(seed)) 378 | 379 | 380 | def test_test_runtests_action_command_is_in_output(newmocksession): 381 | mocksession = newmocksession( 382 | [], 383 | """\ 384 | [testenv] 385 | commands = echo foo bar 386 | """, 387 | ) 388 | venv = mocksession.getvenv("python") 389 | with mocksession.newaction(venv.name, "update") as action: 390 | venv.update(action) 391 | venv.test() 392 | mocksession.report.expect("verbosity0", "*run-test:*commands?0? | echo foo bar") 393 | 394 | 395 | def test_install_error(newmocksession): 396 | mocksession = newmocksession( 397 | ["--recreate"], 398 | """\ 399 | [testenv] 400 | deps=xyz 401 | commands= 402 | qwelkqw 403 | """, 404 | ) 405 | venv = mocksession.getvenv("python") 406 | venv.test() 407 | mocksession.report.expect("error", "*not find*qwelkqw*") 408 | assert venv.status == "commands failed" 409 | 410 | 411 | def test_install_command_not_installed(newmocksession): 412 | mocksession = newmocksession( 413 | ["--recreate"], 414 | """\ 415 | [testenv] 416 | commands= 417 | pytest 418 | """, 419 | ) 420 | venv = mocksession.getvenv("python") 421 | venv.status = 0 422 | venv.test() 423 | mocksession.report.expect("warning", "*test command found but not*") 424 | assert venv.status == 0 425 | 426 | 427 | def test_install_command_whitelisted(newmocksession): 428 | mocksession = newmocksession( 429 | ["--recreate"], 430 | """\ 431 | [testenv] 432 | whitelist_externals = pytest 433 | xy* 434 | commands= 435 | pytest 436 | xyz 437 | """, 438 | ) 439 | venv = mocksession.getvenv("python") 440 | venv.test() 441 | mocksession.report.expect("warning", "*test command found but not*", invert=True) 442 | assert venv.status == "commands failed" 443 | 444 | 445 | def test_install_command_not_installed_bash(newmocksession): 446 | mocksession = newmocksession( 447 | ["--recreate"], 448 | """\ 449 | [testenv] 450 | commands= 451 | bash 452 | """, 453 | ) 454 | venv = mocksession.getvenv("python") 455 | venv.test() 456 | mocksession.report.expect("warning", "*test command found but not*") 457 | 458 | 459 | def test_install_python3(newmocksession): 460 | if not py.path.local.sysfind("python3") or tox.INFO.IS_PYPY: 461 | pytest.skip("needs cpython3") 462 | mocksession = newmocksession( 463 | [], 464 | """\ 465 | [testenv:py123] 466 | basepython=python3 467 | deps= 468 | dep1 469 | dep2 470 | """, 471 | ) 472 | venv = mocksession.getvenv("py123") 473 | with mocksession.newaction(venv.name, "getenv") as action: 474 | tox_testenv_create(action=action, venv=venv) 475 | pcalls = mocksession._pcalls 476 | assert len(pcalls) == 1 477 | args = pcalls[0].args 478 | assert str(args[2]) == "venv" 479 | pcalls[:] = [] 480 | with mocksession.newaction(venv.name, "hello") as action: 481 | venv._install(["hello"], action=action) 482 | assert len(pcalls) == 1 483 | args = pcalls[0].args 484 | assert py.path.local.sysfind("python") == args[0] 485 | assert ["-m", "pip"] == args[1:3] 486 | for _ in args: 487 | assert "--download-cache" not in args, args 488 | 489 | 490 | class TestCreationConfig: 491 | def test_basic(self, newconfig, mocksession, tmpdir): 492 | config = newconfig([], "") 493 | mocksession.new_config(config) 494 | venv = mocksession.getvenv("python") 495 | cconfig = venv._getliveconfig() 496 | assert cconfig.matches(cconfig) 497 | path = tmpdir.join("configdump") 498 | cconfig.writeconfig(path) 499 | newconfig = CreationConfig.readconfig(path) 500 | assert newconfig.matches(cconfig) 501 | assert cconfig.matches(newconfig) 502 | 503 | def test_matchingdependencies(self, newconfig, mocksession): 504 | config = newconfig( 505 | [], 506 | """\ 507 | [testenv] 508 | deps=abc 509 | """, 510 | ) 511 | mocksession.new_config(config) 512 | venv = mocksession.getvenv("python") 513 | cconfig = venv._getliveconfig() 514 | config = newconfig( 515 | [], 516 | """\ 517 | [testenv] 518 | deps=xyz 519 | """, 520 | ) 521 | mocksession.new_config(config) 522 | venv = mocksession.getvenv("python") 523 | otherconfig = venv._getliveconfig() 524 | assert not cconfig.matches(otherconfig) 525 | 526 | def test_matchingdependencies_file(self, newconfig, mocksession): 527 | config = newconfig( 528 | [], 529 | """\ 530 | [tox] 531 | distshare={toxworkdir}/distshare 532 | [testenv] 533 | deps=abc 534 | {distshare}/xyz.zip 535 | """, 536 | ) 537 | xyz = config.distshare.join("xyz.zip") 538 | xyz.ensure() 539 | mocksession.new_config(config) 540 | venv = mocksession.getvenv("python") 541 | cconfig = venv._getliveconfig() 542 | assert cconfig.matches(cconfig) 543 | xyz.write("hello") 544 | newconfig = venv._getliveconfig() 545 | assert not cconfig.matches(newconfig) 546 | 547 | def test_matchingdependencies_latest(self, newconfig, mocksession): 548 | config = newconfig( 549 | [], 550 | """\ 551 | [tox] 552 | distshare={toxworkdir}/distshare 553 | [testenv] 554 | deps={distshare}/xyz-* 555 | """, 556 | ) 557 | config.distshare.ensure("xyz-1.2.0.zip") 558 | xyz2 = config.distshare.ensure("xyz-1.2.1.zip") 559 | mocksession.new_config(config) 560 | venv = mocksession.getvenv("python") 561 | cconfig = venv._getliveconfig() 562 | sha256, path = cconfig.deps[0] 563 | assert path == xyz2 564 | assert sha256 == path.computehash("sha256") 565 | 566 | def test_python_recreation(self, tmpdir, newconfig, mocksession): 567 | pkg = tmpdir.ensure("package.tar.gz") 568 | config = newconfig(["-v"], "") 569 | mocksession.new_config(config) 570 | venv = mocksession.getvenv("python") 571 | create_config = venv._getliveconfig() 572 | with mocksession.newaction(venv.name, "update") as action: 573 | venv.update(action) 574 | assert not venv.path_config.check() 575 | installpkg(venv, pkg) 576 | assert venv.path_config.check() 577 | assert mocksession._pcalls 578 | args1 = map(str, mocksession._pcalls[0].args) 579 | module = "venv" if use_builtin_venv(venv) else "virtualenv" 580 | assert module in " ".join(args1) 581 | mocksession.report.expect("*", "*create*") 582 | # modify config and check that recreation happens 583 | mocksession._clearmocks() 584 | with mocksession.newaction(venv.name, "update") as action: 585 | venv.update(action) 586 | mocksession.report.expect("*", "*reusing*") 587 | mocksession._clearmocks() 588 | with mocksession.newaction(venv.name, "update") as action: 589 | create_config.base_resolved_python_path = py.path.local("balla") 590 | create_config.writeconfig(venv.path_config) 591 | venv.update(action) 592 | mocksession.report.expect("verbosity0", "*recreate*") 593 | 594 | def test_dep_recreation(self, newconfig, mocksession): 595 | config = newconfig([], "") 596 | mocksession.new_config(config) 597 | venv = mocksession.getvenv("python") 598 | with mocksession.newaction(venv.name, "update") as action: 599 | venv.update(action) 600 | cconfig = venv._getliveconfig() 601 | cconfig.deps[:] = [("1" * 32, "xyz.zip")] 602 | cconfig.writeconfig(venv.path_config) 603 | mocksession._clearmocks() 604 | with mocksession.newaction(venv.name, "update") as action: 605 | venv.update(action) 606 | mocksession.report.expect("*", "*recreate*") 607 | 608 | def test_develop_recreation(self, newconfig, mocksession): 609 | config = newconfig([], "") 610 | mocksession.new_config(config) 611 | venv = mocksession.getvenv("python") 612 | with mocksession.newaction(venv.name, "update") as action: 613 | venv.update(action) 614 | cconfig = venv._getliveconfig() 615 | cconfig.usedevelop = True 616 | cconfig.writeconfig(venv.path_config) 617 | mocksession._clearmocks() 618 | with mocksession.newaction(venv.name, "update") as action: 619 | venv.update(action) 620 | mocksession.report.expect("verbosity0", "*recreate*") 621 | 622 | 623 | class TestVenvTest: 624 | def test_envbindir_path(self, newmocksession, monkeypatch): 625 | monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") 626 | mocksession = newmocksession( 627 | [], 628 | """\ 629 | [testenv:python] 630 | commands=abc 631 | """, 632 | ) 633 | venv = mocksession.getvenv("python") 634 | with mocksession.newaction(venv.name, "getenv") as action: 635 | monkeypatch.setenv("PATH", "xyz") 636 | sysfind_calls = [] 637 | monkeypatch.setattr( 638 | "py.path.local.sysfind", 639 | classmethod(lambda *args, **kwargs: sysfind_calls.append(kwargs) or 0 / 0), 640 | ) 641 | 642 | with pytest.raises(ZeroDivisionError): 643 | venv._install(list("123"), action=action) 644 | assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] 645 | with pytest.raises(ZeroDivisionError): 646 | venv.test(action) 647 | assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] 648 | with pytest.raises(ZeroDivisionError): 649 | venv.run_install_command(["qwe"], action=action) 650 | assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] 651 | monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") 652 | monkeypatch.setenv("PIP_REQUIRE_VIRTUALENV", "1") 653 | monkeypatch.setenv("__PYVENV_LAUNCHER__", "1") 654 | 655 | prev_pcall = venv._pcall 656 | 657 | def collect(*args, **kwargs): 658 | env = kwargs["env"] 659 | assert "PIP_RESPECT_VIRTUALENV" not in env 660 | assert "PIP_REQUIRE_VIRTUALENV" not in env 661 | assert "__PYVENV_LAUNCHER__" not in env 662 | assert env["PIP_USER"] == "0" 663 | assert env["PIP_NO_DEPS"] == "0" 664 | return prev_pcall(*args, **kwargs) 665 | 666 | monkeypatch.setattr(venv, "_pcall", collect) 667 | with pytest.raises(ZeroDivisionError): 668 | venv.run_install_command(["qwe"], action=action) 669 | 670 | def test_pythonpath_remove(self, newmocksession, monkeypatch, caplog): 671 | monkeypatch.setenv("PYTHONPATH", "/my/awesome/library") 672 | mocksession = newmocksession( 673 | [], 674 | """\ 675 | [testenv:python] 676 | commands=abc 677 | """, 678 | ) 679 | venv = mocksession.getvenv("python") 680 | with mocksession.newaction(venv.name, "getenv") as action: 681 | venv.run_install_command(["qwe"], action=action) 682 | mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") 683 | 684 | pcalls = mocksession._pcalls 685 | assert len(pcalls) == 1 686 | assert "PYTHONPATH" not in pcalls[0].env 687 | 688 | def test_pythonpath_keep(self, newmocksession, monkeypatch, caplog): 689 | # passenv = PYTHONPATH allows PYTHONPATH to stay in environment 690 | monkeypatch.setenv("PYTHONPATH", "/my/awesome/library") 691 | mocksession = newmocksession( 692 | [], 693 | """\ 694 | [testenv:python] 695 | commands=abc 696 | passenv = PYTHONPATH 697 | """, 698 | ) 699 | venv = mocksession.getvenv("python") 700 | with mocksession.newaction(venv.name, "getenv") as action: 701 | venv.run_install_command(["qwe"], action=action) 702 | mocksession.report.not_expect("warning", "*Discarding $PYTHONPATH from environment*") 703 | assert "PYTHONPATH" in os.environ 704 | 705 | pcalls = mocksession._pcalls 706 | assert len(pcalls) == 1 707 | assert pcalls[0].env["PYTHONPATH"] == "/my/awesome/library" 708 | 709 | def test_pythonpath_empty(self, newmocksession, monkeypatch, caplog): 710 | monkeypatch.setenv("PYTHONPATH", "") 711 | mocksession = newmocksession( 712 | [], 713 | """\ 714 | [testenv:python] 715 | commands=abc 716 | """, 717 | ) 718 | venv = mocksession.getvenv("python") 719 | with mocksession.newaction(venv.name, "getenv") as action: 720 | venv.run_install_command(["qwe"], action=action) 721 | if sys.version_info < (3, 4): 722 | mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") 723 | else: 724 | with pytest.raises(AssertionError): 725 | mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") 726 | pcalls = mocksession._pcalls 727 | assert len(pcalls) == 1 728 | assert "PYTHONPATH" not in pcalls[0].env 729 | 730 | 731 | def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch): 732 | monkeypatch.delenv("PYTHONPATH", raising=False) 733 | pkg = tmpdir.ensure("package.tar.gz") 734 | monkeypatch.setenv("X123", "123") 735 | monkeypatch.setenv("YY", "456") 736 | config = newconfig( 737 | [], 738 | """\ 739 | [testenv:python] 740 | commands=python -V 741 | passenv = x123 742 | setenv = 743 | ENV_VAR = value 744 | PYTHONPATH = value 745 | """, 746 | ) 747 | mocksession._clearmocks() 748 | mocksession.new_config(config) 749 | venv = mocksession.getvenv("python") 750 | installpkg(venv, pkg) 751 | venv.test() 752 | 753 | pcalls = mocksession._pcalls 754 | assert len(pcalls) == 2 755 | for x in pcalls: 756 | env = x.env 757 | assert env is not None 758 | assert "ENV_VAR" in env 759 | assert env["ENV_VAR"] == "value" 760 | assert env["VIRTUAL_ENV"] == str(venv.path) 761 | assert env["X123"] == "123" 762 | assert "PYTHONPATH" in env 763 | assert env["PYTHONPATH"] == "value" 764 | # all env variables are passed for installation 765 | assert pcalls[0].env["YY"] == "456" 766 | assert "YY" not in pcalls[1].env 767 | 768 | assert {"ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", "X123", "PATH"}.issubset(pcalls[1].env) 769 | 770 | # setenv does not trigger PYTHONPATH warnings 771 | mocksession.report.not_expect("warning", "*Discarding $PYTHONPATH from environment*") 772 | 773 | # for e in os.environ: 774 | # assert e in env 775 | 776 | 777 | def test_installpkg_no_upgrade(tmpdir, newmocksession): 778 | pkg = tmpdir.ensure("package.tar.gz") 779 | mocksession = newmocksession([], "") 780 | venv = mocksession.getvenv("python") 781 | venv.just_created = True 782 | venv.envconfig.envdir.ensure(dir=1) 783 | installpkg(venv, pkg) 784 | pcalls = mocksession._pcalls 785 | assert len(pcalls) == 1 786 | assert pcalls[0].args[1:-1] == ["-m", "pip", "install", "--exists-action", "w"] 787 | 788 | 789 | @pytest.mark.parametrize("count, level", [(0, 0), (1, 0), (2, 0), (3, 1), (4, 2), (5, 3), (6, 3)]) 790 | def test_install_command_verbosity(tmpdir, newmocksession, count, level): 791 | pkg = tmpdir.ensure("package.tar.gz") 792 | mock_session = newmocksession(["-{}".format("v" * count)], "") 793 | env = mock_session.getvenv("python") 794 | env.just_created = True 795 | env.envconfig.envdir.ensure(dir=1) 796 | installpkg(env, pkg) 797 | pcalls = mock_session._pcalls 798 | assert len(pcalls) == 1 799 | expected = ["-m", "pip", "install", "--exists-action", "w"] + (["-v"] * level) 800 | assert pcalls[0].args[1:-1] == expected 801 | 802 | 803 | def test_installpkg_upgrade(newmocksession, tmpdir): 804 | pkg = tmpdir.ensure("package.tar.gz") 805 | mocksession = newmocksession([], "") 806 | venv = mocksession.getvenv("python") 807 | assert not hasattr(venv, "just_created") 808 | installpkg(venv, pkg) 809 | pcalls = mocksession._pcalls 810 | assert len(pcalls) == 1 811 | index = pcalls[0].args.index(pkg.basename) 812 | assert index >= 0 813 | assert "-U" in pcalls[0].args[:index] 814 | assert "--no-deps" in pcalls[0].args[:index] 815 | 816 | 817 | def test_run_install_command(newmocksession): 818 | mocksession = newmocksession([], "") 819 | venv = mocksession.getvenv("python") 820 | venv.just_created = True 821 | venv.envconfig.envdir.ensure(dir=1) 822 | with mocksession.newaction(venv.name, "hello") as action: 823 | venv.run_install_command(packages=["whatever"], action=action) 824 | pcalls = mocksession._pcalls 825 | assert len(pcalls) == 1 826 | args = pcalls[0].args 827 | assert py.path.local.sysfind("python") == args[0] 828 | assert ["-m", "pip"] == args[1:3] 829 | assert "install" in args 830 | env = pcalls[0].env 831 | assert env is not None 832 | 833 | 834 | def test_run_custom_install_command(newmocksession): 835 | mocksession = newmocksession( 836 | [], 837 | """\ 838 | [testenv] 839 | install_command=easy_install {opts} {packages} 840 | """, 841 | ) 842 | venv = mocksession.getvenv("python") 843 | venv.just_created = True 844 | venv.envconfig.envdir.ensure(dir=1) 845 | with mocksession.newaction(venv.name, "hello") as action: 846 | venv.run_install_command(packages=["whatever"], action=action) 847 | pcalls = mocksession._pcalls 848 | assert len(pcalls) == 1 849 | assert "easy_install" in pcalls[0].args[0] 850 | assert pcalls[0].args[1:] == ["whatever"] 851 | 852 | 853 | def test_command_relative_issue36(newmocksession, tmpdir, monkeypatch): 854 | mocksession = newmocksession( 855 | [], 856 | """\ 857 | [testenv] 858 | """, 859 | ) 860 | x = tmpdir.ensure("x") 861 | venv = mocksession.getvenv("python") 862 | x2 = venv.getcommandpath("./x", cwd=tmpdir) 863 | assert x == x2 864 | mocksession.report.not_expect("warning", "*test command found but not*") 865 | x3 = venv.getcommandpath("/bin/bash", cwd=tmpdir) 866 | assert x3 == "/bin/bash" 867 | mocksession.report.not_expect("warning", "*test command found but not*") 868 | monkeypatch.setenv("PATH", str(tmpdir)) 869 | x4 = venv.getcommandpath("x", cwd=tmpdir) 870 | assert x4.endswith(os.sep + "x") 871 | mocksession.report.expect("warning", "*test command found but not*") 872 | 873 | 874 | def test_ignore_outcome_failing_cmd(newmocksession): 875 | mocksession = newmocksession( 876 | [], 877 | """\ 878 | [testenv] 879 | commands=testenv_fail 880 | ignore_outcome=True 881 | """, 882 | ) 883 | 884 | venv = mocksession.getvenv("python") 885 | venv.test() 886 | assert venv.status == "ignored failed command" 887 | mocksession.report.expect("warning", "*command failed but result from testenv is ignored*") 888 | 889 | 890 | def test_tox_testenv_create(newmocksession): 891 | log = [] 892 | 893 | class Plugin: 894 | @tox.hookimpl 895 | def tox_testenv_create(self, action, venv): 896 | assert isinstance(action, tox.session.Action) 897 | assert isinstance(venv, VirtualEnv) 898 | log.append(1) 899 | 900 | @tox.hookimpl 901 | def tox_testenv_install_deps(self, action, venv): 902 | assert isinstance(action, tox.session.Action) 903 | assert isinstance(venv, VirtualEnv) 904 | log.append(2) 905 | 906 | mocksession = newmocksession( 907 | [], 908 | """\ 909 | [testenv] 910 | commands=testenv_fail 911 | ignore_outcome=True 912 | """, 913 | plugins=[Plugin()], 914 | ) 915 | 916 | venv = mocksession.getvenv("python") 917 | with mocksession.newaction(venv.name, "getenv") as action: 918 | venv.update(action=action) 919 | assert log == [1, 2] 920 | 921 | 922 | def test_tox_testenv_pre_post(newmocksession): 923 | log = [] 924 | 925 | class Plugin: 926 | @tox.hookimpl 927 | def tox_runtest_pre(self): 928 | log.append("started") 929 | 930 | @tox.hookimpl 931 | def tox_runtest_post(self): 932 | log.append("finished") 933 | 934 | mocksession = newmocksession( 935 | [], 936 | """\ 937 | [testenv] 938 | commands=testenv_fail 939 | """, 940 | plugins=[Plugin()], 941 | ) 942 | 943 | venv = mocksession.getvenv("python") 944 | venv.status = None 945 | assert log == [] 946 | runtestenv(venv, venv.envconfig.config) 947 | assert log == ["started", "finished"] 948 | 949 | 950 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 951 | def test_tox_testenv_interpret_shebang_empty_instance(tmpdir): 952 | testfile = tmpdir.join("check_shebang_empty_instance.py") 953 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 954 | 955 | # empty instance 956 | testfile.write("") 957 | args = prepend_shebang_interpreter(base_args) 958 | assert args == base_args 959 | 960 | 961 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 962 | def test_tox_testenv_interpret_shebang_empty_interpreter(tmpdir): 963 | testfile = tmpdir.join("check_shebang_empty_interpreter.py") 964 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 965 | 966 | # empty interpreter 967 | testfile.write("#!") 968 | args = prepend_shebang_interpreter(base_args) 969 | assert args == base_args 970 | 971 | 972 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 973 | def test_tox_testenv_interpret_shebang_empty_interpreter_ws(tmpdir): 974 | testfile = tmpdir.join("check_shebang_empty_interpreter_ws.py") 975 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 976 | 977 | # empty interpreter (whitespaces) 978 | testfile.write("#! \n") 979 | args = prepend_shebang_interpreter(base_args) 980 | assert args == base_args 981 | 982 | 983 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 984 | def test_tox_testenv_interpret_shebang_non_utf8(tmpdir): 985 | testfile = tmpdir.join("check_non_utf8.py") 986 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 987 | 988 | testfile.write_binary(b"#!\x9a\xef\x12\xaf\n") 989 | args = prepend_shebang_interpreter(base_args) 990 | assert args == base_args 991 | 992 | 993 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 994 | def test_tox_testenv_interpret_shebang_interpreter_simple(tmpdir): 995 | testfile = tmpdir.join("check_shebang_interpreter_simple.py") 996 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 997 | 998 | # interpreter (simple) 999 | testfile.write("#!interpreter") 1000 | args = prepend_shebang_interpreter(base_args) 1001 | assert args == ["interpreter"] + base_args 1002 | 1003 | 1004 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 1005 | def test_tox_testenv_interpret_shebang_interpreter_ws(tmpdir): 1006 | testfile = tmpdir.join("check_shebang_interpreter_ws.py") 1007 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 1008 | 1009 | # interpreter (whitespaces) 1010 | testfile.write("#! interpreter \n\n") 1011 | args = prepend_shebang_interpreter(base_args) 1012 | assert args == ["interpreter"] + base_args 1013 | 1014 | 1015 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 1016 | def test_tox_testenv_interpret_shebang_interpreter_arg(tmpdir): 1017 | testfile = tmpdir.join("check_shebang_interpreter_arg.py") 1018 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 1019 | 1020 | # interpreter with argument 1021 | testfile.write("#!interpreter argx\n") 1022 | args = prepend_shebang_interpreter(base_args) 1023 | assert args == ["interpreter", "argx"] + base_args 1024 | 1025 | 1026 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 1027 | def test_tox_testenv_interpret_shebang_interpreter_args(tmpdir): 1028 | testfile = tmpdir.join("check_shebang_interpreter_args.py") 1029 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 1030 | 1031 | # interpreter with argument (ensure single argument) 1032 | testfile.write("#!interpreter argx argx-part2\n") 1033 | args = prepend_shebang_interpreter(base_args) 1034 | assert args == ["interpreter", "argx argx-part2"] + base_args 1035 | 1036 | 1037 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 1038 | def test_tox_testenv_interpret_shebang_real(tmpdir): 1039 | testfile = tmpdir.join("check_shebang_real.py") 1040 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 1041 | 1042 | # interpreter (real example) 1043 | testfile.write("#!/usr/bin/env python\n") 1044 | args = prepend_shebang_interpreter(base_args) 1045 | assert args == ["/usr/bin/env", "python"] + base_args 1046 | 1047 | 1048 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") 1049 | def test_tox_testenv_interpret_shebang_long_example(tmpdir): 1050 | testfile = tmpdir.join("check_shebang_long_example.py") 1051 | base_args = [str(testfile), "arg1", "arg2", "arg3"] 1052 | 1053 | # interpreter (long example) 1054 | testfile.write( 1055 | "#!this-is-an-example-of-a-very-long-interpret-directive-what-should-" 1056 | "be-directly-invoked-when-tox-needs-to-invoked-the-provided-script-" 1057 | "name-in-the-argument-list" 1058 | ) 1059 | args = prepend_shebang_interpreter(base_args) 1060 | expected = [ 1061 | "this-is-an-example-of-a-very-long-interpret-directive-what-should-be-" 1062 | "directly-invoked-when-tox-needs-to-invoked-the-provided-script-name-" 1063 | "in-the-argument-list" 1064 | ] 1065 | 1066 | assert args == expected + base_args 1067 | 1068 | 1069 | @pytest.mark.parametrize("download", [True, False, None]) 1070 | def test_create_download(mocksession, newconfig, download): 1071 | config = newconfig( 1072 | [], 1073 | """\ 1074 | [testenv:env] 1075 | {} 1076 | """.format( 1077 | "download={}".format(download) if download else "" 1078 | ), 1079 | ) 1080 | mocksession.new_config(config) 1081 | venv = mocksession.getvenv("env") 1082 | with mocksession.newaction(venv.name, "getenv") as action: 1083 | tox_testenv_create(action=action, venv=venv) 1084 | pcalls = mocksession._pcalls 1085 | assert len(pcalls) >= 1 1086 | args = pcalls[0].args 1087 | # builtin venv does not support --no-download 1088 | if download or use_builtin_venv(venv): 1089 | assert "--no-download" not in map(str, args) 1090 | else: 1091 | assert "--no-download" in map(str, args) 1092 | mocksession._clearmocks() 1093 | -------------------------------------------------------------------------------- /tests/test_z_cmdline.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import shutil 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | import pathlib2 10 | import py 11 | import pytest 12 | 13 | import tox 14 | from tox.config import parseconfig 15 | from tox.reporter import Verbosity 16 | from tox.session import Session 17 | 18 | from tox_venv.hooks import use_builtin_venv 19 | 20 | pytest_plugins = "pytester" 21 | 22 | 23 | class TestSession: 24 | def test_log_pcall(self, mocksession): 25 | mocksession.logging_levels(quiet=Verbosity.DEFAULT, verbose=Verbosity.INFO) 26 | mocksession.config.logdir.ensure(dir=1) 27 | assert not mocksession.config.logdir.listdir() 28 | with mocksession.newaction("what", "something") as action: 29 | action.popen(["echo"]) 30 | match = mocksession.report.getnext("logpopen") 31 | log_name = py.path.local(match[1].split(">")[-1].strip()).relto( 32 | mocksession.config.logdir 33 | ) 34 | assert log_name == "what-0.log" 35 | 36 | def test_summary_status(self, initproj, capfd): 37 | initproj( 38 | "logexample123-0.5", 39 | filedefs={ 40 | "tests": {"test_hello.py": "def test_hello(): pass"}, 41 | "tox.ini": """ 42 | [testenv:hello] 43 | [testenv:world] 44 | """, 45 | }, 46 | ) 47 | config = parseconfig([]) 48 | session = Session(config) 49 | envs = list(session.venv_dict.values()) 50 | assert len(envs) == 2 51 | env1, env2 = envs 52 | env1.status = "FAIL XYZ" 53 | assert env1.status 54 | env2.status = 0 55 | assert not env2.status 56 | session._summary() 57 | out, err = capfd.readouterr() 58 | exp = "{}: FAIL XYZ".format(env1.envconfig.envname) 59 | assert exp in out 60 | exp = "{}: commands succeeded".format(env2.envconfig.envname) 61 | assert exp in out 62 | 63 | def test_getvenv(self, initproj): 64 | initproj( 65 | "logexample123-0.5", 66 | filedefs={ 67 | "tests": {"test_hello.py": "def test_hello(): pass"}, 68 | "tox.ini": """ 69 | [testenv:hello] 70 | [testenv:world] 71 | """, 72 | }, 73 | ) 74 | config = parseconfig([]) 75 | session = Session(config) 76 | venv1 = session.getvenv("hello") 77 | venv2 = session.getvenv("hello") 78 | assert venv1 is venv2 79 | venv1 = session.getvenv("world") 80 | venv2 = session.getvenv("world") 81 | assert venv1 is venv2 82 | with pytest.raises(LookupError): 83 | session.getvenv("qwe") 84 | 85 | 86 | def test_notoxini_help_still_works(initproj, cmd): 87 | initproj("example123-0.5", filedefs={"tests": {"test_hello.py": "def test_hello(): pass"}}) 88 | result = cmd("-h") 89 | msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" 90 | assert result.err == msg 91 | assert result.out.startswith("usage: ") 92 | assert any("--help" in l for l in result.outlines), result.outlines 93 | result.assert_success(is_run_test_env=False) 94 | 95 | 96 | def test_notoxini_help_ini_still_works(initproj, cmd): 97 | initproj("example123-0.5", filedefs={"tests": {"test_hello.py": "def test_hello(): pass"}}) 98 | result = cmd("--help-ini") 99 | assert any("setenv" in l for l in result.outlines), result.outlines 100 | result.assert_success(is_run_test_env=False) 101 | 102 | 103 | def test_envdir_equals_toxini_errors_out(cmd, initproj): 104 | initproj( 105 | "interp123-0.7", 106 | filedefs={ 107 | "tox.ini": """ 108 | [testenv] 109 | envdir={toxinidir} 110 | """ 111 | }, 112 | ) 113 | result = cmd() 114 | assert result.outlines[1] == "ERROR: ConfigError: envdir must not equal toxinidir" 115 | assert re.match( 116 | r"ERROR: venv \'python\' in .* would delete project", result.outlines[0] 117 | ), result.outlines[0] 118 | result.assert_fail() 119 | 120 | 121 | def test_envdir_would_delete_some_directory(cmd, initproj): 122 | projdir = initproj( 123 | "example-123", 124 | filedefs={ 125 | "tox.ini": """\ 126 | [tox] 127 | 128 | [testenv:venv] 129 | envdir=example 130 | commands= 131 | """ 132 | }, 133 | ) 134 | 135 | result = cmd("-e", "venv") 136 | assert projdir.join("example/__init__.py").exists() 137 | result.assert_fail() 138 | assert "cowardly refusing to delete `envdir`" in result.out 139 | 140 | 141 | def test_recreate(cmd, initproj): 142 | initproj("example-123", filedefs={"tox.ini": ""}) 143 | cmd("-e", "py", "--notest").assert_success() 144 | cmd("-r", "-e", "py", "--notest").assert_success() 145 | 146 | 147 | def test_run_custom_install_command_error(cmd, initproj): 148 | initproj( 149 | "interp123-0.5", 150 | filedefs={ 151 | "tox.ini": """ 152 | [testenv] 153 | install_command=./tox.ini {opts} {packages} 154 | """ 155 | }, 156 | ) 157 | result = cmd() 158 | result.assert_fail() 159 | re.match( 160 | r"ERROR: python: InvocationError for command .* \(exited with code \d+\)", 161 | result.outlines[-1], 162 | ), result.out 163 | 164 | 165 | def test_unknown_interpreter_and_env(cmd, initproj): 166 | initproj( 167 | "interp123-0.5", 168 | filedefs={ 169 | "tests": {"test_hello.py": "def test_hello(): pass"}, 170 | "tox.ini": """\ 171 | [testenv:python] 172 | basepython=xyz_unknown_interpreter 173 | [testenv] 174 | changedir=tests 175 | skip_install = true 176 | """, 177 | }, 178 | ) 179 | result = cmd() 180 | result.assert_fail() 181 | assert "ERROR: InterpreterNotFound: xyz_unknown_interpreter" in result.outlines 182 | 183 | result = cmd("-exyz") 184 | result.assert_fail() 185 | assert result.out == "ERROR: unknown environment 'xyz'\n" 186 | 187 | 188 | def test_unknown_interpreter_factor(cmd, initproj): 189 | initproj("py21", filedefs={"tox.ini": "[testenv]\nskip_install=true"}) 190 | result = cmd("-e", "py21") 191 | result.assert_fail() 192 | assert "ERROR: InterpreterNotFound: python2.1" in result.outlines 193 | 194 | 195 | def test_unknown_interpreter(cmd, initproj): 196 | initproj( 197 | "interp123-0.5", 198 | filedefs={ 199 | "tests": {"test_hello.py": "def test_hello(): pass"}, 200 | "tox.ini": """ 201 | [testenv:python] 202 | basepython=xyz_unknown_interpreter 203 | [testenv] 204 | changedir=tests 205 | """, 206 | }, 207 | ) 208 | result = cmd() 209 | result.assert_fail() 210 | assert any( 211 | "ERROR: InterpreterNotFound: xyz_unknown_interpreter" == l for l in result.outlines 212 | ), result.outlines 213 | 214 | 215 | def test_skip_platform_mismatch(cmd, initproj): 216 | initproj( 217 | "interp123-0.5", 218 | filedefs={ 219 | "tests": {"test_hello.py": "def test_hello(): pass"}, 220 | "tox.ini": """ 221 | [testenv] 222 | changedir=tests 223 | platform=x123 224 | """, 225 | }, 226 | ) 227 | result = cmd() 228 | result.assert_success() 229 | assert any( 230 | "SKIPPED: python: platform mismatch ({!r} does not match 'x123')".format(sys.platform) 231 | == l 232 | for l in result.outlines 233 | ), result.outlines 234 | 235 | 236 | def test_skip_unknown_interpreter(cmd, initproj): 237 | initproj( 238 | "interp123-0.5", 239 | filedefs={ 240 | "tests": {"test_hello.py": "def test_hello(): pass"}, 241 | "tox.ini": """ 242 | [testenv:python] 243 | basepython=xyz_unknown_interpreter 244 | [testenv] 245 | changedir=tests 246 | """, 247 | }, 248 | ) 249 | result = cmd("--skip-missing-interpreters") 250 | result.assert_success() 251 | msg = "SKIPPED: python: InterpreterNotFound: xyz_unknown_interpreter" 252 | assert any(msg == l for l in result.outlines), result.outlines 253 | 254 | 255 | def test_skip_unknown_interpreter_result_json(cmd, initproj, tmpdir): 256 | report_path = tmpdir.join("toxresult.json") 257 | initproj( 258 | "interp123-0.5", 259 | filedefs={ 260 | "tests": {"test_hello.py": "def test_hello(): pass"}, 261 | "tox.ini": """ 262 | [testenv:python] 263 | basepython=xyz_unknown_interpreter 264 | [testenv] 265 | changedir=tests 266 | """, 267 | }, 268 | ) 269 | result = cmd("--skip-missing-interpreters", "--result-json", report_path) 270 | result.assert_success() 271 | msg = "SKIPPED: python: InterpreterNotFound: xyz_unknown_interpreter" 272 | assert any(msg == l for l in result.outlines), result.outlines 273 | setup_result_from_json = json.load(report_path)["testenvs"]["python"]["setup"] 274 | for setup_step in setup_result_from_json: 275 | assert "InterpreterNotFound" in setup_step["output"] 276 | assert setup_step["retcode"] == 0 277 | 278 | 279 | def test_unknown_dep(cmd, initproj): 280 | initproj( 281 | "dep123-0.7", 282 | filedefs={ 283 | "tests": {"test_hello.py": "def test_hello(): pass"}, 284 | "tox.ini": """ 285 | [testenv] 286 | deps=qweqwe123 287 | changedir=tests 288 | """, 289 | }, 290 | ) 291 | result = cmd() 292 | result.assert_fail() 293 | assert result.outlines[-1].startswith("ERROR: python: could not install deps [qweqwe123];") 294 | 295 | 296 | def test_venv_special_chars_issue252(cmd, initproj): 297 | initproj( 298 | "pkg123-0.7", 299 | filedefs={ 300 | "tests": {"test_hello.py": "def test_hello(): pass"}, 301 | "tox.ini": """ 302 | [tox] 303 | envlist = special&&1 304 | [testenv:special&&1] 305 | changedir=tests 306 | """, 307 | }, 308 | ) 309 | result = cmd() 310 | result.assert_success() 311 | pattern = re.compile("special&&1 installed: .*pkg123==0.7.*") 312 | assert any(pattern.match(line) for line in result.outlines), result.outlines 313 | 314 | 315 | def test_unknown_environment(cmd, initproj): 316 | initproj("env123-0.7", filedefs={"tox.ini": ""}) 317 | result = cmd("-e", "qpwoei") 318 | result.assert_fail() 319 | assert result.out == "ERROR: unknown environment 'qpwoei'\n" 320 | 321 | 322 | def test_unknown_environment_with_envlist(cmd, initproj): 323 | initproj( 324 | "pkg123", 325 | filedefs={ 326 | "tox.ini": """ 327 | [tox] 328 | envlist = py{36,37}-django{20,21} 329 | """ 330 | }, 331 | ) 332 | result = cmd("-e", "py36-djagno21") 333 | result.assert_fail() 334 | assert result.out == "ERROR: unknown environment 'py36-djagno21'\n" 335 | 336 | 337 | def test_minimal_setup_py_empty(cmd, initproj): 338 | initproj( 339 | "pkg123-0.7", 340 | filedefs={ 341 | "tests": {"test_hello.py": "def test_hello(): pass"}, 342 | "setup.py": """ 343 | """, 344 | "tox.ini": "", 345 | }, 346 | ) 347 | result = cmd() 348 | result.assert_fail() 349 | assert result.outlines[-1] == "ERROR: setup.py is empty" 350 | 351 | 352 | def test_minimal_setup_py_comment_only(cmd, initproj): 353 | initproj( 354 | "pkg123-0.7", 355 | filedefs={ 356 | "tests": {"test_hello.py": "def test_hello(): pass"}, 357 | "setup.py": """\n# some comment 358 | 359 | """, 360 | "tox.ini": "", 361 | }, 362 | ) 363 | result = cmd() 364 | result.assert_fail() 365 | assert result.outlines[-1] == "ERROR: setup.py is empty" 366 | 367 | 368 | def test_minimal_setup_py_non_functional(cmd, initproj): 369 | initproj( 370 | "pkg123-0.7", 371 | filedefs={ 372 | "tests": {"test_hello.py": "def test_hello(): pass"}, 373 | "setup.py": """ 374 | import sys 375 | 376 | """, 377 | "tox.ini": "", 378 | }, 379 | ) 380 | result = cmd() 381 | result.assert_fail() 382 | assert any(re.match(r".*ERROR.*check setup.py.*", l) for l in result.outlines), result.outlines 383 | 384 | 385 | def test_sdist_fails(cmd, initproj): 386 | initproj( 387 | "pkg123-0.7", 388 | filedefs={ 389 | "tests": {"test_hello.py": "def test_hello(): pass"}, 390 | "setup.py": """ 391 | syntax error 392 | """, 393 | "tox.ini": "", 394 | }, 395 | ) 396 | result = cmd() 397 | result.assert_fail() 398 | assert any( 399 | re.match(r".*FAIL.*could not package project.*", l) for l in result.outlines 400 | ), result.outlines 401 | 402 | 403 | def test_no_setup_py_exits(cmd, initproj): 404 | initproj( 405 | "pkg123-0.7", 406 | filedefs={ 407 | "tox.ini": """ 408 | [testenv] 409 | commands=python -c "2 + 2" 410 | """ 411 | }, 412 | ) 413 | os.remove("setup.py") 414 | result = cmd() 415 | result.assert_fail() 416 | assert any( 417 | re.match(r".*ERROR.*No pyproject.toml or setup.py file found.*", l) 418 | for l in result.outlines 419 | ), result.outlines 420 | 421 | 422 | def test_no_setup_py_exits_but_pyproject_toml_does(cmd, initproj): 423 | initproj( 424 | "pkg123-0.7", 425 | filedefs={ 426 | "tox.ini": """ 427 | [testenv] 428 | commands=python -c "2 + 2" 429 | """ 430 | }, 431 | ) 432 | os.remove("setup.py") 433 | pathlib2.Path("pyproject.toml").touch() 434 | result = cmd() 435 | result.assert_fail() 436 | assert any( 437 | re.match(r".*ERROR.*pyproject.toml file found.*", l) for l in result.outlines 438 | ), result.outlines 439 | assert any( 440 | re.match(r".*To use a PEP 517 build-backend you are required to*", l) 441 | for l in result.outlines 442 | ), result.outlines 443 | 444 | 445 | def test_package_install_fails(cmd, initproj): 446 | initproj( 447 | "pkg123-0.7", 448 | filedefs={ 449 | "tests": {"test_hello.py": "def test_hello(): pass"}, 450 | "setup.py": """ 451 | from setuptools import setup 452 | setup( 453 | name='pkg123', 454 | description='pkg123 project', 455 | version='0.7', 456 | license='MIT', 457 | platforms=['unix', 'win32'], 458 | packages=['pkg123',], 459 | install_requires=['qweqwe123'], 460 | ) 461 | """, 462 | "tox.ini": "", 463 | }, 464 | ) 465 | result = cmd() 466 | result.assert_fail() 467 | assert result.outlines[-1].startswith("ERROR: python: InvocationError for command ") 468 | 469 | 470 | @pytest.fixture 471 | def example123(initproj): 472 | yield initproj( 473 | "example123-0.5", 474 | filedefs={ 475 | "tests": { 476 | "test_hello.py": """ 477 | def test_hello(pytestconfig): 478 | pass 479 | """ 480 | }, 481 | "tox.ini": """ 482 | [testenv] 483 | changedir=tests 484 | commands= pytest --basetemp={envtmpdir} \ 485 | --junitxml=junit-{envname}.xml 486 | deps=pytest 487 | """, 488 | }, 489 | ) 490 | 491 | 492 | def test_toxuone_env(cmd, example123): 493 | result = cmd() 494 | result.assert_success() 495 | assert re.match( 496 | r".*generated\W+xml\W+file.*junit-python\.xml" r".*\W+1\W+passed.*", result.out, re.DOTALL 497 | ) 498 | result = cmd("-epython") 499 | result.assert_success() 500 | assert re.match( 501 | r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", 502 | result.out, 503 | re.DOTALL, 504 | ) 505 | 506 | 507 | def test_different_config_cwd(cmd, example123): 508 | # see that things work with a different CWD 509 | with example123.dirpath().as_cwd(): 510 | result = cmd("-c", "example123/tox.ini") 511 | result.assert_success() 512 | assert re.match( 513 | r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", 514 | result.out, 515 | re.DOTALL, 516 | ) 517 | 518 | 519 | def test_result_json(cmd, initproj, example123): 520 | cwd = initproj( 521 | "example123", 522 | filedefs={ 523 | "tox.ini": """ 524 | [testenv] 525 | deps = setuptools 526 | commands_pre = python -c 'print("START")' 527 | commands = python -c 'print("OK")' 528 | - python -c 'print("1"); raise SystemExit(1)' 529 | python -c 'print("1"); raise SystemExit(2)' 530 | python -c 'print("SHOULD NOT HAPPEN")' 531 | commands_post = python -c 'print("END")' 532 | """ 533 | }, 534 | ) 535 | json_path = cwd / "res.json" 536 | result = cmd("--result-json", json_path) 537 | result.assert_fail() 538 | data = json.loads(json_path.read_text(encoding="utf-8")) 539 | 540 | assert data["reportversion"] == "1" 541 | assert data["toxversion"] == tox.__version__ 542 | 543 | for env_data in data["testenvs"].values(): 544 | for command_type in ("setup", "test"): 545 | if command_type not in env_data: 546 | assert False, "missing {}".format(command_type) 547 | for command in env_data[command_type]: 548 | assert isinstance(command["command"], list) 549 | # builtin venv creation does not have output 550 | if command["command"][1:] != ['-m', 'venv', 'python']: 551 | assert command["output"] 552 | assert "retcode" in command 553 | assert isinstance(command["retcode"], int) 554 | # virtualenv, deps install, package install, freeze 555 | assert len(env_data["setup"]) == 4 556 | # 1 pre + 3 command + 1 post 557 | assert len(env_data["test"]) == 5 558 | assert isinstance(env_data["installed_packages"], list) 559 | pyinfo = env_data["python"] 560 | assert isinstance(pyinfo["version_info"], list) 561 | assert pyinfo["version"] 562 | assert pyinfo["executable"] 563 | assert "write json report at: {}".format(json_path) == result.outlines[-1] 564 | 565 | 566 | def test_developz(initproj, cmd): 567 | initproj( 568 | "example123", 569 | filedefs={ 570 | "tox.ini": """ 571 | """ 572 | }, 573 | ) 574 | result = cmd("-vv", "--develop") 575 | result.assert_success() 576 | assert "sdist-make" not in result.out 577 | 578 | 579 | def test_usedevelop(initproj, cmd): 580 | initproj( 581 | "example123", 582 | filedefs={ 583 | "tox.ini": """ 584 | [testenv] 585 | usedevelop=True 586 | """ 587 | }, 588 | ) 589 | result = cmd("-vv") 590 | result.assert_success() 591 | assert "sdist-make" not in result.out 592 | 593 | 594 | def test_usedevelop_mixed(initproj, cmd): 595 | initproj( 596 | "example123", 597 | filedefs={ 598 | "tox.ini": """ 599 | [testenv:dev] 600 | usedevelop=True 601 | [testenv:nondev] 602 | usedevelop=False 603 | """ 604 | }, 605 | ) 606 | 607 | # running only 'dev' should not do sdist 608 | result = cmd("-vv", "-e", "dev") 609 | result.assert_success() 610 | assert "sdist-make" not in result.out 611 | 612 | # running all envs should do sdist 613 | result = cmd("-vv") 614 | result.assert_success() 615 | assert "sdist-make" in result.out 616 | 617 | 618 | @pytest.mark.parametrize("skipsdist", [False, True]) 619 | @pytest.mark.parametrize("src_root", [".", "src"]) 620 | def test_test_usedevelop(cmd, initproj, src_root, skipsdist): 621 | name = "example123-spameggs" 622 | base = initproj( 623 | (name, "0.5"), 624 | src_root=src_root, 625 | filedefs={ 626 | "tests": { 627 | "test_hello.py": """ 628 | def test_hello(pytestconfig): 629 | pass 630 | """ 631 | }, 632 | "tox.ini": """ 633 | [testenv] 634 | usedevelop=True 635 | changedir=tests 636 | commands= 637 | pytest --basetemp={envtmpdir} --junitxml=junit-{envname}.xml [] 638 | deps=pytest""" 639 | + """ 640 | skipsdist={} 641 | """.format( 642 | skipsdist 643 | ), 644 | }, 645 | ) 646 | result = cmd("-v") 647 | result.assert_success() 648 | assert re.match( 649 | r".*generated\W+xml\W+file.*junit-python\.xml" r".*\W+1\W+passed.*", result.out, re.DOTALL 650 | ) 651 | assert "sdist-make" not in result.out 652 | result = cmd("-epython") 653 | result.assert_success() 654 | assert "develop-inst-noop" in result.out 655 | assert re.match( 656 | r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", 657 | result.out, 658 | re.DOTALL, 659 | ) 660 | 661 | # see that things work with a different CWD 662 | with base.dirpath().as_cwd(): 663 | result = cmd("-c", "{}/tox.ini".format(name)) 664 | result.assert_success() 665 | assert "develop-inst-noop" in result.out 666 | assert re.match( 667 | r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", 668 | result.out, 669 | re.DOTALL, 670 | ) 671 | 672 | # see that tests can also fail and retcode is correct 673 | testfile = py.path.local("tests").join("test_hello.py") 674 | assert testfile.check() 675 | testfile.write("def test_fail(): assert 0") 676 | result = cmd() 677 | result.assert_fail() 678 | assert "develop-inst-noop" in result.out 679 | assert re.match( 680 | r".*\W+1\W+failed.*" r"summary.*" r"python:\W+commands\W+failed.*", result.out, re.DOTALL 681 | ) 682 | 683 | # test develop is called if setup.py changes 684 | setup_py = py.path.local("setup.py") 685 | setup_py.write(setup_py.read() + " ") 686 | result = cmd() 687 | result.assert_fail() 688 | assert "develop-inst-nodeps" in result.out 689 | 690 | 691 | def test_warning_emitted(cmd, initproj): 692 | initproj( 693 | "spam-0.0.1", 694 | filedefs={ 695 | "tox.ini": """ 696 | [testenv] 697 | skipsdist=True 698 | usedevelop=True 699 | """, 700 | "setup.py": """ 701 | from setuptools import setup 702 | from warnings import warn 703 | warn("I am a warning") 704 | 705 | setup(name="spam", version="0.0.1") 706 | """, 707 | }, 708 | ) 709 | cmd() 710 | result = cmd() 711 | assert "develop-inst-noop" in result.out 712 | assert "I am a warning" in result.err 713 | 714 | 715 | def test_alwayscopy(initproj, cmd, mocksession): 716 | initproj( 717 | "example123", 718 | filedefs={ 719 | "tox.ini": """ 720 | [testenv] 721 | commands={envpython} --version 722 | alwayscopy=True 723 | """ 724 | }, 725 | ) 726 | venv = mocksession.getvenv("python") 727 | result = cmd("-vv") 728 | result.assert_success() 729 | if use_builtin_venv(venv): 730 | assert "venv --copies" in result.out 731 | else: 732 | assert "virtualenv --always-copy" in result.out 733 | 734 | 735 | def test_alwayscopy_default(initproj, cmd, mocksession): 736 | initproj( 737 | "example123", 738 | filedefs={ 739 | "tox.ini": """ 740 | [testenv] 741 | commands={envpython} --version 742 | """ 743 | }, 744 | ) 745 | venv = mocksession.getvenv("python") 746 | result = cmd("-vv") 747 | result.assert_success() 748 | if use_builtin_venv(venv): 749 | assert "venv --copies" not in result.out 750 | else: 751 | assert "virtualenv --always-copy" not in result.out 752 | 753 | 754 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no echo on Windows") 755 | def test_empty_activity_ignored(initproj, cmd): 756 | initproj( 757 | "example123", 758 | filedefs={ 759 | "tox.ini": """ 760 | [testenv] 761 | list_dependencies_command=echo 762 | commands={envpython} --version 763 | """ 764 | }, 765 | ) 766 | result = cmd() 767 | result.assert_success() 768 | assert "installed:" not in result.out 769 | 770 | 771 | @pytest.mark.skipif("sys.platform == 'win32'", reason="no echo on Windows") 772 | def test_empty_activity_shown_verbose(initproj, cmd): 773 | initproj( 774 | "example123", 775 | filedefs={ 776 | "tox.ini": """ 777 | [testenv] 778 | list_dependencies_command=echo 779 | commands={envpython} --version 780 | whitelist_externals = echo 781 | """ 782 | }, 783 | ) 784 | result = cmd("-v") 785 | result.assert_success() 786 | assert "installed:" in result.out 787 | 788 | 789 | def test_test_piphelp(initproj, cmd): 790 | initproj( 791 | "example123", 792 | filedefs={ 793 | "tox.ini": """ 794 | # content of: tox.ini 795 | [testenv] 796 | commands=pip -h 797 | """ 798 | }, 799 | ) 800 | result = cmd("-vv") 801 | result.assert_success() 802 | 803 | 804 | def test_notest(initproj, cmd): 805 | initproj( 806 | "example123", 807 | filedefs={ 808 | "tox.ini": """\ 809 | # content of: tox.ini 810 | [testenv:py26] 811 | basepython={} 812 | """.format( 813 | sys.executable 814 | ) 815 | }, 816 | ) 817 | result = cmd("-v", "--notest") 818 | result.assert_success() 819 | assert re.match(r".*summary.*" r"py26\W+skipped\W+tests.*", result.out, re.DOTALL) 820 | result = cmd("-v", "--notest", "-epy26") 821 | result.assert_success() 822 | assert re.match(r".*py26\W+reusing.*", result.out, re.DOTALL) 823 | 824 | 825 | def test_notest_setup_py_error(initproj, cmd): 826 | initproj( 827 | "example123", 828 | filedefs={ 829 | "setup.py": """\ 830 | from setuptools import setup 831 | setup(name='x', install_requires=['fakefakefakefakefakefake']), 832 | """, 833 | "tox.ini": "", 834 | }, 835 | ) 836 | result = cmd("--notest") 837 | result.assert_fail() 838 | assert re.search("ERROR:.*InvocationError", result.out) 839 | 840 | 841 | def test_devenv(initproj, cmd): 842 | initproj( 843 | "example123", 844 | filedefs={ 845 | "setup.py": """\ 846 | from setuptools import setup 847 | setup(name='x') 848 | """, 849 | "tox.ini": """\ 850 | [tox] 851 | # envlist is ignored for --devenv 852 | envlist = foo,bar,baz 853 | 854 | [testenv] 855 | # --devenv implies --notest 856 | commands = python -c "exit(1)" 857 | """, 858 | }, 859 | ) 860 | result = cmd("--devenv", "venv") 861 | result.assert_success() 862 | # `--devenv` defaults to the `py` environment and a develop install 863 | assert "py develop-inst:" in result.out 864 | assert re.search("py create:.*venv", result.out) 865 | 866 | 867 | def test_devenv_does_not_allow_multiple_environments(initproj, cmd): 868 | initproj( 869 | "example123", 870 | filedefs={ 871 | "setup.py": """\ 872 | from setuptools import setup 873 | setup(name='x') 874 | """, 875 | "tox.ini": """\ 876 | [tox] 877 | envlist=foo,bar,baz 878 | """, 879 | }, 880 | ) 881 | 882 | result = cmd("--devenv", "venv", "-e", "foo,bar") 883 | result.assert_fail() 884 | assert result.err == "ERROR: --devenv requires only a single -e\n" 885 | 886 | 887 | def test_devenv_does_not_delete_project(initproj, cmd): 888 | initproj( 889 | "example123", 890 | filedefs={ 891 | "setup.py": """\ 892 | from setuptools import setup 893 | setup(name='x') 894 | """, 895 | "tox.ini": """\ 896 | [tox] 897 | envlist=foo,bar,baz 898 | """, 899 | }, 900 | ) 901 | 902 | result = cmd("--devenv", "") 903 | result.assert_fail() 904 | assert "would delete project" in result.out 905 | assert "ERROR: ConfigError: envdir must not equal toxinidir" in result.out 906 | 907 | 908 | def test_PYC(initproj, cmd, monkeypatch): 909 | initproj("example123", filedefs={"tox.ini": ""}) 910 | monkeypatch.setenv("PYTHONDOWNWRITEBYTECODE", "1") 911 | result = cmd("-v", "--notest") 912 | result.assert_success() 913 | assert "create" in result.out 914 | 915 | 916 | def test_env_VIRTUALENV_PYTHON(initproj, cmd, monkeypatch): 917 | initproj("example123", filedefs={"tox.ini": ""}) 918 | monkeypatch.setenv("VIRTUALENV_PYTHON", "/FOO") 919 | result = cmd("-v", "--notest") 920 | result.assert_success() 921 | assert "create" in result.out 922 | 923 | 924 | def test_setup_prints_non_ascii(initproj, cmd): 925 | initproj( 926 | "example123", 927 | filedefs={ 928 | "setup.py": """\ 929 | import sys 930 | getattr(sys.stdout, 'buffer', sys.stdout).write(b'\\xe2\\x98\\x83\\n') 931 | 932 | import setuptools 933 | setuptools.setup(name='example123') 934 | """, 935 | "tox.ini": "", 936 | }, 937 | ) 938 | result = cmd("--notest") 939 | result.assert_success() 940 | assert "create" in result.out 941 | 942 | 943 | def test_envsitepackagesdir(cmd, initproj): 944 | initproj( 945 | "pkg512-0.0.5", 946 | filedefs={ 947 | "tox.ini": """ 948 | [testenv] 949 | commands= 950 | python -c "print(r'X:{envsitepackagesdir}')" 951 | """ 952 | }, 953 | ) 954 | result = cmd() 955 | result.assert_success() 956 | assert re.match(r".*\nX:.*tox.*site-packages.*", result.out, re.DOTALL) 957 | 958 | 959 | def test_envsitepackagesdir_skip_missing_issue280(cmd, initproj): 960 | initproj( 961 | "pkg513-0.0.5", 962 | filedefs={ 963 | "tox.ini": """ 964 | [testenv] 965 | basepython=/usr/bin/qwelkjqwle 966 | commands= 967 | {envsitepackagesdir} 968 | """ 969 | }, 970 | ) 971 | result = cmd("--skip-missing-interpreters") 972 | result.assert_success() 973 | assert re.match(r".*SKIPPED:.*qwelkj.*", result.out, re.DOTALL) 974 | 975 | 976 | @pytest.mark.parametrize("verbosity", ["", "-v", "-vv"]) 977 | def test_verbosity(cmd, initproj, verbosity): 978 | initproj( 979 | "pkgX-0.0.5", 980 | # Note: This is related to https://github.com/tox-dev/tox#935 981 | # For some reason, the .egg-info/ directory is interacting with the 982 | # PYTHONPATH on Appveyor, causing the package to *not* be installed 983 | # since pip already thinks it is. By setting the `src_root`, we can 984 | # avoid the issue. 985 | src_root="src", 986 | filedefs={ 987 | "tox.ini": """ 988 | [testenv] 989 | """ 990 | }, 991 | ) 992 | result = cmd(verbosity) 993 | result.assert_success() 994 | 995 | needle = "Successfully installed pkgX-0.0.5" 996 | if verbosity == "-vv": 997 | assert any(needle in line for line in result.outlines), result.outlines 998 | else: 999 | assert all(needle not in line for line in result.outlines), result.outlines 1000 | 1001 | 1002 | def test_envtmpdir(initproj, cmd): 1003 | initproj( 1004 | "foo", 1005 | filedefs={ 1006 | # This file first checks that envtmpdir is existent and empty. Then it 1007 | # creates an empty file in that directory. The tox command is run 1008 | # twice below, so this is to test whether the directory is cleared 1009 | # before the second run. 1010 | "check_empty_envtmpdir.py": """if True: 1011 | import os 1012 | from sys import argv 1013 | envtmpdir = argv[1] 1014 | assert os.path.exists(envtmpdir) 1015 | assert os.listdir(envtmpdir) == [] 1016 | open(os.path.join(envtmpdir, 'test'), 'w').close() 1017 | """, 1018 | "tox.ini": """ 1019 | [testenv] 1020 | commands=python check_empty_envtmpdir.py {envtmpdir} 1021 | """, 1022 | }, 1023 | ) 1024 | 1025 | result = cmd() 1026 | result.assert_success() 1027 | 1028 | result = cmd() 1029 | result.assert_success() 1030 | 1031 | 1032 | def test_missing_env_fails(initproj, cmd): 1033 | initproj("foo", filedefs={"tox.ini": "[testenv:foo]\ncommands={env:VAR}"}) 1034 | result = cmd() 1035 | result.assert_fail() 1036 | assert result.out.endswith( 1037 | "foo: unresolvable substitution(s): 'VAR'." 1038 | " Environment variables are missing or defined recursively.\n" 1039 | ) 1040 | 1041 | 1042 | def test_tox_console_script(initproj): 1043 | initproj("help", filedefs={"tox.ini": ""}) 1044 | result = subprocess.check_call(["tox", "--help"]) 1045 | assert result == 0 1046 | 1047 | 1048 | def test_tox_quickstart_script(initproj): 1049 | initproj("help", filedefs={"tox.ini": ""}) 1050 | result = subprocess.check_call(["tox-quickstart", "--help"]) 1051 | assert result == 0 1052 | 1053 | 1054 | def test_tox_cmdline_no_args(monkeypatch, initproj): 1055 | initproj("help", filedefs={"tox.ini": ""}) 1056 | monkeypatch.setattr(sys, "argv", ["caller_script", "--help"]) 1057 | with pytest.raises(SystemExit): 1058 | tox.cmdline() 1059 | 1060 | 1061 | def test_tox_cmdline_args(initproj): 1062 | initproj("help", filedefs={"tox.ini": ""}) 1063 | with pytest.raises(SystemExit): 1064 | tox.cmdline(["caller_script", "--help"]) 1065 | 1066 | 1067 | @pytest.mark.parametrize("exit_code", [0, 6]) 1068 | def test_exit_code(initproj, cmd, exit_code, mocker): 1069 | """ Check for correct InvocationError, with exit code, 1070 | except for zero exit code """ 1071 | import tox.exception 1072 | 1073 | mocker.spy(tox.exception, "exit_code_str") 1074 | tox_ini_content = "[testenv:foo]\ncommands=python -c 'import sys; sys.exit({:d})'".format( 1075 | exit_code 1076 | ) 1077 | initproj("foo", filedefs={"tox.ini": tox_ini_content}) 1078 | cmd() 1079 | if exit_code: 1080 | # need mocker.spy above 1081 | assert tox.exception.exit_code_str.call_count == 1 1082 | (args, kwargs) = tox.exception.exit_code_str.call_args 1083 | assert kwargs == {} 1084 | (call_error_name, call_command, call_exit_code) = args 1085 | assert call_error_name == "InvocationError" 1086 | # quotes are removed in result.out 1087 | # do not include "python" as it is changed to python.EXE by appveyor 1088 | expected_command_arg = " -c 'import sys; sys.exit({:d})'".format(exit_code) 1089 | assert expected_command_arg in call_command 1090 | assert call_exit_code == exit_code 1091 | else: 1092 | # need mocker.spy above 1093 | assert tox.exception.exit_code_str.call_count == 0 1094 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27,py35,py36,py37,py38,pypy 4 | coverage,isort,lint 5 | 6 | [testenv] 7 | commands = pytest {posargs:tests} 8 | setenv = 9 | PYTHONDONTWRITEBYTECODE=1 10 | deps = 11 | tox[testing]>=3.14.4 12 | 13 | [testenv:coverage] 14 | commands = coverage run -m pytest {posargs:tests} 15 | usedevelop = True 16 | deps = 17 | {[testenv]deps} 18 | coverage 19 | 20 | 21 | ; Don't lint tests, since they're mostly copied from tox 22 | [testenv:isort] 23 | commands = isort --check-only --recursive src {posargs} 24 | deps = 25 | isort 26 | 27 | [testenv:lint] 28 | commands = flake8 src {posargs} 29 | deps = 30 | flake8 31 | flake8-bugbear 32 | flake8-commas 33 | flake8-quotes 34 | --------------------------------------------------------------------------------