Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parallel invocation of tox environments #439 #1102

Merged
merged 27 commits into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
494b147
implement parallel invocation of tox environments #439
gaborbernat Dec 2, 2018
8a1567b
report failed environments output
gaborbernat Jan 2, 2019
f4ddfb2
allow to control degree of parallel similar to pytest-xdist
gaborbernat Jan 2, 2019
aa944ac
Add progress spinner to parallel and dependencies
gaborbernat Jan 3, 2019
c008a2c
Document parallel mode
gaborbernat Jan 3, 2019
36c6cab
expose detox as a tox -p all
gaborbernat Jan 3, 2019
958c94b
fix upload, report in Azure
gaborbernat Jan 4, 2019
406a565
add spinner
gaborbernat Jan 5, 2019
726bab5
try improve tests
gaborbernat Jan 8, 2019
f15f9cc
fix spinner tests
gaborbernat Jan 8, 2019
529f978
add graph tests
gaborbernat Jan 8, 2019
a660045
stable order for the spinner
gaborbernat Jan 8, 2019
70b3cbe
stable order for the graph circle detection
gaborbernat Jan 8, 2019
95bc123
Windows has no atty cursor disable control characters
gaborbernat Jan 8, 2019
78488da
default auto for detox
gaborbernat Jan 8, 2019
c0c35c3
use != for string comparision
gaborbernat Jan 8, 2019
9661832
move changelog documentation into the main documentation, note depend…
gaborbernat Jan 8, 2019
af84b54
explicitly set expected for spinner tests
gaborbernat Jan 8, 2019
917e24a
add test that spinner works in background
gaborbernat Jan 8, 2019
3ccf390
declare unicode files
gaborbernat Jan 8, 2019
1092d6e
tests for parallel with dependencies and parallel config
gaborbernat Jan 9, 2019
d0de9ab
add detox test
gaborbernat Jan 9, 2019
bbdb6a3
no cover for exoteric process getters
gaborbernat Jan 9, 2019
2a965aa
colors to report
gaborbernat Jan 9, 2019
7b97d3a
Merge branch 'master' into parallel
gaborbernat Jan 11, 2019
0da3bc7
remove detox alias :-) as it does not adhere to detox interface
gaborbernat Jan 11, 2019
2ea0546
use isolated build for parallel tests
gaborbernat Jan 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/439.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Parallel mode added (effectively ``detox`` now is merged into tox), for more details see :ref:`parallel_mode` - by :user:`gaborbernat`.
3 changes: 2 additions & 1 deletion docs/changelog/template.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
{% if definitions[category]['showcontent'] %}

{% for text, values in sections[section][category].items() %}
- {{ text }} ({{ values|join(', ') }})
- {{ text }}
{{ values|join(',\n ') }}
{% endfor %}

{% else %}
Expand Down
20 changes: 20 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,26 @@ Global settings are defined under the ``tox`` section as:
Name of the virtual environment used to create a source distribution from the
source tree.

.. conf:: parallel_show_output ^ bool ^ false

.. versionadded:: 3.7.0

If set to True the content of the output will always be shown when running in parallel mode.

.. conf:: depends ^ comma separated values

.. versionadded:: 3.7.0

tox environments this depends on. tox will try to run all dependent environments before running this
environment. Format is same as :conf:`envlist` (allows factor usage).

.. warning::

``depends`` does not pull in dependencies into the run target, for example if you select ``py27,py36,coverage``
via the ``-e`` tox will only run those three (even if ``coverage`` may specify as ``depends`` other targets too -
such as ``py27, py35, py36, py37``).


Jenkins override
++++++++++++++++

Expand Down
55 changes: 55 additions & 0 deletions docs/example/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,58 @@ meant exactly for that purpose, by setting the ``alwayscopy`` directive in your

[testenv]
alwayscopy = True

.. _`parallel_mode`:

Parallel mode
-------------
``tox`` allows running environments in parallel:

- Invoke by using the ``--parallel`` or ``-p`` flag. After the packaging phase completes tox will run in parallel
processes tox environments (spins a new instance of the tox interpreter, but passes through all host flags and
environment variables).
- ``-p`` takes an argument specifying the degree of parallelization:

- ``all`` to run all invoked environments in parallel,
- ``auto`` to limit it to CPU count,
- or pass an integer to set that limit.
- Parallel mode displays a progress spinner while running tox environments in parallel, and reports outcome of
these as soon as completed with a human readable duration timing attached.
- Parallel mode by default shows output only of failed environments and ones marked as :conf:`parallel_show_output`
``=True``.
- There's now a concept of dependency between environments (specified via :conf:`depends`), tox will re-order the
environment list to be run to satisfy these dependencies (in sequential run too). Furthermore, in parallel mode,
will only schedule a tox environment to run once all of its dependencies finished (independent of their outcome).

.. warning::

``depends`` does not pull in dependencies into the run target, for example if you select ``py27,py36,coverage``
via the ``-e`` tox will only run those three (even if ``coverage`` may specify as ``depends`` other targets too -
such as ``py27, py35, py36, py37``).

- ``--parallel-live``/``-o`` allows showing the live output of the standard output and error, also turns off reporting
described above.
- ``python -m detox`` or ``detox`` is now provided as an alias to ``tox -p all``.
- Note: parallel evaluation disables standard input. Use non parallel invocation if you need standard input.

Example final output:

.. code-block:: bash

$ tox -e py27,py36,coverage -p all
✔ OK py36 in 9.533 seconds
✔ OK py27 in 9.96 seconds
✔ OK coverage in 2.0 seconds
___________________________ summary ______________________________________________________
py27: commands succeeded
py36: commands succeeded
coverage: commands succeeded
congratulations :)


Example progress bar, showing a rotating spinner, the number of environments running and their list (limited up to \
120 characters):

.. code-block:: bash

⠹ [2] py27 | py36
9 changes: 8 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
author_email=" tox-dev@python.org",
packages=find_packages("src"),
package_dir={"": "src"},
entry_points={"console_scripts": ["tox=tox:cmdline", "tox-quickstart=tox._quickstart:main"]},
entry_points={
"console_scripts": [
"tox=tox:cmdline",
"tox-quickstart=tox._quickstart:main",
"detox=detox:run",
]
},
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
install_requires=[
"setuptools >= 30.0.0",
Expand All @@ -40,6 +46,7 @@
setup_requires=["setuptools-scm>2, <4"], # readthedocs needs it
extras_require={
"testing": [
"freezegun >= 0.3.11",
"pytest >= 3.0.0, <4",
"pytest-cov >= 2.5.1, <3",
"pytest-mock >= 1.10.0, <2",
Expand Down
12 changes: 12 additions & 0 deletions src/detox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import absolute_import, unicode_literals

from tox import cmdline
from tox.config import parallel


def run():
parallel.DEFAULT_PARALLEL = "auto"
cmdline()


__all__ = ("run",)
6 changes: 6 additions & 0 deletions src/detox/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import absolute_import, unicode_literals

from . import run

if __name__ == "__main__":
run()
27 changes: 23 additions & 4 deletions src/tox/config.py → src/tox/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import tox
from tox.constants import INFO
from tox.interpreters import Interpreters, NoInterpreterInfo
from .parallel import add_parallel_flags, ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY, add_parallel_config

hookimpl = tox.hookimpl
"""DEPRECATED - REMOVE - this is left for compatibility with plugins importing this from here.
Expand Down Expand Up @@ -59,7 +60,13 @@ class Parser:
"""Command line and ini-parser control object."""

def __init__(self):
self.argparser = argparse.ArgumentParser(description="tox options", add_help=False)
class HelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
def __init__(self, prog):
super(HelpFormatter, self).__init__(prog, max_help_position=35, width=190)

self.argparser = argparse.ArgumentParser(
description="tox options", add_help=False, prog="tox", formatter_class=HelpFormatter
)
self._testenv_attr = []

def add_argument(self, *args, **kwargs):
Expand Down Expand Up @@ -274,7 +281,9 @@ def parse_cli(args, pm):
print(get_version_info(pm))
raise SystemExit(0)
interpreters = Interpreters(hook=pm.hook)
config = Config(pluginmanager=pm, option=option, interpreters=interpreters, parser=parser)
config = Config(
pluginmanager=pm, option=option, interpreters=interpreters, parser=parser, args=args
)
return config, option


Expand Down Expand Up @@ -413,6 +422,7 @@ def tox_addoption(parser):
dest="sdistonly",
help="only perform the sdist packaging activity.",
)
add_parallel_flags(parser)
parser.add_argument(
"--parallel--safe-build",
action="store_true",
Expand Down Expand Up @@ -799,6 +809,8 @@ def develop(testenv_config, value):
help="list of extras to install with the source distribution or develop install",
)

add_parallel_config(parser)


def cli_skip_missing_interpreter(parser):
class SkipMissingInterpreterAction(argparse.Action):
Expand All @@ -822,7 +834,7 @@ def __call__(self, parser, namespace, values, option_string=None):
class Config(object):
"""Global Tox config object."""

def __init__(self, pluginmanager, option, interpreters, parser):
def __init__(self, pluginmanager, option, interpreters, parser, args):
self.envconfigs = OrderedDict()
"""Mapping envname -> envconfig"""
self.invocationcwd = py.path.local()
Expand All @@ -831,6 +843,7 @@ def __init__(self, pluginmanager, option, interpreters, parser):
self.option = option
self._parser = parser
self._testenv_attr = parser._testenv_attr
self.args = args

"""option namespace containing all parsed command line options"""

Expand Down Expand Up @@ -1040,7 +1053,7 @@ def __init__(self, config, ini_path, ini_data): # noqa
# factors stated in config envlist
stated_envlist = reader.getstring("envlist", replace=False)
if stated_envlist:
for env in _split_env(stated_envlist):
for env in config.envlist:
known_factors.update(env.split("-"))

# configure testenvs
Expand Down Expand Up @@ -1119,6 +1132,9 @@ def make_envconfig(self, name, section, subs, config, replace=True):
res = reader.getlist(env_attr.name, sep=" ")
elif atype == "line-list":
res = reader.getlist(env_attr.name, sep="\n")
elif atype == "env-list":
res = reader.getstring(env_attr.name, replace=False)
res = tuple(_split_env(res))
else:
raise ValueError("unknown type {!r}".format(atype))
if env_attr.postprocess:
Expand All @@ -1133,6 +1149,7 @@ def make_envconfig(self, name, section, subs, config, replace=True):

def _getenvdata(self, reader, config):
candidates = (
os.environ.get(PARALLEL_ENV_VAR_KEY),
self.config.option.env,
os.environ.get("TOXENV"),
reader.getstring("envlist", replace=False),
Expand Down Expand Up @@ -1167,6 +1184,8 @@ def _getenvdata(self, reader, config):

def _split_env(env):
"""if handed a list, action="append" was used for -e """
if env is None:
return []
if not isinstance(env, list):
env = [e.split("#", 1)[0].strip() for e in env.split("\n")]
env = ",".join([e for e in env if e])
Expand Down
77 changes: 77 additions & 0 deletions src/tox/config/parallel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import absolute_import, unicode_literals

from argparse import ArgumentTypeError

ENV_VAR_KEY = "TOX_PARALLEL_ENV"
OFF_VALUE = 0
DEFAULT_PARALLEL = OFF_VALUE


def auto_detect_cpus():
try:
from os import sched_getaffinity # python 3 only

def cpu_count():
return len(sched_getaffinity(0))

except ImportError:
# python 2 options
try:
from os import cpu_count
except ImportError:
from multiprocessing import cpu_count

try:
n = cpu_count()
except NotImplementedError: # pragma: no cov
n = None # pragma: no cov
return n if n else 1


def parse_num_processes(s):
if s == "all":
return None
if s == "auto":
return auto_detect_cpus()
else:
value = int(s)
if value < 0:
raise ArgumentTypeError("value must be positive")
return value


def add_parallel_flags(parser):
parser.add_argument(
"-p",
"--parallel",
dest="parallel",
help="run tox environments in parallel, the argument controls limit: all,"
" auto - cpu count, some positive number, zero is turn off",
action="store",
type=parse_num_processes,
default=DEFAULT_PARALLEL,
metavar="VAL",
)
parser.add_argument(
"-o",
"--parallel-live",
action="store_true",
dest="parallel_live",
help="connect to stdout while running environments",
)


def add_parallel_config(parser):
parser.add_testenv_attribute(
"depends",
type="env-list",
help="tox environments that this environment depends on (must be run after those)",
)

parser.add_testenv_attribute(
"parallel_show_output",
type="bool",
default=False,
help="if set to True the content of the output will always be shown "
"when running in parallel mode",
)
Loading