Skip to content

Commit

Permalink
Use _TOX_PARALLEL_ENV environment variable to control the behav… (#1446)
Browse files Browse the repository at this point in the history
...but keep the TOX_PARALLEL_ENV variable for others to consume.

The problem with using environment variables to let tox know it's being run
as a parallel worker is the following:

Either you don't pass the environment variable, but the test frameworks and
test suites are unaware that tox is running in parallel.
This makes it hard for cases like pytest-django:
#1139

Or you pass the environment variable, but it makes tox invoked within tox
think it is a parallel worker even when it isn't.
This makes it hard to test various tox plugins that invoke tox in their
integration test suite:
Fixes #1444

By introducing two variables we can use one ("private", _TOX_PARALLEL_ENV) to
control the behavior of tox (and don't pass it) and another ("public",
TOX_PARALLEL_ENV) to let the tests and test frameworks know tox is running
in parallel (and pass it by default).

The integration test is a tad complicated invoking subprocess.Popen instead
of subprocess.run, to support Python 2.7 and 3.4.
  • Loading branch information
hroncok authored and gaborbernat committed Nov 11, 2019
1 parent 3980da7 commit 0a44dcc
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 16 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/1444.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix nested tox execution in the parallel mode by separating the environment
variable that let's tox know it is invoked in the parallel mode
(``_TOX_PARALLEL_ENV``) from the variable that informs the tests that tox is
running in parallel mode (``TOX_PARALLEL_ENV``).
— by :user:`hroncok`
10 changes: 8 additions & 2 deletions src/tox/_pytestplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import tox.session
from tox import venv
from tox.config import parseconfig
from tox.config.parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC
from tox.reporter import update_default_reporter
from tox.venv import CreationConfig, VirtualEnv, getdigest

Expand Down Expand Up @@ -66,7 +67,12 @@ def check_os_environ_stable():

to_clean = {
k: os.environ.pop(k, None)
for k in {PARALLEL_ENV_VAR_KEY, str("TOX_WORK_DIR"), str("PYTHONPATH")}
for k in {
PARALLEL_ENV_VAR_KEY_PRIVATE,
PARALLEL_ENV_VAR_KEY_PUBLIC,
str("TOX_WORK_DIR"),
str("PYTHONPATH"),
}
}

yield
Expand Down
9 changes: 5 additions & 4 deletions src/tox/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
from tox.util.path import ensure_empty_dir
from tox.util.stdlib import importlib_metadata

from .parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from .parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from .parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC
from .parallel import add_parallel_config, add_parallel_flags
from .reporter import add_verbosity_commands

Expand Down Expand Up @@ -670,7 +671,7 @@ def passenv(testenv_config, value):
"LD_LIBRARY_PATH",
"TOX_WORK_DIR",
str(REPORTER_TIMESTAMP_ON_ENV),
str(PARALLEL_ENV_VAR_KEY),
str(PARALLEL_ENV_VAR_KEY_PUBLIC),
}

# read in global passenv settings
Expand Down Expand Up @@ -1030,7 +1031,7 @@ def line_of_default_to_zero(section, name=None):
config.sdistsrc = reader.getpath("sdistsrc", None)
config.setupdir = reader.getpath("setupdir", "{toxinidir}")
config.logdir = config.toxworkdir.join("log")
within_parallel = PARALLEL_ENV_VAR_KEY in os.environ
within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ
if not within_parallel and not WITHIN_PROVISION:
ensure_empty_dir(config.logdir)

