Skip to content

Commit

Permalink
Allow to run multiple tox instances in parallel #849 (#934)
Browse files Browse the repository at this point in the history
At the moment the build phase is not thread safe. Running multiple
instances of tox will highly likely cause one of the instances
to fail as the two processes will step on each others toe while
creating the sdist package. This is especially annoying in CI
environments where you want to run tox targets in parallle (e..g
Jenkins).

By passing in ``--parallel--safe-build`` flag tox automatically
generates a unique dist folder for the current build. This way
each build can use it's own version built package (in the install
phase after the build), and we avoid the need to lock while
building. Once the tox session finishes remove such build folders
to avoid ever expanding source trees when using this feature.
  • Loading branch information
gaborbernat authored Aug 9, 2018
1 parent e557705 commit 75fdd74
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 3 deletions.
2 changes: 2 additions & 0 deletions changelog/849.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow to run multiple tox instances in parallel by providing the
```--parallel--safe-build`` flag. - by :user:`gaborbernat`
23 changes: 23 additions & 0 deletions doc/example/jenkins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,27 @@ Linux as a limit of 128). There are two methods to workaround this issue:
:ref:`long interpreter directives` for more information).


Running tox environments in parallel
------------------------------------

Jenkins has parallel stages allowing you to run commands in parallel, however tox package
building it is not parallel safe. Use the ``--parallel--safe-build`` flag to enable parallel safe
builds. Here's a generic stage definition demonstrating this:

.. code-block:: groovy
stage('run tox envs') {
steps {
script {
def envs = sh(returnStdout: true, script: "tox -l").trim().split('\n')
def cmds = envs.collectEntries({ tox_env ->
[tox_env, {
sh "tox --parallel--safe-build -vve $tox_env"
}]
})
parallel(cmds)
}
}
}
.. include:: ../links.rst
49 changes: 48 additions & 1 deletion src/tox/_pytestplugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import print_function, unicode_literals

import os
import sys
import textwrap
import time
from fnmatch import fnmatch
Expand All @@ -10,10 +11,11 @@
import six

import tox
from tox import venv
from tox.config import parseconfig
from tox.result import ResultLog
from tox.session import Session, main
from tox.venv import VirtualEnv
from tox.venv import CreationConfig, VirtualEnv, getdigest

mark_dont_run_on_windows = pytest.mark.skipif(os.name == "nt", reason="non windows test")
mark_dont_run_on_posix = pytest.mark.skipif(os.name == "posix", reason="non posix test")
Expand Down Expand Up @@ -70,7 +72,16 @@ def run(*argv):
key = str(b"PYTHONPATH")
python_paths = (i for i in (str(os.getcwd()), os.getenv(key)) if i)
monkeypatch.setenv(key, os.pathsep.join(python_paths))

with RunResult(capfd, argv) as result:
prev_run_command = Session.runcommand

def run_command(self):
result.session = self
return prev_run_command(self)

monkeypatch.setattr(Session, "runcommand", run_command)

try:
main([str(x) for x in argv])
assert False # this should always exist with SystemExit
Expand All @@ -91,6 +102,7 @@ def __init__(self, capfd, args):
self.duration = None
self.out = None
self.err = None
self.session = None

def __enter__(self):
self._start = time.time()
Expand Down Expand Up @@ -373,3 +385,38 @@ def create_files(base, filedefs):
elif isinstance(value, six.string_types):
s = textwrap.dedent(value)
base.join(key).write(s)


@pytest.fixture()
def mock_venv(monkeypatch):
"""This creates a mock virtual environment (e.g. will inherit the current interpreter).
Note: because we inherit, to keep things sane you must call the py environment and only that;
and cannot install any packages. """

class ProxyCurrentPython:
@classmethod
def readconfig(cls, path):
assert path.dirname.endswith("{}py".format(os.sep))
return CreationConfig(
md5=getdigest(sys.executable),
python=sys.executable,
version=tox.__version__,
sitepackages=False,
usedevelop=False,
deps=[],
alwayscopy=False,
)

monkeypatch.setattr(CreationConfig, "readconfig", ProxyCurrentPython.readconfig)

def venv_lookup(venv, name):
assert name == "python"
return sys.executable

monkeypatch.setattr(VirtualEnv, "_venv_lookup", venv_lookup)

@tox.hookimpl
def tox_runenvreport(venv, action):
return []

monkeypatch.setattr(venv, "tox_runenvreport", tox_runenvreport)
13 changes: 12 additions & 1 deletion src/tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shlex
import string
import sys
import uuid
import warnings
from fnmatch import fnmatchcase
from subprocess import list2cmdline
Expand Down Expand Up @@ -390,6 +391,12 @@ def tox_addoption(parser):
dest="sdistonly",
help="only perform the sdist packaging activity.",
)
parser.add_argument(
"--parallel--safe-build",
action="store_true",
dest="parallel_safe_build",
help="ensure two tox builds can run in parallel",
)
parser.add_argument(
"--installpkg",
action="store",
Expand Down Expand Up @@ -864,7 +871,7 @@ def make_hashseed():


class parseini:
def __init__(self, config, inipath):
def __init__(self, config, inipath): # noqa
config.toxinipath = inipath
config.toxinidir = config.toxinipath.dirpath()

Expand Down Expand Up @@ -950,6 +957,10 @@ def __init__(self, config, inipath):

reader.addsubstitutions(toxworkdir=config.toxworkdir)
config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
if config.option.parallel_safe_build:
config.distdir = py.path.local(config.distdir.dirname).join(
"{}-{}".format(config.distdir.basename, str(uuid.uuid4()))
)
reader.addsubstitutions(distdir=config.distdir)
config.distshare = reader.getpath("distshare", distshare_default)
reader.addsubstitutions(distshare=config.distshare)
Expand Down
6 changes: 5 additions & 1 deletion src/tox/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ def cmdline(args=None):
def main(args):
try:
config = prepare(args)
retcode = Session(config).runcommand()
try:
retcode = Session(config).runcommand()
finally:
if config.option.parallel_safe_build:
config.distdir.remove(ignore_errors=True)
if retcode is None:
retcode = 0
raise SystemExit(retcode)
Expand Down
21 changes: 21 additions & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import uuid

import pytest

Expand Down Expand Up @@ -76,3 +77,23 @@ def test_minversion(cmd, initproj):
r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out
)
assert result.ret


def test_tox_parallel_build_safe(initproj, cmd, mock_venv):
initproj(
"env_var_test",
filedefs={
"tox.ini": """
[tox]
envlist = py
[testenv]
skip_install = true
commands = python --version
"""
},
)
result = cmd("--parallel--safe-build")
basename = result.session.config.distdir.basename
assert basename.startswith("dist-")
assert uuid.UUID(basename[len("dist-") :], version=4)
assert not result.session.config.distdir.exists()

0 comments on commit 75fdd74

Please sign in to comment.