Skip to content

Commit

Permalink
Fix python hash seed not being set (#2739)
Browse files Browse the repository at this point in the history
Resolves #2645
  • Loading branch information
gaborbernat committed Dec 16, 2022
1 parent d074f3f commit 5cef030
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/changelog/2645.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix python hash seed not being set - by :user:`gaborbernat`.
29 changes: 26 additions & 3 deletions src/tox/session/cmd/run/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import logging
import os
import random
import sys
import time
from argparse import Action, ArgumentError, ArgumentParser, Namespace
from concurrent.futures import CancelledError, Future, ThreadPoolExecutor, as_completed
Expand Down Expand Up @@ -108,14 +110,35 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None:
help="install package in development mode",
dest="develop",
)
if mode not in ("config", "depends"):
if mode not in ("depends",):

class SeedAction(Action):
def __call__(
self,
parser: ArgumentParser, # noqa: U100
namespace: Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None, # noqa: U100
) -> None:
if values == "notset":
result = None
else:
try:
result = int(cast(str, values))
if result <= 0:
raise ValueError("must be greater than zero")
except ValueError as exc:
raise ArgumentError(self, str(exc))
setattr(namespace, self.dest, result)

parser.add_argument(
"--hashseed",
metavar="SEED",
help="set PYTHONHASHSEED to SEED before running commands. Defaults to a random integer in the range "
"[1, 4294967295] ([1, 1024] on Windows). Passing 'noset' suppresses this behavior.",
type=str,
default="noset",
action=SeedAction,
of_type=Optional[int],
default=random.randint(1, 1024 if sys.platform == "win32" else 4294967295),
dest="hash_seed",
)
parser.add_argument(
Expand Down
7 changes: 7 additions & 0 deletions src/tox/tox_env/python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ def validate_base_python(value: list[str]) -> list[str]:
self.conf.add_constant("py_dot_ver", "<python major>.<python minor>", value=self.py_dot_ver)
self.conf.add_constant("py_impl", "python implementation", value=self.py_impl)

def _default_set_env(self) -> dict[str, str]:
env = super()._default_set_env()
hash_seed: int | None = getattr(self.options, "hash_seed", None)
if hash_seed is not None:
env["PYTHONHASHSEED"] = str(hash_seed)
return env

def py_dot_ver(self) -> str:
return self.base_python.version_dot

Expand Down
5 changes: 3 additions & 2 deletions tests/config/cli/test_cli_env_var.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Callable
from unittest.mock import ANY

import pytest

Expand Down Expand Up @@ -53,7 +54,7 @@ def test_verbose_no_test() -> None:
"package_only": False,
"install_pkg": None,
"develop": False,
"hash_seed": "noset",
"hash_seed": ANY,
"discover": [],
"parallel": 0,
"parallel_live": False,
Expand Down Expand Up @@ -91,7 +92,7 @@ def test_env_var_exhaustive_parallel_values(
"discover": [],
"env": CliEnv(["py37", "py36"]),
"force_dep": [],
"hash_seed": "noset",
"hash_seed": ANY,
"install_pkg": None,
"no_provision": False,
"list_envs": False,
Expand Down
6 changes: 4 additions & 2 deletions tests/config/cli/test_cli_ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import textwrap
from pathlib import Path
from typing import Any, Callable
from unittest.mock import ANY

import pytest
from pytest_mock import MockerFixture
Expand Down Expand Up @@ -67,6 +68,7 @@ def test_ini_empty(

to.unlink()
missing_options = get_options("r")
missing_options.parsed.hash_seed = ANY
assert vars(missing_options.parsed) == vars(options.parsed)


Expand All @@ -79,7 +81,7 @@ def default_options(tmp_path: Path) -> dict[str, Any]:
"develop": False,
"discover": [],
"env": CliEnv(),
"hash_seed": "noset",
"hash_seed": ANY,
"install_pkg": None,
"no_test": False,
"override": [],
Expand Down Expand Up @@ -112,7 +114,7 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dic
"develop": False,
"discover": [],
"env": CliEnv(["py37", "py36"]),
"hash_seed": "noset",
"hash_seed": ANY,
"install_pkg": None,
"no_test": True,
"override": [Override("a=b"), Override("c=d")],
Expand Down
14 changes: 11 additions & 3 deletions tests/config/test_set_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
from pathlib import Path
from typing import Any
from unittest.mock import ANY

import pytest
from pytest_mock import MockerFixture
Expand Down Expand Up @@ -60,9 +61,9 @@ def func(tox_ini: str, extra_files: dict[str, Any] | None = None, from_cwd: Path
def test_set_env_default(eval_set_env: EvalSetEnv) -> None:
set_env = eval_set_env("")
keys = list(set_env)
assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK", "PYTHONIOENCODING"]
assert keys == ["PYTHONHASHSEED", "PIP_DISABLE_PIP_VERSION_CHECK", "PYTHONIOENCODING"]
values = [set_env.load(k) for k in keys]
assert values == ["1", "utf-8"]
assert values == [ANY, "1", "utf-8"]


def test_set_env_self_key(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None:
Expand Down Expand Up @@ -120,7 +121,13 @@ def test_set_env_replacer(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) ->
monkeypatch.setenv("MAGIC", "\nb=2\n")
set_env = eval_set_env("[testenv]\npackage=skip\nset_env=a=1\n {env:MAGIC}")
env = {k: set_env.load(k) for k in set_env}
assert env == {"PIP_DISABLE_PIP_VERSION_CHECK": "1", "a": "1", "b": "2", "PYTHONIOENCODING": "utf-8"}
assert env == {
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"a": "1",
"b": "2",
"PYTHONIOENCODING": "utf-8",
"PYTHONHASHSEED": ANY,
}


def test_set_env_honor_override(eval_set_env: EvalSetEnv) -> None:
Expand All @@ -143,6 +150,7 @@ def test_set_env_environment_file(eval_set_env: EvalSetEnv) -> None:
content = {k: set_env.load(k) for k in set_env}
assert content == {
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONHASHSEED": ANY,
"A": "1",
"B": "2",
"C": "1",
Expand Down
48 changes: 48 additions & 0 deletions tests/tox_env/python/test_python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,51 @@ def test_base_python_env_conflict_show_conf(tox_project: ToxProjectCreator, igno
f" base python py{py_ver_next}'{',' if comma_in_exc else ''})\n"
)
result.assert_out_err(out, "")


def test_python_set_hash_seed(tox_project: ToxProjectCreator) -> None:
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ[\"PYTHONHASHSEED\"])'"
prj = tox_project({"tox.ini": ini})
result = prj.run("r", "-e", "py", "--hashseed", "10")
result.assert_success()
assert result.out.splitlines()[1] == "10"


def test_python_generate_hash_seed(tox_project: ToxProjectCreator) -> None:
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ[\"PYTHONHASHSEED\"])'"
prj = tox_project({"tox.ini": ini})
result = prj.run("r", "-e", "py")
result.assert_success()
assert 1 <= int(result.out.splitlines()[1]) <= (1024 if sys.platform == "win32" else 4294967295)


def test_python_keep_hash_seed(tox_project: ToxProjectCreator) -> None:
ini = """
[testenv]
package=skip
set_env=PYTHONHASHSEED=12
commands=python -c 'import os; print(os.environ["PYTHONHASHSEED"])'
"""
result = tox_project({"tox.ini": ini}).run("r", "-e", "py")
result.assert_success()
assert result.out.splitlines()[1] == "12"


def test_python_disable_hash_seed(tox_project: ToxProjectCreator) -> None:
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ.get(\"PYTHONHASHSEED\"))'"
prj = tox_project({"tox.ini": ini})
result = prj.run("r", "-e", "py", "--hashseed", "notset")
result.assert_success()
assert result.out.splitlines()[1] == "None"


def test_python_set_hash_seed_negative(tox_project: ToxProjectCreator) -> None:
result = tox_project({"tox.ini": ""}).run("r", "-e", "py", "--hashseed", "-1")
result.assert_failed(2)
assert "tox run: error: argument --hashseed: must be greater than zero" in result.err


def test_python_set_hash_seed_incorrect(tox_project: ToxProjectCreator) -> None:
result = tox_project({"tox.ini": ""}).run("r", "-e", "py", "--hashseed", "ok")
result.assert_failed(2)
assert "tox run: error: argument --hashseed: invalid literal for int() with base 10: 'ok'" in result.err
5 changes: 0 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ commands =
pre-commit run --all-files --show-diff-on-failure {tty:--color=always} {posargs}
python -c 'print(r"hint: run {envbindir}{/}pre-commit install to add checks as pre-commit hook")'

[testenv:py311]
setenv =
{[testenv]setenv}
AIOHTTP_NO_EXTENSIONS = 1

[testenv:type]
description = run type check on code base
setenv =
Expand Down

0 comments on commit 5cef030

Please sign in to comment.