diff --git a/src/Math.php b/src/Math.php index 7f939da..a579131 100644 --- a/src/Math.php +++ b/src/Math.php @@ -92,9 +92,15 @@ public static function rand($min, $max, $strong = false) 'The supplied range is too great to generate' ); } - $bytes = (int) max(log($range,2) / 8, 1); - $rnd = hexdec(bin2hex(self::randBytes($bytes, $strong))); - return $min + $rnd % ($range+1); + $log = log($range, 2); + $bytes = (int) ($log / 8) + 1; + $bits = (int) $log + 1; + $filter = (int) (1 << $bits) - 1; + do { + $rnd = hexdec(bin2hex(self::randBytes($bytes, $strong))); + $rnd = $rnd & $filter; + } while ($rnd > $range); + return $min + $rnd; } /** diff --git a/test/MathTest.php b/test/MathTest.php index 1ed3104..3a48a50 100644 --- a/test/MathTest.php +++ b/test/MathTest.php @@ -33,22 +33,65 @@ */ class MathTest extends \PHPUnit_Framework_TestCase { + public static function provideRandInt() + { + return array( + array(2, 1, 10000, 100, 0.9, 1.1, false), + array(2, 1, 10000, 100, 0.8, 1.2, true) + ); + } + public function testRandBytes() { for ($length=1; $length<4096; $length++) { $rand = Math::randBytes($length); $this->assertTrue(!empty($rand)); - $this->assertEquals(strlen($rand), $length); + $this->assertEquals($length, strlen($rand)); } } - public function testRand() + /** + * A Monte Carlo test that generates $cycles numbers from 0 to $tot + * and test if the numbers are above or below the line y=x with a + * frequency range of [$min, $max] + * + * Note: this code is inspired by the random number generator test + * included in the PHP-CryptLib project of Anthony Ferrara + * @see https://github.com/ircmaxell/PHP-CryptLib + * + * @dataProvider provideRandInt + */ + public function testRandInt($num, $valid, $cycles, $tot, $min, $max, $strong) { - for ($i=0; $i<10000; $i++) { - $min = mt_rand(0,10000); - $max = $min + mt_rand(0,10000); - $rand = Math::rand($min, $max); - $this->assertTrue(($rand >= $min) && ($rand <= $max)); + try { + $test = Math::randBytes(1, $strong); + } catch (\Zend\Math\Exception\RuntimeException $e) { + $this->markTestSkipped($e->getMessage()); + } + $i = 0; + $count = 0; + do { + $up = 0; + $down = 0; + for ($i=0; $i<$cycles; $i++) { + $x = Math::rand(0, $tot, $strong); + $y = Math::rand(0, $tot, $strong); + if ($x > $y) { + $up++; + } elseif ($x < $y) { + $down++; + } + } + $this->assertGreaterThan(0, $up); + $this->assertGreaterThan(0, $down); + $ratio = $up / $down; + if ($ratio > $min && $ratio < $max) { + $count++; + } + $i++; + } while ($i < $num && $count < $valid); + if ($count < $valid) { + $this->fail('The random number generator failed the Monte Carlo test'); } }