diff --git a/hwe/ts/util/RandUtil.ts b/hwe/ts/util/RandUtil.ts index e6ac149e..5e3bfa89 100644 --- a/hwe/ts/util/RandUtil.ts +++ b/hwe/ts/util/RandUtil.ts @@ -37,6 +37,9 @@ export class RandUtil { public shuffle(srcArray: T[]): T[] { const cnt = srcArray.length; + if(cnt === 0){ + return []; + } if (cnt > this.rng.getMaxInt()) { throw 'Invalid random int range'; } @@ -54,6 +57,9 @@ export class RandUtil { public choice(items: T[] | Record | Set): T { if (items instanceof Array) { + if(items.length === 0){ + throw new Error('Empty items'); + } const idx = this.rng.nextInt(items.length - 1); return items[idx]; } @@ -66,6 +72,9 @@ export class RandUtil { } public choiceUsingWeight(items: Record): string | number { + if(Object.keys(items).length === 0){ + throw new Error('Empty items'); + } let sum = 0; for (const value of Object.values(items)) { if (value <= 0) { @@ -94,6 +103,9 @@ export class RandUtil { } public choiceUsingWeightPair(items: [T, number][]): T { + if(items.length === 0){ + throw new Error('Empty items'); + } let sum = 0; for (const [, value] of items) { if (value <= 0) { diff --git a/src/sammo/RandUtil.php b/src/sammo/RandUtil.php index ab776e7d..67b11294 100644 --- a/src/sammo/RandUtil.php +++ b/src/sammo/RandUtil.php @@ -47,6 +47,9 @@ class RandUtil public function shuffle(array $srcArray): array { + if(!$srcArray){ + return $srcArray; + } $cnt = count($srcArray); if ($cnt > $this->rng->getMaxInt()) { throw new \InvalidArgumentException("Invalid random int range"); @@ -69,6 +72,9 @@ class RandUtil public function shuffleAssoc(array $srcArray): array { + if(!$srcArray){ + return $srcArray; + } $result = []; foreach ($this->shuffle(array_keys($srcArray)) as $key) { $result[$key] = $srcArray[$key]; diff --git a/tests/RNGTest.php b/tests/RNGTest.php new file mode 100644 index 00000000..66172a43 --- /dev/null +++ b/tests/RNGTest.php @@ -0,0 +1,212 @@ + 0; $remainFill--) { + $tmp[] = $filler; + } + + $result = join("", $tmp); + + $moreLen = $length - strlen($result); + if ($moreLen === 0) { + return $result; + } + + return $result . substr($filler, 0, $moreLen); +} + + +class DummyBlockRNG extends LiteHashDRBG +{ + private int $repeatBlockCnt; + /** + * + * @param string[] $repeatBlock + * @param int $stateIdx + * @return void + * @throws RuntimeException + */ + public function __construct(protected array $repeatBlock, protected int $stateIdx = 0) + { + foreach ($repeatBlock as $block) { + if (strlen($block) != BLOCK_SIZE) { + throw new RuntimeException('Invalid repeat block'); + } + } + $this->repeatBlockCnt = count($this->repeatBlock); + $this->genNextBlock(); + } + + protected function genNextBlock(): void + { + $this->buffer = $this->repeatBlock[$this->stateIdx]; + $this->bufferIdx = 0; + $this->stateIdx = ($this->stateIdx + 1) % $this->repeatBlockCnt; + } +} + +class RNGTest extends PHPUnit\Framework\TestCase +{ + + const FIXED_KEY = 'HelloWorld'; + public function testDummyRNG() + { + //Block의 값을 고정하고 입력값을 확인 + $rng = new DummyBlockRNG([ + fillBlock('', "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff") + ]); + + $this->setName('DummyRNG-SimpleByte'); + $this->assertEquals("\x00", $rng->nextBytes(1)); + $this->assertEquals("\x11\x22", $rng->nextBytes(2)); + $this->assertEquals("\x33\x44\x55", $rng->nextBytes(3)); + $this->assertEquals("\x66\x77\x88\x99", $rng->nextBytes(4)); + + $this->setName('DummyRNG-OverflowBlock'); + foreach (range(1, 16) as $_i) { + $this->assertEquals( + "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", + $rng->nextBytes(16) + ); + } + + $this->setName('DummyRNG-MultiBlock'); + $this->assertEquals( + fillBlock('', "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", BLOCK_SIZE * 2), + $rng->nextBytes(BLOCK_SIZE * 2) + ); + + $this->setName('DummyRNG-BitTest'); + $this->assertEquals("\x00", $rng->nextBits(1)); //aa + $this->assertEquals("\x01", $rng->nextBits(1)); //bb + $this->assertEquals("\xcc", $rng->nextBits(8)); //cc + $this->assertEquals("\xdd\x02", $rng->nextBits(10)); //ddee + $this->assertEquals("\x7f", $rng->nextBits(7)); //ff + $this->assertEquals("\x00\x11\x22\x33\x44\x55\x06", $rng->nextBits(53)); + + $this->setName('DummyRNG-Int'); + $this->assertEquals(0x77, $rng->nextInt(0xff)); + $this->assertEquals(0x9988, $rng->nextInt((1 << 16) - 1)); + $this->assertEquals(0xddccbbaa, $rng->nextInt((1 << 32) - 1)); + $this->assertEquals(0x0433221100ffee, $rng->nextInt()); + $this->assertEquals(0x05, $rng->nextInt(0x0f)); //55 + $this->assertEquals(0x06, $rng->nextInt(0x12)); //66 + $this->assertEquals(0x08, $rng->nextInt(99)); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8) + $this->assertEquals(0x99, $rng->nextInt(0x99)); //99 + $this->assertEquals(0xaa, $rng->nextInt(0xaa)); //aa (fit Max) + + $floatMax = 1 << 53; + $this->setName('DummyRNG-Float'); //7개씩 + $fa = $rng->nextFloat1(); + $this->assertEquals(0x1100ffeeddccbb / $floatMax, $fa, 'float1-1'); + $this->assertIsFloat($fa); + $this->assertLessThan(0.5313720384, $fa); + $this->assertGreaterThan(0.5313720383, $fa); + + $fb = $rng->nextFloat1(); + $this->assertEquals(0x08776655443322 / $floatMax, $fb, 'float1-2'); + } + + public function testRNGInvalidNextBits0() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBits(0); + } + + public function testRNGInvalidNextBitsMinus1() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBits(-1); + } + + public function testRNGInvalidNextBytes0() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBytes(0); + } + + public function testRNGInvalidNextBytesMinus1() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBytes(-1); + } + + public function testRNGInvalidNextIntOverflow() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextInt(1 << 53); + } + + public function testRandUtilEmptyChoice() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choice([]); + } + + public function testRandUtilEmptychoiceUsingWeight() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choiceUsingWeight([]); + } + + public function testRandUtilEmptychoiceUsingWeightPair() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choiceUsingWeightPair([]); + } + + /** + * @doesNotPerformAssertions + */ + public function testRNGAcceptable() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $rng->nextInt(0); + $rng->nextInt(1 << 53 - 1); + $rng->nextBytes(65); + $rng->nextBits(512); + + $randUtil = new RandUtil($rng); + $randUtil->choice([0, 0, 0]); + $randUtil->choiceUsingWeight([0 => 0, 1 => -1]); + $randUtil->choiceUsingWeightPair([ + [0, 0], + [1, 0], + [2, -1], + ]); + $randUtil->nextBool(1.1); + $randUtil->nextBool(-0.1); + $randUtil->shuffle([]); + $randUtil->shuffle([1]); + $randUtil->shuffleAssoc([]); + $randUtil->shuffleAssoc([1]); + $randUtil->nextRange(0, 0); + $randUtil->nextRangeInt(0, 0); + $randUtil->nextRange(1, -1); + $randUtil->nextRangeInt(1, -1); + } +}