mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 21:21:22 +00:00 
			
		
		
		
	gh-135376: Fix and improve test_random (GH-135377)
* Remove duplicated code. Tests for Random and SystemRandom now share the code. * Move implementation agnostic tests that was only run for SystemRandom, so they are now run for Random too. * Add tests for __index__() support. * Add tests for randint().
This commit is contained in:
		
							parent
							
								
									21f3d15534
								
							
						
					
					
						commit
						c55512311b
					
				
					 1 changed files with 134 additions and 161 deletions
				
			
		|  | @ -151,6 +151,7 @@ def test_sample(self): | ||||||
|         # Exception raised if size of sample exceeds that of population |         # Exception raised if size of sample exceeds that of population | ||||||
|         self.assertRaises(ValueError, self.gen.sample, population, N+1) |         self.assertRaises(ValueError, self.gen.sample, population, N+1) | ||||||
|         self.assertRaises(ValueError, self.gen.sample, [], -1) |         self.assertRaises(ValueError, self.gen.sample, [], -1) | ||||||
|  |         self.assertRaises(TypeError, self.gen.sample, population, 1.0) | ||||||
| 
 | 
 | ||||||
|     def test_sample_distribution(self): |     def test_sample_distribution(self): | ||||||
|         # For the entire allowable range of 0 <= k <= N, validate that |         # For the entire allowable range of 0 <= k <= N, validate that | ||||||
|  | @ -268,6 +269,7 @@ def test_choices(self): | ||||||
|             choices(data, range(4), k=5), |             choices(data, range(4), k=5), | ||||||
|             choices(k=5, population=data, weights=range(4)), |             choices(k=5, population=data, weights=range(4)), | ||||||
|             choices(k=5, population=data, cum_weights=range(4)), |             choices(k=5, population=data, cum_weights=range(4)), | ||||||
|  |             choices(data, k=MyIndex(5)), | ||||||
|         ]: |         ]: | ||||||
|             self.assertEqual(len(sample), 5) |             self.assertEqual(len(sample), 5) | ||||||
|             self.assertEqual(type(sample), list) |             self.assertEqual(type(sample), list) | ||||||
|  | @ -378,122 +380,40 @@ def test_gauss(self): | ||||||
|             self.assertEqual(x1, x2) |             self.assertEqual(x1, x2) | ||||||
|             self.assertEqual(y1, y2) |             self.assertEqual(y1, y2) | ||||||
| 
 | 
 | ||||||
|  |     @support.requires_IEEE_754 | ||||||
|  |     def test_53_bits_per_float(self): | ||||||
|  |         span = 2 ** 53 | ||||||
|  |         cum = 0 | ||||||
|  |         for i in range(100): | ||||||
|  |             cum |= int(self.gen.random() * span) | ||||||
|  |         self.assertEqual(cum, span-1) | ||||||
|  | 
 | ||||||
|     def test_getrandbits(self): |     def test_getrandbits(self): | ||||||
|  |         getrandbits = self.gen.getrandbits | ||||||
|         # Verify ranges |         # Verify ranges | ||||||
|         for k in range(1, 1000): |         for k in range(1, 1000): | ||||||
|             self.assertTrue(0 <= self.gen.getrandbits(k) < 2**k) |             self.assertTrue(0 <= getrandbits(k) < 2**k) | ||||||
|         self.assertEqual(self.gen.getrandbits(0), 0) |         self.assertEqual(getrandbits(0), 0) | ||||||
| 
 | 
 | ||||||
|         # Verify all bits active |         # Verify all bits active | ||||||
|         getbits = self.gen.getrandbits |  | ||||||
|         for span in [1, 2, 3, 4, 31, 32, 32, 52, 53, 54, 119, 127, 128, 129]: |         for span in [1, 2, 3, 4, 31, 32, 32, 52, 53, 54, 119, 127, 128, 129]: | ||||||
|             all_bits = 2**span-1 |             all_bits = 2**span-1 | ||||||
|             cum = 0 |             cum = 0 | ||||||
|             cpl_cum = 0 |             cpl_cum = 0 | ||||||
|             for i in range(100): |             for i in range(100): | ||||||
|                 v = getbits(span) |                 v = getrandbits(span) | ||||||
|                 cum |= v |                 cum |= v | ||||||
|                 cpl_cum |= all_bits ^ v |                 cpl_cum |= all_bits ^ v | ||||||
|             self.assertEqual(cum, all_bits) |             self.assertEqual(cum, all_bits) | ||||||
|             self.assertEqual(cpl_cum, all_bits) |             self.assertEqual(cpl_cum, all_bits) | ||||||
| 
 | 
 | ||||||
|         # Verify argument checking |         # Verify argument checking | ||||||
|         self.assertRaises(TypeError, self.gen.getrandbits) |         self.assertRaises(TypeError, getrandbits) | ||||||
|         self.assertRaises(TypeError, self.gen.getrandbits, 1, 2) |         self.assertRaises(TypeError, getrandbits, 1, 2) | ||||||
|         self.assertRaises(ValueError, self.gen.getrandbits, -1) |         self.assertRaises(ValueError, getrandbits, -1) | ||||||
|         self.assertRaises(OverflowError, self.gen.getrandbits, 1<<1000) |         self.assertRaises(OverflowError, getrandbits, 1<<1000) | ||||||
|         self.assertRaises(ValueError, self.gen.getrandbits, -1<<1000) |         self.assertRaises(ValueError, getrandbits, -1<<1000) | ||||||
|         self.assertRaises(TypeError, self.gen.getrandbits, 10.1) |         self.assertRaises(TypeError, getrandbits, 10.1) | ||||||
| 
 |  | ||||||
|     def test_pickling(self): |  | ||||||
|         for proto in range(pickle.HIGHEST_PROTOCOL + 1): |  | ||||||
|             state = pickle.dumps(self.gen, proto) |  | ||||||
|             origseq = [self.gen.random() for i in range(10)] |  | ||||||
|             newgen = pickle.loads(state) |  | ||||||
|             restoredseq = [newgen.random() for i in range(10)] |  | ||||||
|             self.assertEqual(origseq, restoredseq) |  | ||||||
| 
 |  | ||||||
|     def test_bug_1727780(self): |  | ||||||
|         # verify that version-2-pickles can be loaded |  | ||||||
|         # fine, whether they are created on 32-bit or 64-bit |  | ||||||
|         # platforms, and that version-3-pickles load fine. |  | ||||||
|         files = [("randv2_32.pck", 780), |  | ||||||
|                  ("randv2_64.pck", 866), |  | ||||||
|                  ("randv3.pck", 343)] |  | ||||||
|         for file, value in files: |  | ||||||
|             with open(support.findfile(file),"rb") as f: |  | ||||||
|                 r = pickle.load(f) |  | ||||||
|             self.assertEqual(int(r.random()*1000), value) |  | ||||||
| 
 |  | ||||||
|     def test_bug_9025(self): |  | ||||||
|         # Had problem with an uneven distribution in int(n*random()) |  | ||||||
|         # Verify the fix by checking that distributions fall within expectations. |  | ||||||
|         n = 100000 |  | ||||||
|         randrange = self.gen.randrange |  | ||||||
|         k = sum(randrange(6755399441055744) % 3 == 2 for i in range(n)) |  | ||||||
|         self.assertTrue(0.30 < k/n < .37, (k/n)) |  | ||||||
| 
 |  | ||||||
|     def test_randbytes(self): |  | ||||||
|         # Verify ranges |  | ||||||
|         for n in range(1, 10): |  | ||||||
|             data = self.gen.randbytes(n) |  | ||||||
|             self.assertEqual(type(data), bytes) |  | ||||||
|             self.assertEqual(len(data), n) |  | ||||||
| 
 |  | ||||||
|         self.assertEqual(self.gen.randbytes(0), b'') |  | ||||||
| 
 |  | ||||||
|         # Verify argument checking |  | ||||||
|         self.assertRaises(TypeError, self.gen.randbytes) |  | ||||||
|         self.assertRaises(TypeError, self.gen.randbytes, 1, 2) |  | ||||||
|         self.assertRaises(ValueError, self.gen.randbytes, -1) |  | ||||||
|         self.assertRaises(OverflowError, self.gen.randbytes, 1<<1000) |  | ||||||
|         self.assertRaises((ValueError, OverflowError), self.gen.randbytes, -1<<1000) |  | ||||||
|         self.assertRaises(TypeError, self.gen.randbytes, 1.0) |  | ||||||
| 
 |  | ||||||
|     def test_mu_sigma_default_args(self): |  | ||||||
|         self.assertIsInstance(self.gen.normalvariate(), float) |  | ||||||
|         self.assertIsInstance(self.gen.gauss(), float) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| try: |  | ||||||
|     random.SystemRandom().random() |  | ||||||
| except NotImplementedError: |  | ||||||
|     SystemRandom_available = False |  | ||||||
| else: |  | ||||||
|     SystemRandom_available = True |  | ||||||
| 
 |  | ||||||
| @unittest.skipUnless(SystemRandom_available, "random.SystemRandom not available") |  | ||||||
| class SystemRandom_TestBasicOps(TestBasicOps, unittest.TestCase): |  | ||||||
|     gen = random.SystemRandom() |  | ||||||
| 
 |  | ||||||
|     def test_autoseed(self): |  | ||||||
|         # Doesn't need to do anything except not fail |  | ||||||
|         self.gen.seed() |  | ||||||
| 
 |  | ||||||
|     def test_saverestore(self): |  | ||||||
|         self.assertRaises(NotImplementedError, self.gen.getstate) |  | ||||||
|         self.assertRaises(NotImplementedError, self.gen.setstate, None) |  | ||||||
| 
 |  | ||||||
|     def test_seedargs(self): |  | ||||||
|         # Doesn't need to do anything except not fail |  | ||||||
|         self.gen.seed(100) |  | ||||||
| 
 |  | ||||||
|     def test_gauss(self): |  | ||||||
|         self.gen.gauss_next = None |  | ||||||
|         self.gen.seed(100) |  | ||||||
|         self.assertEqual(self.gen.gauss_next, None) |  | ||||||
| 
 |  | ||||||
|     def test_pickling(self): |  | ||||||
|         for proto in range(pickle.HIGHEST_PROTOCOL + 1): |  | ||||||
|             self.assertRaises(NotImplementedError, pickle.dumps, self.gen, proto) |  | ||||||
| 
 |  | ||||||
|     def test_53_bits_per_float(self): |  | ||||||
|         # This should pass whenever a C double has 53 bit precision. |  | ||||||
|         span = 2 ** 53 |  | ||||||
|         cum = 0 |  | ||||||
|         for i in range(100): |  | ||||||
|             cum |= int(self.gen.random() * span) |  | ||||||
|         self.assertEqual(cum, span-1) |  | ||||||
| 
 | 
 | ||||||
|     def test_bigrand(self): |     def test_bigrand(self): | ||||||
|         # The randrange routine should build-up the required number of bits |         # The randrange routine should build-up the required number of bits | ||||||
|  | @ -572,6 +492,10 @@ def test_randrange_step(self): | ||||||
|             randrange(1000, step=100) |             randrange(1000, step=100) | ||||||
|         with self.assertRaises(TypeError): |         with self.assertRaises(TypeError): | ||||||
|             randrange(1000, None, step=100) |             randrange(1000, None, step=100) | ||||||
|  |         with self.assertRaises(TypeError): | ||||||
|  |             randrange(1000, step=MyIndex(1)) | ||||||
|  |         with self.assertRaises(TypeError): | ||||||
|  |             randrange(1000, None, step=MyIndex(1)) | ||||||
| 
 | 
 | ||||||
|     def test_randbelow_logic(self, _log=log, int=int): |     def test_randbelow_logic(self, _log=log, int=int): | ||||||
|         # check bitcount transition points:  2**i and 2**(i+1)-1 |         # check bitcount transition points:  2**i and 2**(i+1)-1 | ||||||
|  | @ -594,6 +518,116 @@ def test_randbelow_logic(self, _log=log, int=int): | ||||||
|             self.assertEqual(k, numbits)        # note the stronger assertion |             self.assertEqual(k, numbits)        # note the stronger assertion | ||||||
|             self.assertTrue(2**k > n > 2**(k-1))   # note the stronger assertion |             self.assertTrue(2**k > n > 2**(k-1))   # note the stronger assertion | ||||||
| 
 | 
 | ||||||
