1<?php defined('PHPREDIS_TESTRUN') or die("Use TestRedis.php to run tests!\n");
2require_once(dirname($_SERVER['PHP_SELF'])."/TestSuite.php");
3
4define('REDIS_ARRAY_DATA_SIZE', 1000);
5
6function custom_hash($str) {
7    // str has the following format: $APPID_fb$FACEBOOKID_$key.
8    $pos = strpos($str, '_fb');
9    if(preg_match("#\w+_fb(?<facebook_id>\d+)_\w+#", $str, $out)) {
10            return $out['facebook_id'];
11    }
12    return $str;
13}
14
15function parseHostPort($str, &$host, &$port) {
16    $pos = strrpos($str, ':');
17    $host = substr($str, 0, $pos);
18    $port = substr($str, $pos+1);
19}
20
21function getRedisVersion($obj_r) {
22    $arr_info = $obj_r->info();
23    if (!$arr_info || !isset($arr_info['redis_version'])) {
24        return "0.0.0";
25    }
26    return $arr_info['redis_version'];
27}
28
29/* Determine the lowest redis version attached to this RedisArray object */
30function getMinVersion($obj_ra) {
31    $min_version = "0.0.0";
32    foreach ($obj_ra->_hosts() as $host) {
33        $version = getRedisVersion($obj_ra->_instance($host));
34        if (version_compare($version, $min_version) > 0) {
35            $min_version = $version;
36        }
37    }
38
39    return $min_version;
40}
41
42class Redis_Array_Test extends TestSuite
43{
44    private $min_version;
45    private $strings;
46    public $ra = NULL;
47    private $data = NULL;
48
49    public function setUp() {
50        // initialize strings.
51        $n = REDIS_ARRAY_DATA_SIZE;
52        $this->strings = array();
53        for($i = 0; $i < $n; $i++) {
54            $this->strings['key-'.$i] = 'val-'.$i;
55        }
56
57        global $newRing, $oldRing, $useIndex;
58        $options = ['previous' => $oldRing, 'index' => $useIndex];
59        if ($this->getAuth()) {
60            $options['auth'] = $this->getAuth();
61        }
62        $this->ra = new RedisArray($newRing, $options);
63        $this->min_version = getMinVersion($this->ra);
64    }
65
66    public function testMSet() {
67        // run mset
68        $this->assertTrue(TRUE === $this->ra->mset($this->strings));
69
70        // check each key individually using the array
71        foreach($this->strings as $k => $v) {
72            $this->assertTrue($v === $this->ra->get($k));
73        }
74
75        // check each key individually using a new connection
76        foreach($this->strings as $k => $v) {
77            parseHostPort($this->ra->_target($k), $host, $port);
78
79            $target = $this->ra->_target($k);
80            $pos = strrpos($target, ':');
81
82            $host = substr($target, 0, $pos);
83            $port = substr($target, $pos+1);
84
85            $r = new Redis;
86            $r->pconnect($host, (int)$port);
87            if ($this->getAuth()) {
88                $this->assertTrue($r->auth($this->getAuth()));
89            }
90            $this->assertTrue($v === $r->get($k));
91        }
92    }
93
94    public function testMGet() {
95        $this->assertTrue(array_values($this->strings) === $this->ra->mget(array_keys($this->strings)));
96    }
97
98    private function addData($commonString) {
99        $this->data = array();
100        for($i = 0; $i < REDIS_ARRAY_DATA_SIZE; $i++) {
101            $k = rand().'_'.$commonString.'_'.rand();
102            $this->data[$k] = rand();
103        }
104        $this->ra->mset($this->data);
105    }
106
107    private function checkCommonLocality() {
108        // check that they're all on the same node.
109        $lastNode = NULL;
110        foreach($this->data as $k => $v) {
111                $node = $this->ra->_target($k);
112                if($lastNode) {
113                    $this->assertTrue($node === $lastNode);
114                }
115                $this->assertTrue($this->ra->get($k) == $v);
116                $lastNode = $node;
117        }
118    }
119
120    public function testKeyLocality() {
121
122        // basic key locality with default hash
123        $this->addData('{hashed part of the key}');
124        $this->checkCommonLocality();
125
126        // with common hashing function
127        global $newRing, $oldRing, $useIndex;
128        $options = ['previous' => $oldRing, 'index' => $useIndex, 'function' => 'custom_hash'];
129        if ($this->getAuth()) {
130            $options['auth'] = $this->getAuth();
131        }
132        $this->ra = new RedisArray($newRing, $options);
133
134        // basic key locality with custom hash
135        $this->addData('fb'.rand());
136        $this->checkCommonLocality();
137    }
138
139    public function customDistributor($key)
140    {
141        $a = unpack("N*", md5($key, true));
142        global $newRing;
143        $pos = abs($a[1]) % count($newRing);
144
145        return $pos;
146    }
147
148    public function testKeyDistributor()
149    {
150        global $newRing, $useIndex;
151        $options = ['index' => $useIndex, 'function' => 'custom_hash', 'distributor' => [$this, "customDistributor"]];
152        if ($this->getAuth()) {
153            $options['auth'] = $this->getAuth();
154        }
155        $this->ra = new RedisArray($newRing, $options);
156
157        // custom key distribution function.
158        $this->addData('fb'.rand());
159
160        // check that they're all on the expected node.
161        $lastNode = NULL;
162        foreach($this->data as $k => $v) {
163            $node = $this->ra->_target($k);
164            $pos = $this->customDistributor($k);
165            $this->assertTrue($node === $newRing[$pos]);
166        }
167    }
168
169}
170
171class Redis_Rehashing_Test extends TestSuite
172{
173
174    public $ra = NULL;
175    private $useIndex;
176
177    private $min_version;
178
179    // data
180    private $strings;
181    private $sets;
182    private $lists;
183    private $hashes;
184    private $zsets;
185
186    public function setUp() {
187
188        // initialize strings.
189        $n = REDIS_ARRAY_DATA_SIZE;
190        $this->strings = array();
191        for($i = 0; $i < $n; $i++) {
192            $this->strings['key-'.$i] = 'val-'.$i;
193        }
194
195        // initialize sets
196        for($i = 0; $i < $n; $i++) {
197            // each set has 20 elements
198            $this->sets['set-'.$i] = range($i, $i+20);
199        }
200
201        // initialize lists
202        for($i = 0; $i < $n; $i++) {
203            // each list has 20 elements
204            $this->lists['list-'.$i] = range($i, $i+20);
205        }
206
207        // initialize hashes
208        for($i = 0; $i < $n; $i++) {
209            // each hash has 5 keys
210            $this->hashes['hash-'.$i] = array('A' => $i, 'B' => $i+1, 'C' => $i+2, 'D' => $i+3, 'E' => $i+4);
211        }
212
213        // initialize sorted sets
214        for($i = 0; $i < $n; $i++) {
215            // each sorted sets has 5 elements
216            $this->zsets['zset-'.$i] = array($i, 'A', $i+1, 'B', $i+2, 'C', $i+3, 'D', $i+4, 'E');
217        }
218
219        global $newRing, $oldRing, $useIndex;
220        $options = ['previous' => $oldRing, 'index' => $useIndex];
221        if ($this->getAuth()) {
222            $options['auth'] = $this->getAuth();
223        }
224        // create array
225        $this->ra = new RedisArray($newRing, $options);
226        $this->min_version = getMinVersion($this->ra);
227    }
228
229    public function testFlush() {
230        // flush all servers first.
231        global $serverList;
232        foreach($serverList as $s) {
233            parseHostPort($s, $host, $port);
234
235            $r = new Redis();
236            $r->pconnect($host, (int)$port, 0);
237            if ($this->getAuth()) {
238                $this->assertTrue($r->auth($this->getAuth()));
239            }
240            $r->flushdb();
241        }
242    }
243
244
245    private function distributeKeys() {
246
247        // strings
248        foreach($this->strings as $k => $v) {
249            $this->ra->set($k, $v);
250        }
251
252        // sets
253        foreach($this->sets as $k => $v) {
254            call_user_func_array(array($this->ra, 'sadd'), array_merge(array($k), $v));
255        }
256
257        // lists
258        foreach($this->lists as $k => $v) {
259            call_user_func_array(array($this->ra, 'rpush'), array_merge(array($k), $v));
260        }
261
262        // hashes
263        foreach($this->hashes as $k => $v) {
264            $this->ra->hmset($k, $v);
265        }
266
267        // sorted sets
268        foreach($this->zsets as $k => $v) {
269            call_user_func_array(array($this->ra, 'zadd'), array_merge(array($k), $v));
270        }
271    }
272
273    public function testDistribution() {
274        $this->distributeKeys();
275    }
276
277    public function testSimpleRead() {
278        $this->readAllvalues();
279    }
280
281    private function readAllvalues() {
282
283        // strings
284        foreach($this->strings as $k => $v) {
285            $this->assertTrue($this->ra->get($k) === $v);
286        }
287
288        // sets
289        foreach($this->sets as $k => $v) {
290            $ret = $this->ra->smembers($k); // get values
291
292            // sort sets
293            sort($v);
294            sort($ret);
295
296            $this->assertTrue($ret == $v);
297        }
298
299        // lists
300        foreach($this->lists as $k => $v) {
301            $ret = $this->ra->lrange($k, 0, -1);
302            $this->assertTrue($ret == $v);
303        }
304
305        // hashes
306        foreach($this->hashes as $k => $v) {
307            $ret = $this->ra->hgetall($k); // get values
308            $this->assertTrue($ret == $v);
309        }
310
311        // sorted sets
312        foreach($this->zsets as $k => $v) {
313            $ret = $this->ra->zrange($k, 0, -1, TRUE); // get values with scores
314
315            // create assoc array from local dataset
316            $tmp = array();
317            for($i = 0; $i < count($v); $i += 2) {
318                $tmp[$v[$i+1]] = $v[$i];
319            }
320
321            // compare to RA value
322            $this->assertTrue($ret == $tmp);
323        }
324    }
325
326    // add a new node.
327    public function testCreateSecondRing() {
328
329        global $newRing, $oldRing, $serverList;
330        $oldRing = $newRing; // back up the original.
331        $newRing = $serverList; // add a new node to the main ring.
332    }
333
334    public function testReadUsingFallbackMechanism() {
335        $this->readAllvalues(); // some of the reads will fail and will go to another target node.
336    }
337
338    public function testRehash() {
339        $this->ra->_rehash(); // this will redistribute the keys
340    }
341
342    public function testRehashWithCallback() {
343        $total = 0;
344        $this->ra->_rehash(function ($host, $count) use (&$total) {
345            $total += $count;
346        });
347        $this->assertTrue($total > 0);
348    }
349
350    public function testReadRedistributedKeys() {
351        $this->readAllvalues(); // we shouldn't have any missed reads now.
352    }
353}
354
355// Test auto-migration of keys
356class Redis_Auto_Rehashing_Test extends TestSuite {
357
358    public $ra = NULL;
359    private $min_version;
360
361    // data
362    private $strings;
363
364    public function setUp() {
365        // initialize strings.
366        $n = REDIS_ARRAY_DATA_SIZE;
367        $this->strings = array();
368        for($i = 0; $i < $n; $i++) {
369            $this->strings['key-'.$i] = 'val-'.$i;
370        }
371
372        global $newRing, $oldRing, $useIndex;
373        $options = ['previous' => $oldRing, 'index' => $useIndex, 'autorehash' => TRUE];
374        if ($this->getAuth()) {
375            $options['auth'] = $this->getAuth();
376        }
377        // create array
378        $this->ra = new RedisArray($newRing, $options);
379        $this->min_version = getMinVersion($this->ra);
380    }
381
382    public function testDistribute() {
383        // strings
384        foreach($this->strings as $k => $v) {
385            $this->ra->set($k, $v);
386        }
387    }
388
389    private function readAllvalues() {
390        foreach($this->strings as $k => $v) {
391            $this->assertTrue($this->ra->get($k) === $v);
392        }
393    }
394
395
396    public function testReadAll() {
397        $this->readAllvalues();
398    }
399
400    // add a new node.
401    public function testCreateSecondRing() {
402        global $newRing, $oldRing, $serverList;
403        $oldRing = $newRing; // back up the original.
404        $newRing = $serverList; // add a new node to the main ring.
405    }
406
407    // Read and migrate keys on fallback, causing the whole ring to be rehashed.
408    public function testReadAndMigrateAll() {
409        $this->readAllvalues();
410    }
411
412    // Read and migrate keys on fallback, causing the whole ring to be rehashed.
413    public function testAllKeysHaveBeenMigrated() {
414        foreach($this->strings as $k => $v) {
415            parseHostPort($this->ra->_target($k), $host, $port);
416
417            $r = new Redis;
418            $r->pconnect($host, $port);
419            if ($this->getAuth()) {
420                $this->assertTrue($r->auth($this->getAuth()));
421            }
422
423            $this->assertTrue($v === $r->get($k));  // check that the key has actually been migrated to the new node.
424        }
425    }
426}
427
428// Test node-specific multi/exec
429class Redis_Multi_Exec_Test extends TestSuite {
430    public $ra = NULL;
431    private $min_version;
432
433    public function setUp() {
434        global $newRing, $oldRing, $useIndex;
435        $options = ['previous' => $oldRing, 'index' => $useIndex];
436        if ($this->getAuth()) {
437            $options['auth'] = $this->getAuth();
438        }
439        // create array
440        $this->ra = new RedisArray($newRing, $options);
441        $this->min_version = getMinVersion($this->ra);
442    }
443
444    public function testInit() {
445        $this->ra->set('{groups}:managers', 2);
446        $this->ra->set('{groups}:executives', 3);
447
448        $this->ra->set('1_{employee:joe}_name', 'joe');
449        $this->ra->set('1_{employee:joe}_group', 2);
450        $this->ra->set('1_{employee:joe}_salary', 2000);
451    }
452
453    public function testKeyDistribution() {
454        // check that all of joe's keys are on the same instance
455        $lastNode = NULL;
456        foreach(array('name', 'group', 'salary') as $field) {
457            $node = $this->ra->_target('1_{employee:joe}_'.$field);
458            if($lastNode) {
459                $this->assertTrue($node === $lastNode);
460            }
461            $lastNode = $node;
462        }
463    }
464
465    public function testMultiExec() {
466
467        // Joe gets a promotion
468        $newGroup = $this->ra->get('{groups}:executives');
469        $newSalary = 4000;
470
471        // change both in a transaction.
472        $host = $this->ra->_target('{employee:joe}');   // transactions are per-node, so we need a reference to it.
473        $tr = $this->ra->multi($host)
474            ->set('1_{employee:joe}_group', $newGroup)
475            ->set('1_{employee:joe}_salary', $newSalary)
476            ->exec();
477
478        // check that the group and salary have been changed
479        $this->assertTrue($this->ra->get('1_{employee:joe}_group') === $newGroup);
480        $this->assertTrue($this->ra->get('1_{employee:joe}_salary') == $newSalary);
481
482    }
483
484    public function testMultiExecMSet() {
485
486        global $newGroup, $newSalary;
487        $newGroup = 1;
488        $newSalary = 10000;
489
490        // test MSET, making Joe a top-level executive
491        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
492                ->mset(array('1_{employee:joe}_group' => $newGroup, '1_{employee:joe}_salary' => $newSalary))
493                ->exec();
494
495        $this->assertTrue($out[0] === TRUE);
496    }
497
498    public function testMultiExecMGet() {
499
500        global $newGroup, $newSalary;
501
502        // test MGET
503        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
504                ->mget(array('1_{employee:joe}_group', '1_{employee:joe}_salary'))
505                ->exec();
506
507        $this->assertTrue($out[0][0] == $newGroup);
508        $this->assertTrue($out[0][1] == $newSalary);
509    }
510
511    public function testMultiExecDel() {
512
513        // test DEL
514        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
515            ->del('1_{employee:joe}_group', '1_{employee:joe}_salary')
516            ->exec();
517
518        $this->assertTrue($out[0] === 2);
519        $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_group'));
520        $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_salary'));
521    }
522
523    public function testMutliExecUnlink() {
524        if (version_compare($this->min_version, "4.0.0", "lt")) {
525            $this->markTestSkipped();
526        }
527
528        $this->ra->set('{unlink}:key1', 'bar');
529        $this->ra->set('{unlink}:key2', 'bar');
530
531        $out = $this->ra->multi($this->ra->_target('{unlink}'))
532            ->del('{unlink}:key1', '{unlink}:key2')
533            ->exec();
534
535        $this->assertTrue($out[0] === 2);
536    }
537
538    public function testDiscard() {
539        /* phpredis issue #87 */
540        $key = 'test_err';
541
542        $this->assertTrue($this->ra->set($key, 'test'));
543        $this->assertTrue('test' === $this->ra->get($key));
544
545        $this->ra->watch($key);
546
547        // After watch, same
548        $this->assertTrue('test' === $this->ra->get($key));
549
550        // change in a multi/exec block.
551        $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test1')->exec();
552        $this->assertTrue($ret === array(true));
553
554        // Get after exec, 'test1':
555        $this->assertTrue($this->ra->get($key) === 'test1');
556
557        $this->ra->watch($key);
558
559        // After second watch, still test1.
560        $this->assertTrue($this->ra->get($key) === 'test1');
561
562        $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test2')->discard();
563        // Ret after discard: NULL";
564        $this->assertTrue($ret === NULL);
565
566        // Get after discard, unchanged:
567        $this->assertTrue($this->ra->get($key) === 'test1');
568    }
569
570}
571
572// Test custom distribution function
573class Redis_Distributor_Test extends TestSuite {
574
575    public $ra = NULL;
576    private $min_version;
577
578    public function setUp() {
579        global $newRing, $oldRing, $useIndex;
580        $options = ['previous' => $oldRing, 'index' => $useIndex, 'distributor' => [$this, 'distribute']];
581        if ($this->getAuth()) {
582            $options['auth'] = $this->getAuth();
583        }
584        // create array
585        $this->ra = new RedisArray($newRing, $options);
586        $this->min_version = getMinVersion($this->ra);
587    }
588
589    public function testInit() {
590        $this->ra->set('{uk}test', 'joe');
591        $this->ra->set('{us}test', 'bob');
592    }
593
594    public function distribute($key) {
595        $matches = array();
596        if (preg_match('/{([^}]+)}.*/', $key, $matches) == 1) {
597            $countries = array('uk' => 0, 'us' => 1);
598            if (array_key_exists($matches[1], $countries)) {
599                return $countries[$matches[1]];
600            }
601        }
602        return 2; // default server
603    }
604
605    public function testDistribution() {
606        $ukServer = $this->ra->_target('{uk}test');
607        $usServer = $this->ra->_target('{us}test');
608        $deServer = $this->ra->_target('{de}test');
609        $defaultServer = $this->ra->_target('unknown');
610
611        $nodes = $this->ra->_hosts();
612        $this->assertTrue($ukServer === $nodes[0]);
613        $this->assertTrue($usServer === $nodes[1]);
614        $this->assertTrue($deServer === $nodes[2]);
615        $this->assertTrue($defaultServer === $nodes[2]);
616    }
617}
618
619function run_tests($className, $str_filter, $str_host, $auth) {
620        // reset rings
621        global $newRing, $oldRing, $serverList;
622
623        $newRing = ["$str_host:6379", "$str_host:6380", "$str_host:6381"];
624        $oldRing = [];
625        $serverList = ["$str_host:6379", "$str_host:6380", "$str_host:6381", "$str_host:6382"];
626
627        // run
628        return TestSuite::run($className, $str_filter, $str_host, NULL, $auth);
629}
630
631?>
632