Переглянути джерело

Backported changes from the mainline library to the PHP 5.2 branch (up to commit 35f7ea2)

Daniele Alessandri 15 роки тому
батько
коміт
7806614041
9 змінених файлів з 1966 додано та 381 видалено
  1. 77 0
      CHANGELOG
  2. 1 1
      LICENSE
  3. 6 5
      README.markdown
  4. 5 0
      TODO
  5. 522 146
      lib/Predis.php
  6. 496 0
      lib/Predis_Compatibility.php
  7. 503 0
      test/PredisClientFeatures.php
  8. 66 9
      test/PredisShared.php
  9. 290 220
      test/RedisCommandsTest.php

+ 77 - 0
CHANGELOG

@@ -1,3 +1,80 @@
+v0.6.0
+  * Completely switched to the new multi-bulk request protocol for all of the 
+    commands in the Redis 1.2 and Redis 2.0 profiles. Inline and bulk requests 
+    are now deprecated as they will be removed in future releases of Redis.
+
+  * Support for Redis 1.0 is now optional and it is provided by requiring 
+    'Predis_Compatibility.php' before creating an instance of Predis_Client.
+
+  * New commands added to the Redis 2.0 profile since Predis 0.5.1:
+      - Strings: SETEX, APPEND, SUBSTR
+      - ZSets  : ZCOUNT, ZRANK, ZUNIONSTORE, ZINTERSTORE, ZREMBYRANK, ZREVRANK
+      - Hashes : HSET, HSETNX, HMSET, HINCRBY, HGET, HMGET, HDEL, HEXISTS, 
+                 HLEN, HKEYS, HVALS, HGETALL
+      - PubSub : PUBLISH, SUBSCRIBE, UNSUBSCRIBE
+      - Misc.  : DISCARD, CONFIG
+
+  * Introduced client-level options with the new Predis_ClientOptions class. 
+    Options can be passed to Predis_Client::__construct in its second argument 
+    as an array or an instance of Predis_ClientOptions. For brevity's sake and 
+    compatibility with older versions, the constructor of Predis_Client still 
+    accepts an instance of Predis_RedisServerProfile in its second argument.
+    The currently supported client options are:
+      - profile [default: "2.0" as of Predis 0.6.0]
+        specifies which server profile to use when connecting to Redis. This 
+        option accepts an instance of Predis_RedisServerProfile or a string 
+        that indicates the target version.
+      - key_distribution [default: Predis_Utilities_HashRing]
+        specifies which key distribution algorithm to use to distribute keys 
+        among the servers that compose a cluster. This option accepts an 
+        instance of Predis_IDistributionAlgorithm so that users can implement 
+        their own key distribution strategy. Optionally, the new 
+        Predis_Utilities_KetamaPureRing class also provides a pure-PHP 
+        implementation of the Ketama algorithm.
+      - throw_on_error [default: TRUE]
+        server errors can optionally be handled "silently": instead of throwing 
+        an exception, the client returns an error response type.
+      - iterable_multibulk [EXPERIMENTAL - default: FALSE]
+        in addition to the classic way of fetching a whole multibulk reply 
+        into an array, the client can now optionally stream a multibulk reply 
+        down to the user code by using PHP iterators. It is just a little bit 
+        slower, but it can save a lot of memory in certain scenarios.
+
+  * New parameters for connections: 
+      - alias [default: not set]
+        every connection can now be identified by an alias that is useful to 
+        get a certain connection when connected to a cluster of Redis servers.
+      - weight [default: not set]
+        allows the client to balance the keys asymmetrically across multiple 
+        servers. This might be useful when you have servers with different 
+        amounts of memory and you want to distribute the load of your keys 
+        accordingly.
+      - connection_async [default: FALSE]
+        estabilish connections to servers in a non-blocking way, so that the 
+        client is not blocked while the underlying resource performs the actual 
+        connection.
+      - connection_persistent [default: FALSE]
+        the underlying connection resource is left open when a script ends its 
+        lifecycle. Persistent connections can lead to unpredictable or strange 
+        behaviours, so they should be used with extreme care.
+
+  * Connections now support float values for the connection_timeout parameter 
+    to express timeouts with a microsecond resolution.
+
+  * CommandPipeline and MultiExecBlock return their instances when invoking 
+    commands, thus allowing method chaining in pipelines and multi-exec blocks.
+
+  * MultiExecBlock instances can handle the new DISCARD command.
+
+  * The GET parameter for the SORT command now accepts also multiple key 
+    patterns by passing an array of strings.
+
+  * KEYS will return a multibulk reply starting from Redis 2.0 (DEV). Predis 
+    handles this change in a backwards-compatible way.
+
+  * Switched to class-based handlers instead of anonymous functions to 
+    handle the various server response types.
+
 v0.5.1
   * RPOPLPUSH has been changed from bulk command to inline command in Redis
     1.2.1, so ListPopLastPushHead now extends InlineCommand. The old RPOPLPUSH

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 Copyright (c) 2009-2010 Daniele Alessandri
-qq
+
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation
 files (the "Software"), to deal in the Software without

+ 6 - 5
README.markdown

@@ -3,9 +3,7 @@
 ## About ##
 
 Predis is a flexible and feature-complete PHP client library for the Redis key-value 
-database.
-
-Predis is currently a work-in-progress and it comes in two flavors:
+database. It currently comes in two flavors:
 
  - the mainline client library, which targets PHP 5.3.x and leverages a lot of the 
    features introduced in this new version of the PHP interpreter.
@@ -17,9 +15,9 @@ Please refer to the TODO file to see which issues are still pending and what is
 to be implemented soon in Predis.
 
 
-## Features ##
+## Main features ##
 
-- Client-side sharding (support for consistent hashing of keys)
+- Client-side sharding (support for consistent hashing and custom distribution algorithms)
 - Command pipelining on single and multiple connections (transparent)
 - Lazy connections (connections to Redis instances are only established just in time)
 - Flexible system to define and register your own set of commands to a client instance
@@ -133,6 +131,9 @@ variable set to E_ALL.
 
 [Daniele Alessandri](mailto:suppakilla@gmail.com)
 
