Skip to content

Commit 3ca2bbd

Browse files
committed
Patch random module during unpickling to ensure deterministic seeding in subprocesses
1 parent 364fd4c commit 3ca2bbd

File tree

4 files changed

+37
-1
lines changed

4 files changed

+37
-1
lines changed

src/pynguin/generator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ def _patch_random() -> None:
261261
def _deterministic_random_seed(self: random.Random, x=None) -> None:
262262
if x is None:
263263
x = config.configuration.seeding.seed
264+
elif type(x).__hash__ is object.__hash__:
265+
# If x is an object that uses the default id-based hash, we replace it with
266+
# a deterministic string to avoid non-determinism from memory addresses.
267+
x = f"{type(x).__module__}.{type(x).__name__}"
264268
orig_random_seed(self, x)
265269
tracked.add(self)
266270

src/pynguin/testcase/execution.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,27 @@ def _after_statement_execution(
12671267
SUPPORTED_EXIT_CODE_MESSAGES[-signal.SIGSEGV] = "Segmentation fault detected"
12681268

12691269

1270+
class PatchRandomOnUnpickle:
1271+
"""A hook that patches random when unpickled in a subprocess.
1272+
1273+
This ensures that random.Random.seed is patched before the SUT is unpickled
1274+
and potentially creates new random.Random instances (e.g., mimesis).
1275+
"""
1276+
1277+
def __init__(self):
1278+
"""Create a new hook."""
1279+
self._config = config.configuration
1280+
1281+
def __getstate__(self):
1282+
return {"config": self._config}
1283+
1284+
def __setstate__(self, state):
1285+
config.configuration = state["config"]
1286+
from pynguin.generator import _patch_random # noqa: PLC0415
1287+
1288+
_patch_random()
1289+
1290+
12701291
class SubprocessTestCaseExecutor(TestCaseExecutor):
12711292
"""An executor that executes the generated test cases in a subprocess."""
12721293

@@ -1472,6 +1493,7 @@ def _setup_subprocess_execution(
14721493
remote_observers = tuple(self._yield_remote_observers())
14731494

14741495
args = (
1496+
PatchRandomOnUnpickle(),
14751497
self._subject_properties,
14761498
self._module_provider,
14771499
self._maximum_test_execution_timeout,
@@ -1723,6 +1745,7 @@ def _fix_assertion_trace(
17231745

17241746
@staticmethod
17251747
def _execute_test_cases_in_subprocess( # noqa: PLR0917
1748+
_patch_random_hook: object,
17261749
subject_properties: SubjectProperties,
17271750
module_provider: ModuleProvider,
17281751
maximum_test_execution_timeout: int,

src/pynguin/testcase/export.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ def __create_patch_nodes(seed: int) -> list[ast.stmt]:
296296
" def _pynguin_deterministic_seed(self, x=None):\n"
297297
" if x is None:\n"
298298
f" x = {seed}\n"
299+
" elif type(x).__hash__ is object.__hash__:\n"
300+
" x = f'{type(x).__module__}.{type(x).__name__}'\n"
299301
" _pynguin_orig_seed(self, x)\n"
300302
" _pynguin_tracked.add(self)\n"
301303
" _pynguin_deterministic_seed.__pynguin_patched__ = True\n"
@@ -477,6 +479,7 @@ def save_module_to_file(
477479
output = output.replace(
478480
"random.Random.seed = _pynguin_deterministic_seed",
479481
"random.Random.seed = _pynguin_deterministic_seed\n",
482+
1,
480483
)
481484

482485
if module_name_with_coverage:

tests/testcase/execution/test_subprocesstestcaseexecutor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
from pynguin.analyses.typesystem import InferredSignature, NoneType
2626
from pynguin.instrumentation.machinery import install_import_hook
2727
from pynguin.instrumentation.tracer import SubjectProperties
28-
from pynguin.testcase.execution import ModuleProvider, SubprocessTestCaseExecutor
28+
from pynguin.testcase.execution import (
29+
ModuleProvider,
30+
PatchRandomOnUnpickle,
31+
SubprocessTestCaseExecutor,
32+
)
2933
from pynguin.utils.generic.genericaccessibleobject import GenericFunction
3034
from tests.fixtures.crash.seg_fault import cause_segmentation_fault
3135

@@ -90,6 +94,7 @@ def test_subprocess_exception_suppression(subject_properties: SubjectProperties)
9094
# This should not raise an exception because the exception should be caught
9195
# and suppressed inside the method
9296
SubprocessTestCaseExecutor._execute_test_cases_in_subprocess(
97+
PatchRandomOnUnpickle(),
9398
subject_properties,
9499
module_provider,
95100
maximum_test_execution_timeout,
@@ -141,6 +146,7 @@ def test_subprocess_exception_logging(caplog, subject_properties: SubjectPropert
141146

142147
# Call the method directly
143148
SubprocessTestCaseExecutor._execute_test_cases_in_subprocess(
149+
PatchRandomOnUnpickle(),
144150
subject_properties,
145151
module_provider,
146152
maximum_test_execution_timeout,

0 commit comments

Comments
 (0)