diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..40265da --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.4 + +before_script: + - wget http://getcomposer.org/composer.phar + - php composer.phar install --dev + +script: + - vendor/bin/atoum -d tests -bf tests/bootstrap.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..66c4f82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 M6Web + +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 restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3d86da --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Redis PHP Mock [![Build Status](https://secure.travis-ci.org/M6Web/RedisMock.png)](http://travis-ci.org/M6Web/RedisMock) + +This PHP 5.4+ library provides a Redis PHP mock for your tests. + +## Installation + +Add this line in your `composer.json` : + +```json +{ + "require": { + "m6web/redis-php-mock": "dev-master" + } +} +``` + +Update your vendors : + +``` +$ composer update m6web/redis-php-mock +``` + +## Running the tests + +```shell +$ php composer.phar install --dev +$ ./vendor/bin/atoum -d tests +``` + +## Credits + +Developped by the [Cytron Team](http://cytron.fr/) of [M6 Web](http://tech.m6web.fr/). +Tested with [atoum](http://atoum.org). + +## License + +RedisPhpMock is licensed under the [MIT license](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7780ad8 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "m6web/redis-mock", + "type": "library", + "description" : "Library providing a PHP mock for Redis", + "keywords": ["redis","mock"], + "license": "MIT", + "authors": [ + { + "name": "M6Web", + "email": "opensource@m6web.fr", + "homepage": "http://tech.m6web.fr/" + } + ], + "autoload": { + "psr-0": {"M6Web\\Component\\RedisMock": "src/"} + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "atoum/atoum": "master-dev" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..863ddfc --- /dev/null +++ b/composer.lock @@ -0,0 +1,99 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + ], + "hash": "ba6f0203c91f8fa27814d876aacd1164", + "packages": [ + + ], + "packages-dev": [ + { + "name": "atoum/atoum", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/atoum/atoum.git", + "reference": "a68f36516b49455eec28fc01323bb4dae8b6704f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/atoum/atoum/zipball/a68f36516b49455eec28fc01323bb4dae8b6704f", + "reference": "a68f36516b49455eec28fc01323bb4dae8b6704f", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "replace": { + "mageekguy/atoum": "*" + }, + "suggest": { + "ext-mbstring": "Provides support for UTF-8 strings" + }, + "bin": [ + "bin/atoum" + ], + "type": "library", + "autoload": { + "classmap": [ + "classes/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "Frédéric Hardy", + "email": "frederic.hardy@atoum.org", + "homepage": "http://blog.mageekbox.net" + }, + { + "name": "François Dussert", + "email": "francois.dussert@atoum.org" + }, + { + "name": "Gérald Croes", + "email": "gerald.croes@atoum.org" + }, + { + "name": "Julien Bianchi", + "email": "julien.bianchi@atoum.org" + }, + { + "name": "Ludovic Fleury", + "email": "ludovic.fleury@atoum.org" + } + ], + "description": "Simple modern and intuitive unit testing framework for PHP 5.3+", + "homepage": "http://www.atoum.org", + "keywords": [ + "TDD", + "atoum", + "test", + "unit testing" + ], + "time": "2013-11-14 13:13:05" + } + ], + "aliases": [ + + ], + "minimum-stability": "stable", + "stability-flags": { + "atoum/atoum": 20 + }, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [ + + ] +} diff --git a/src/M6Web/Component/RedisMock/RedisMock.php b/src/M6Web/Component/RedisMock/RedisMock.php new file mode 100644 index 0000000..b38bde0 --- /dev/null +++ b/src/M6Web/Component/RedisMock/RedisMock.php @@ -0,0 +1,328 @@ + +* @author Denis Roussel +*/ +class RedisMock +{ + static protected $data = array(); + static protected $pipeline = false; + + public function reset() + { + self::$data = array(); + + return $this; + } + + public function getData() + { + return self::$data; + } + + // Strings + + public function get($key) + { + if (!isset(self::$data[$key]) || is_array(self::$data[$key])) + { + return null; + } + + return self::$data[$key]; + } + + public function set($key, $value) + { + self::$data[$key] = $value; + + return $this; + } + + public function incr($key) + { + if (!isset(self::$data[$key])) + { + self::$data[$key] = 1; + } + elseif (!is_integer(self::$data[$key])) + { + return self::$pipeline ? $this : null; + } + else + { + self::$data[$key]++; + } + + return self::$pipeline ? $this : self::$data[$key]; + } + + // Keys + + public function del($key) + { + if (!isset(self::$data[$key])) + { + return 0; + } + + $deletedItems = count(self::$data[$key]); + + unset(self::$data[$key]); + + return $deletedItems; + } + + public function keys($pattern) + { + $results = []; + foreach (self::$data as $key => $value) { + if (preg_match('#' . $pattern . '#', $key)) { + $results[] = $key; + } + } + + return $results; + } + + // Sets + + public function sadd($key, $value) + { + $isNew = !isset(self::$data[$key]); + + self::$data[$key][] = $value; + + return $isNew; + } + + public function smembers($key) + { + if (!isset(self::$data[$key])) + { + return array(); + } + + return self::$data[$key]; + } + + public function srem($key, $value) + { + if (!isset(self::$data[$key]) || !in_array($value, self::$data[$key])) + { + return 0; + } + + self::$data[$key] = array_diff(self::$data[$key], array($value)); + + return 1; + } + + // Hashes + + public function hset($key, $field, $value) + { + $isNew = !isset(self::$data[$key][$field]); + + self::$data[$key][$field] = $value; + + return $isNew; + } + + public function hget($key, $field) + { + if (!isset(self::$data[$key][$field])) + { + return null; + } + + return self::$data[$key][$field]; + } + + public function hgetall($key) + { + if (!isset(self::$data[$key])) + { + return null; + } + + return self::$data[$key]; + } + + public function hexists($key, $field) + { + return isset(self::$data[$key][$field]); + } + + // Sorted set + + public function zrangebyscore($key, $min, $max, $options = null) + { + if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) { + return null; + } + + if (!is_array($options) || !is_array($options['limit']) || count($options['limit']) != 2) { + $options['limit'] = [0, count(self::$data[$key])]; + } + + $array = self::$data[$key]; + uksort(self::$data[$key], function($a, $b) use ($array) { + if ($array[$a] < $array[$b]) { + return -1; + } elseif ($array[$a] > $array[$b]) { + return 1; + } else { + return strcmp($a, $b); + } + }); + + if ($min == '-inf' && $max == '+inf') { + return array_keys(array_slice(self::$data[$key], $options['limit'][0], $options['limit'][1], true)); + } + + $isInfMax = function($v) use ($max) { + if (strpos($max, '(') !== false) { + return $v < (int) substr($max, 1); + } else { + return $v <= (int) $max; + } + }; + + $isSupMin = function($v) use ($min) { + if (strpos($min, '(') !== false) { + return $v > (int) substr($min, 1); + } else { + return $v >= (int) $min; + } + }; + + $results = []; + foreach (self::$data[$key] as $k => $v) { + if ($min == '-inf' && $isInfMax($v)) { + $results[] = $k; + } elseif ($max == '+inf' && $isSupMin($v)) { + $results[] = $k; + } elseif ($isSupMin($v) && $isInfMax($v)) { + $results[] = $k; + } else { + continue; + } + } + + return array_values(array_slice($results, $options['limit'][0], $options['limit'][1], true)); + } + + public function zrevrangebyscore($key, $max, $min, $options = null) + { + if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) { + return null; + } + + if (!is_array($options) || !is_array($options['limit']) || count($options['limit']) != 2) { + $options['limit'] = [0, count(self::$data[$key])]; + } + + $array = self::$data[$key]; + uksort(self::$data[$key], function($a, $b) use ($array) { + if ($array[$a] > $array[$b]) { + return -1; + } elseif ($array[$a] < $array[$b]) { + return 1; + } else { + return -strcmp($a, $b); + } + }); + + if ($min == '-inf' && $max == '+inf') { + return array_keys(array_slice(self::$data[$key], $options['limit'][0], $options['limit'][1], true)); + } + + $isInfMax = function($v) use ($max) { + if (strpos($max, '(') !== false) { + return $v < (int) substr($max, 1); + } else { + return $v <= (int) $max; + } + }; + + $isSupMin = function($v) use ($min) { + if (strpos($min, '(') !== false) { + return $v > (int) substr($min, 1); + } else { + return $v >= (int) $min; + } + }; + + $results = []; + foreach (self::$data[$key] as $k => $v) { + if ($min == '-inf' && $isInfMax($v)) { + $results[] = $k; + } elseif ($max == '+inf' && $isSupMin($v)) { + $results[] = $k; + } elseif ($isSupMin($v) && $isInfMax($v)) { + $results[] = $k; + } else { + continue; + } + } + + return array_values(array_slice($results, $options['limit'][0], $options['limit'][1], true)); + } + + public function zadd($key, $score, $member) { + if (isset(self::$data[$key]) && !is_array(self::$data[$key])) { + return null; + } + + $isNew = !isset(self::$data[$key][$member]); + + self::$data[$key][$member] = (int) $score; + + return (int) $isNew; + } + + public function zremrangebyscore($key, $min, $max) { + $results = []; + + if ($toRem = $this->zrangebyscore($key, $min, $max)) { + foreach ($toRem as $member) { + if ($this->zrem($key, $member)) { + $results[] = $member; + } + } + } + + return $results; + } + + public function zrem($key, $member) { + if (isset(self::$data[$key]) && !is_array(self::$data[$key]) || !isset(self::$data[$key][$member])) { + return 0; + } + + unset(self::$data[$key][$member]); + + return 1; + } + + // Mock + public function pipeline() + { + self::$pipeline = true; + + return $this; + } + + public function execute() + { + self::$pipeline = false; + + return $this; + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ffa5070 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,4 @@ +assert + ->variable($redisMock->get('test')) + ->isNull() + ->integer($redisMock->del('test')) + ->isEqualTo(0) + ->object($redisMock->set('test', 'something')) + ->isInstanceOf('M6Web\Component\RedisMock\RedisMock') + ->string($redisMock->get('test')) + ->isEqualTo('something') + ->integer($redisMock->del('test')) + ->isEqualTo(1) + ->variable($redisMock->get('test')) + ->isNull(); + } + + public function testIncr() + { + $redisMock = new Redis(); + + $this->assert + ->variable($redisMock->get('test')) + ->isNull() + ->integer($redisMock->incr('test')) + ->isEqualTo(1) + ->integer($redisMock->incr('test')) + ->isEqualTo(2) + ->integer($redisMock->incr('test')) + ->isEqualTo(3) + ->object($redisMock->set('test', 'something')) + ->isInstanceOf('M6Web\Component\RedisMock\RedisMock') + ->variable($redisMock->incr('test')) + ->isNull() + ->integer($redisMock->del('test')) + ->isEqualTo(1); + } + + public function testZaddZrem() + { + $redisMock = new Redis(); + + $this->assert + ->integer($redisMock->zrem('test', 'test1')) + ->isEqualTo(0) + ->integer($redisMock->zadd('test', 1, 'test1')) + ->isEqualTo(1) + ->integer($redisMock->zadd('test', 2, 'test1')) + ->isEqualTo(0) + ->integer($redisMock->zrem('test', 'test1')) + ->isEqualTo(1) + ->integer($redisMock->zadd('test', 1, 'test1')) + ->isEqualTo(1) + ->integer($redisMock->zadd('test', 1, 'test2')) + ->isEqualTo(1) + ->integer($redisMock->del('test')) + ->isEqualTo(2); + } + + public function testZRangeByScore() + { + $redisMock = new Redis(); + + $redisMock->zadd('test', 1, 'test4'); + $redisMock->zadd('test', 15, 'test2'); + $redisMock->zadd('test', 2, 'test3'); + $redisMock->zadd('test', 1, 'test1'); + $redisMock->zadd('test', 30, 'test5'); + $redisMock->zadd('test', 0, 'test6'); + + $this->assert + ->array($redisMock->zrangebyscore('test', '-inf', '+inf')) + ->isEqualTo(array( + 'test6', + 'test1', + 'test4', + 'test3', + 'test2', + 'test5', + )) + ->array($redisMock->zrangebyscore('test', '-inf', '15')) + ->isEqualTo(array( + 'test6', + 'test1', + 'test4', + 'test3', + 'test2', + )) + ->array($redisMock->zrangebyscore('test', '-inf', '(15')) + ->isEqualTo(array( + 'test6', + 'test1', + 'test4', + 'test3', + )) + ->array($redisMock->zrangebyscore('test', '2', '+inf')) + ->isEqualTo(array( + 'test3', + 'test2', + 'test5', + )) + ->array($redisMock->zrangebyscore('test', '(2', '+inf')) + ->isEqualTo(array( + 'test2', + 'test5', + )) + ->array($redisMock->zrangebyscore('test', '2', '15')) + ->isEqualTo(array( + 'test3', + 'test2', + )) + ->array($redisMock->zrangebyscore('test', '(1', '15')) + ->isEqualTo(array( + 'test3', + 'test2', + )) + ->array($redisMock->zrangebyscore('test', '-inf', '15', ['limit' => [0, 2]])) + ->isEqualTo(array( + 'test6', + 'test1', + )) + ->array($redisMock->zrangebyscore('test', '-inf', '15', ['limit' => [1, 2]])) + ->isEqualTo(array( + 'test1', + 'test4', + )) + ->array($redisMock->zrangebyscore('test', '-inf', '15', ['limit' => [1, 3]])) + ->isEqualTo(array( + 'test1', + 'test4', + 'test3', + )) + ->integer($redisMock->del('test')) + ->isEqualTo(6); + } + + public function testZRevRangeByScore() + { + $redisMock = new Redis(); + + $redisMock->zadd('test', 1, 'test4'); + $redisMock->zadd('test', 15, 'test2'); + $redisMock->zadd('test', 2, 'test3'); + $redisMock->zadd('test', 1, 'test1'); + $redisMock->zadd('test', 30, 'test5'); + $redisMock->zadd('test', 0, 'test6'); + + $this->assert + ->array($redisMock->zrevrangebyscore('test', '+inf', '-inf')) + ->isEqualTo(array( + 'test5', + 'test2', + 'test3', + 'test4', + 'test1', + 'test6', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '-inf')) + ->isEqualTo(array( + 'test2', + 'test3', + 'test4', + 'test1', + 'test6', + )) + ->array($redisMock->zrevrangebyscore('test', '(15', '-inf')) + ->isEqualTo(array( + 'test3', + 'test4', + 'test1', + 'test6', + )) + ->array($redisMock->zrevrangebyscore('test', '+inf', '2')) + ->isEqualTo(array( + 'test5', + 'test2', + 'test3', + )) + ->array($redisMock->zrevrangebyscore('test', '+inf', '(2')) + ->isEqualTo(array( + 'test5', + 'test2', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '2')) + ->isEqualTo(array( + 'test2', + 'test3', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '(1')) + ->isEqualTo(array( + 'test2', + 'test3', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '-inf', ['limit' => [0, 2]])) + ->isEqualTo(array( + 'test2', + 'test3', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '-inf', ['limit' => [1, 2]])) + ->isEqualTo(array( + 'test3', + 'test4', + )) + ->array($redisMock->zrevrangebyscore('test', '15', '-inf', ['limit' => [1, 3]])) + ->isEqualTo(array( + 'test3', + 'test4', + 'test1', + )) + ->integer($redisMock->del('test')) + ->isEqualTo(6); + } +} \ No newline at end of file