|  |     def test_randrange_index(self): | ||||||
|  |         randrange = self.gen.randrange | ||||||
|  |         self.assertIn(randrange(MyIndex(5)), range(5)) | ||||||
|  |         self.assertIn(randrange(MyIndex(2), MyIndex(7)), range(2, 7)) | ||||||
|  |         self.assertIn(randrange(MyIndex(5), MyIndex(15), MyIndex(2)), range(5, 15, 2)) | ||||||
|  | 
 | ||||||
|  |     def test_randint(self): | ||||||
|  |         randint = self.gen.randint | ||||||
|  |         self.assertIn(randint(2, 5), (2, 3, 4, 5)) | ||||||
|  |         self.assertEqual(randint(2, 2), 2) | ||||||
|  |         self.assertIn(randint(MyIndex(2), MyIndex(5)), (2, 3, 4, 5)) | ||||||
|  |         self.assertEqual(randint(MyIndex(2), MyIndex(2)), 2) | ||||||
|  | 
 | ||||||
|  |         self.assertRaises(ValueError, randint, 5, 2) | ||||||
|  |         self.assertRaises(TypeError, randint) | ||||||
|  |         self.assertRaises(TypeError, randint, 2) | ||||||
|  |         self.assertRaises(TypeError, randint, 2, 5, 1) | ||||||
|  |         self.assertRaises(TypeError, randint, 2.0, 5) | ||||||
|  |         self.assertRaises(TypeError, randint, 2, 5.0) | ||||||
|  | 
 | ||||||
|  |     def test_pickling(self): | ||||||
|  |         for proto in range(pickle.HIGHEST_PROTOCOL + 1): | ||||||
|  |             state = pickle.dumps(self.gen, proto) | ||||||
|  |             origseq = [self.gen.random() for i in range(10)] | ||||||
|  |             newgen = pickle.loads(state) | ||||||
|  |             restoredseq = [newgen.random() for i in range(10)] | ||||||
|  |             self.assertEqual(origseq, restoredseq) | ||||||
|  | 
 | ||||||
|  |     def test_bug_1727780(self): | ||||||
|  |         # verify that version-2-pickles can be loaded | ||||||
|  |         # fine, whether they are created on 32-bit or 64-bit | ||||||
|  |         # platforms, and that version-3-pickles load fine. | ||||||
|  |         files = [("randv2_32.pck", 780), | ||||||
|  |                  ("randv2_64.pck", 866), | ||||||
|  |                  ("randv3.pck", 343)] | ||||||
|  |         for file, value in files: | ||||||
|  |             with open(support.findfile(file),"rb") as f: | ||||||
|  |                 r = pickle.load(f) | ||||||
|  |             self.assertEqual(int(r.random()*1000), value) | ||||||
|  | 
 | ||||||
|  |     def test_bug_9025(self): | ||||||
|  |         # Had problem with an uneven distribution in int(n*random()) | ||||||
|  |         # Verify the fix by checking that distributions fall within expectations. | ||||||
|  |         n = 100000 | ||||||
|  |         randrange = self.gen.randrange | ||||||
|  |         k = sum(randrange(6755399441055744) % 3 == 2 for i in range(n)) | ||||||
|  |         self.assertTrue(0.30 < k/n < .37, (k/n)) | ||||||
|  | 
 | ||||||
|  |     def test_randrange_bug_1590891(self): | ||||||
|  |         start = 1000000000000 | ||||||
|  |         stop = -100000000000000000000 | ||||||
|  |         step = -200 | ||||||
|  |         x = self.gen.randrange(start, stop, step) | ||||||
|  |         self.assertTrue(stop < x <= start) | ||||||
|  |         self.assertEqual((x+stop)%step, 0) | ||||||
|  | 
 | ||||||
|  |     def test_randbytes(self): | ||||||
|  |         # Verify ranges | ||||||
|  |         for n in range(1, 10): | ||||||
|  |             data = self.gen.randbytes(n) | ||||||
|  |             self.assertEqual(type(data), bytes) | ||||||
|  |             self.assertEqual(len(data), n) | ||||||
|  | 
 | ||||||
|  |         self.assertEqual(self.gen.randbytes(0), b'') | ||||||
|  | 
 | ||||||
