From 747429341e18439084dcac35f1ef5bb332a3a151 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Thu, 1 Dec 2011 22:18:49 +0100 Subject: [PATCH] memcache profiler storage support added fix CS fix CS + remove unneeded else add documentation, change protected methods as private rename var throw exception for invalid name, index fix memcache profiler storage support added, fix CS and minor bugs fix CS removed unneeded else - memcached support added - improved performance (serialization, index) updated code to last version of Profiler --- .../FrameworkExtension.php | 10 +- .../Profiler/BaseMemcacheProfilerStorage.php | 260 ++++++++++++++++++ .../Profiler/MemcacheProfilerStorage.php | 100 +++++++ .../Profiler/MemcachedProfilerStorage.php | 95 +++++++ .../Profiler/MemcacheProfilerStorageTest.php | 194 +++++++++++++ .../Profiler/MemcachedProfilerStorageTest.php | 194 +++++++++++++ 6 files changed, 849 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php create mode 100644 tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php create mode 100644 tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d86333c701..a58688def9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -202,10 +202,12 @@ class FrameworkExtension extends Extension // Choose storage class based on the DSN $supported = array( - 'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage', - 'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage', - 'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage', - 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', + 'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage', + 'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage', + 'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage', + 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', + 'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage', + 'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage', ); list($class, ) = explode(':', $config['dsn'], 2); if (!isset($supported[$class])) { diff --git a/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php new file mode 100644 index 0000000000..ba4af6e3df --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +/** + * Base Memcache storage for profiling information in a Memcache. + * + * @author Andrej Hudec + */ +abstract class BaseMemcacheProfilerStorage implements ProfilerStorageInterface +{ + const TOKEN_PREFIX = 'sf_profiler_'; + + protected $dsn; + protected $lifetime; + + /** + * Constructor. + * + * @param string $dsn A data source name + * @param string $username + * @param string $password + * @param int $lifetime The lifetime to use for the purge + */ + public function __construct($dsn, $username = '', $password = '', $lifetime = 86400) + { + $this->dsn = $dsn; + $this->lifetime = (int) $lifetime; + } + + /** + * {@inheritdoc} + */ + public function find($ip, $url, $limit, $method) + { + $indexName = $this->getIndexName(); + + $indexContent = $this->getValue($indexName); + if (!$indexContent) { + return array(); + } + + $profileList = explode("\n", $indexContent); + $result = array(); + + foreach ($profileList as $item) { + + if ($limit === 0) { + break; + } + + if ($item=='') { + continue; + } + + list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6); + + if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) { + continue; + } + + $result[$itemToken] = array( + 'token' => $itemToken, + 'ip' => $itemIp, + 'method' => $itemMethod, + 'url' => $itemUrl, + 'time' => $itemTime, + 'parent' => $itemParent, + ); + --$limit; + } + + return array_values($result); + } + + /** + * {@inheritdoc} + */ + public function purge() + { + $this->flush(); + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (empty($token)) { + return false; + } + + $profile = $this->getValue($this->getItemName($token)); + + if (false !== $profile) { + $profile = $this->createProfileFromData($token, $profile); + } + + return $profile; + } + + /** + * {@inheritdoc} + */ + public function write(Profile $profile) + { + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + ); + + if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime)) { + // Add to index + $indexName = $this->getIndexName(); + + $indexRow = implode("\t", array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + ))."\n"; + + return $this->appendValue($indexName, $indexRow, $this->lifetime); + } + + return false; + } + + /** + * Retrieve item from the memcache server + * + * @param string $key + * + * @return mixed + */ + abstract protected function getValue($key); + + /** + * Store an item on the memcache server under the specified key + * + * @param string $key + * @param mixed $value + * @param int $expiration + * + * @return boolean + */ + abstract protected function setValue($key, $value, $expiration = 0); + + /** + * Flush all existing items at the memcache server + * + * @return boolean + */ + abstract protected function flush(); + + /** + * Append data to an existing item on the memcache server + * @param string $key + * @param string $value + * @param int $expiration + * + * @return boolean + */ + abstract protected function appendValue($key, $value, $expiration = 0); + + private function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setIp($data['ip']); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token) { + continue; + } + + if (!$childProfileData = $this->getValue($this->getItemName($token))) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, $childProfileData, $profile)); + } + + return $profile; + } + + /** + * Get item name + * + * @param string $token + * + * @return string + */ + private function getItemName($token) + { + $name = self::TOKEN_PREFIX . $token; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + /** + * Get name of index + * + * @return string + */ + private function getIndexName() + { + $name = self::TOKEN_PREFIX . 'index'; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + private function isItemNameValid($name) + { + $length = strlen($name); + + if ($length > 250) { + throw new \RuntimeException(sprintf('The memcache item key "%s" is too long (%s bytes). Allowed maximum size is 250 bytes.', $name, $length)); + } + + return true; + } + +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php new file mode 100644 index 0000000000..e6e43e27a7 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Memcache; + +/** + * Memcache Profiler Storage + * + * @author Andrej Hudec + */ +class MemcacheProfilerStorage extends BaseMemcacheProfilerStorage +{ + + /** + * @var Memcache + */ + private $memcache; + + /** + * Internal convenience method that returns the instance of the Memcache + * + * @return Memcache + */ + protected function getMemcache() + { + if (null === $this->memcache) { + if (!preg_match('#^memcache://(.*)/(.*)$#', $this->dsn, $matches)) { + throw new \RuntimeException('Please check your configuration. You are trying to use Memcache with an invalid dsn. "' . $this->dsn . '"'); + } + + $host = $matches[1]; + $port = $matches[2]; + + $memcache = new Memcache; + $memcache->addServer($host, $port); + + $this->memcache = $memcache; + } + + return $this->memcache; + } + + /** + * {@inheritdoc} + */ + protected function getValue($key) + { + return $this->getMemcache()->get($key); + } + + /** + * {@inheritdoc} + */ + protected function setValue($key, $value, $expiration = 0) + { + return $this->getMemcache()->set($key, $value, false, $expiration); + } + + /** + * {@inheritdoc} + */ + protected function flush() + { + return $this->getMemcache()->flush(); + } + + /** + * {@inheritdoc} + */ + protected function appendValue($key, $value, $expiration = 0) + { + $memcache = $this->getMemcache(); + + if (method_exists($memcache, 'append')) { + + //Memcache v3.0 + if (!$result = $memcache->append($key, $value, false, $expiration)) { + return $memcache->set($key, $value, false, $expiration); + } + + return $result; + } + + //simulate append in Memcache <3.0 + $content = $memcache->get($key); + + return $memcache->set($key, $content . $value, false, $expiration); + } + +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php new file mode 100644 index 0000000000..f9b4e10b03 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Memcached; + +/** + * Memcached Profiler Storage + * + * @author Andrej Hudec + */ +class MemcachedProfilerStorage extends BaseMemcacheProfilerStorage +{ + + /** + * @var Memcached + */ + private $memcached; + + /** + * Internal convenience method that returns the instance of the Memcached + * + * @return Memcached + */ + protected function getMemcached() + { + if (null === $this->memcached) { + if (!preg_match('#^memcached://(.*)/(.*)$#', $this->dsn, $matches)) { + throw new \RuntimeException('Please check your configuration. You are trying to use Memcached with an invalid dsn. "' . $this->dsn . '"'); + } + + $host = $matches[1]; + $port = $matches[2]; + + $memcached = new Memcached; + + //disable compression to allow appending + $memcached->setOption(Memcached::OPT_COMPRESSION, false); + + $memcached->addServer($host, $port); + + $this->memcached = $memcached; + } + + return $this->memcached; + } + + /** + * {@inheritdoc} + */ + protected function getValue($key) + { + return $this->getMemcached()->get($key); + } + + /** + * {@inheritdoc} + */ + protected function setValue($key, $value, $expiration = 0) + { + return $this->getMemcached()->set($key, $value, false, $expiration); + } + + /** + * {@inheritdoc} + */ + protected function flush() + { + return $this->getMemcached()->flush(); + } + + /** + * {@inheritdoc} + */ + protected function appendValue($key, $value, $expiration = 0) + { + $memcached = $this->getMemcached(); + + if (!$result = $memcached->append($key, $value)) { + return $memcached->set($key, $value, $expiration); + } + + return $result; + } + +} diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php new file mode 100644 index 0000000000..596a207d4b --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class DummyMemcacheProfilerStorage extends MemcacheProfilerStorage +{ + public function getMemcache() + { + return parent::getMemcache(); + } +} + +class MemcacheProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + protected static $storage; + + public static function tearDownAfterClass() + { + if (self::$storage) { + self::$storage->purge(); + } + } + + protected function setUp() + { + if (!extension_loaded('memcache')) { + $this->markTestSkipped('MemcacheProfilerStorageTest requires that the extension memcache is loaded'); + } + + self::$storage = new DummyMemcacheProfilerStorage('memcache://127.0.0.1/11211', '', '', 86400); + try { + self::$storage->getMemcache(); + } catch(\Exception $e) { + $this->markTestSkipped('MemcacheProfilerStorageTest requires that there is a Memcache server present on localhost'); + } + + if (self::$storage) { + self::$storage->purge(); + } + } + + public function testStore() + { + for ($i = 0; $i < 10; $i ++) { + $profile = new Profile('token_'.$i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setMethod('GET'); + self::$storage->write($profile); + } + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache'); + } + + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $parentProfile->setIp('127.0.0.1'); + $parentProfile->setUrl('http://foo.bar/parent'); + + $childProfile = new Profile('token_child'); + $childProfile->setIp('127.0.0.1'); + $childProfile->setUrl('http://foo.bar/child'); + + $parentProfile->addChild($childProfile); + + self::$storage->write($parentProfile); + self::$storage->write($childProfile); + + // Load them from storage + $parentProfile = self::$storage->read('token_parent'); + $childProfile = self::$storage->read('token_child'); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParent()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertEquals(1, count($children)); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The SQLite storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $profile = new Profile('simple_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\''); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL'); + + $profile = new Profile('double_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/"'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL'); + + $profile = new Profile('backslash'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\\'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL'); + + $profile = new Profile('comma'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/,'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $profile = new Profile('token'); + $profile->setUrl('http://example.com/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique'); + + $profile->setUrl('http://example.net/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache'); + $this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data'); + + $this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $profile = new Profile('token'); + $profile->setIp('127.0.0.1'); + $profile->setMethod('GET'); + + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP'); + $this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP'); + $this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $profile = new Profile('simple_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/\''); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('double_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/"'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('backslash'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo\\bar/'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('percent'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/%'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('underscore'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/_'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('semicolon'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/;'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL'); + } +} diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php new file mode 100644 index 0000000000..527274253c --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class DummyMemcachedProfilerStorage extends MemcachedProfilerStorage +{ + public function getMemcached() + { + return parent::getMemcached(); + } +} + +class MemcachedProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + protected static $storage; + + public static function tearDownAfterClass() + { + if (self::$storage) { + self::$storage->purge(); + } + } + + protected function setUp() + { + if (!extension_loaded('memcached')) { + $this->markTestSkipped('MemcachedProfilerStorageTest requires that the extension memcached is loaded'); + } + + self::$storage = new DummyMemcachedProfilerStorage('memcached://127.0.0.1/11211', '', '', 86400); + try { + self::$storage->getMemcached(); + } catch(\Exception $e) { + $this->markTestSkipped('MemcachedProfilerStorageTest requires that there is a Memcache server present on localhost'); + } + + if (self::$storage) { + self::$storage->purge(); + } + } + + public function testStore() + { + for ($i = 0; $i < 10; $i ++) { + $profile = new Profile('token_'.$i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setMethod('GET'); + self::$storage->write($profile); + } + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache'); + } + + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $parentProfile->setIp('127.0.0.1'); + $parentProfile->setUrl('http://foo.bar/parent'); + + $childProfile = new Profile('token_child'); + $childProfile->setIp('127.0.0.1'); + $childProfile->setUrl('http://foo.bar/child'); + + $parentProfile->addChild($childProfile); + + self::$storage->write($parentProfile); + self::$storage->write($childProfile); + + // Load them from storage + $parentProfile = self::$storage->read('token_parent'); + $childProfile = self::$storage->read('token_child'); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParent()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertEquals(1, count($children)); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The SQLite storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $profile = new Profile('simple_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\''); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL'); + + $profile = new Profile('double_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/"'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL'); + + $profile = new Profile('backslash'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\\'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL'); + + $profile = new Profile('comma'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/,'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $profile = new Profile('token'); + $profile->setUrl('http://example.com/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique'); + + $profile->setUrl('http://example.net/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache'); + $this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data'); + + $this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $profile = new Profile('token'); + $profile->setIp('127.0.0.1'); + $profile->setMethod('GET'); + + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP'); + $this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP'); + $this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $profile = new Profile('simple_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/\''); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('double_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/"'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('backslash'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo\\bar/'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('percent'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/%'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('underscore'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/_'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('semicolon'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/;'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL'); + } +}