|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 |
|
3 | 3 | import unittest |
| 4 | +import unittest.mock |
4 | 5 | import random |
5 | 6 | import time |
6 | 7 | import pickle |
7 | 8 | import warnings |
| 9 | +from functools import partial |
8 | 10 | from math import log, exp, pi, fsum, sin |
9 | 11 | from test import support |
10 | 12 |
|
@@ -46,6 +48,16 @@ def __hash__(self): |
46 | 48 | self.assertRaises(TypeError, self.gen.seed, 1, 2, 3, 4) |
47 | 49 | self.assertRaises(TypeError, type(self.gen), []) |
48 | 50 |
|
| 51 | + @unittest.mock.patch('random._urandom') # os.urandom |
| 52 | + def test_seed_when_randomness_source_not_found(self, urandom_mock): |
| 53 | + # Random.seed() uses time.time() when an operating system specific |
| 54 | + # randomness source is not found. To test this on machines were it |
| 55 | + # exists, run the above test, test_seedargs(), again after mocking |
| 56 | + # os.urandom() so that it raises the exception expected when the |
| 57 | + # randomness source is not available. |
| 58 | + urandom_mock.side_effect = NotImplementedError |
| 59 | + self.test_seedargs() |
| 60 | + |
49 | 61 | def test_shuffle(self): |
50 | 62 | shuffle = self.gen.shuffle |
51 | 63 | lst = [] |
@@ -98,6 +110,8 @@ def test_sample(self): |
98 | 110 | self.assertEqual(len(uniq), k) |
99 | 111 | self.assertTrue(uniq <= set(population)) |
100 | 112 | self.assertEqual(self.gen.sample([], 0), []) # test edge case N==k==0 |
| 113 | + # Exception raised if size of sample exceeds that of population |
| 114 | + self.assertRaises(ValueError, self.gen.sample, population, N+1) |
101 | 115 |
|
102 | 116 | def test_sample_distribution(self): |
103 | 117 | # For the entire allowable range of 0 <= k <= N, validate that |
@@ -230,6 +244,25 @@ def test_rangelimits(self): |
230 | 244 | self.assertEqual(set(range(start,stop)), |
231 | 245 | set([self.gen.randrange(start,stop) for i in range(100)])) |
232 | 246 |
|
| 247 | + def test_randrange_nonunit_step(self): |
| 248 | + rint = self.gen.randrange(0, 10, 2) |
| 249 | + self.assertIn(rint, (0, 2, 4, 6, 8)) |
| 250 | + rint = self.gen.randrange(0, 2, 2) |
| 251 | + self.assertEqual(rint, 0) |
| 252 | + |
| 253 | + def test_randrange_errors(self): |
| 254 | + raises = partial(self.assertRaises, ValueError, self.gen.randrange) |
| 255 | + # Empty range |
| 256 | + raises(3, 3) |
| 257 | + raises(-721) |
| 258 | + raises(0, 100, -12) |
| 259 | + # Non-integer start/stop |
| 260 | + raises(3.14159) |
| 261 | + raises(0, 2.71828) |
| 262 | + # Zero and non-integer step |
| 263 | + raises(0, 42, 0) |
| 264 | + raises(0, 42, 3.14159) |
| 265 | + |
233 | 266 | def test_genrandbits(self): |
234 | 267 | # Verify ranges |
235 | 268 | for k in range(1, 1000): |
@@ -299,6 +332,16 @@ def test_setstate_middle_arg(self): |
299 | 332 | # Last element s/b an int also |
300 | 333 | self.assertRaises(TypeError, self.gen.setstate, (2, (0,)*624+('a',), None)) |
301 | 334 |
|
| 335 | + # Little trick to make "tuple(x % (2**32) for x in internalstate)" |
| 336 | + # raise ValueError. I cannot think of a simple way to achieve this, so |
| 337 | + # I am opting for using a generator as the middle argument of setstate |
| 338 | + # which attempts to cast a NaN to integer. |
| 339 | + state_values = self.gen.getstate()[1] |
| 340 | + state_values = list(state_values) |
| 341 | + state_values[-1] = float('nan') |
| 342 | + state = (int(x) for x in state_values) |
| 343 | + self.assertRaises(TypeError, self.gen.setstate, (2, state, None)) |
| 344 | + |
302 | 345 | def test_referenceImplementation(self): |
303 | 346 | # Compare the python implementation with results from the original |
304 | 347 | # code. Create 2000 53-bit precision random floats. Compare only |
@@ -438,6 +481,38 @@ def test_randbelow_logic(self, _log=log, int=int): |
438 | 481 | self.assertEqual(k, numbits) # note the stronger assertion |
439 | 482 | self.assertTrue(2**k > n > 2**(k-1)) # note the stronger assertion |
440 | 483 |
|
| 484 | + @unittest.mock.patch('random.Random.random') |
| 485 | + def test_randbelow_overriden_random(self, random_mock): |
| 486 | + # Random._randbelow() can only use random() when the built-in one |
| 487 | + # has been overridden but no new getrandbits() method was supplied. |
| 488 | + random_mock.side_effect = random.SystemRandom().random |
| 489 | + maxsize = 1<<random.BPF |
| 490 | + with warnings.catch_warnings(): |
| 491 | + warnings.simplefilter("ignore", UserWarning) |
| 492 | + # Population range too large (n >= maxsize) |
| 493 | + self.gen._randbelow(maxsize+1, maxsize = maxsize) |
| 494 | + self.gen._randbelow(5640, maxsize = maxsize) |
| 495 | + |
| 496 | + # This might be going too far to test a single line, but because of our |
| 497 | + # noble aim of achieving 100% test coverage we need to write a case in |
| 498 | + # which the following line in Random._randbelow() gets executed: |
| 499 | + # |
| 500 | + # rem = maxsize % n |
| 501 | + # limit = (maxsize - rem) / maxsize |
| 502 | + # r = random() |
| 503 | + # while r >= limit: |
| 504 | + # r = random() # <== *This line* <==< |
| 505 | + # |
| 506 | + # Therefore, to guarantee that the while loop is executed at least |
| 507 | + # once, we need to mock random() so that it returns a number greater |
| 508 | + # than 'limit' the first time it gets called. |
| 509 | + |
| 510 | + n = 42 |
| 511 | + epsilon = 0.01 |
| 512 | + limit = (maxsize - (maxsize % n)) / maxsize |
| 513 | + random_mock.side_effect = [limit + epsilon, limit - epsilon] |
| 514 | + self.gen._randbelow(n, maxsize = maxsize) |
| 515 | + |
441 | 516 | def test_randrange_bug_1590891(self): |
442 | 517 | start = 1000000000000 |
443 | 518 | stop = -100000000000000000000 |
@@ -555,6 +630,106 @@ def test_von_mises_large_kappa(self): |
555 | 630 | random.vonmisesvariate(0, 1e15) |
556 | 631 | random.vonmisesvariate(0, 1e100) |
557 | 632 |
|
| 633 | + def test_gammavariate_errors(self): |
| 634 | + # Both alpha and beta must be > 0.0 |
| 635 | + self.assertRaises(ValueError, random.gammavariate, -1, 3) |
| 636 | + self.assertRaises(ValueError, random.gammavariate, 0, 2) |
| 637 | + self.assertRaises(ValueError, random.gammavariate, 2, 0) |
| 638 | + self.assertRaises(ValueError, random.gammavariate, 1, -3) |
| 639 | + |
| 640 | + @unittest.mock.patch('random.Random.random') |
| 641 | + def test_gammavariate_full_code_coverage(self, random_mock): |
| 642 | + # There are three different possibilities in the current implementation |
| 643 | + # of random.gammavariate(), depending on the value of 'alpha'. What we |
| 644 | + # are going to do here is to fix the values returned by random() to |
| 645 | + # generate test cases that provide 100% line coverage of the method. |
| 646 | + |
| 647 | + # #1: alpha > 1.0: we want the first random number to be outside the |
| 648 | + # [1e-7, .9999999] range, so that the continue statement executes |
| 649 | + # once. The values of u1 and u2 will be 0.5 and 0.3, respectively. |
| 650 | + random_mock.side_effect = [1e-8, 0.5, 0.3] |
| 651 | + returned_value = random.gammavariate(1.1, 2.3) |
| 652 | + self.assertAlmostEqual(returned_value, 2.53) |
| 653 | + |
| 654 | + # #2: alpha == 1: first random number less than 1e-7 to that the body |
| 655 | + # of the while loop executes once. Then random.random() returns 0.45, |
| 656 | + # which causes while to stop looping and the algorithm to terminate. |
| 657 | + random_mock.side_effect = [1e-8, 0.45] |
| 658 | + returned_value = random.gammavariate(1.0, 3.14) |
| 659 | + self.assertAlmostEqual(returned_value, 2.507314166123803) |
| 660 | + |
| 661 | + # #3: 0 < alpha < 1. This is the most complex region of code to cover, |
| 662 | + # as there are multiple if-else statements. Let's take a look at the |
| 663 | + # source code, and determine the values that we need accordingly: |
| 664 | + # |
| 665 | + # while 1: |
| 666 | + # u = random() |
| 667 | + # b = (_e + alpha)/_e |
| 668 | + # p = b*u |
| 669 | + # if p <= 1.0: # <=== (A) |
| 670 | + # x = p ** (1.0/alpha) |
| 671 | + # else: # <=== (B) |
| 672 | + # x = -_log((b-p)/alpha) |
| 673 | + # u1 = random() |
| 674 | + # if p > 1.0: # <=== (C) |
| 675 | + # if u1 <= x ** (alpha - 1.0): # <=== (D) |
| 676 | + # break |
| 677 | + # elif u1 <= _exp(-x): # <=== (E) |
| 678 | + # break |
| 679 | + # return x * beta |
| 680 | + # |
| 681 | + # First, we want (A) to be True. For that we need that: |
| 682 | + # b*random() <= 1.0 |
| 683 | + # r1 = random() <= 1.0 / b |
| 684 | + # |
| 685 | + # We now get to the second if-else branch, and here, since p <= 1.0, |
| 686 | + # (C) is False and we take the elif branch, (E). For it to be True, |
| 687 | + # so that the break is executed, we need that: |
| 688 | + # r2 = random() <= _exp(-x) |
| 689 | + # r2 <= _exp(-(p ** (1.0/alpha))) |
| 690 | + # r2 <= _exp(-((b*r1) ** (1.0/alpha))) |
| 691 | + |
| 692 | + _e = random._e |
| 693 | + _exp = random._exp |
| 694 | + _log = random._log |
| 695 | + alpha = 0.35 |
| 696 | + beta = 1.45 |
| 697 | + b = (_e + alpha)/_e |
| 698 | + epsilon = 0.01 |
| 699 | + |
| 700 | + r1 = 0.8859296441566 # 1.0 / b |
| 701 | + r2 = 0.3678794411714 # _exp(-((b*r1) ** (1.0/alpha))) |
| 702 | + |
| 703 | + # These four "random" values result in the following trace: |
| 704 | + # (A) True, (E) False --> [next iteration of while] |
| 705 | + # (A) True, (E) True --> [while loop breaks] |
| 706 | + random_mock.side_effect = [r1, r2 + epsilon, r1, r2] |
| 707 | + returned_value = random.gammavariate(alpha, beta) |
| 708 | + self.assertAlmostEqual(returned_value, 1.4499999999997544) |
| 709 | + |
| 710 | + # Let's now make (A) be False. If this is the case, when we get to the |
| 711 | + # second if-else 'p' is greater than 1, so (C) evaluates to True. We |
| 712 | + # now encounter a second if statement, (D), which in order to execute |
| 713 | + # must satisfy the following condition: |
| 714 | + # r2 <= x ** (alpha - 1.0) |
| 715 | + # r2 <= (-_log((b-p)/alpha)) ** (alpha - 1.0) |
| 716 | + # r2 <= (-_log((b-(b*r1))/alpha)) ** (alpha - 1.0) |
| 717 | + r1 = 0.8959296441566 # (1.0 / b) + epsilon -- so that (A) is False |
| 718 | + r2 = 0.9445400408898141 |
| 719 | + |
| 720 | + # And these four values result in the following trace: |
| 721 | + # (B) and (C) True, (D) False --> [next iteration of while] |
| 722 | + # (B) and (C) True, (D) True [while loop breaks] |
| 723 | + random_mock.side_effect = [r1, r2 + epsilon, r1, r2] |
| 724 | + returned_value = random.gammavariate(alpha, beta) |
| 725 | + self.assertAlmostEqual(returned_value, 1.5830349561760781) |
| 726 | + |
| 727 | + @unittest.mock.patch('random.Random.gammavariate') |
| 728 | + def test_betavariate_return_zero(self, gammavariate_mock): |
| 729 | + # betavariate() returns zero when the Gamma distribution |
| 730 | + # that it uses internally returns this same value. |
| 731 | + gammavariate_mock.return_value = 0.0 |
| 732 | + self.assertEqual(0.0, random.betavariate(2.71828, 3.14159)) |
558 | 733 |
|
559 | 734 | class TestModule(unittest.TestCase): |
560 | 735 | def testMagicConstants(self): |
|
0 commit comments