Expand Down Expand Up @@ -1282,7 +1283,7 @@ def _getenvdata(self, reader, config):
all_envs = self._getallenvs(reader)
else:
candidates = (
(os.environ.get(PARALLEL_ENV_VAR_KEY), True),
(os.environ.get(PARALLEL_ENV_VAR_KEY_PRIVATE), True),
(from_option, True),
(from_environ, True),
("py" if self.config.option.devenv is not None else None, False),
Expand Down
3 changes: 2 additions & 1 deletion src/tox/config/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from argparse import ArgumentTypeError

ENV_VAR_KEY = "TOX_PARALLEL_ENV"
ENV_VAR_KEY_PUBLIC = "TOX_PARALLEL_ENV"
ENV_VAR_KEY_PRIVATE = "_TOX_PARALLEL_ENV"
OFF_VALUE = 0
DEFAULT_PARALLEL = OFF_VALUE

Expand Down
6 changes: 3 additions & 3 deletions src/tox/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from tox import reporter
from tox.action import Action
from tox.config import parseconfig
from tox.config.parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.config.parallel import OFF_VALUE as PARALLEL_OFF
from tox.logs.result import ResultLog
from tox.reporter import update_default_reporter
Expand Down Expand Up @@ -212,7 +212,7 @@ def subcommand_test(self):
if self.config.option.sdistonly:
return

within_parallel = PARALLEL_ENV_VAR_KEY in os.environ
within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ
try:
if not within_parallel and self.config.option.parallel != PARALLEL_OFF:
run_parallel(self.config, self.venv_dict)
Expand Down Expand Up @@ -241,7 +241,7 @@ def _load_parallel_env_report(tox_env):
return data

def _summary(self):
is_parallel_child = PARALLEL_ENV_VAR_KEY in os.environ
is_parallel_child = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ
if not is_parallel_child:
reporter.separator("_", "summary", reporter.Verbosity.QUIET)
exit_code = 0
Expand Down
6 changes: 4 additions & 2 deletions src/tox/session/commands/run/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from threading import Event, Semaphore, Thread

from tox import reporter
from tox.config.parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC
from tox.exception import InvocationError
from tox.util.main import MAIN_FILE
from tox.util.spinner import Spinner
Expand Down Expand Up @@ -37,7 +38,8 @@ def run_in_thread(tox_env, os_env, processes):
env_name = tox_env.envconfig.envname
status = "skipped tests" if config.option.notest else None
try:
os_env[str(PARALLEL_ENV_VAR_KEY)] = str(env_name)
os_env[str(PARALLEL_ENV_VAR_KEY_PRIVATE)] = str(env_name)
os_env[str(PARALLEL_ENV_VAR_KEY_PUBLIC)] = str(env_name)
args_sub = list(args)
if hasattr(tox_env, "package"):
args_sub.insert(position, str(tox_env.package))
Expand Down
4 changes: 2 additions & 2 deletions src/tox/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import tox
from tox import reporter
from tox.action import Action
from tox.config.parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.constants import INFO, PARALLEL_RESULT_JSON_PREFIX, PARALLEL_RESULT_JSON_SUFFIX
from tox.package.local import resolve_package
from tox.util.lock import get_unique_file
Expand Down Expand Up @@ -693,7 +693,7 @@ def tox_testenv_create(venv, action):


def cleanup_for_venv(venv):
within_parallel = PARALLEL_ENV_VAR_KEY in os.environ
within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ
# if the directory exists and it doesn't look like a virtualenv, produce
# an error
if venv.path.exists():
Expand Down
52 changes: 52 additions & 0 deletions tests/integration/test_parallel_inception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
def test_parallel_inception(initproj, cmd):
initproj(
"inception-1.2.3",
filedefs={
# the outer config just has one env: graham
"tox.ini": """
[tox]
envlist = graham
skipsdist = True
[testenv]
commands =
python runner.py
""",
# the inner config has 3 different envs, 1 of them is graham
"inner": {
"tox.ini": """
[tox]
envlist = graham,john,terry
skipsdist = True
[testenv]
commands =
python -c 'pass'
"""
},
# the outer test runs the inner tox and asserts all 3 envs were run
"runner.py": """
import os
import subprocess
import sys
os.chdir("inner")
p = subprocess.Popen(("tox"), stdout=subprocess.PIPE, universal_newlines=True)
stdout, _ = p.communicate()
sys.stdout.write(stdout)
assert "graham" in stdout
assert "john" in stdout
assert "terry" in stdout
""",
},
add_missing_setup_py=False,
)

result = cmd("-p", "all", "-o")
result.assert_success()

# 1 from the outer, 1 from the inner
assert result.out.count("graham: commands succeeded") == 2
# those gentlemen are only inside
assert result.out.count("john: commands succeeded") == 1
assert result.out.count("terry: commands succeeded") == 1
6 changes: 4 additions & 2 deletions tests/unit/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
is_section_substitution,
parseconfig,
)
from tox.config.parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC


class TestVenvConfig:
Expand Down Expand Up @@ -1073,7 +1074,8 @@ def test_passenv_as_multiline_list(self, newconfig, monkeypatch, plat):
assert "LANG" in envconfig.passenv
assert "LANGUAGE" in envconfig.passenv
assert "LD_LIBRARY_PATH" in envconfig.passenv
assert PARALLEL_ENV_VAR_KEY in envconfig.passenv
assert PARALLEL_ENV_VAR_KEY_PUBLIC in envconfig.passenv
assert PARALLEL_ENV_VAR_KEY_PRIVATE not in envconfig.passenv
assert "A123A" in envconfig.passenv
assert "A123B" in envconfig.passenv

Expand Down

0 comments on commit 0a44dcc

Please sign in to comment.