From 2578e154d447dd74aa00bf40774a1f87c4ec5575 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Thu, 17 May 2012 17:17:35 +0200 Subject: [PATCH] New commands: BITOP, BITCOUNT (Redis v2.6-dev). --- .../Command/Hash/CommandHashStrategy.php | 17 ++ lib/Predis/Command/PrefixHelpers.php | 18 ++ lib/Predis/Command/StringBitCount.php | 27 +++ lib/Predis/Command/StringBitOp.php | 49 +++++ .../Connection/MasterSlaveReplication.php | 1 + lib/Predis/Profile/ServerVersion26.php | 2 + .../Command/Hash/CommandHashStrategyTest.php | 18 ++ tests/Predis/Command/StringBitCountTest.php | 108 ++++++++++ tests/Predis/Command/StringBitOpTest.php | 193 ++++++++++++++++++ tests/Predis/Profile/ServerVersion26Test.php | 12 +- .../Predis/Profile/ServerVersionNextTest.php | 12 +- 11 files changed, 447 insertions(+), 10 deletions(-) create mode 100644 lib/Predis/Command/StringBitCount.php create mode 100644 lib/Predis/Command/StringBitOp.php create mode 100644 tests/Predis/Command/StringBitCountTest.php create mode 100644 tests/Predis/Command/StringBitOpTest.php diff --git a/lib/Predis/Command/Hash/CommandHashStrategy.php b/lib/Predis/Command/Hash/CommandHashStrategy.php index fc436694..f878d782 100644 --- a/lib/Predis/Command/Hash/CommandHashStrategy.php +++ b/lib/Predis/Command/Hash/CommandHashStrategy.php @@ -75,6 +75,8 @@ class CommandHashStrategy implements CommandHashStrategyInterface 'SETRANGE' => $keyIsFirstArgument, 'STRLEN' => $keyIsFirstArgument, 'SUBSTR' => $keyIsFirstArgument, + 'BITOP' => array($this, 'getKeyFromBitOp'), + 'BITCOUNT' => $keyIsFirstArgument, /* commands operating on lists */ 'LINSERT' => $keyIsFirstArgument, @@ -217,6 +219,21 @@ class CommandHashStrategy implements CommandHashStrategyInterface } } + /** + * Extracts the key from BITOP command. + * + * @param CommandInterface $command Command instance. + * @return string + */ + protected function getKeyFromBitOp(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if ($this->checkSameHashForKeys(array_slice($arguments, 1, count($arguments)))) { + return $arguments[1]; + } + } + /** * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands. * diff --git a/lib/Predis/Command/PrefixHelpers.php b/lib/Predis/Command/PrefixHelpers.php index 4bc5675b..5cd62839 100644 --- a/lib/Predis/Command/PrefixHelpers.php +++ b/lib/Predis/Command/PrefixHelpers.php @@ -67,6 +67,24 @@ class PrefixHelpers $command->setRawArguments($arguments); } + /** + * Applies the specified prefix to all the arguments but the first one. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function skipFirst(CommandInterface $command, $prefix) + { + $arguments = $command->getArguments(); + $length = count($arguments); + + for ($i = 1; $i < $length; $i++) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + /** * Applies the specified prefix to all the arguments but the last one. * diff --git a/lib/Predis/Command/StringBitCount.php b/lib/Predis/Command/StringBitCount.php new file mode 100644 index 00000000..19d534b0 --- /dev/null +++ b/lib/Predis/Command/StringBitCount.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitcount + * @author Daniele Alessandri + */ +class StringBitCount extends PrefixableCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITCOUNT'; + } +} diff --git a/lib/Predis/Command/StringBitOp.php b/lib/Predis/Command/StringBitOp.php new file mode 100644 index 00000000..0236bb17 --- /dev/null +++ b/lib/Predis/Command/StringBitOp.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/bitop + * @author Daniele Alessandri + */ +class StringBitOp extends AbstractCommand implements PrefixableCommandInterface +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITOP'; + } + + /** + * {@inheritdoc} + */ + protected function filterArguments(Array $arguments) + { + if (count($arguments) === 3 && is_array($arguments[2])) { + list($operation, $destination, ) = $arguments; + $arguments = $arguments[2]; + array_unshift($arguments, $operation, $destination); + } + + return $arguments; + } + + /** + * {@inheritdoc} + */ + public function prefixKeys($prefix) + { + PrefixHelpers::skipFirst($this, $prefix); + } +} diff --git a/lib/Predis/Connection/MasterSlaveReplication.php b/lib/Predis/Connection/MasterSlaveReplication.php index 8bec2209..32e38892 100644 --- a/lib/Predis/Connection/MasterSlaveReplication.php +++ b/lib/Predis/Connection/MasterSlaveReplication.php @@ -409,6 +409,7 @@ class MasterSlaveReplication implements ReplicationConnectionInterface 'ECHO' => true, 'QUIT' => true, 'OBJECT' => true, + 'BITCOUNT' => true, 'TIME' => true, 'SORT' => array($this, 'isSortReadOnly'), ); diff --git a/lib/Predis/Profile/ServerVersion26.php b/lib/Predis/Profile/ServerVersion26.php index 5cf36701..337f8cee 100644 --- a/lib/Predis/Profile/ServerVersion26.php +++ b/lib/Predis/Profile/ServerVersion26.php @@ -217,6 +217,8 @@ class ServerVersion26 extends ServerProfile /* commands operating on string values */ 'psetex' => 'Predis\Command\StringPreciseSetExpire', 'incrbyfloat' => 'Predis\Command\StringIncrementByFloat', + 'bitop' => 'Predis\Command\StringBitOp', + 'bitcount' => 'Predis\Command\StringBitCount', /* commands operating on hashes */ 'hincrbyfloat' => 'Predis\Command\HashIncrementByFloat', diff --git a/tests/Predis/Command/Hash/CommandHashStrategyTest.php b/tests/Predis/Command/Hash/CommandHashStrategyTest.php index c735f5e3..178d5168 100644 --- a/tests/Predis/Command/Hash/CommandHashStrategyTest.php +++ b/tests/Predis/Command/Hash/CommandHashStrategyTest.php @@ -140,6 +140,22 @@ class CommandHashStrategyTest extends StandardTestCase } } + /** + * @group disconnected + */ + public function testKeysForBitOpCommand() + { + $distribution = new HashRing(); + $hashstrategy = new CommandHashStrategy(); + $profile = ServerProfile::getDevelopment(); + $arguments = array('AND', '{key}:destination', '{key}:src:1', '{key}:src:2'); + + foreach ($this->getExpectedCommands('keys-bitop') as $commandID) { + $command = $profile->createCommand($commandID, $arguments); + $this->assertNotNull($hashstrategy->getHash($distribution, $command), $commandID); + } + } + // ******************************************************************** // // ---- HELPER METHODS ------------------------------------------------ // // ******************************************************************** // @@ -186,6 +202,8 @@ class CommandHashStrategyTest extends StandardTestCase 'SETRANGE' => 'keys-first', 'STRLEN' => 'keys-first', 'SUBSTR' => 'keys-first', + 'BITOP' => 'keys-bitop', + 'BITCOUNT' => 'keys-first', /* commands operating on lists */ 'LINSERT' => 'keys-first', diff --git a/tests/Predis/Command/StringBitCountTest.php b/tests/Predis/Command/StringBitCountTest.php new file mode 100644 index 00000000..0454e14d --- /dev/null +++ b/tests/Predis/Command/StringBitCountTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +use \PHPUnit_Framework_TestCase as StandardTestCase; + +/** + * @group commands + * @group realm-string + */ +class StringBitCountTest extends CommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand() + { + return 'Predis\Command\StringBitCount'; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId() + { + return 'BITCOUNT'; + } + + /** + * @group disconnected + */ + public function testFilterArguments() + { + $arguments = array('key', 0, 10); + $expected = array('key', 0, 10); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse() + { + $raw = 10; + $expected = 10; + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group disconnected + */ + public function testPrefixKeys() + { + $arguments = array('key', 0, 10); + $expected = array('prefix:key', 0, 10); + + $command = $this->getCommandWithArgumentsArray($arguments); + $command->prefixKeys('prefix:'); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group connected + */ + public function testReturnsNumberOfBitsSet() + { + $redis = $this->getClient(); + + $redis->setbit('key', 1, 1); + $redis->setbit('key', 10, 1); + $redis->setbit('key', 16, 1); + $redis->setbit('key', 22, 1); + $redis->setbit('key', 32, 1); + + $this->assertSame(5, $redis->bitcount('key'), 'Count bits set (without range)'); + $this->assertSame(3, $redis->bitcount('key', 2, 4), 'Count bits set (with range)'); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage ERR Operation against a key holding the wrong kind of value + */ + public function testThrowsExceptionOnWrongType() + { + $redis = $this->getClient(); + + $redis->lpush('key', 'list'); + $redis->bitcount('key'); + } +} diff --git a/tests/Predis/Command/StringBitOpTest.php b/tests/Predis/Command/StringBitOpTest.php new file mode 100644 index 00000000..93de7a7c --- /dev/null +++ b/tests/Predis/Command/StringBitOpTest.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +use \PHPUnit_Framework_TestCase as StandardTestCase; + +/** + * @group commands + * @group realm-string + */ +class StringBitOpTest extends CommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand() + { + return 'Predis\Command\StringBitOp'; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId() + { + return 'BITOP'; + } + + /** + * @group disconnected + */ + public function testFilterArguments() + { + $arguments = array('AND', 'key:dst', 'key:01', 'key:02'); + $expected = array('AND', 'key:dst', 'key:01', 'key:02'); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsKeysAsSingleArray() + { + $arguments = array('AND', 'key:dst', array('key:01', 'key:02')); + $expected = array('AND', 'key:dst', 'key:01', 'key:02'); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse() + { + $raw = 10; + $expected = 10; + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group disconnected + */ + public function testPrefixKeys() + { + $arguments = array('AND', 'key:dst', 'key:01', 'key:02'); + $expected = array('AND', 'prefix:key:dst', 'prefix:key:01', 'prefix:key:02'); + + $command = $this->getCommandWithArgumentsArray($arguments); + $command->prefixKeys('prefix:'); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group connected + */ + public function testCanPerformBitwiseAND() + { + $redis = $this->getClient(); + + $redis->set('key:src:1', "h\x80"); + $redis->set('key:src:2', "R"); + + $this->assertSame(2, $redis->bitop('AND', 'key:dst', 'key:src:1', 'key:src:2')); + $this->assertSame("@\x00", $redis->get('key:dst')); + } + + /** + * @group connected + */ + public function testCanPerformBitwiseOR() + { + $redis = $this->getClient(); + + $redis->set('key:src:1', "h\x80"); + $redis->set('key:src:2', "R"); + + $this->assertSame(2, $redis->bitop('OR', 'key:dst', 'key:src:1', 'key:src:2')); + $this->assertSame("z\x80", $redis->get('key:dst')); + } + + /** + * @group connected + */ + public function testCanPerformBitwiseXOR() + { + $redis = $this->getClient(); + + $redis->set('key:src:1', "h\x80"); + $redis->set('key:src:2', "R"); + + $this->assertSame(2, $redis->bitop('XOR', 'key:dst', 'key:src:1', 'key:src:2')); + $this->assertSame(":\x80", $redis->get('key:dst')); + } + + /** + * @group connected + */ + public function testCanPerformBitwiseNOT() + { + $redis = $this->getClient(); + + $redis->set('key:src:1', "h\x80"); + + $this->assertSame(2, $redis->bitop('NOT', 'key:dst', 'key:src:1')); + $this->assertSame("\x97\x7f", $redis->get('key:dst')); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage ERR BITOP NOT must be called with a single source key. + */ + public function testBitwiseNOTAcceptsOnlyOneSourceKey() + { + $this->getClient()->bitop('NOT', 'key:dst', 'key:src:1', 'key:src:2'); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage ERR syntax error + */ + public function testThrowsExceptionOnInvalidOperation() + { + $this->getClient()->bitop('NOOP', 'key:dst', 'key:src:1', 'key:src:2'); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage ERR Operation against a key holding the wrong kind of value + */ + public function testThrowsExceptionOnInvalidSourceKey() + { + $redis = $this->getClient(); + + $redis->lpush('key:src:1', 'list'); + $redis->bitop('AND', 'key:dst', 'key:src:1', 'key:src:2'); + } + + /** + * @group connected + */ + public function testDoesNotThrowExceptionOnInvalidDestinationKey() + { + $redis = $this->getClient(); + + $redis->lpush('key:dst', 'list'); + $redis->bitop('AND', 'key:dst', 'key:src:1', 'key:src:2'); + + $this->assertSame('none', $redis->type('key:dst')); + } +} diff --git a/tests/Predis/Profile/ServerVersion26Test.php b/tests/Predis/Profile/ServerVersion26Test.php index 25f5791c..64b87bd5 100644 --- a/tests/Predis/Profile/ServerVersion26Test.php +++ b/tests/Predis/Profile/ServerVersion26Test.php @@ -164,11 +164,13 @@ class ServerVersion26Test extends ServerVersionTestCase 123 => 'pexpireat', 124 => 'psetex', 125 => 'incrbyfloat', - 126 => 'hincrbyfloat', - 127 => 'eval', - 128 => 'evalsha', - 129 => 'script', - 130 => 'time', + 126 => 'bitop', + 127 => 'bitcount', + 128 => 'hincrbyfloat', + 129 => 'eval', + 130 => 'evalsha', + 131 => 'script', + 132 => 'time', ); } } diff --git a/tests/Predis/Profile/ServerVersionNextTest.php b/tests/Predis/Profile/ServerVersionNextTest.php index 2583e3a6..477d5511 100644 --- a/tests/Predis/Profile/ServerVersionNextTest.php +++ b/tests/Predis/Profile/ServerVersionNextTest.php @@ -164,11 +164,13 @@ class ServerVersionNextTest extends ServerVersionTestCase 123 => 'pexpireat', 124 => 'psetex', 125 => 'incrbyfloat', - 126 => 'hincrbyfloat', - 127 => 'eval', - 128 => 'evalsha', - 129 => 'script', - 130 => 'time', + 126 => 'bitop', + 127 => 'bitcount', + 128 => 'hincrbyfloat', + 129 => 'eval', + 130 => 'evalsha', + 131 => 'script', + 132 => 'time', ); } }