├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── detox ├── __init__.py ├── __main__.py ├── cli.py ├── proc.py └── tox_proclimit.py ├── setup.cfg ├── setup.py ├── tests ├── conftest.py ├── test_detox.py └── test_main.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /.cache/ 3 | /.tox/ 4 | /detox.egg-info/ 5 | **/__pycache__/ 6 | build/* 7 | dist/* 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.19 4 | 5 | - as tox added a parallel mode in 3.7 detox will only work with older versions of tox now so the installation requires a version of tox < 3.7 6 | - mark repository as unmaintained 7 | - add a warning at the end of the testrun hinting the user to try it without detox and a version of tox > 3.6 8 | 9 | ## 0.18 10 | 11 | Note that usedevelop still does not seem to be fixed (PRs welcome) 12 | 13 | - integrate usable fixes from stale PR7 14 | - (fix [#20](https://github.com/tox-dev/detox/issues/20) detox 15 | ignores/breaks usedevelop - by Kendall Chuang 16 | - (fix [#21](https://github.com/tox-dev/detox/issues/21) 17 | detox doesn't support skipsdist config option - by Kendall Chuang 18 | - convert changelog to markdown and render as part of description on PyPI 19 | - detox version now prints own version and then what tox has to say 20 | (it's a plugin after all and that should be made clear) 21 | - [Internal changes] 22 | - add extra dependencies in setup.py 23 | - update tests to current pytest API 24 | - use black for code formatting 25 | - use flake8 for linting 26 | - add descriptions to tox envs 27 | - add a "dev" tox env 28 | 29 | ## 0.17 (botched release) 30 | ## 0.16 (not released) 31 | 32 | ## 0.15 33 | 34 | - (fix [#23](https://github.com/tox-dev/detox/issues/23)) do not 35 | swallow exceptions - by @vlaci 36 | - (fix [#16](https://github.com/tox-dev/detox/issues/16)) use tox 37 | quiet level to make detox quiet - by Oliver Bestwalter 38 | 39 | ## 0.14.post3 40 | 41 | - and now the markdown description might even be rendered 42 | 43 | ## 0.14.post2 44 | 45 | - *sigh* replace hardcoded long description with actual content of 46 | `README.md` 47 | 48 | ## 0.14.post1 49 | 50 | - propagate information about new location of issie tracker to PyPI 51 | 52 | ## 0.14.0 53 | 54 | - (fix [#15](https://github.com/tox-dev/detox/issues/15)) make detox 55 | aware of new way to fetch a package in tox 3.3 - by Oliver 56 | Bestwalter 57 | - (fix [#15](https://github.com/tox-dev/detox/issues/15)) make detox 58 | aware of new way to fetch a package in tox 3.3 - by Oliver 59 | Bestwalter 60 | - (fix [#25](https://github.com/tox-dev/detox/issues/25)) print out 61 | detox version rather than tox version including detox version as 62 | plugin, when invoking [detox --version]{.title-ref} - by Oliver 63 | Bestwalter 64 | 65 | ## 0.13.0 66 | 67 | - (fix [#283](https://github.com/tox-dev/tox/issues/283)) detox 68 | creates virtualenvs repeatedly and unnecessarily - by Thomas Steinke 69 | 70 | ## 0.12.0 71 | 72 | - (fix [#792](https://github.com/tox-dev/tox/issues/792)) bump tox 73 | version constraint to <4.0 - by Pi Delport 74 | - support and test with Python 2.7, 3.4+ - by Miro Hrončok 75 | - fix project url to point ot github - by Neil Halelamien 76 | - remove some unused imports - by Nir Soffer 77 | 78 | ## 0.11.0 79 | 80 | - #406: Add support for running detox as python -m detox Thanks André 81 | Caron (@AndreLouisCaron). 82 | - (infrastructure) add Travis CI setup. Thanks Timothée Mazzucotelli 83 | (@Pawamoy). 84 | - add "-n NUMPROC" option to set number of processes. The default is 85 | the number of CPUs as determined by multiprocessing.cpu_count() or 86 | "2" if the call does not work (e.g. on py27/windows). Thanks 87 | Timothée Mazzucotelli (@Pawamoy). 88 | 89 | ## 0.10.0 90 | 91 | 92 | - get compatible again to tox-2.0 93 | 94 | ## 0.9.4 95 | 96 | - get compatible again to eventlet by avoiding to import 97 | eventlet.processes, thanks Takeshi Komiya for the PR. 98 | - make detox honor skipsdist. Thanks Timoth Messier for the PR. 99 | - change license to MIT 100 | 101 | ## 0.9.3 102 | 103 | - fix issue6: quickly make detox work with tox-1.6 again (although not 104 | all 1.6 features supported, e.g. --develop does not work) 105 | - fix issue3: don't claim a TROVE identifier of "python3" because 106 | detox itself depends on eventlet which does not work on py3 yet. 107 | (Nevertheless detox will create py3 environments through tox of 108 | course) 109 | - fix issue1: support python2.5 again (although we might drop it in 110 | the future -- it's enough of tox/detox can _[create]() and handle 111 | py25 environments, they don't neccessarily need to support running 112 | themselv on py25) 113 | 114 | ## 0.9.2 115 | 116 | - fix issue4 - fail properly if sdist-packaging fails 117 | 118 | ## 0.9.1 119 | 120 | - fix issue5 - small adjustments to work with latest tox-1.4.3 version 121 | 122 | ## 0.9 123 | 124 | - initial release 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. 19 | 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include setup.py 3 | include tox.ini 4 | include LICENSE 5 | graft tests 6 | 7 | global-exclude __pycache__ 8 | global-exclude *.py[cod] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Project Status: Unsupported – The project has reached a stable, usable state but the author(s) have ceased all work on it.](https://www.repostatus.org/badges/latest/unsupported.svg)](https://www.repostatus.org/#unsupported) 2 | 3 | # detox is unmaintained and incompatible with tox > 3.6 4 | 5 | `detox` was a plugin for [`tox`](https://pypi.org/project/tox/) to enable parallel environment execution. `tox` 3.7 added a native possibility to do this (`tox -p|--parallel`) and effectively supercedes detox. 6 | 7 | --- 8 | 9 | [![Build Status](https://travis-ci.org/tox-dev/detox.svg?branch=master)](https://travis-ci.org/tox-dev/detox) 10 | [![Latest Version on PyPI](https://badge.fury.io/py/detox.svg)](https://badge.fury.io/py/detox) 11 | [![Supported Python Versions](https://img.shields.io/pypi/pyversions/detox.svg)](https://pypi.org/project/detox/) 12 | [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 13 | 14 | # What is detox? 15 | 16 | detox is the distributed version of [tox](https://pypi.org/project/tox/). It makes efficient use of multiple CPUs by running all possible activities in parallel. It has the same options and configuration that tox has so after installation can just run: 17 | 18 | detox 19 | 20 | in the same way and with the same options with which you would run `tox`, see the [tox home page](http://tox.readthedocs.io) for more info. 21 | 22 | Additionally, detox offers a `-n` or `--num` option to set the number of concurrent processes to use. 23 | 24 | **NOTE** due to the concurrent execution of the testenvs the output of the different testruns is not printed to the terminal. Instead they are logged into separate files inside the `log` directories of the testenvs. 25 | -------------------------------------------------------------------------------- /detox/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.19" 2 | -------------------------------------------------------------------------------- /detox/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from detox.cli import main 4 | 5 | # Enable ``python -m detox ...``. 6 | if __name__ == "__main__": 7 | sys.exit(main()) 8 | -------------------------------------------------------------------------------- /detox/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | 5 | from tox.session import prepare as tox_prepare 6 | 7 | from detox import __version__ 8 | from detox.proc import Detox 9 | 10 | 11 | def main(args=None): 12 | args = sys.argv[1:] if args is None else args 13 | if args and args[0] == "--version": 14 | print("detox {} running as plugin in tox:".format(__version__)) 15 | # fall through to let tox add its own version info ... 16 | config = tox_prepare(args) 17 | detox = Detox(config) 18 | if not hasattr(config.option, "quiet_level") or not config.option.quiet_level: 19 | detox.startloopreport() 20 | ret = detox.runtestsmulti(detox.toxsession.evaluated_env_list()) 21 | print("### WARNING ###\n\n" 22 | "detox is not compatible with versions of tox > 3.6." 23 | "Consider uninstalling detox and upgrading tox to >= 3.7" 24 | "to use its parallel mode.") 25 | return ret 26 | -------------------------------------------------------------------------------- /detox/proc.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import eventlet 4 | from eventlet.timeout import Timeout 5 | from eventlet.green.subprocess import Popen 6 | from eventlet import GreenPool 7 | import tox 8 | import tox.session 9 | 10 | 11 | def timelimited(secs, func): 12 | if secs is not None: 13 | with Timeout(secs): 14 | return func() 15 | return func() 16 | 17 | 18 | class FileSpinner: 19 | chars = r"- \ | / - \ | /".split() 20 | 21 | def __init__(self): 22 | self.path2last = {} 23 | 24 | def getchar(self, path): 25 | try: 26 | lastsize, charindex = self.path2last[path] 27 | except KeyError: 28 | lastsize, charindex = 0, 0 29 | newsize = 0 if not path else path.size() 30 | if newsize != lastsize: 31 | charindex += 1 32 | self.path2last[path] = (lastsize, charindex) 33 | return self.chars[charindex % len(self.chars)] 34 | 35 | 36 | class ToxReporter(tox.session.Reporter): 37 | sortorder = ( 38 | "runtests command installdeps installpkg inst inst-nodeps " 39 | "sdist-make create recreate".split() 40 | ) 41 | 42 | def __init__(self, session): 43 | super(ToxReporter, self).__init__(session) 44 | self._actionmayfinish = set() 45 | 46 | def _loopreport(self): 47 | filespinner = FileSpinner() 48 | while 1: 49 | eventlet.sleep(0.2) 50 | msg = [] 51 | ac2popenlist = {} 52 | for action in self.session._actions: 53 | for popen in action._popenlist: 54 | if popen.poll() is None: 55 | ol = ac2popenlist.setdefault(action.activity, []) 56 | ol.append(popen) 57 | if not action._popenlist and action in self._actionmayfinish: 58 | super(ToxReporter, self).logaction_finish(action) 59 | self._actionmayfinish.remove(action) 60 | 61 | for acname in self.sortorder: 62 | try: 63 | popenlist = ac2popenlist.pop(acname) 64 | except KeyError: 65 | continue 66 | sublist = [] 67 | for popen in popenlist: 68 | name = getattr(popen.action.venv, "name", "INLINE") 69 | char = filespinner.getchar(popen.outpath) 70 | sublist.append("%s%s" % (name, char)) 71 | msg.append("%s %s" % (acname, " ".join(sublist))) 72 | assert not ac2popenlist, ac2popenlist 73 | if msg: 74 | msg = " ".join(msg) 75 | if len(msg) >= self.tw.fullwidth: 76 | msg = msg[: self.tw.fullwidth - 3] + ".." 77 | self.tw.reline(msg) 78 | 79 | def __getattr__(self, name): 80 | if name[0] == "_": 81 | raise AttributeError(name) 82 | 83 | def generic_report(*args): 84 | self._calls.append((name,) + args) 85 | if self.config.option.verbosity >= 2: 86 | print("%s" % (self._calls[-1],)) 87 | 88 | return generic_report 89 | 90 | def logaction_finish(self, action): 91 | if action._popenlist: 92 | # defer finishing output to spinner loop 93 | self._actionmayfinish.add(action) 94 | else: 95 | super(ToxReporter, self).logaction_finish(action) 96 | 97 | 98 | class Detox: 99 | def __init__(self, toxconfig): 100 | self._toxconfig = toxconfig 101 | self._resources = Resources(self) 102 | self._sdistpath = None 103 | self._toxsession = None 104 | 105 | def startloopreport(self): 106 | if self.toxsession.report.tw.hasmarkup: 107 | eventlet.spawn_n(self.toxsession.report._loopreport) 108 | 109 | @property 110 | def toxsession(self): 111 | if not self._toxsession: 112 | self._toxsession = tox.session.Session( 113 | self._toxconfig, Report=ToxReporter, popen=Popen 114 | ) 115 | return self._toxsession 116 | 117 | def provide_sdist(self): 118 | try: 119 | sdistpath = self.toxsession.get_installpkg_path() # tox < 3.3 120 | except AttributeError: 121 | from tox.package import get_package 122 | 123 | sdistpath = get_package(self.toxsession) 124 | if not sdistpath: 125 | raise SystemExit(1) 126 | return sdistpath 127 | 128 | def provide_venv(self, venvname): 129 | venv = self.toxsession.getvenv(venvname) 130 | if self.toxsession.setupenv(venv): 131 | return venv 132 | 133 | def provide_installpkg(self, venvname, sdistpath): 134 | venv = self.toxsession.getvenv(venvname) 135 | return self.toxsession.installpkg(venv, sdistpath) 136 | 137 | def provide_developpkg(self, venvname): 138 | venv = self.toxsession.getvenv(venvname) 139 | return self.toxsession.developpkg(venv, self.toxsession.config.setupdir) 140 | 141 | def runtests(self, venvname): 142 | if self.toxsession.config.option.sdistonly: 143 | self._sdistpath = self.getresources("sdist") 144 | return 145 | if self.toxsession.config.skipsdist: 146 | venv, = self.getresources("venv:%s" % venvname) 147 | if venv: 148 | venv.finish() 149 | self.toxsession.runtestenv(venv, redirect=True) 150 | else: 151 | venv, sdist = self.getresources("venv:%s" % venvname, "sdist") 152 | if venv and sdist: 153 | # tox >= 3.5 returns a tuple, the first one is the session package 154 | if isinstance(sdist, tuple): 155 | sdist = sdist[0] 156 | if self.toxsession.installpkg(venv, sdist): 157 | self.toxsession.runtestenv(venv, redirect=True) 158 | 159 | def runtestsmulti(self, envlist): 160 | pool = GreenPool(size=self._toxconfig.option.numproc) 161 | threads = [] 162 | for env in envlist: 163 | threads.append(pool.spawn(self.runtests, env)) 164 | 165 | for t in threads: 166 | # re-raises any exceptions of the worker thread 167 | t.wait() 168 | if not self.toxsession.config.option.sdistonly: 169 | retcode = self._toxsession._summary() 170 | return retcode 171 | 172 | def getresources(self, *specs): 173 | return self._resources.getresources(*specs) 174 | 175 | 176 | class Resources: 177 | def __init__(self, providerbase): 178 | self._providerbase = providerbase 179 | self._spec2thread = {} 180 | self._pool = GreenPool() 181 | self._resources = {} 182 | 183 | def _dispatchprovider(self, spec): 184 | parts = spec.split(":") 185 | name = parts.pop(0) 186 | provider = getattr(self._providerbase, "provide_" + name) 187 | self._resources[spec] = res = provider(*parts) 188 | return res 189 | 190 | def getresources(self, *specs): 191 | for spec in specs: 192 | if spec not in self._resources: 193 | if spec not in self._spec2thread: 194 | t = self._pool.spawn(self._dispatchprovider, spec) 195 | self._spec2thread[spec] = t 196 | resources = [] 197 | for spec in specs: 198 | if spec not in self._resources: 199 | self._spec2thread[spec].wait() 200 | resources.append(self._resources[spec]) 201 | return resources 202 | -------------------------------------------------------------------------------- /detox/tox_proclimit.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import multiprocessing 3 | 4 | from tox import hookimpl 5 | 6 | 7 | @hookimpl 8 | def tox_addoption(parser): 9 | def positive_integer(value): 10 | ivalue = int(value) 11 | if ivalue <= 0: 12 | raise argparse.ArgumentTypeError("{} must be greater 0".format(value)) 13 | return ivalue 14 | 15 | try: 16 | num_proc = multiprocessing.cpu_count() 17 | except Exception: 18 | num_proc = 2 19 | parser.add_argument( 20 | "-n", 21 | "--num", 22 | type=positive_integer, 23 | action="store", 24 | default=num_proc, 25 | dest="numproc", 26 | help="set the number of concurrent processes " "(default {}).".format(num_proc), 27 | ) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [devpi:upload] 5 | formats = sdist.tgz,bdist_wheel 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | from setuptools import setup 3 | 4 | 5 | def make_long_description(): 6 | with io.open("README.md", encoding='UTF-8') as f: 7 | readme = f.read() 8 | with io.open("CHANGELOG", encoding='UTF-8') as f: 9 | changelog = f.read() 10 | return readme + "\n\n" + changelog 11 | 12 | 13 | setup( 14 | name="detox", 15 | description="distributing activities of the tox tool", 16 | long_description=make_long_description(), 17 | long_description_content_type="text/markdown", 18 | version="0.19", # Note: keep in sync with detox/__init__.py 19 | url="https://github.com/tox-dev/detox", 20 | license="MIT", 21 | platforms=["unix", "linux", "osx", "cygwin", "win32"], 22 | author="holger krekel", 23 | author_email="holger@merlinux.eu", 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Framework :: tox", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: POSIX", 30 | "Operating System :: Microsoft :: Windows", 31 | "Operating System :: MacOS :: MacOS X", 32 | "Programming Language :: Python :: 2.7", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Topic :: Software Development :: Testing", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Utilities", 40 | "Programming Language :: Python", 41 | ], 42 | packages=["detox"], 43 | install_requires=["tox>=3.5,<3.7", "py>=1.4.27", "eventlet>=0.15.0"], 44 | extras_require={"lint": ["black", "flake8"], "dev": ["pytest >= 3.8"]}, 45 | entry_points={ 46 | "console_scripts": "detox=detox.cli:main", 47 | "tox": ["proclimit = detox.tox_proclimit"], 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement, print_function 2 | 3 | import sys 4 | import time 5 | 6 | import eventlet 7 | import py 8 | import pytest 9 | from eventlet.green.subprocess import Popen 10 | from textwrap import dedent as d 11 | 12 | from detox.proc import Detox 13 | from detox.cli import main as detox_main, tox_prepare 14 | 15 | pytest_plugins = "pytester" 16 | 17 | 18 | def create_example1(tmpdir): 19 | tmpdir.join("setup.py").write( 20 | d( 21 | """ 22 | from setuptools import setup 23 | 24 | def main(): 25 | setup( 26 | name='example1', 27 | description='example1 project for testing detox', 28 | version='0.4', 29 | packages=['example1',], 30 | ) 31 | if __name__ == '__main__': 32 | main() 33 | """ 34 | ) 35 | ) 36 | tmpdir.join("tox.ini").write( 37 | d( 38 | """ 39 | [testenv:py] 40 | """ 41 | ) 42 | ) 43 | tmpdir.join("example1", "__init__.py").ensure() 44 | 45 | 46 | def create_example2(tmpdir): 47 | tmpdir.join("tox.ini").write( 48 | d( 49 | """ 50 | [tox] 51 | skipsdist = True 52 | 53 | [testenv:py] 54 | """ 55 | ) 56 | ) 57 | tmpdir.join("example2", "__init__.py").ensure() 58 | 59 | 60 | def create_example3(tmpdir): 61 | tmpdir.join("tox.ini").write( 62 | d( 63 | """ 64 | [tox] 65 | skipsdist = True 66 | 67 | [testenv] 68 | commands = python -c 'import time; time.sleep(1)' 69 | 70 | [testenv:py1] 71 | [testenv:py2] 72 | """ 73 | ) 74 | ) 75 | tmpdir.join("example3", "__init__.py").ensure() 76 | 77 | 78 | def pytest_configure(config): 79 | config.addinivalue_line("markers", "example1: use example1 for setup") 80 | config.addinivalue_line("markers", "example2: use example2 for setup") 81 | config.addinivalue_line( 82 | "markers", 83 | "timeout(N): stop test function " "after N seconds, throwing a Timeout.", 84 | ) 85 | 86 | 87 | @pytest.fixture 88 | def exampledir(request, tmpdir): 89 | for x in dir(request.function): 90 | if x.startswith("example"): 91 | exampledir = tmpdir.mkdir(x) 92 | globals()["create_" + x](exampledir) 93 | print("%s created at %s" % (x, exampledir)) 94 | break 95 | else: 96 | raise request.LookupError("test function has example") 97 | return exampledir 98 | 99 | 100 | @pytest.fixture 101 | def detox(exampledir): 102 | old = exampledir.chdir() 103 | try: 104 | return Detox(tox_prepare([])) 105 | finally: 106 | old.chdir() 107 | 108 | 109 | @pytest.fixture 110 | def cmd(request, exampledir): 111 | cmd = Cmd(exampledir, request) 112 | return cmd 113 | 114 | 115 | class Cmd: 116 | def __init__(self, basedir, request): 117 | self.basedir = basedir 118 | self.tmpdir = basedir.mkdir(".cmdtmp") 119 | self.request = request 120 | 121 | def main(self, *args): 122 | self.basedir.chdir() 123 | return detox_main(args) 124 | 125 | def rundetox(self, *args): 126 | self.basedir.chdir() 127 | script = py.path.local.sysfind("detox") 128 | assert script, "could not find 'detox' script" 129 | return self._run(script, *args) 130 | 131 | def _run(self, *cmdargs): 132 | from _pytest.pytester import RunResult, getdecoded 133 | 134 | cmdargs = [str(x) for x in cmdargs] 135 | p1 = self.tmpdir.join("stdout") 136 | p2 = self.tmpdir.join("stderr") 137 | print("running", cmdargs, "curdir=", py.path.local()) 138 | f1 = p1.open("wb") 139 | f2 = p2.open("wb") 140 | now = time.time() 141 | popen = Popen( 142 | cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") 143 | ) 144 | ret = popen.wait() 145 | f1.close() 146 | f2.close() 147 | out = p1.read("rb") 148 | out = getdecoded(out).splitlines() 149 | err = p2.read("rb") 150 | err = getdecoded(err).splitlines() 151 | 152 | def dump_lines(lines, fp): 153 | try: 154 | for line in lines: 155 | print(line, file=fp) 156 | except UnicodeEncodeError: 157 | print("couldn't print to %s because of encoding" % (fp,)) 158 | 159 | dump_lines(out, sys.stdout) 160 | dump_lines(err, sys.stderr) 161 | return RunResult(ret, out, err, time.time() - now) 162 | 163 | 164 | @pytest.fixture(autouse=True) 165 | def with_timeout(request): 166 | marker = request.node.get_closest_marker("timeout") 167 | timeout = marker.args[0] if marker else 5.0 168 | with eventlet.Timeout(timeout): 169 | yield 170 | 171 | 172 | def test_hang(testdir): 173 | p = py.path.local(__file__).dirpath("conftest.py") 174 | p.copy(testdir.tmpdir.join(p.basename)) 175 | testdir.makepyfile( 176 | """ 177 | import pytest 178 | from eventlet.green import time 179 | @pytest.mark.timeout(0.01) 180 | def test_hang(): 181 | time.sleep(3.0) 182 | """ 183 | ) 184 | result = testdir.runpytest() 185 | assert "failed to timeout" not in result.stdout.str() 186 | result.stdout.fnmatch_lines(["*Timeout: 0.01*"]) 187 | -------------------------------------------------------------------------------- /tests/test_detox.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import pytest 4 | import eventlet 5 | 6 | from detox.proc import Resources 7 | 8 | 9 | class TestResources: 10 | def test_getresources(self): 11 | x = [] 12 | 13 | class Provider: 14 | def provide_abc(self): 15 | x.append(1) 16 | return 42 17 | 18 | resources = Resources(Provider()) 19 | res, = resources.getresources("abc") 20 | assert res == 42 21 | assert len(x) == 1 22 | res, = resources.getresources("abc") 23 | assert len(x) == 1 24 | assert res == 42 25 | 26 | def test_getresources_param(self): 27 | class Provider: 28 | def provide_abc(self, param): 29 | return param 30 | 31 | resources = Resources(Provider()) 32 | res, = resources.getresources("abc:123") 33 | return res == "123" 34 | 35 | def test_getresources_parallel(self): 36 | x = [] 37 | 38 | class Provider: 39 | def provide_abc(self): 40 | x.append(1) 41 | return 42 42 | 43 | resources = Resources(Provider()) 44 | pool = eventlet.GreenPool(2) 45 | pool.spawn(lambda: resources.getresources("abc")) 46 | pool.spawn(lambda: resources.getresources("abc")) 47 | pool.waitall() 48 | assert len(x) == 1 49 | 50 | def test_getresources_multi(self): 51 | x = [] 52 | 53 | class Provider: 54 | def provide_abc(self): 55 | x.append(1) 56 | return 42 57 | 58 | def provide_def(self): 59 | x.append(1) 60 | return 23 61 | 62 | resources = Resources(Provider()) 63 | a, d = resources.getresources("abc", "def") 64 | assert a == 42 65 | assert d == 23 66 | 67 | 68 | class TestDetoxExample1: 69 | pytestmark = [pytest.mark.example1, pytest.mark.timeout(20)] 70 | 71 | def test_createsdist(self, detox): 72 | sdists, = detox.getresources("sdist") 73 | for sdist in sdists: 74 | assert sdist.check() 75 | 76 | def test_getvenv(self, detox): 77 | venv, = detox.getresources("venv:py") 78 | assert venv.envconfig.envdir.check() 79 | venv2, = detox.getresources("venv:py") 80 | assert venv == venv2 81 | 82 | def test_test(self, detox): 83 | detox.runtests("py") 84 | 85 | 86 | class TestDetoxExample2: 87 | pytestmark = [pytest.mark.example2, pytest.mark.timeout(20)] 88 | 89 | def test_test(self, detox): 90 | detox.runtests("py") 91 | 92 | def test_developpkg(self, detox): 93 | detox.getresources("venv:py") 94 | developpkg, = detox.getresources("developpkg:py") 95 | assert developpkg is False 96 | 97 | 98 | class TestCmdline: 99 | pytestmark = [pytest.mark.example1] 100 | 101 | @pytest.mark.timeout(20) 102 | def test_runtests(self, cmd): 103 | result = cmd.rundetox("-e", "py", "-v", "-v") 104 | result.stdout.fnmatch_lines(["py*getenv*", "py*create:*"]) 105 | 106 | 107 | class TestProcLimitOption: 108 | pytestmark = [pytest.mark.example3] 109 | 110 | def test_runtestmulti(self): 111 | class MyConfig: 112 | class MyOption: 113 | numproc = 7 114 | 115 | option = MyOption() 116 | 117 | x = [] 118 | 119 | def MyGreenPool(**kw): 120 | x.append(kw) 121 | # Building a Detox object will already call GreenPool(), 122 | # so we have to let MyGreenPool being called twice before raise 123 | if len(x) == 2: 124 | raise ValueError 125 | 126 | from detox import proc 127 | 128 | setattr(proc, "GreenPool", MyGreenPool) 129 | with pytest.raises(ValueError): 130 | d = proc.Detox(MyConfig()) 131 | d.runtestsmulti(["env1", "env2", "env3"]) # Fake env list 132 | 133 | assert x[0] == {} # When building Detox object 134 | assert x[1] == {"size": 7} # When calling runtestsmulti 135 | 136 | @pytest.mark.timeout(60) 137 | def test_runtests(self, cmd): 138 | now1 = datetime.now() 139 | cmd.rundetox("-n", "1", "-epy1,py2") 140 | then1 = datetime.now() 141 | delta1 = then1 - now1 142 | assert delta1 >= timedelta(seconds=2) 143 | 144 | now2 = datetime.now() 145 | cmd.rundetox("--num", "2", "-epy1,py2") 146 | then2 = datetime.now() 147 | delta2 = then2 - now2 148 | assert delta2 >= timedelta(seconds=1) 149 | 150 | assert delta1 >= delta2, "pool size=2 took much time than pool size=1" 151 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import subprocess 5 | import sys 6 | 7 | 8 | def invoke(command, success_codes=(0,)): 9 | try: 10 | output = subprocess.check_output(command, stderr=subprocess.STDOUT) 11 | status = 0 12 | except subprocess.CalledProcessError as error: 13 | output = error.output 14 | status = error.returncode 15 | output = output.decode("utf-8") 16 | if status not in success_codes: 17 | raise Exception( 18 | 'Command %r return exit code %d and output: """%s""".' 19 | % (command, status, output) 20 | ) 21 | return status, output 22 | 23 | 24 | def test_run_as_module(): 25 | """Can be run as `python -m detox ...`.""" 26 | status, output = invoke([sys.executable, "-m", "detox", "--help"]) 27 | assert status == 0 28 | assert output.startswith("usage:") 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.0 3 | envlist = py27,py34,py35,py36,py37,lint 4 | 5 | [flake8] 6 | max-line-length = 89 7 | 8 | [testenv:lint] 9 | extras = lint 10 | commands = 11 | black -v . 12 | flake8 13 | 14 | [testenv] 15 | description = test project with {basepython} 16 | extras = dev 17 | commands = pytest {posargs} 18 | 19 | [testenv:tox-master] 20 | deps = 21 | -e git://github.com/tox-dev/tox#egg=tox 22 | 23 | [testenv:dev] 24 | description = create a development environment with all necessities 25 | extras = 26 | lint 27 | dev 28 | usedevelop = True 29 | commands = 30 | python --version 31 | detox {posargs:--version} 32 | --------------------------------------------------------------------------------