+## Contributors ##
+
+[Lorenzo Castelli](http://github.com/lcastelli)
 
 ## License ##
 

+ 5 - 0
TODO

@@ -4,3 +4,8 @@
 * The included test suite covers almost all the Redis server commands, but a 
   full battery of tests targeting specific functions of this library is still 
   missing.
+
+* Add a PubSubBlock class.
+
+* Missing tests for new commands: 
+    PubSub  : PUBLISH, SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE

Різницю між файлами не показано, бо вона завелика
+ 522 - 146
lib/Predis.php


+ 496 - 0
lib/Predis_Compatibility.php

@@ -0,0 +1,496 @@
+<?php
+Predis_RedisServerProfile::registerProfile('Predis_RedisServer_v1_0', '1.0');
+
+class Predis_RedisServer_v1_0 extends Predis_RedisServerProfile {
+    public function getVersion() { return '1.0'; }
+    public function getSupportedCommands() {
+        return array(
+            /* miscellaneous commands */
+            'ping'      => 'Predis_Compatibility_v1_0_Commands_Ping',
+            'echo'      => 'Predis_Compatibility_v1_0_Commands_DoEcho',
+            'auth'      => 'Predis_Compatibility_v1_0_Commands_Auth',
+
+            /* connection handling */
+            'quit'      => 'Predis_Compatibility_v1_0_Commands_Quit',
+
+            /* commands operating on string values */
+            'set'                     => 'Predis_Compatibility_v1_0_Commands_Set',
+            'setnx'                   => 'Predis_Compatibility_v1_0_Commands_SetPreserve',
+                'setPreserve'         => 'Predis_Compatibility_v1_0_Commands_SetPreserve',
+            'get'                     => 'Predis_Compatibility_v1_0_Commands_Get',
+            'mget'                    => 'Predis_Compatibility_v1_0_Commands_GetMultiple',
+                'getMultiple'         => 'Predis_Compatibility_v1_0_Commands_GetMultiple',
+            'getset'                  => 'Predis_Compatibility_v1_0_Commands_GetSet',
+                'getSet'              => 'Predis_Compatibility_v1_0_Commands_GetSet',
+            'incr'                    => 'Predis_Compatibility_v1_0_Commands_Increment',
+                'increment'           => 'Predis_Compatibility_v1_0_Commands_Increment',
+            'incrby'                  => 'Predis_Compatibility_v1_0_Commands_IncrementBy',
+                'incrementBy'         => 'Predis_Compatibility_v1_0_Commands_IncrementBy',
+            'decr'                    => 'Predis_Compatibility_v1_0_Commands_Decrement',
+                'decrement'           => 'Predis_Compatibility_v1_0_Commands_Decrement',
+            'decrby'                  => 'Predis_Compatibility_v1_0_Commands_DecrementBy',
+                'decrementBy'         => 'Predis_Compatibility_v1_0_Commands_DecrementBy',
+            'exists'                  => 'Predis_Compatibility_v1_0_Commands_Exists',
+            'del'                     => 'Predis_Compatibility_v1_0_Commands_Delete',
+                'delete'              => 'Predis_Compatibility_v1_0_Commands_Delete',
+            'type'                    => 'Predis_Compatibility_v1_0_Commands_Type',
+
+            /* commands operating on the key space */
+            'keys'               => 'Predis_Compatibility_v1_0_Commands_Keys',
+            'randomkey'          => 'Predis_Compatibility_v1_0_Commands_RandomKey',
+                'randomKey'      => 'Predis_Compatibility_v1_0_Commands_RandomKey',
+            'rename'             => 'Predis_Compatibility_v1_0_Commands_Rename',
+            'renamenx'           => 'Predis_Compatibility_v1_0_Commands_RenamePreserve',
+                'renamePreserve' => 'Predis_Compatibility_v1_0_Commands_RenamePreserve',
+            'expire'             => 'Predis_Compatibility_v1_0_Commands_Expire',
+            'expireat'           => 'Predis_Compatibility_v1_0_Commands_ExpireAt',
+                'expireAt'       => 'Predis_Compatibility_v1_0_Commands_ExpireAt',
+            'dbsize'             => 'Predis_Compatibility_v1_0_Commands_DatabaseSize',
+                'databaseSize'   => 'Predis_Compatibility_v1_0_Commands_DatabaseSize',
+            'ttl'                => 'Predis_Compatibility_v1_0_Commands_TimeToLive',
+                'timeToLive'     => 'Predis_Compatibility_v1_0_Commands_TimeToLive',
+
+            /* commands operating on lists */
+            'rpush'            => 'Predis_Compatibility_v1_0_Commands_ListPushTail',
+                'pushTail'     => 'Predis_Compatibility_v1_0_Commands_ListPushTail',
+            'lpush'            => 'Predis_Compatibility_v1_0_Commands_ListPushHead',
+                'pushHead'     => 'Predis_Compatibility_v1_0_Commands_ListPushHead',
+            'llen'             => 'Predis_Compatibility_v1_0_Commands_ListLength',
+                'listLength'   => 'Predis_Compatibility_v1_0_Commands_ListLength',
+            'lrange'           => 'Predis_Compatibility_v1_0_Commands_ListRange',
+                'listRange'    => 'Predis_Compatibility_v1_0_Commands_ListRange',
+            'ltrim'            => 'Predis_Compatibility_v1_0_Commands_ListTrim',
+                'listTrim'     => 'Predis_Compatibility_v1_0_Commands_ListTrim',
+            'lindex'           => 'Predis_Compatibility_v1_0_Commands_ListIndex',
+                'listIndex'    => 'Predis_Compatibility_v1_0_Commands_ListIndex',
+            'lset'             => 'Predis_Compatibility_v1_0_Commands_ListSet',
+                'listSet'      => 'Predis_Compatibility_v1_0_Commands_ListSet',
+            'lrem'             => 'Predis_Compatibility_v1_0_Commands_ListRemove',
+                'listRemove'   => 'Predis_Compatibility_v1_0_Commands_ListRemove',
+            'lpop'             => 'Predis_Compatibility_v1_0_Commands_ListPopFirst',
+                'popFirst'     => 'Predis_Compatibility_v1_0_Commands_ListPopFirst',
+            'rpop'             => 'Predis_Compatibility_v1_0_Commands_ListPopLast',
+                'popLast'      => 'Predis_Compatibility_v1_0_Commands_ListPopLast',
+
+            /* commands operating on sets */
+            'sadd'                      => 'Predis_Compatibility_v1_0_Commands_SetAdd', 
+                'setAdd'                => 'Predis_Compatibility_v1_0_Commands_SetAdd',
+            'srem'                      => 'Predis_Compatibility_v1_0_Commands_SetRemove', 
+                'setRemove'             => 'Predis_Compatibility_v1_0_Commands_SetRemove',
+            'spop'                      => 'Predis_Compatibility_v1_0_Commands_SetPop',
+                'setPop'                => 'Predis_Compatibility_v1_0_Commands_SetPop',
+            'smove'                     => 'Predis_Compatibility_v1_0_Commands_SetMove', 
+                'setMove'               => 'Predis_Compatibility_v1_0_Commands_SetMove',
+            'scard'                     => 'Predis_Compatibility_v1_0_Commands_SetCardinality', 
+                'setCardinality'        => 'Predis_Compatibility_v1_0_Commands_SetCardinality',
+            'sismember'                 => 'Predis_Compatibility_v1_0_Commands_SetIsMember', 
+                'setIsMember'           => 'Predis_Compatibility_v1_0_Commands_SetIsMember',
+            'sinter'                    => 'Predis_Compatibility_v1_0_Commands_SetIntersection', 
+                'setIntersection'       => 'Predis_Compatibility_v1_0_Commands_SetIntersection',
+            'sinterstore'               => 'Predis_Compatibility_v1_0_Commands_SetIntersectionStore', 
+                'setIntersectionStore'  => 'Predis_Compatibility_v1_0_Commands_SetIntersectionStore',
+            'sunion'                    => 'Predis_Compatibility_v1_0_Commands_SetUnion', 
+                'setUnion'              => 'Predis_Compatibility_v1_0_Commands_SetUnion',
+            'sunionstore'               => 'Predis_Compatibility_v1_0_Commands_SetUnionStore', 
+                'setUnionStore'         => 'Predis_Compatibility_v1_0_Commands_SetUnionStore',
+            'sdiff'                     => 'Predis_Compatibility_v1_0_Commands_SetDifference', 
+                'setDifference'         => 'Predis_Compatibility_v1_0_Commands_SetDifference',
+            'sdiffstore'                => 'Predis_Compatibility_v1_0_Commands_SetDifferenceStore', 
+                'setDifferenceStore'    => 'Predis_Compatibility_v1_0_Commands_SetDifferenceStore',
+            'smembers'                  => 'Predis_Compatibility_v1_0_Commands_SetMembers', 
+                'setMembers'            => 'Predis_Compatibility_v1_0_Commands_SetMembers',
+            'srandmember'               => 'Predis_Compatibility_v1_0_Commands_SetRandomMember', 
+                'setRandomMember'       => 'Predis_Compatibility_v1_0_Commands_SetRandomMember',
+
+            /* multiple databases handling commands */
+            'select'                => 'Predis_Compatibility_v1_0_Commands_SelectDatabase', 
+                'selectDatabase'    => 'Predis_Compatibility_v1_0_Commands_SelectDatabase',
+            'move'                  => 'Predis_Compatibility_v1_0_Commands_MoveKey', 
+                'moveKey'           => 'Predis_Compatibility_v1_0_Commands_MoveKey',
+            'flushdb'               => 'Predis_Compatibility_v1_0_Commands_FlushDatabase', 
+                'flushDatabase'     => 'Predis_Compatibility_v1_0_Commands_FlushDatabase',
+            'flushall'              => 'Predis_Compatibility_v1_0_Commands_FlushAll', 
+                'flushDatabases'    => 'Predis_Compatibility_v1_0_Commands_FlushAll',
+
+            /* sorting */
+            'sort'                  => 'Predis_Compatibility_v1_0_Commands_Sort',
+
+            /* remote server control commands */
+            'info'                  => 'Predis_Compatibility_v1_0_Commands_Info',
+            'slaveof'               => 'Predis_Compatibility_v1_0_Commands_SlaveOf', 
+                'slaveOf'           => 'Predis_Compatibility_v1_0_Commands_SlaveOf',
+
+            /* persistence control commands */
+            'save'                  => 'Predis_Compatibility_v1_0_Commands_Save',
+            'bgsave'                => 'Predis_Compatibility_v1_0_Commands_BackgroundSave', 
+                'backgroundSave'    => 'Predis_Compatibility_v1_0_Commands_BackgroundSave',
+            'lastsave'              => 'Predis_Compatibility_v1_0_Commands_LastSave', 
+                'lastSave'          => 'Predis_Compatibility_v1_0_Commands_LastSave',
+            'shutdown'              => 'Predis_Compatibility_v1_0_Commands_Shutdown',
+        );
+    }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/* miscellaneous commands */
+class Predis_Compatibility_v1_0_Commands_Ping extends  Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'PING'; }
+    public function parseResponse($data) {
+        return $data === 'PONG' ? true : false;
+    }
+}
+
+class Predis_Compatibility_v1_0_Commands_DoEcho extends Predis_BulkCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'ECHO'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Auth extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'AUTH'; }
+}
+
+/* connection handling */
+class Predis_Compatibility_v1_0_Commands_Quit extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'QUIT'; }
+    public function closesConnection() { return true; }
+}
+
+/* commands operating on string values */
+class Predis_Compatibility_v1_0_Commands_Set extends Predis_BulkCommand {
+    public function getCommandId() { return 'SET'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetPreserve extends Predis_BulkCommand {
+    public function getCommandId() { return 'SETNX'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Get extends Predis_InlineCommand {
+    public function getCommandId() { return 'GET'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_GetMultiple extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'MGET'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_GetSet extends Predis_BulkCommand {
+    public function getCommandId() { return 'GETSET'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Increment extends Predis_InlineCommand {
+    public function getCommandId() { return 'INCR'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_IncrementBy extends Predis_InlineCommand {
+    public function getCommandId() { return 'INCRBY'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Decrement extends Predis_InlineCommand {
+    public function getCommandId() { return 'DECR'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_DecrementBy extends Predis_InlineCommand {
+    public function getCommandId() { return 'DECRBY'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Exists extends Predis_InlineCommand {
+    public function getCommandId() { return 'EXISTS'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Delete extends Predis_InlineCommand {
+    public function getCommandId() { return 'DEL'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Type extends Predis_InlineCommand {
+    public function getCommandId() { return 'TYPE'; }
+}
+
+/* commands operating on the key space */
+class Predis_Compatibility_v1_0_Commands_Keys extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'KEYS'; }
+    public function parseResponse($data) { 
+        return strlen($data) > 0 ? explode(' ', $data) : array();
+    }
+}
+
+class Predis_Compatibility_v1_0_Commands_RandomKey extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'RANDOMKEY'; }
+    public function parseResponse($data) { return $data !== '' ? $data : null; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Rename extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'RENAME'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_RenamePreserve extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'RENAMENX'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Expire extends Predis_InlineCommand {
+    public function getCommandId() { return 'EXPIRE'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ExpireAt extends Predis_InlineCommand {
+    public function getCommandId() { return 'EXPIREAT'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_DatabaseSize extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'DBSIZE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_TimeToLive extends Predis_InlineCommand {
+    public function getCommandId() { return 'TTL'; }
+}
+
+/* commands operating on lists */
+class Predis_Compatibility_v1_0_Commands_ListPushTail extends Predis_BulkCommand {
+    public function getCommandId() { return 'RPUSH'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListPushHead extends Predis_BulkCommand {
+    public function getCommandId() { return 'LPUSH'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListLength extends Predis_InlineCommand {
+    public function getCommandId() { return 'LLEN'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListRange extends Predis_InlineCommand {
+    public function getCommandId() { return 'LRANGE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListTrim extends Predis_InlineCommand {
+    public function getCommandId() { return 'LTRIM'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListIndex extends Predis_InlineCommand {
+    public function getCommandId() { return 'LINDEX'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListSet extends Predis_BulkCommand {
+    public function getCommandId() { return 'LSET'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListRemove extends Predis_BulkCommand {
+    public function getCommandId() { return 'LREM'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListPopFirst extends Predis_InlineCommand {
+    public function getCommandId() { return 'LPOP'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_ListPopLast extends Predis_InlineCommand {
+    public function getCommandId() { return 'RPOP'; }
+}
+
+/* commands operating on sets */
+class Predis_Compatibility_v1_0_Commands_SetAdd extends Predis_BulkCommand {
+    public function getCommandId() { return 'SADD'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetRemove extends Predis_BulkCommand {
+    public function getCommandId() { return 'SREM'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetPop  extends Predis_InlineCommand {
+    public function getCommandId() { return 'SPOP'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetMove extends Predis_BulkCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'SMOVE'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetCardinality extends Predis_InlineCommand {
+    public function getCommandId() { return 'SCARD'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetIsMember extends Predis_BulkCommand {
+    public function getCommandId() { return 'SISMEMBER'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetIntersection extends Predis_InlineCommand {
+    public function getCommandId() { return 'SINTER'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetIntersectionStore extends Predis_InlineCommand {
+    public function getCommandId() { return 'SINTERSTORE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetUnion extends Predis_InlineCommand {
+    public function getCommandId() { return 'SUNION'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetUnionStore extends Predis_InlineCommand {
+    public function getCommandId() { return 'SUNIONSTORE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetDifference extends Predis_InlineCommand {
+    public function getCommandId() { return 'SDIFF'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetDifferenceStore extends Predis_InlineCommand {
+    public function getCommandId() { return 'SDIFFSTORE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetMembers extends Predis_InlineCommand {
+    public function getCommandId() { return 'SMEMBERS'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_SetRandomMember extends Predis_InlineCommand {
+    public function getCommandId() { return 'SRANDMEMBER'; }
+}
+
+/* multiple databases handling commands */
+class Predis_Compatibility_v1_0_Commands_SelectDatabase extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'SELECT'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_MoveKey extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'MOVE'; }
+    public function parseResponse($data) { return (bool) $data; }
+}
+
+class Predis_Compatibility_v1_0_Commands_FlushDatabase extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'FLUSHDB'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_FlushAll extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'FLUSHALL'; }
+}
+
+/* sorting */
+class Predis_Compatibility_v1_0_Commands_Sort extends Predis_InlineCommand {
+    public function getCommandId() { return 'SORT'; }
+    public function filterArguments(Array $arguments) {
+        if (count($arguments) === 1) {
+            return $arguments;
+        }
+
+        // TODO: add more parameters checks
+        $query = array($arguments[0]);
+        $sortParams = $arguments[1];
+
+        if (isset($sortParams['by'])) {
+            $query[] = 'BY';
+            $query[] = $sortParams['by'];
+        }
+        if (isset($sortParams['get'])) {
+            $getargs = $sortParams['get'];
+            if (is_array($getargs)) {
+                foreach ($getargs as $getarg) {
+                    $query[] = 'GET';
+                    $query[] = $getarg;
+                }
+            }
+            else {
+                $query[] = 'GET';
+                $query[] = $getargs;
+            }
+        }
+        if (isset($sortParams['limit']) && is_array($sortParams['limit'])) {
+            $query[] = 'LIMIT';
+            $query[] = $sortParams['limit'][0];
+            $query[] = $sortParams['limit'][1];
+        }
+        if (isset($sortParams['sort'])) {
+            $query[] = strtoupper($sortParams['sort']);
+        }
+        if (isset($sortParams['alpha']) && $sortParams['alpha'] == true) {
+            $query[] = 'ALPHA';
+        }
+        if (isset($sortParams['store']) && $sortParams['store'] == true) {
+            $query[] = 'STORE';
+            $query[] = $sortParams['store'];
+        }
+
+        return $query;
+    }
+}
+
+/* persistence control commands */
+class Predis_Compatibility_v1_0_Commands_Save extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'SAVE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_BackgroundSave extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'BGSAVE'; }
+    public function parseResponse($data) {
+        if ($data == 'Background saving started') {
+            return true;
+        }
+        return $data;
+    }
+}
+
+class Predis_Compatibility_v1_0_Commands_LastSave extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'LASTSAVE'; }
+}
+
+class Predis_Compatibility_v1_0_Commands_Shutdown extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'SHUTDOWN'; }
+    public function closesConnection() { return true; }
+}
+
+/* remote server control commands */
+class Predis_Compatibility_v1_0_Commands_Info extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'INFO'; }
+    public function parseResponse($data) {
+        $info      = array();
+        $infoLines = explode("\r\n", $data, -1);
+        foreach ($infoLines as $row) {
+            list($k, $v) = explode(':', $row);
+            if (!preg_match('/^db\d+$/', $k)) {
+                $info[$k] = $v;
+            }
+            else {
+                $db = array();
+                foreach (explode(',', $v) as $dbvar) {
+                    list($dbvk, $dbvv) = explode('=', $dbvar);
+                    $db[trim($dbvk)] = $dbvv;
+                }
+                $info[$k] = $db;
+            }
+        }
+        return $info;
+    }
+}
+
+class Predis_Compatibility_v1_0_Commands_SlaveOf extends Predis_InlineCommand {
+    public function canBeHashed()  { return false; }
+    public function getCommandId() { return 'SLAVEOF'; }
+    public function filterArguments(Array $arguments) {
+        if (count($arguments) === 0 || $arguments[0] === 'NO ONE') {
+            return array('NO', 'ONE');
+        }
+        return $arguments;
+    }
+}
+?>

+ 503 - 0
test/PredisClientFeatures.php

@@ -0,0 +1,503 @@
+<?php
+define('I_AM_AWARE_OF_THE_DESTRUCTIVE_POWER_OF_THIS_TEST_SUITE', false);
+
+require_once 'PHPUnit/Framework.php';
+require_once 'PredisShared.php';
+require_once '../lib/Predis_Compatibility.php';
+
+class PredisClientFeaturesTestSuite extends PHPUnit_Framework_TestCase {
+    public $redis;
+
+    protected function setUp() { 
+        $this->redis = RC::getConnection();
+        $this->redis->flushDatabase();
+    }
+
+    protected function tearDown() { 
+    }
+
+    protected function onNotSuccessfulTest(Exception $exception) {
+        // drops and reconnect to a redis server on uncaught exceptions
+        RC::resetConnection();
+        parent::onNotSuccessfulTest($exception);
+    }
+
+
+    /* ConnectionParameters */
+
+    function testConnectionParametersDefaultValues() {
+        $params = new Predis_ConnectionParameters();
+
+        $this->assertEquals(Predis_ConnectionParameters::DEFAULT_HOST, $params->host);
+        $this->assertEquals(Predis_ConnectionParameters::DEFAULT_PORT, $params->port);
+        $this->assertEquals(Predis_ConnectionParameters::DEFAULT_TIMEOUT, $params->connection_timeout);
+        $this->assertNull($params->read_write_timeout);
+        $this->assertNull($params->database);
+        $this->assertNull($params->password);
+        $this->assertNull($params->alias);
+    }
+
+    function testConnectionParametersSetupValuesArray() {
+        $paramsArray = RC::getConnectionParametersArgumentsArray();
+        $params = new Predis_ConnectionParameters($paramsArray);
+
+        $this->assertEquals($paramsArray['host'], $params->host);
+        $this->assertEquals($paramsArray['port'], $params->port);
+        $this->assertEquals($paramsArray['connection_timeout'], $params->connection_timeout);
+        $this->assertEquals($paramsArray['read_write_timeout'], $params->read_write_timeout);
+        $this->assertEquals($paramsArray['database'], $params->database);
+        $this->assertEquals($paramsArray['password'], $params->password);
+        $this->assertEquals($paramsArray['alias'], $params->alias);
+    }
+
+    function testConnectionParametersSetupValuesString() {
+        $paramsArray  = RC::getConnectionParametersArgumentsArray();
+        $paramsString = RC::getConnectionParametersArgumentsString($paramsArray);
+        $params = new Predis_ConnectionParameters($paramsArray);
+
+        $this->assertEquals($paramsArray['host'], $params->host);
+        $this->assertEquals($paramsArray['port'], $params->port);
+        $this->assertEquals($paramsArray['connection_timeout'], $params->connection_timeout);
+        $this->assertEquals($paramsArray['read_write_timeout'], $params->read_write_timeout);
+        $this->assertEquals($paramsArray['database'], $params->database);
+        $this->assertEquals($paramsArray['password'], $params->password);
+        $this->assertEquals($paramsArray['alias'], $params->alias);
+    }
+
+
+    /* Command and derivates */
+
+    function testCommand_TestArguments() {
+        $cmdArgs = array('key1', 'key2', 'key3');
+
+        $cmd = new Predis_Commands_GetMultiple();
+        $cmd->setArgumentsArray($cmdArgs);
+        $this->assertEquals($cmdArgs[0], $cmd->getArgument(0));
+        $this->assertEquals($cmdArgs[1], $cmd->getArgument(1));
+        $this->assertEquals($cmdArgs[2], $cmd->getArgument(2));
+
+        $cmd = new Predis_Commands_GetMultiple();
+        $cmd->setArguments('key1', 'key2', 'key3');
+        $this->assertEquals($cmdArgs[0], $cmd->getArgument(0));
+        $this->assertEquals($cmdArgs[1], $cmd->getArgument(1));
+        $this->assertEquals($cmdArgs[2], $cmd->getArgument(2));
+
+        $cmd = new Predis_Commands_Ping();
+        $this->assertNull($cmd->getArgument(0));
+    }
+
+    function testCommand_InlineWithNoArguments() {
+        $cmd = new Predis_Compatibility_v1_0_Commands_Ping();
+
+        $this->assertType('Predis_InlineCommand', $cmd);
+        $this->assertEquals('PING', $cmd->getCommandId());
+        $this->assertFalse($cmd->closesConnection());
+        $this->assertFalse($cmd->canBeHashed());
+        $this->assertNull($cmd->getHash(new Predis_Utilities_HashRing()));
+        $this->assertEquals("PING\r\n", $cmd->invoke());
+    }
+
+    function testCommand_InlineWithArguments() {
+        $cmd = new Predis_Compatibility_v1_0_Commands_Get();
+        $cmd->setArgumentsArray(array('key'));
+
+        $this->assertType('Predis_InlineCommand', $cmd);
+        $this->assertEquals('GET', $cmd->getCommandId());
+        $this->assertFalse($cmd->closesConnection());
+        $this->assertTrue($cmd->canBeHashed());
+        $this->assertNotNull($cmd->getHash(new Predis_Utilities_HashRing()));
+        $this->assertEquals("GET key\r\n", $cmd->invoke());
+    }
+
+    function testCommand_BulkWithArguments() {
+        $cmd = new Predis_Compatibility_v1_0_Commands_Set();
+        $cmd->setArgumentsArray(array('key', 'value'));
+
+        $this->assertType('Predis_BulkCommand', $cmd);
+        $this->assertEquals('SET', $cmd->getCommandId());
+        $this->assertFalse($cmd->closesConnection());
+        $this->assertTrue($cmd->canBeHashed());
+        $this->assertNotNull($cmd->getHash(new Predis_Utilities_HashRing()));
+        $this->assertEquals("SET key 5\r\nvalue\r\n", $cmd->invoke());
+    }
+
+    function testCommand_MultiBulkWithArguments() {
+        $cmd = new Predis_Commands_SetMultiple();
+        $cmd->setArgumentsArray(array('key1', 'value1', 'key2', 'value2'));
+
+        $this->assertType('Predis_MultiBulkCommand', $cmd);
+        $this->assertEquals('MSET', $cmd->getCommandId());
+        $this->assertFalse($cmd->closesConnection());
+        $this->assertFalse($cmd->canBeHashed());
+        $this->assertNull($cmd->getHash(new Predis_Utilities_HashRing()));
+        $this->assertEquals("*5\r\n$4\r\nMSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n", $cmd->invoke());
+    }
+
+    function testCommand_ParseResponse() {
+        // default parser
+        $cmd = new Predis_Commands_Get();
+        $this->assertEquals('test', $cmd->parseResponse('test'));
+
+        // overridden parser (boolean)
+        $cmd = new Predis_Commands_Exists();
+        $this->assertTrue($cmd->parseResponse('1'));
+        $this->assertFalse($cmd->parseResponse('0'));
+
+        // overridden parser (boolean)
+        $cmd = new Predis_Commands_Ping();
+        $this->assertTrue($cmd->parseResponse('PONG'));
+
+        // overridden parser (complex)
+        // TODO: emulate a respons to INFO
+    }
+
+
+    /* RedisServerProfile and derivates */
+
+    function testRedisServerProfile_GetSpecificVersions() {
+        $this->assertType('Predis_RedisServer_v1_0', Predis_RedisServerProfile::get('1.0'));
+        $this->assertType('Predis_RedisServer_v1_2', Predis_RedisServerProfile::get('1.2'));
+        $this->assertType('Predis_RedisServer_v2_0', Predis_RedisServerProfile::get('2.0'));
+        $this->assertType('Predis_RedisServer_vNext', Predis_RedisServerProfile::get('dev'));
+        $this->assertType('Predis_RedisServerProfile', Predis_RedisServerProfile::get('default'));
+        $this->assertEquals(Predis_RedisServerProfile::get('default'), Predis_RedisServerProfile::getDefault());
+    }
+
+    function testRedisServerProfile_SupportedCommands() {
+        $profile_10 = Predis_RedisServerProfile::get('1.0');
+        $profile_12 = Predis_RedisServerProfile::get('1.2');
+
+        $this->assertTrue($profile_10->supportsCommand('info'));
+        $this->assertTrue($profile_12->supportsCommand('info'));
+
+        $this->assertFalse($profile_10->supportsCommand('mset'));
+        $this->assertTrue($profile_12->supportsCommand('mset'));
+
+        $this->assertFalse($profile_10->supportsCommand('multi'));
+        $this->assertFalse($profile_12->supportsCommand('multi'));
+    }
+
+    function testRedisServerProfile_CommandsCreation() {
+        $profile = Predis_RedisServerProfile::get('1.0');
+
+        $cmdNoArgs = $profile->createCommand('info');
+        $this->assertType('Predis_Compatibility_v1_0_Commands_Info', $cmdNoArgs);
+        $this->assertNull($cmdNoArgs->getArgument());
+
+        $args = array('key1', 'key2');
+        $cmdWithArgs = $profile->createCommand('mget', $args);
+        $this->assertType('Predis_Compatibility_v1_0_Commands_GetMultiple', $cmdWithArgs);
+        $this->assertEquals($args[0], $cmdWithArgs->getArgument()); // TODO: why?
+        $this->assertEquals($args[0], $cmdWithArgs->getArgument(0));
+        $this->assertEquals($args[1], $cmdWithArgs->getArgument(1));
+
+        $bogusCommand    = 'not_existing_command';
+        $expectedMessage = "'$bogusCommand' is not a registered Redis command";
+        RC::testForClientException($this, $expectedMessage, p_anon("\$test", "
+            \$profile = Predis_RedisServerProfile::getDefault();
+            \$profile->createCommand('$bogusCommand');
+        "));
+    }
+
+    function testRedisServerProfile_CommandsRegistration() {
+        $profile  = Predis_RedisServerProfile::get('1.0');
+        $cmdId    = 'mset';
+        $cmdClass = 'Predis_Commands_SetMultiple';
+
+        $this->assertFalse($profile->supportsCommand($cmdId));
+        $profile->registerCommand(new $cmdClass(), $cmdId);
+        $this->assertTrue($profile->supportsCommand($cmdId));
+        $this->assertType($cmdClass, $profile->createCommand($cmdId));
+    }
+
+
+    /* ResponseQueued */
+
+    function testResponseQueued() {
+        $response = new Predis_ResponseQueued();
+        $this->assertTrue($response->queued);
+        $this->assertEquals(Predis_Protocol::QUEUED, (string)$response);
+    }
+
+
+    /* ResponseError */
+
+    function testResponseError() {
+        $errorMessage = 'ERROR MESSAGE';
+        $response = new Predis_ResponseError($errorMessage);
+
+        $this->assertTrue($response->error);
+        $this->assertEquals($errorMessage, $response->message);
+        $this->assertEquals($errorMessage, (string)$response);
+    }
+
+
+    /* Connection */
+
+    function testConnection_StringCastReturnsIPAndPort() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $this->assertEquals(RC::SERVER_HOST . ':' . RC::SERVER_PORT, (string) $connection);
+    }
+
+    function testConnection_ConnectDisconnect() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+
+        $this->assertFalse($connection->isConnected());
+        $connection->connect();
+        $this->assertTrue($connection->isConnected());
+        $connection->disconnect();
+        $this->assertFalse($connection->isConnected());
+    }
+
+    function testConnection_WriteAndReadCommand() {
+        $cmd = Predis_RedisServerProfile::getDefault()->createCommand('ping');
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $connection->connect();
+
+        $connection->writeCommand($cmd);
+        $this->assertTrue($connection->readResponse($cmd));
+    }
+
+    function testConnection_WriteCommandAndCloseConnection() {
+        $cmd = Predis_RedisServerProfile::getDefault()->createCommand('quit');
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $connection->connect();
+
+        $this->assertTrue($connection->isConnected());
+        $connection->writeCommand($cmd);
+        $expectedMessage = 'Error while reading line from the server';
+        $thrownException = null;
+        try {
+            $connection->readResponse($cmd);
+        }
+        catch (Predis_CommunicationException $exception) {
+            $thrownException = $exception;
+        }
+        $this->assertType('Predis_CommunicationException', $thrownException);
+        $this->assertEquals($expectedMessage, $thrownException->getMessage());
+        //$this->assertFalse($connection->isConnected());
+    }
+
+    function testConnection_GetSocketOpensConnection() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+
+        $this->assertFalse($connection->isConnected());
+        $this->assertType('resource', $connection->getSocket());
+        $this->assertTrue($connection->isConnected());
+    }
+
+    function testConnection_LazyConnect() {
+        $cmd = Predis_RedisServerProfile::getDefault()->createCommand('ping');
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+
+        $this->assertFalse($connection->isConnected());
+        $connection->writeCommand($cmd);
+        $this->assertTrue($connection->isConnected());
+        $this->assertTrue($connection->readResponse($cmd));
+    }
+
+    function testConnection_RawCommand() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $this->assertEquals('PONG', $connection->rawCommand("PING\r\n"));
+    }
+
+    function testConnection_Alias() {
+        $connection1 = new Predis_Connection(RC::getConnectionParameters());
+        $this->assertNull($connection1->getParameters()->alias);
+
+        $args = array_merge(RC::getConnectionArguments(), array('alias' => 'servername'));
+        $connection2 = new Predis_Connection(new Predis_ConnectionParameters($args));
+        $this->assertEquals('servername', $connection2->getParameters()->alias);
+    }
+
+    function testConnection_ConnectionTimeout() {
+        $timeout = 3;
+        $args    = array('host' => '1.0.0.1', 'connection_timeout' => $timeout);
+        $connection = new Predis_Connection(new Predis_ConnectionParameters($args));
+
+        $start = time();
+        $thrownException = null;
+        try {
+            $connection->connect();
+        }
+        catch (Predis_CommunicationException $exception) {
+            $thrownException = $exception;
+        }
+        $this->assertType('Predis_CommunicationException', $thrownException);
+        $this->assertEquals((float)(time() - $start), $timeout, '', 1);
+    }
+
+    function testConnection_ReadTimeout() {
+        $timeout = 1;
+        $args    = array_merge(RC::getConnectionArguments(), array('read_write_timeout' => $timeout));
+        $cmdFake = Predis_RedisServerProfile::getDefault()->createCommand('ping');
+        $connection = new Predis_Connection(new Predis_ConnectionParameters($args));
+
+        $expectedMessage = 'Error while reading line from the server';
+        $start = time();
+        $thrownException = null;
+        try {
+            $connection->readResponse($cmdFake);
+        }
+        catch (Predis_CommunicationException $exception) {
+            $thrownException = $exception;
+        }
+        $this->assertType('Predis_CommunicationException', $thrownException);
+        $this->assertEquals($expectedMessage, $thrownException->getMessage());
+        $this->assertEquals((float)(time() - $start), $timeout, '', 1);
+    }
+
+
+    /* ResponseReader */
+
+    function testResponseReader_OptionIterableMultiBulkReplies() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $responseReader = $connection->getResponseReader();
+
+        $responseReader->setHandler(
+            Predis_Protocol::PREFIX_MULTI_BULK, 
+            new Predis_ResponseMultiBulkHandler()
+        );
+        $this->assertType('array', $connection->rawCommand("KEYS *\r\n"));
+
+        $responseReader->setHandler(
+            Predis_Protocol::PREFIX_MULTI_BULK, 
+            new Predis_ResponseMultiBulkStreamHandler()
+        );
+        $this->assertType('Iterator', $connection->rawCommand("KEYS *\r\n"));
+    }
+
+    function testResponseReader_OptionExceptionOnError() {
+        $connection = new Predis_Connection(RC::getConnectionParameters());
+        $responseReader = $connection->getResponseReader();
+        $connection->rawCommand("SET key 5\r\nvalue\r\n");
+        $rawCmdUnexpected = "LPUSH key 5\r\nvalue\r\n";
+
+        $responseReader->setHandler(
+            Predis_Protocol::PREFIX_ERROR,  
+            new Predis_ResponseErrorSilentHandler()
+        );
+        $errorReply = $connection->rawCommand($rawCmdUnexpected);
+        $this->assertType('Predis_ResponseError', $errorReply);
+        $this->assertEquals(RC::EXCEPTION_WRONG_TYPE, $errorReply->message);
+
+        $responseReader->setHandler(
+            Predis_Protocol::PREFIX_ERROR, 
+            new Predis_ResponseErrorHandler()
+        );
+        $thrownException = null;
+        try {
+            $connection->rawCommand($rawCmdUnexpected);
+        }
+        catch (Predis_ServerException $exception) {
+            $thrownException = $exception;
+        }
+        $this->assertType('Predis_ServerException', $thrownException);
+        $this->assertEquals(RC::EXCEPTION_WRONG_TYPE, $thrownException->getMessage());
+    }
+
+
+    /* Client + CommandPipeline */
+
+    function testCommandPipeline_Simple() {
+        $client = RC::getConnection();
+        $client->flushdb();
+
+        $pipe = $client->pipeline();
+
+        $this->assertType('Predis_CommandPipeline', $pipe);
+        $this->assertType('Predis_CommandPipeline', $pipe->set('foo', 'bar'));
+        $this->assertType('Predis_CommandPipeline', $pipe->set('hoge', 'piyo'));
+        $this->assertType('Predis_CommandPipeline', $pipe->mset(array(
+            'foofoo' => 'barbar', 'hogehoge' => 'piyopiyo'
+        )));
+        $this->assertType('Predis_CommandPipeline', $pipe->mget(array(
+            'foo', 'hoge', 'foofoo', 'hogehoge'
+        )));
+
+        $replies = $pipe->execute();
+        $this->assertType('array', $replies);
+        $this->assertEquals(4, count($replies));
+        $this->assertEquals(4, count($replies[3]));
+        $this->assertEquals('barbar', $replies[3][2]);
+    }
+
+    function testCommandPipeline_FluentInterface() {
+        $client = RC::getConnection();
+        $client->flushdb();
+
+        $replies = $client->pipeline()->ping()->set('foo', 'bar')->get('foo')->execute();
+        $this->assertType('array', $replies);
+        $this->assertEquals('bar', $replies[2]);
+    }
+
+    function testCommandPipeline_CallableAnonymousBlock() {
+        $client = RC::getConnection();
+        $client->flushdb();
+
+        $replies = $client->pipeline(p_anon("\$pipe", "
+            \$pipe->ping();
+            \$pipe->set('foo', 'bar');
+            \$pipe->get('foo');
+        "));
+
+        $this->assertType('array', $replies);
+        $this->assertEquals('bar', $replies[2]);
+    }
+
+    function testCommandPipeline_ClientExceptionInCallableBlock() {
+        $client = RC::getConnection();
+        $client->flushdb();
+
+        $expectedMessage = 'TEST';
+        $thrownException = null;
+        try {
+            $client->pipeline(p_anon("\$pipe", "
+                \$pipe->ping();
+                \$pipe->set('foo', 'bar');
+                throw new Predis_ClientException('$expectedMessage');
+            "));
+        }
+        catch (Predis_ClientException $exception) {
+            $thrownException = $exception;
+        }
+        $this->assertType('Predis_ClientException', $thrownException);
+        $this->assertEquals($expectedMessage, $thrownException->getMessage());
+
+        $this->assertFalse($client->exists('foo'));
+    }
+
+    function testCommandPipeline_ServerExceptionInCallableBlock() {
+        $client = RC::getConnection();
+        $client->flushdb();
+        $client->getResponseReader()->setHandler('-', new Predis_ResponseErrorSilentHandler());
+
+        $replies = $client->pipeline(p_anon("\$pipe", "
+            \$pipe->set('foo', 'bar');
+            \$pipe->lpush('foo', 'piyo'); // LIST operation on STRING type returns an ERROR
+            \$pipe->set('hoge', 'piyo');
+        "));
+
+        $this->assertType('array', $replies);
+        $this->assertType('Predis_ResponseError', $replies[1]);
+        $this->assertTrue($client->exists('foo'));
+        $this->assertTrue($client->exists('hoge'));
+    }
+
+    function testCommandPipeline_Flush() {
+        $client = RC::getConnection();
+        $client->flushdb();
+
+        $pipe = $client->pipeline();
+        $pipe->set('foo', 'bar')->set('hoge', 'piyo');
+        $pipe->flushPipeline();
+        $pipe->ping()->mget(array('foo', 'hoge'));
+        $replies = $pipe->execute();
+
+        $this->assertType('array', $replies);
+        $this->assertEquals(4, count($replies));
+        $this->assertEquals('bar', $replies[3][0]);
+        $this->assertEquals('piyo', $replies[3][1]);
+    }
+}
+?>

+ 66 - 9
test/PredisShared.php

@@ -25,15 +25,26 @@ class RC {
     const EXCEPTION_NO_SUCH_KEY    = 'no such key';
     const EXCEPTION_OUT_OF_RANGE   = 'index out of range';
     const EXCEPTION_INVALID_DB_IDX = 'invalid DB index';
+    const EXCEPTION_VALUE_NOT_INT  = 'value is not an integer';
     const EXCEPTION_EXEC_NO_MULTI  = 'EXEC without MULTI';
+    const EXCEPTION_SETEX_TTL      = 'invalid expire time in SETEX';
+    const EXCEPTION_HASH_VALNOTINT = 'hash value is not an integer';
 
     private static $_connection;
 
+    public static function getConnectionArguments() { 
+        return array('host' => RC::SERVER_HOST, 'port' => RC::SERVER_PORT);
+    }
+
+    public static function getConnectionParameters() { 
+        return new Predis_ConnectionParameters(array('host' => RC::SERVER_HOST, 'port' => RC::SERVER_PORT));
+    }
+
     private static function createConnection() {
         $serverProfile = Predis_RedisServerProfile::get('dev');
-        $connection = new Predis_Client(array('host' => RC::SERVER_HOST, 'port' => RC::SERVER_PORT), $serverProfile);
+        $connection = new Predis_Client(RC::getConnectionArguments(), $serverProfile);
         $connection->connect();
-        $connection->selectDatabase(RC::DEFAULT_DATABASE);
+        $connection->select(RC::DEFAULT_DATABASE);
         return $connection;
     }
 
@@ -109,25 +120,55 @@ class RC {
             $thrownException = $exception;
         }
         $testcaseInstance->assertType('Predis_ServerException', $thrownException);
-        $testcaseInstance->assertEquals($expectedMessage, $thrownException->getMessage());
+        if (isset($expectedMessage)) {
+            $testcaseInstance->assertEquals($expectedMessage, $thrownException->getMessage());
+        }
+    }
+
+    public static function testForClientException($testcaseInstance, $expectedMessage, $wrapFunction) {
+        $thrownException = null;
+        try {
+            $wrapFunction($testcaseInstance);
+        }
+        catch (Predis_ClientException $exception) {
+            $thrownException = $exception;
+        }
+        $testcaseInstance->assertType('Predis_ClientException', $thrownException);
+        if (isset($expectedMessage)) {
+            $testcaseInstance->assertEquals($expectedMessage, $thrownException->getMessage());
+        }
+    }
+
+    public static function testForCommunicationException($testcaseInstance, $expectedMessage, $wrapFunction) {
+        $thrownException = null;
+        try {
+            $wrapFunction($testcaseInstance);
+        }
+        catch (Predis_CommunicationException $exception) {
+            $thrownException = $exception;
+        }
+        $testcaseInstance->assertType('Predis_CommunicationException', $thrownException);
+        if (isset($expectedMessage)) {
+            $testcaseInstance->assertEquals($expectedMessage, $thrownException->getMessage());
+        }
     }
 
     public static function pushTailAndReturn(Predis_Client $client, $keyName, Array $values, $wipeOut = 0) {
         if ($wipeOut == true) {
-            $client->delete($keyName);
+            $client->del($keyName);
         }
         foreach ($values as $value) {
-            $client->pushTail($keyName, $value);
+            $client->rpush($keyName, $value);
         }
         return $values;
     }
 
     public static function setAddAndReturn(Predis_Client $client, $keyName, Array $values, $wipeOut = 0) {
         if ($wipeOut == true) {
-            $client->delete($keyName);
+            $client->del($keyName);
         }
         foreach ($values as $value) {
-            $client->setAdd($keyName, $value);
+            $client->sadd($keyName, $value);
         }
         return $values;
     }
@@ -135,12 +176,28 @@ class RC {
     public static function zsetAddAndReturn(Predis_Client $client, $keyName, Array $values, $wipeOut = 0) {
         // $values: array(SCORE => VALUE, ...);
         if ($wipeOut == true) {
-            $client->delete($keyName);
+            $client->del($keyName);
         }
         foreach ($values as $value => $score) {
-            $client->zsetAdd($keyName, $score, $value);
+            $client->zadd($keyName, $score, $value);
         }
         return $values;
     }
+
+    public static function getConnectionParametersArgumentsArray() {
+        return array(
+            'host' => '10.0.0.1', 'port' => 6380, 'connection_timeout' => 10, 'read_write_timeout' => 30, 
+            'database' => 5, 'password' => 'dbpassword', 'alias' => 'connection_alias'
+        );
+    }
+
+    public static function getConnectionParametersArgumentsString($arguments = null) {
+        // TODO: must be improved
+        $args = $arguments !== null ? $arguments : RC::getConnectionParametersArgumentsArray();
+        $paramsString = "redis://{$args['host']}:{$args['port']}/";
+        $paramsString .= "?connection_timeout={$args['connection_timeout']}&read_write_timeout={$args['read_write_timeout']}";
+        $paramsString .= "&database={$args['database']}&password={$args['password']}&alias={$args['alias']}";
+        return $paramsString;
+    }
 }
 ?>

Різницю між файлами не показано, бо вона завелика
+ 290 - 220
test/RedisCommandsTest.php


Деякі файли не було показано, через те що забагато файлів було змінено