|  |         # Verify argument checking | ||||||
|  |         self.assertRaises(TypeError, self.gen.randbytes) | ||||||
|  |         self.assertRaises(TypeError, self.gen.randbytes, 1, 2) | ||||||
|  |         self.assertRaises(ValueError, self.gen.randbytes, -1) | ||||||
|  |         self.assertRaises(OverflowError, self.gen.randbytes, 1<<1000) | ||||||
|  |         self.assertRaises((ValueError, OverflowError), self.gen.randbytes, -1<<1000) | ||||||
|  |         self.assertRaises(TypeError, self.gen.randbytes, 1.0) | ||||||
|  | 
 | ||||||
|  |     def test_mu_sigma_default_args(self): | ||||||
|  |         self.assertIsInstance(self.gen.normalvariate(), float) | ||||||
|  |         self.assertIsInstance(self.gen.gauss(), float) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     random.SystemRandom().random() | ||||||
|  | except NotImplementedError: | ||||||
|  |     SystemRandom_available = False | ||||||
|  | else: | ||||||
|  |     SystemRandom_available = True | ||||||
|  | 
 | ||||||
|  | @unittest.skipUnless(SystemRandom_available, "random.SystemRandom not available") | ||||||
|  | class SystemRandom_TestBasicOps(TestBasicOps, unittest.TestCase): | ||||||
|  |     gen = random.SystemRandom() | ||||||
|  | 
 | ||||||
|  |     def test_autoseed(self): | ||||||
|  |         # Doesn't need to do anything except not fail | ||||||
|  |         self.gen.seed() | ||||||
|  | 
 | ||||||
|  |     def test_saverestore(self): | ||||||
|  |         self.assertRaises(NotImplementedError, self.gen.getstate) | ||||||
|  |         self.assertRaises(NotImplementedError, self.gen.setstate, None) | ||||||
|  | 
 | ||||||
|  |     def test_seedargs(self): | ||||||
|  |         # Doesn't need to do anything except not fail | ||||||
|  |         self.gen.seed(100) | ||||||
|  | 
 | ||||||
|  |     def test_gauss(self): | ||||||
|  |         self.gen.gauss_next = None | ||||||
|  |         self.gen.seed(100) | ||||||
|  |         self.assertEqual(self.gen.gauss_next, None) | ||||||
|  | 
 | ||||||
|  |     def test_pickling(self): | ||||||
|  |         for proto in range(pickle.HIGHEST_PROTOCOL + 1): | ||||||
|  |             self.assertRaises(NotImplementedError, pickle.dumps, self.gen, proto) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class TestRawMersenneTwister(unittest.TestCase): | class TestRawMersenneTwister(unittest.TestCase): | ||||||
|     @test.support.cpython_only |     @test.support.cpython_only | ||||||
|  | @ -779,38 +813,6 @@ def test_long_seed(self): | ||||||
|         seed = (1 << (10000 * 8)) - 1  # about 10K bytes |         seed = (1 << (10000 * 8)) - 1  # about 10K bytes | ||||||
|         self.gen.seed(seed) |         self.gen.seed(seed) | ||||||
| 
 | 
 | ||||||
|     def test_53_bits_per_float(self): |  | ||||||
|         # This should pass whenever a C double has 53 bit precision. |  | ||||||
|         span = 2 ** 53 |  | ||||||
|         cum = 0 |  | ||||||
|         for i in range(100): |  | ||||||
|             cum |= int(self.gen.random() * span) |  | ||||||
|         self.assertEqual(cum, span-1) |  | ||||||
| 
 |  | ||||||
|     def test_bigrand(self): |  | ||||||
|         # The randrange routine should build-up the required number of bits |  | ||||||
|         # in stages so that all bit positions are active. |  | ||||||
|         span = 2 ** 500 |  | ||||||
|         cum = 0 |  | ||||||
|         for i in range(100): |  | ||||||
|             r = self.gen.randrange(span) |  | ||||||
|             self.assertTrue(0 <= r < span) |  | ||||||
|             cum |= r |  | ||||||
|         self.assertEqual(cum, span-1) |  | ||||||
| 
 |  | ||||||
