bpo-33144: random.Random and subclasses: split _randbelow implementat… · python/cpython@ba3a87a
@@ -5,6 +5,7 @@
55import time
66import pickle
77import warnings
8+import logging
89from functools import partial
910from math import log, exp, pi, fsum, sin, factorial
1011from test import support
@@ -619,6 +620,16 @@ def test_genrandbits(self):
619620self.assertRaises(ValueError, self.gen.getrandbits, 0)
620621self.assertRaises(ValueError, self.gen.getrandbits, -1)
621622623+def test_randrange_uses_getrandbits(self):
624+# Verify use of getrandbits by randrange
625+# Use same seed as in the cross-platform repeatability test
626+# in test_genrandbits above.
627+self.gen.seed(1234567)
628+# If randrange uses getrandbits, it should pick getrandbits(100)
629+# when called with a 100-bits stop argument.
630+self.assertEqual(self.gen.randrange(2**99),
631+97904845777343510404718956115)
632+622633def test_randbelow_logic(self, _log=log, int=int):
623634# check bitcount transition points: 2**i and 2**(i+1)-1
624635# show that: k = int(1.001 + _log(n, 2))
@@ -640,21 +651,22 @@ def test_randbelow_logic(self, _log=log, int=int):
640651self.assertEqual(k, numbits) # note the stronger assertion
641652self.assertTrue(2**k > n > 2**(k-1)) # note the stronger assertion
642653643-@unittest.mock.patch('random.Random.random')
644-def test_randbelow_overridden_random(self, random_mock):
654+def test_randbelow_without_getrandbits(self):
645655# Random._randbelow() can only use random() when the built-in one
646656# has been overridden but no new getrandbits() method was supplied.
647-random_mock.side_effect = random.SystemRandom().random
648657maxsize = 1<<random.BPF
649658with warnings.catch_warnings():
650659warnings.simplefilter("ignore", UserWarning)
651660# Population range too large (n >= maxsize)
652-self.gen._randbelow(maxsize+1, maxsize = maxsize)
653-self.gen._randbelow(5640, maxsize = maxsize)
661+self.gen._randbelow_without_getrandbits(
662+maxsize+1, maxsize=maxsize
663+ )
664+self.gen._randbelow_without_getrandbits(5640, maxsize=maxsize)
654665# issue 33203: test that _randbelow raises ValueError on
655666# n == 0 also in its getrandbits-independent branch.
656667with self.assertRaises(ValueError):
657-self.gen._randbelow(0, maxsize=maxsize)
668+self.gen._randbelow_without_getrandbits(0, maxsize=maxsize)
669+658670# This might be going too far to test a single line, but because of our
659671# noble aim of achieving 100% test coverage we need to write a case in
660672# which the following line in Random._randbelow() gets executed:
@@ -672,8 +684,10 @@ def test_randbelow_overridden_random(self, random_mock):
672684n = 42
673685epsilon = 0.01
674686limit = (maxsize - (maxsize % n)) / maxsize
675-random_mock.side_effect = [limit + epsilon, limit - epsilon]
676-self.gen._randbelow(n, maxsize = maxsize)
687+with unittest.mock.patch.object(random.Random, 'random') as random_mock:
688+random_mock.side_effect = [limit + epsilon, limit - epsilon]
689+self.gen._randbelow_without_getrandbits(n, maxsize=maxsize)
690+self.assertEqual(random_mock.call_count, 2)
677691678692def test_randrange_bug_1590891(self):
679693start = 1000000000000
@@ -926,6 +940,49 @@ def test_betavariate_return_zero(self, gammavariate_mock):
926940gammavariate_mock.return_value = 0.0
927941self.assertEqual(0.0, random.betavariate(2.71828, 3.14159))
928942943+class TestRandomSubclassing(unittest.TestCase):
944+def test_random_subclass_with_kwargs(self):
945+# SF bug #1486663 -- this used to erroneously raise a TypeError
946+class Subclass(random.Random):
947+def __init__(self, newarg=None):
948+random.Random.__init__(self)
949+Subclass(newarg=1)
950+951+def test_subclasses_overriding_methods(self):
952+# Subclasses with an overridden random, but only the original
953+# getrandbits method should not rely on getrandbits in for randrange,
954+# but should use a getrandbits-independent implementation instead.
955+956+# subclass providing its own random **and** getrandbits methods
957+# like random.SystemRandom does => keep relying on getrandbits for
958+# randrange
959+class SubClass1(random.Random):
960+def random(self):
961+return super().random()
962+963+def getrandbits(self, n):
964+logging.getLogger('getrandbits').info('used getrandbits')
965+return super().getrandbits(n)
966+with self.assertLogs('getrandbits'):
967+SubClass1().randrange(42)
968+969+# subclass providing only random => can only use random for randrange
970+class SubClass2(random.Random):
971+def random(self):
972+logging.getLogger('random').info('used random')
973+return super().random()
974+with self.assertLogs('random'):
975+SubClass2().randrange(42)
976+977+# subclass defining getrandbits to complement its inherited random
978+# => can now rely on getrandbits for randrange again
979+class SubClass3(SubClass2):
980+def getrandbits(self, n):
981+logging.getLogger('getrandbits').info('used getrandbits')
982+return super().getrandbits(n)
983+with self.assertLogs('getrandbits'):
984+SubClass3().randrange(42)
985+929986class TestModule(unittest.TestCase):
930987def testMagicConstants(self):
931988self.assertAlmostEqual(random.NV_MAGICCONST, 1.71552776992141)
@@ -937,13 +994,6 @@ def test__all__(self):
937994# tests validity but not completeness of the __all__ list
938995self.assertTrue(set(random.__all__) <= set(dir(random)))
939996940-def test_random_subclass_with_kwargs(self):
941-# SF bug #1486663 -- this used to erroneously raise a TypeError
942-class Subclass(random.Random):
943-def __init__(self, newarg=None):
944-random.Random.__init__(self)
945-Subclass(newarg=1)
946-947997@unittest.skipUnless(hasattr(os, "fork"), "fork() required")
948998def test_after_fork(self):
949999# Test the global Random instance gets reseeded in child