2626import json
2727import logging
2828import math
29+ import random
2930import sys
31+ import types
3032from pathlib import Path
3133from typing import TYPE_CHECKING , cast
3234
3941except ImportError :
4042 FANDANGO_FAKER_AVAILABLE = False
4143
44+ import weakref
45+
4246import pynguin .assertion .assertiongenerator as ag
4347import pynguin .assertion .llmassertiongenerator as lag
4448import pynguin .assertion .mutation_analysis .mutators as mu
@@ -240,16 +244,44 @@ def _setup_report_dir() -> bool:
240244 return True
241245
242246
247+ def _patch_random () -> None :
248+ """Patch random.Random.seed to use the Pynguin seed for determinism.
249+
250+ This must be called BEFORE the SUT is imported, because the SUT (or its
251+ dependencies) may create random.Random instances at module level.
252+
253+ All seeded instances are collected and stored as
254+ ``random.Random.seed.__pynguin_instances__`` so callers can reseed every
255+ living SUT-related instance before each test-case execution.
256+ """
257+ if not getattr (random .Random .seed , "__pynguin_patched__" , False ):
258+ orig_random_seed = random .Random .seed
259+ tracked : weakref .WeakSet [random .Random ] = weakref .WeakSet ()
260+
261+ def _deterministic_random_seed (self : random .Random , x = None ) -> None :
262+ if x is None :
263+ 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__ } "
268+ orig_random_seed (self , x )
269+ tracked .add (self )
270+
271+ _deterministic_random_seed .__pynguin_patched__ = True # type: ignore[attr-defined]
272+ _deterministic_random_seed .__pynguin_instances__ = tracked # type: ignore[attr-defined]
273+ random .Random .seed = _deterministic_random_seed # type: ignore[assignment,method-assign]
274+
275+
243276def _setup_random_number_generator () -> None :
244277 """Setup RNG."""
245278 _LOGGER .info ("Using seed %d" , config .configuration .seeding .seed )
246279 randomness .RNG .seed (config .configuration .seeding .seed )
280+ random .seed (config .configuration .seeding .seed )
247281 if config .configuration .pynguinml .ml_testing_enabled :
248282 np_rng .init_rng (config .configuration .seeding .seed )
249283
250284 if FANDANGO_FAKER_AVAILABLE :
251- # Seed Fandango
252- random .seed (config .configuration .seeding .seed )
253285 # Seed Faker
254286 Faker .seed (config .configuration .seeding .seed )
255287
@@ -309,6 +341,34 @@ def _verify_config() -> None:
309341 )
310342
311343
344+ def _check_sut_uses_random (new_module_names : set [str ]) -> bool :
345+ """Return True if any module loaded by the SUT import references Python's random.
346+
347+ Args:
348+ new_module_names: Module names that were added to sys.modules as a side
349+ effect of importing the SUT.
350+
351+ Returns:
352+ True if the SUT or any transitive dependency uses Python's random module.
353+ """
354+ random_module = sys .modules .get ("random" )
355+ if random_module is None :
356+ return False
357+
358+ if "random" in new_module_names :
359+ return True
360+
361+ for n in new_module_names :
362+ module = sys .modules .get (n )
363+ if module is None or not isinstance (module , types .ModuleType ):
364+ continue
365+ for value in vars (module ).values ():
366+ if value is random_module :
367+ return True
368+
369+ return False
370+
371+
312372def _setup_and_check () -> tuple [TestCaseExecutor , ModuleTestCluster , ConstantProvider ] | None :
313373 """Load the System Under Test (SUT) i.e. the module that is tested.
314374
@@ -321,15 +381,19 @@ def _setup_and_check() -> tuple[TestCaseExecutor, ModuleTestCluster, ConstantPro
321381 return None
322382 wrapped_constant_provider , dynamic_constant_provider = _setup_constant_seeding ()
323383 subject_properties = _setup_import_hook (dynamic_constant_provider )
384+ _patch_random ()
385+ modules_before_sut = set (sys .modules .keys ())
324386 if not _load_sut (subject_properties ):
325387 return None
388+ new_sut_module_names = set (sys .modules .keys ()) - modules_before_sut
326389 if not _setup_report_dir ():
327390 return None
328391
329392 # Analyzing the SUT should not cause any coverage.
330393 with subject_properties .instrumentation_tracer .temporarily_disable ():
331394 if (test_cluster := _setup_test_cluster ()) is None :
332395 return None
396+ test_cluster .sut_uses_random = _check_sut_uses_random (new_sut_module_names )
333397
334398 # Make alias to make the following lines shorter...
335399 stop = config .configuration .stopping
@@ -656,7 +720,7 @@ def _run() -> ReturnCode: # noqa: C901
656720 # Export the generated test suites
657721 if config .configuration .test_case_output .export_strategy == config .ExportStrategy .PY_TEST :
658722 try :
659- _export_chromosome (generation_result )
723+ _export_chromosome (generation_result , sut_uses_random = test_cluster . sut_uses_random )
660724 except Exception as ex :
661725 _LOGGER .exception ("Export to PyTest failed: %s" , ex )
662726
@@ -1048,13 +1112,17 @@ def _collect_miscellaneous_statistics(test_cluster: ModuleTestCluster) -> None:
10481112def _export_chromosome (
10491113 chromosome : chrom .Chromosome ,
10501114 file_name_suffix : str = "" ,
1115+ * ,
1116+ sut_uses_random : bool = False ,
10511117) -> None :
10521118 """Export the given chromosome.
10531119
10541120 Args:
10551121 chromosome: the chromosome to export.
10561122 file_name_suffix: Suffix that can be added to the file name to distinguish
10571123 between different results e.g., failing and succeeding test cases.
1124+ sut_uses_random: Whether the SUT transitively uses Python's random module.
1125+ If True, an autouse fixture that reseeds random is emitted.
10581126
10591127 Returns:
10601128 The name of the target file
@@ -1071,6 +1139,7 @@ def _export_chromosome(
10711139 store_call_return = store_call_return ,
10721140 no_xfail = config .configuration .test_case_output .no_xfail ,
10731141 sut_module_name = config .configuration .module_name ,
1142+ pynguin_seed = config .configuration .seeding .seed if sut_uses_random else None ,
10741143 )
10751144
10761145 chromosome .accept (export_visitor )
0 commit comments