|     def test_bigrand_ranges(self): |  | ||||||
|         for i in [40,80, 160, 200, 211, 250, 375, 512, 550]: |  | ||||||
|             start = self.gen.randrange(2 ** (i-2)) |  | ||||||
|             stop = self.gen.randrange(2 ** i) |  | ||||||
|             if stop <= start: |  | ||||||
|                 continue |  | ||||||
|             self.assertTrue(start <= self.gen.randrange(start, stop) < stop) |  | ||||||
| 
 |  | ||||||
|     def test_rangelimits(self): |  | ||||||
|         for start, stop in [(-2,0), (-(2**60)-2,-(2**60)), (2**60,2**60+2)]: |  | ||||||
|             self.assertEqual(set(range(start,stop)), |  | ||||||
|                 set([self.gen.randrange(start,stop) for i in range(100)])) |  | ||||||
| 
 |  | ||||||
|     def test_getrandbits(self): |     def test_getrandbits(self): | ||||||
|         super().test_getrandbits() |         super().test_getrandbits() | ||||||
| 
 | 
 | ||||||
|  | @ -848,27 +850,6 @@ def test_randrange_uses_getrandbits(self): | ||||||
|         self.assertEqual(self.gen.randrange(2**99), |         self.assertEqual(self.gen.randrange(2**99), | ||||||
|                          97904845777343510404718956115) |                          97904845777343510404718956115) | ||||||
| 
 | 
 | ||||||
|     def test_randbelow_logic(self, _log=log, int=int): |  | ||||||
|         # check bitcount transition points:  2**i and 2**(i+1)-1 |  | ||||||
|         # show that: k = int(1.001 + _log(n, 2)) |  | ||||||
|         # is equal to or one greater than the number of bits in n |  | ||||||
|         for i in range(1, 1000): |  | ||||||
|             n = 1 << i # check an exact power of two |  | ||||||
|             numbits = i+1 |  | ||||||
|             k = int(1.00001 + _log(n, 2)) |  | ||||||
|             self.assertEqual(k, numbits) |  | ||||||
|             self.assertEqual(n, 2**(k-1)) |  | ||||||
| 
 |  | ||||||
|             n += n - 1      # check 1 below the next power of two |  | ||||||
|             k = int(1.00001 + _log(n, 2)) |  | ||||||
|             self.assertIn(k, [numbits, numbits+1]) |  | ||||||
|             self.assertTrue(2**k > n > 2**(k-2)) |  | ||||||
| 
 |  | ||||||
|             n -= n >> 15     # check a little farther below the next power of two |  | ||||||
|             k = int(1.00001 + _log(n, 2)) |  | ||||||
|             self.assertEqual(k, numbits)        # note the stronger assertion |  | ||||||
|             self.assertTrue(2**k > n > 2**(k-1))   # note the stronger assertion |  | ||||||
| 
 |  | ||||||
|     def test_randbelow_without_getrandbits(self): |     def test_randbelow_without_getrandbits(self): | ||||||
|         # Random._randbelow() can only use random() when the built-in one |         # Random._randbelow() can only use random() when the built-in one | ||||||
|         # has been overridden but no new getrandbits() method was supplied. |         # has been overridden but no new getrandbits() method was supplied. | ||||||
|  | @ -903,14 +884,6 @@ def test_randbelow_without_getrandbits(self): | ||||||
|             self.gen._randbelow_without_getrandbits(n, maxsize=maxsize) |             self.gen._randbelow_without_getrandbits(n, maxsize=maxsize) | ||||||
|             self.assertEqual(random_mock.call_count, 2) |             self.assertEqual(random_mock.call_count, 2) | ||||||
| 
 | 
 | ||||||
|     def test_randrange_bug_1590891(self): |  | ||||||
|         start = 1000000000000 |  | ||||||
|         stop = -100000000000000000000 |  | ||||||
|         step = -200 |  | ||||||
|         x = self.gen.randrange(start, stop, step) |  | ||||||
|         self.assertTrue(stop < x <= start) |  | ||||||
|         self.assertEqual((x+stop)%step, 0) |  | ||||||
| 
 |  | ||||||
|     def test_choices_algorithms(self): |     def test_choices_algorithms(self): | ||||||
|         # The various ways of specifying weights should produce the same results |         # The various ways of specifying weights should produce the same results | ||||||
|         choices = self.gen.choices |         choices = self.gen.choices | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Serhiy Storchaka
						Serhiy Storchaka