1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/5630201c3d53/ Changeset: 5630201c3d53 User: cjerdonek Date: 2013-11-14 10:51:26 Summary: Address issue #125 by adding a --hashseed command-line option.
This commit also causes Tox to set PYTHONHASHSEED for test commands to a random integer generated when tox is invoked. See the issue here: https://bitbucket.org/hpk42/tox/issue/125 Affected #: 5 files diff -r 72ba41dbebee260b19f8ef1a337c83d45ab8fcf3 -r 5630201c3d53266e9a8fb0b9b37310256ddb213e doc/example/basic.txt --- a/doc/example/basic.txt +++ b/doc/example/basic.txt @@ -175,6 +175,27 @@ from the ``subdir`` below the directory where your ``tox.ini`` file resides. +special handling of PYTHONHASHSEED +------------------------------------------- + +.. versionadded:: 1.6.2 + +By default, Tox sets PYTHONHASHSEED_ for test commands to a random integer +generated when ``tox`` is invoked. This mimics Python's hash randomization +enabled by default starting `in Python 3.3`_. To aid in reproducing test +failures, Tox displays the value of ``PYTHONHASHSEED`` in the test output. + +You can tell Tox to use an explicit hash seed value via the ``--hashseed`` +command-line option to ``tox``. You can also override the hash seed value +per test environment in ``tox.ini`` as follows:: + + [testenv:hash] + setenv = + PYTHONHASHSEED = 100 + +.. _`in Python 3.3`: http://docs.python.org/3/whatsnew/3.3.html#builtin-functions-and-types +.. _PYTHONHASHSEED: http://docs.python.org/using/cmdline.html#envvar-PYTHONHASHSEED + Integration with setuptools/distribute test commands ---------------------------------------------------- diff -r 72ba41dbebee260b19f8ef1a337c83d45ab8fcf3 -r 5630201c3d53266e9a8fb0b9b37310256ddb213e tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ from textwrap import dedent import py +import tox._config from tox._config import * from tox._config import _split_env @@ -405,7 +406,13 @@ assert envconfig.sitepackages == False assert envconfig.develop == False assert envconfig.envlogdir == envconfig.envdir.join("log") - assert envconfig.setenv is None + assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED'] + hashseed = envconfig.setenv['PYTHONHASHSEED'] + assert isinstance(hashseed, str) + # The following line checks that hashseed parses to an integer. + int_hashseed = int(hashseed) + # hashseed is random by default, so we can't assert a specific value. + assert int_hashseed > 0 def test_installpkg_tops_develop(self, newconfig): config = newconfig(["--installpkg=abc"], """ @@ -899,6 +906,120 @@ assert env.basepython == "python2.4" assert env.commands == [['xyz']] +class TestHashseedOption: + + def _get_envconfigs(self, newconfig, args=None, tox_ini=None, + make_hashseed=None): + if args is None: + args = [] + if tox_ini is None: + tox_ini = """ + [testenv] + """ + if make_hashseed is None: + make_hashseed = lambda: '123456789' + original_make_hashseed = tox._config.make_hashseed + tox._config.make_hashseed = make_hashseed + try: + config = newconfig(args, tox_ini) + finally: + tox._config.make_hashseed = original_make_hashseed + return config.envconfigs + + def _get_envconfig(self, newconfig, args=None, tox_ini=None): + envconfigs = self._get_envconfigs(newconfig, args=args, + tox_ini=tox_ini) + return envconfigs["python"] + + def _check_hashseed(self, envconfig, expected): + assert envconfig.setenv == {'PYTHONHASHSEED': expected} + + def _check_testenv(self, newconfig, expected, args=None, tox_ini=None): + envconfig = self._get_envconfig(newconfig, args=args, tox_ini=tox_ini) + self._check_hashseed(envconfig, expected) + + def test_default(self, tmpdir, newconfig): + self._check_testenv(newconfig, '123456789') + + def test_passing_integer(self, tmpdir, newconfig): + args = ['--hashseed', '1'] + self._check_testenv(newconfig, '1', args=args) + + def test_passing_string(self, tmpdir, newconfig): + args = ['--hashseed', 'random'] + self._check_testenv(newconfig, 'random', args=args) + + def test_passing_empty_string(self, tmpdir, newconfig): + args = ['--hashseed', ''] + self._check_testenv(newconfig, '', args=args) + + def test_passing_no_argument(self, tmpdir, newconfig): + """Test that passing no arguments to --hashseed is not allowed.""" + args = ['--hashseed'] + try: + self._check_testenv(newconfig, '', args=args) + except SystemExit: + e = sys.exc_info()[1] + assert e.code == 2 + return + assert False # getting here means we failed the test. + + def test_setenv(self, tmpdir, newconfig): + """Check that setenv takes precedence.""" + tox_ini = """ + [testenv] + setenv = + PYTHONHASHSEED = 2 + """ + self._check_testenv(newconfig, '2', tox_ini=tox_ini) + args = ['--hashseed', '1'] + self._check_testenv(newconfig, '2', args=args, tox_ini=tox_ini) + + def test_noset(self, tmpdir, newconfig): + args = ['--hashseed', 'noset'] + envconfig = self._get_envconfig(newconfig, args=args) + assert envconfig.setenv is None + + def test_noset_with_setenv(self, tmpdir, newconfig): + tox_ini = """ + [testenv] + setenv = + PYTHONHASHSEED = 2 + """ + args = ['--hashseed', 'noset'] + self._check_testenv(newconfig, '2', args=args, tox_ini=tox_ini) + + def test_one_random_hashseed(self, tmpdir, newconfig): + """Check that different testenvs use the same random seed.""" + tox_ini = """ + [testenv:hash1] + [testenv:hash2] + """ + next_seed = [1000] + # This function is guaranteed to generate a different value each time. + def make_hashseed(): + next_seed[0] += 1 + return str(next_seed[0]) + # Check that make_hashseed() works. + assert make_hashseed() == '1001' + envconfigs = self._get_envconfigs(newconfig, tox_ini=tox_ini, + make_hashseed=make_hashseed) + self._check_hashseed(envconfigs["hash1"], '1002') + # Check that hash2's value is not '1003', for example. + self._check_hashseed(envconfigs["hash2"], '1002') + + def test_setenv_in_one_testenv(self, tmpdir, newconfig): + """Check using setenv in one of multiple testenvs.""" + tox_ini = """ + [testenv:hash1] + setenv = + PYTHONHASHSEED = 2 + [testenv:hash2] + """ + envconfigs = self._get_envconfigs(newconfig, tox_ini=tox_ini) + self._check_hashseed(envconfigs["hash1"], '2') + self._check_hashseed(envconfigs["hash2"], '123456789') + class TestIndexServer: def test_indexserver(self, tmpdir, newconfig): config = newconfig(""" diff -r 72ba41dbebee260b19f8ef1a337c83d45ab8fcf3 -r 5630201c3d53266e9a8fb0b9b37310256ddb213e tests/test_venv.py --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -2,6 +2,7 @@ import tox import pytest import os, sys +import tox._config from tox._venv import * py25calls = int(sys.version_info[:2] == (2,5)) @@ -231,6 +232,20 @@ venv.update() mocksession.report.expect("verbosity0", "*recreate*") +def test_test_hashseed_is_in_output(newmocksession): + original_make_hashseed = tox._config.make_hashseed + tox._config.make_hashseed = lambda: '123456789' + try: + mocksession = newmocksession([], ''' + [testenv] + ''') + finally: + tox._config.make_hashseed = original_make_hashseed + venv = mocksession.getenv('python') + venv.update() + venv.test() + mocksession.report.expect("verbosity0", "python runtests: PYTHONHASHSEED='123456789'") + def test_test_runtests_action_command_is_in_output(newmocksession): mocksession = newmocksession([], ''' [testenv] diff -r 72ba41dbebee260b19f8ef1a337c83d45ab8fcf3 -r 5630201c3d53266e9a8fb0b9b37310256ddb213e tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -1,6 +1,7 @@ import argparse import distutils.sysconfig import os +import random import sys import re import shlex @@ -117,6 +118,12 @@ "all commands and results involved. This will turn off " "pass-through output from running test commands which is " "instead captured into the json result file.") + # We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED. + parser.add_argument("--hashseed", action="store", + metavar="SEED", default=None, + help="set PYTHONHASHSEED to SEED before running commands. " + "Defaults to a random integer in the range 1 to 4294967295. " + "Passing 'noset' suppresses this behavior.") parser.add_argument("args", nargs="*", help="additional arguments available to command positional substitution") return parser @@ -180,6 +187,9 @@ except Exception: return None +def make_hashseed(): + return str(random.randint(1, 4294967295)) + class parseini: def __init__(self, config, inipath): config.toxinipath = inipath @@ -200,6 +210,13 @@ else: raise ValueError("invalid context") + if config.option.hashseed is None: + hashseed = make_hashseed() + elif config.option.hashseed == 'noset': + hashseed = None + else: + hashseed = config.option.hashseed + config.hashseed = hashseed reader.addsubstitutions(toxinidir=config.toxinidir, homedir=config.homedir) @@ -306,7 +323,11 @@ arg = vc.changedir.bestrelpath(origpath) args.append(arg) reader.addsubstitutions(args) - vc.setenv = reader.getdict(section, 'setenv') + setenv = {} + if config.hashseed is not None: + setenv['PYTHONHASHSEED'] = config.hashseed + setenv.update(reader.getdict(section, 'setenv')) + vc.setenv = setenv if not vc.setenv: vc.setenv = None diff -r 72ba41dbebee260b19f8ef1a337c83d45ab8fcf3 -r 5630201c3d53266e9a8fb0b9b37310256ddb213e tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -336,14 +336,13 @@ self.run_install_command(packages=packages, options=options, action=action, extraenv=extraenv) - def _getenv(self): - env = self.envconfig.setenv - if env: - env_arg = os.environ.copy() - env_arg.update(env) - else: - env_arg = None - return env_arg + def _getenv(self, extraenv={}): + env = os.environ.copy() + setenv = self.envconfig.setenv + if setenv: + env.update(setenv) + env.update(extraenv) + return env def test(self, redirect=False): action = self.session.newaction(self, "runtests") @@ -351,6 +350,9 @@ self.status = 0 self.session.make_emptydir(self.envconfig.envtmpdir) cwd = self.envconfig.changedir + env = self._getenv() + # Display PYTHONHASHSEED to assist with reproducibility. + action.setactivity("runtests", "PYTHONHASHSEED=%r" % env.get('PYTHONHASHSEED')) for i, argv in enumerate(self.envconfig.commands): # have to make strings as _pcall changes argv[0] to a local() # happens if the same environment is invoked twice @@ -380,8 +382,7 @@ old = self.patchPATH() try: args[0] = self.getcommandpath(args[0], venv, cwd) - env = self._getenv() or os.environ.copy() - env.update(extraenv) + env = self._getenv(extraenv) return action.popen(args, cwd=cwd, env=env, redirect=redirect) finally: os.environ['PATH'] = old Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit