Skip to content

Commit 75fdd74

Browse files
authored
Allow to run multiple tox instances in parallel #849 (#934)
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.
1 parent e557705 commit 75fdd74

File tree

6 files changed

+111
-3
lines changed

6 files changed

+111
-3
lines changed

changelog/849.feature.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow to run multiple tox instances in parallel by providing the
2+
```--parallel--safe-build`` flag. - by :user:`gaborbernat`

doc/example/jenkins.rst

+23
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,27 @@ Linux as a limit of 128). There are two methods to workaround this issue:
188188
:ref:`long interpreter directives` for more information).
189189

190190

191+
Running tox environments in parallel
192+
------------------------------------
193+
194+
Jenkins has parallel stages allowing you to run commands in parallel, however tox package
195+
building it is not parallel safe. Use the ``--parallel--safe-build`` flag to enable parallel safe
196+
builds. Here's a generic stage definition demonstrating this:
197+
198+
.. code-block:: groovy
199+
200+
stage('run tox envs') {
201+
steps {
202+
script {
203+
def envs = sh(returnStdout: true, script: "tox -l").trim().split('\n')
204+
def cmds = envs.collectEntries({ tox_env ->
205+
[tox_env, {
206+
sh "tox --parallel--safe-build -vve $tox_env"
207+
}]
208+
})
209+
parallel(cmds)
210+
}
211+
}
212+
}
213+
191214
.. include:: ../links.rst

src/tox/_pytestplugin.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import print_function, unicode_literals
22

33
import os
4+
import sys
45
import textwrap
56
import time
67
from fnmatch import fnmatch
@@ -10,10 +11,11 @@
1011
import six
1112

1213
import tox
14+
from tox import venv
1315
from tox.config import parseconfig
1416
from tox.result import ResultLog
1517
from tox.session import Session, main
16-
from tox.venv import VirtualEnv
18+
from tox.venv import CreationConfig, VirtualEnv, getdigest
1719

1820
mark_dont_run_on_windows = pytest.mark.skipif(os.name == "nt", reason="non windows test")
1921
mark_dont_run_on_posix = pytest.mark.skipif(os.name == "posix", reason="non posix test")
@@ -70,7 +72,16 @@ def run(*argv):
7072
key = str(b"PYTHONPATH")
7173
python_paths = (i for i in (str(os.getcwd()), os.getenv(key)) if i)
7274
monkeypatch.setenv(key, os.pathsep.join(python_paths))
75+
7376
with RunResult(capfd, argv) as result:
77+
prev_run_command = Session.runcommand
78+
79+
def run_command(self):
80+
result.session = self
81+
return prev_run_command(self)
82+
83+
monkeypatch.setattr(Session, "runcommand", run_command)
84+
7485
try:
7586
main([str(x) for x in argv])
7687
assert False # this should always exist with SystemExit
@@ -91,6 +102,7 @@ def __init__(self, capfd, args):
91102
self.duration = None
92103
self.out = None
93104
self.err = None
105+
self.session = None
94106

95107
def __enter__(self):
96108
self._start = time.time()
@@ -373,3 +385,38 @@ def create_files(base, filedefs):
373385
elif isinstance(value, six.string_types):
374386
s = textwrap.dedent(value)
375387
base.join(key).write(s)
388+
389+
390+
@pytest.fixture()
391+
def mock_venv(monkeypatch):
392+
"""This creates a mock virtual environment (e.g. will inherit the current interpreter).
393+
Note: because we inherit, to keep things sane you must call the py environment and only that;
394+
and cannot install any packages. """
395+
396+
class ProxyCurrentPython:
397+
@classmethod
398+
def readconfig(cls, path):
399+
assert path.dirname.endswith("{}py".format(os.sep))
400+
return CreationConfig(
401+
md5=getdigest(sys.executable),
402+
python=sys.executable,
403+
version=tox.__version__,
404+
sitepackages=False,
405+
usedevelop=False,
406+
deps=[],
407+
alwayscopy=False,
408+
)
409+
410+
monkeypatch.setattr(CreationConfig, "readconfig", ProxyCurrentPython.readconfig)
411+
412+
def venv_lookup(venv, name):
413+
assert name == "python"
414+
return sys.executable
415+
416+
monkeypatch.setattr(VirtualEnv, "_venv_lookup", venv_lookup)
417+
418+
@tox.hookimpl
419+
def tox_runenvreport(venv, action):
420+
return []
421+
422+
monkeypatch.setattr(venv, "tox_runenvreport", tox_runenvreport)

src/tox/config.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import shlex
99
import string
1010
import sys
11+
import uuid
1112
import warnings
1213
from fnmatch import fnmatchcase
1314
from subprocess import list2cmdline
@@ -390,6 +391,12 @@ def tox_addoption(parser):
390391
dest="sdistonly",
391392
help="only perform the sdist packaging activity.",
392393
)
394+
parser.add_argument(
395+
"--parallel--safe-build",
396+
action="store_true",
397+
dest="parallel_safe_build",
398+
help="ensure two tox builds can run in parallel",
399+
)
393400
parser.add_argument(
394401
"--installpkg",
395402
action="store",
@@ -864,7 +871,7 @@ def make_hashseed():
864871

865872

866873
class parseini:
867-
def __init__(self, config, inipath):
874+
def __init__(self, config, inipath): # noqa
868875
config.toxinipath = inipath
869876
config.toxinidir = config.toxinipath.dirpath()
870877

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

951958
reader.addsubstitutions(toxworkdir=config.toxworkdir)
952959
config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
960+
if config.option.parallel_safe_build:
961+
config.distdir = py.path.local(config.distdir.dirname).join(
962+
"{}-{}".format(config.distdir.basename, str(uuid.uuid4()))
963+
)
953964
reader.addsubstitutions(distdir=config.distdir)
954965
config.distshare = reader.getpath("distshare", distshare_default)
955966
reader.addsubstitutions(distshare=config.distshare)

src/tox/session.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ def cmdline(args=None):
4242
def main(args):
4343
try:
4444
config = prepare(args)
45-
retcode = Session(config).runcommand()
45+
try:
46+
retcode = Session(config).runcommand()
47+
finally:
48+
if config.option.parallel_safe_build:
49+
config.distdir.remove(ignore_errors=True)
4650
if retcode is None:
4751
retcode = 0
4852
raise SystemExit(retcode)

tests/test_session.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import uuid
23

34
import pytest
45

@@ -76,3 +77,23 @@ def test_minversion(cmd, initproj):
7677
r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out
7778
)
7879
assert result.ret
80+
81+
82+
def test_tox_parallel_build_safe(initproj, cmd, mock_venv):
83+
initproj(
84+
"env_var_test",
85+
filedefs={
86+
"tox.ini": """
87+
[tox]
88+
envlist = py
89+
[testenv]
90+
skip_install = true
91+
commands = python --version
92+
"""
93+
},
94+
)
95+
result = cmd("--parallel--safe-build")
96+
basename = result.session.config.distdir.basename
97+
assert basename.startswith("dist-")
98+
assert uuid.UUID(basename[len("dist-") :], version=4)
99+
assert not result.session.config.distdir.exists()

0 commit comments

Comments
 (0)