From d3078e4b0ec0245e7ee7058b06abf99754e87841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Mon, 9 Feb 2015 11:27:51 +0100 Subject: [PATCH 1/5] Bump minimum PHP version --- .travis.yml | 1 - README.md | 2 +- composer.json | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 63c4ca3..bfff8b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ env: - NETTE=nette-2.2 php: - - 5.3.3 - 5.4 - 5.5 - 5.6 diff --git a/README.md b/README.md index dbe8f9f..7ca87ad 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Kdyby/Redis Requirements ------------ -Kdyby/Redis requires PHP 5.3.2 or higher. +Kdyby/Redis requires PHP 5.4 or higher. - [Nette Framework](https://github.com/nette/nette) - [Redis database](http://redis.io) diff --git a/composer.json b/composer.json index e7c0e99..f1de658 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "issues": "https://github.com/kdyby/redis/issues" }, "require": { + "php": ">=5.4", "nette/di": "~2.2@dev", "nette/caching": "~2.2@dev", "nette/http": "~2.2@dev", From a3bde85bce6b92fc9d92ca9b9ee481f25accf2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Mon, 9 Feb 2015 10:15:36 +0100 Subject: [PATCH 2/5] Refactored RedisSessionHandler --- composer.json | 3 + src/Kdyby/Redis/DI/RedisExtension.php | 2 +- src/Kdyby/Redis/RedisSessionHandler.php | 120 ++++---- src/Kdyby/Redis/exceptions.php | 10 + .../Redis/AbstractRedisTestCase.php | 128 ++++++++- .../KdybyTests/Redis/RedisSessionHandler.phpt | 269 +++++++++++++++++- tests/KdybyTests/bootstrap.php | 1 - tests/php.ini-unix | 3 + 8 files changed, 469 insertions(+), 67 deletions(-) diff --git a/composer.json b/composer.json index f1de658..8a11e1a 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,9 @@ "src/Kdyby/Redis/exceptions.php" ] }, + "autoload-dev": { + "classmap": ["tests/KdybyTests/Redis"] + }, "extra": { "branch-alias": { "dev-master": "3.2-dev" diff --git a/src/Kdyby/Redis/DI/RedisExtension.php b/src/Kdyby/Redis/DI/RedisExtension.php index ba6ee71..a417324 100644 --- a/src/Kdyby/Redis/DI/RedisExtension.php +++ b/src/Kdyby/Redis/DI/RedisExtension.php @@ -222,7 +222,7 @@ protected function loadSession(array $config) ->setClass('Kdyby\Redis\RedisSessionHandler', array($this->prefix('@sessionHandler_client'))); $builder->getDefinition('session') - ->addSetup('setStorage', array($this->prefix('@sessionHandler'))); + ->addSetup('setHandler', array($this->prefix('@sessionHandler'))); } } diff --git a/src/Kdyby/Redis/RedisSessionHandler.php b/src/Kdyby/Redis/RedisSessionHandler.php index ba31868..56a0df3 100644 --- a/src/Kdyby/Redis/RedisSessionHandler.php +++ b/src/Kdyby/Redis/RedisSessionHandler.php @@ -25,7 +25,7 @@ * * @author Filip Procházka */ -class RedisSessionHandler extends Nette\Object implements Nette\Http\ISessionStorage +class RedisSessionHandler extends Nette\Object implements \SessionHandlerInterface { /** @internal cache structure */ @@ -41,6 +41,11 @@ class RedisSessionHandler extends Nette\Object implements Nette\Http\ISessionSto */ private $client; + /** + * @var integer + */ + private $ttl; + /** @@ -49,39 +54,46 @@ class RedisSessionHandler extends Nette\Object implements Nette\Http\ISessionSto public function __construct(RedisClient $redisClient) { $this->client = $redisClient; + $this->ttl = ini_get("session.gc_maxlifetime"); } /** - * @param $savePath - * @param $sessionName - * + * @param int $ttl + */ + public function setTtl($ttl) + { + $this->ttl = max($ttl, 0); + } + + + + /** + * @param string $savePath + * @param string $sessionName * @return bool */ public function open($savePath, $sessionName) { - return TRUE; + $id = &$_COOKIE[$sessionName]; // prevent notice + if (!is_string($id) || !preg_match('#^[0-9a-zA-Z,-]{22,128}\z#i', $id)) { + return FALSE; // Wtf? + } + + return (bool) $this->lock($id); } /** * @param string $id - * + * @throws SessionHandlerException * @return string */ public function read($id) { - try { - $key = $this->formatKey($id); - $this->ssIds[$key] = $this->client->lock($key); - return (string) $this->client->get($key); - - } catch (Exception $e) { - Debugger::log($e, 'redis-session'); - return FALSE; - } + return (string) $this->client->get($this->lock($id)); } @@ -89,21 +101,15 @@ public function read($id) /** * @param string $id * @param string $data - * * @return bool */ public function write($id, $data) { - try { - $key = $this->formatKey($id); - $this->ssIds[$key] = $this->client->lock($key); - $this->client->setex($key, ini_get("session.gc_maxlifetime"), $data); - return TRUE; - - } catch (Exception $e) { - Debugger::log($e, 'redis-session'); + if (!isset($this->ssIds[$id])) { return FALSE; } + + return $this->client->setex($this->formatKey($id), $this->ttl, $data); } @@ -113,33 +119,19 @@ public function write($id, $data) * * @return bool */ - public function remove($id) + public function destroy($id) { - try { - $key = $this->formatKey($id); - $this->client->multi(function (RedisClient $client) use ($key) { - $client->del($key); - $client->unlock($key); - }); - unset($this->ssIds[$key]); - return TRUE; - - } catch (Exception $e) { - Debugger::log($e, 'redis-session'); + if (!isset($this->ssIds[$id])) { return FALSE; } - } + $key = $this->formatKey($id); + $this->client->multi(function (RedisClient $client) use ($key) { + $client->del($key); + $client->unlock($key); + }); - - /** - * @param string $id - * - * @return string - */ - private function formatKey($id) - { - return self::NS_NETTE . $id; + return TRUE; } @@ -149,7 +141,7 @@ private function formatKey($id) */ public function close() { - foreach ($this->ssIds as $key => $yes) { + foreach ($this->ssIds as $id => $key) { $this->client->unlock($key); } $this->ssIds = array(); @@ -164,13 +156,45 @@ public function close() * * @return bool */ - public function clean($maxLifeTime) + public function gc($maxLifeTime) { return TRUE; } + /** + * @param string $id + * @return string + */ + protected function lock($id) + { + try { + $key = $this->formatKey($id); + $this->client->lock($key); + $this->ssIds[$id] = $key; + + return $key; + + } catch (LockException $e) { + throw new SessionHandlerException(sprintf('Cannot work with non-locked session id %s: %s', $id, $e->getMessage()), 0, $e); + } + } + + + + /** + * @param string $id + * + * @return string + */ + private function formatKey($id) + { + return self::NS_NETTE . $id; + } + + + public function __destruct() { $this->close(); diff --git a/src/Kdyby/Redis/exceptions.php b/src/Kdyby/Redis/exceptions.php index 0e19a7a..f3b4679 100644 --- a/src/Kdyby/Redis/exceptions.php +++ b/src/Kdyby/Redis/exceptions.php @@ -52,6 +52,16 @@ class ConnectionException extends \RuntimeException implements Exception +/** + * @author Filip Procházka + */ +class SessionHandlerException extends \RuntimeException implements Exception +{ + +} + + + /** * @author Filip Procházka */ diff --git a/tests/KdybyTests/Redis/AbstractRedisTestCase.php b/tests/KdybyTests/Redis/AbstractRedisTestCase.php index 8395a61..9a0ae55 100644 --- a/tests/KdybyTests/Redis/AbstractRedisTestCase.php +++ b/tests/KdybyTests/Redis/AbstractRedisTestCase.php @@ -94,8 +94,10 @@ protected function tearDown() * @param callable $closure * @param int $repeat * @param int $threads + * @param bool $outputFailures + * @return array */ - protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30) + protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30, $outputFailures = TRUE) { $runTest = Tracy\Helpers::findTrace(debug_backtrace(), 'Tester\TestCase::runTest') ?: array('args' => array(0 => 'test')); $scriptFile = TEMP_DIR . '/scripts/' . str_replace('%5C', '_', urlencode(get_class($this))) . '.' . urlencode($runTest['args'][0]) . '.php'; @@ -112,13 +114,13 @@ protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30) $runner->paths = array($scriptFile); $runner->run(); - $result = $runner->getResults(); - - foreach ($messages->results as $result) { - echo 'FAILURE ' . $result[0] . "\n" . $result[1] . "\n"; + if ($outputFailures) { + foreach ($messages->results as $process) { + echo 'FAILURE ' . $process[0] . "\n" . $process[1] . "\n"; + } } - Tester\Assert::equal($repeat, $result[Tester\Runner\Runner::PASSED]); + return $runner->getResults(); } } @@ -204,7 +206,8 @@ public function buildScript(\ReflectionClass $class, $repeat) // bootstrap $code .= Code\Helpers::formatArgs('require_once ?;', array(__DIR__ . '/../bootstrap.php')) . "\n"; - $code .= '\Tester\Environment::$checkAssertions = FALSE;' . "\n\n\n"; + $code .= '\Tester\Environment::$checkAssertions = FALSE;' . "\n"; + $code .= Code\Helpers::formatArgs('\Tracy\Debugger::$logDirectory = ?;', array(TEMP_DIR)) . "\n\n\n"; // script $code .= Code\Helpers::formatArgs('extract(?);', array($this->closure->getStaticVariables())) . "\n\n"; @@ -526,3 +529,114 @@ private function parseFile() } } + + +class SessionHandlerDecorator implements \SessionHandlerInterface +{ + + /** + * @var array + */ + public $methods = array(); + + /** + * @var bool + */ + public $log = FALSE; + + /** + * @var \SessionHandlerInterface + */ + private $handler; + + + + public function __construct(\SessionHandlerInterface $handler) + { + $this->handler = $handler; + } + + + + private function log($msg) + { + if (!$this->log) { + return; + } + + file_put_contents(dirname(TEMP_DIR) . '/session.log', sprintf('[%s] [%s]: %s', date('Y-m-d H:i:s'), str_pad(getmypid(), 6, '0', STR_PAD_LEFT), $msg) . "\n", FILE_APPEND); + } + + + + public function open($save_path, $session_id) + { + $this->log(sprintf('%s: %s', __FUNCTION__, $session_id)); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + return $this->handler->open($save_path, $session_id); + } + + + + public function close() + { + $this->log(__FUNCTION__); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + return $this->handler->close(); + } + + + + public function read($session_id) + { + $this->log(sprintf('%s: %s', __FUNCTION__, $session_id)); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + try { + return $this->handler->read($session_id); + + } catch (\Exception $e) { + $this->log(sprintf('%s: %s', __FUNCTION__, $e->getMessage())); + throw $e; + } + } + + + + public function destroy($session_id) + { + $this->log(sprintf('%s: %s', __FUNCTION__, $session_id)); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + try { + return $this->handler->destroy($session_id); + + } catch (\Exception $e) { + $this->log(sprintf('%s: %s', __FUNCTION__, $e->getMessage())); + throw $e; + } + } + + + + public function write($session_id, $session_data) + { + $this->log(sprintf('%s: %s', __FUNCTION__, $session_id)); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + try { + return $this->handler->write($session_id, $session_data); + + } catch (\Exception $e) { + $this->log(sprintf('%s: %s', __FUNCTION__, $e->getMessage())); + throw $e; + } + } + + + + public function gc($maxlifetime) + { + $this->log(__FUNCTION__); + $this->methods[] = array(__FUNCTION__ => func_get_args()); + return $this->handler->gc($maxlifetime); + } + +} diff --git a/tests/KdybyTests/Redis/RedisSessionHandler.phpt b/tests/KdybyTests/Redis/RedisSessionHandler.phpt index f20147c..3742422 100644 --- a/tests/KdybyTests/Redis/RedisSessionHandler.phpt +++ b/tests/KdybyTests/Redis/RedisSessionHandler.phpt @@ -16,9 +16,9 @@ use Nette; use Tester; use Tester\Assert; -require_once __DIR__ . '/../bootstrap.php'; +require_once __DIR__ . '/../bootstrap.php'; /** * @author Filip Procházka @@ -31,18 +31,19 @@ class SessionHandlerTest extends AbstractRedisTestCase */ public function testConsistency() { - $userId = md5(1); - - $this->threadStress(function () use ($userId) { - $handler = new RedisSessionHandler(new RedisClient()); + $sessionId = md5(1); - \Tracy\Debugger::$logDirectory = __DIR__; + $result = $this->threadStress(function () use ($sessionId) { \Tracy\Debugger::log(getmypid()); + $handler = new RedisSessionHandler(new RedisClient()); + // read - $handler->open('path', 'session_id'); + $_COOKIE[session_name()] = $sessionId; + Assert::true($handler->open('path', session_name())); + $session = array('counter' => 0); - if ($data = $handler->read($userId)) { + if ($data = $handler->read($sessionId)) { $session = unserialize($data); } @@ -51,20 +52,268 @@ class SessionHandlerTest extends AbstractRedisTestCase usleep(100000); // write - $handler->write($userId, serialize($session)); + $handler->write($sessionId, serialize($session)); $handler->close(); }); + Assert::same(100, $result[Tester\Runner\Runner::PASSED]); $handler = new RedisSessionHandler($this->client); + + $_COOKIE[session_name()] = $sessionId; $handler->open('path', 'session_id'); - $data = $handler->read($userId); + $data = $handler->read($sessionId); Assert::false(empty($data)); $session = unserialize($data); Assert::true(is_array($session)); Assert::true(array_key_exists('counter', $session)); Assert::same(100, $session['counter']); + + $handler->close(); // unlock + + Assert::count(1, $this->client->keys('Nette.Session:*')); + } + + + + /** + * @group concurrency + */ + public function testIntegration_existingSession() + { + $sessionId = md5(1); + $session = self::createSession(array(session_name() => $sessionId, 'nette-browser' => $B = '1lm7e5iqsk')); + + $session->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($this->client))); + $this->client->setupLockDuration(60, 20); + + // fake session + $this->client->set('Nette.Session:' . $sessionId, '__NF|' . serialize(array('Time' => $T = time() - 1000, 'B' => $B))); + + $counter = $session->getSection('counter'); + $counter->visits += 1; + Assert::same(1, $counter->visits); + + // close session + $session->close(); + + // reopen the session "on next request" + $counter = $session->getSection('counter'); + $counter->visits += 1; + Assert::same(2, $counter->visits); + + // close session + $session->close(); + + Assert::same(array( + array('open' => array('', 'PHPSESSID')), + array('read' => array($sessionId)), + array('write' => array($sessionId, '__NF|a:3:{s:4:"Time";i:' . $T . ';s:1:"B";s:10:"' . $B . '";s:4:"DATA";a:1:{s:7:"counter";a:1:{s:6:"visits";i:1;}}}')), + array('close' => array()), + array('open' => array('', 'PHPSESSID')), + array('read' => array($sessionId)), + array('write' => array($sessionId, '__NF|a:3:{s:4:"Time";i:' . $T . ';s:1:"B";s:10:"' . $B . '";s:4:"DATA";a:1:{s:7:"counter";a:1:{s:6:"visits";i:2;}}}')), + array('close' => array()), + ), $handler->methods); + + Assert::count(1, $this->client->keys('Nette.Session:*')); + } + + + + /** + * @group concurrency + */ + public function testIntegration_emptySession_regenerate() + { + $sessionId = md5(1); + + $session1 = self::createSession(array(session_name() => $sessionId)); // no browser, empty session + $session1->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($client = $this->client))); + $client->setupLockDuration(60, 20); + + $counter = $session1->getSection('counter'); + $counter->visits += 1; + Assert::same(1, $counter->visits); + + // close session + $session1->close(); + + Assert::count(9, $handler->methods); + + // regenerate + Assert::same(array('open' => array('', 'PHPSESSID')), $handler->methods[0]); + Assert::same(array('read' => array($sessionId)), $handler->methods[1]); + Assert::same(array('destroy' => array($sessionId)), $handler->methods[2]); + Assert::match('%S%', $regeneratedId = $handler->methods[3]['write'][0]); + Assert::match('__NF|a:2:{s:4:"Time";i:%S%;s:1:"B";s:10:"%S%";}', $handler->methods[3]['write'][1]); + Assert::same(array('close' => array()), $handler->methods[4]); + + // open regenerated + Assert::same(array('open' => array('', 'PHPSESSID')), $handler->methods[5]); + Assert::same(array('read' => array($regeneratedId)), $handler->methods[6]); + Assert::same($regeneratedId, $handler->methods[7]['write'][0]); + Assert::match('__NF|a:3:{s:4:"Time";i:%S%;s:1:"B";s:10:"%S%";s:4:"DATA";a:1:{s:7:"counter";a:1:{s:6:"visits";i:1;}}}', $handler->methods[7]['write'][1]); + Assert::same(array('close' => array()), $handler->methods[8]); + + Assert::notSame($sessionId, $regeneratedId); + + $session2 = self::createSession(array(session_name() => $regeneratedId, 'nette-browser' => $_SESSION['__NF']['B'])); // no browser, empty session + $session2->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($client = new RedisClient()))); + $client->setupLockDuration(60, 20); + + $counter = $session2->getSection('counter'); + $counter->visits += 1; + Assert::same(2, $counter->visits); + + // close session + $session2->close(); + + Assert::same(array( + array('open' => array('', 'PHPSESSID')), + array('read' => array($regeneratedId)), + array('write' => array($regeneratedId, '__NF|a:3:{s:4:"Time";i:' . $_SESSION['__NF']['Time'] . ';s:1:"B";s:10:"' . $_SESSION['__NF']['B'] . '";s:4:"DATA";a:1:{s:7:"counter";a:1:{s:6:"visits";i:2;}}}')), + array('close' => array()), + ), $handler->methods); + + Assert::count(1, $this->client->keys('Nette.Session:*')); + } + + + + /** + * @group concurrency + */ + public function testIntegration_timeout() + { + $sessionId = md5(1); + + $client = $this->client; + $client->setupLockDuration(50, 20); + $client->lock('Nette.Session:' . $sessionId); + + sleep(3); // working for a looong time :) + + $session = self::createSession(array(session_name() => $sessionId)); + $session->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($client = new RedisClient()))); + $client->setupLockDuration(5, 2); + + Assert::exception(function () use ($session) { + $counter = $session->getSection('counter'); + $counter->visits += 1; + Assert::same(1, $counter->visits); + }, 'Kdyby\Redis\SessionHandlerException', sprintf('Cannot work with non-locked session id %s: Lock couldn\'t be acquired. The locking mechanism is giving up. You should kill the request.', $sessionId)); + + Assert::count(1, $this->client->keys('Nette.Session:*')); + } + + + + /** + * @group concurrency + */ + public function testConsistency_Integration() + { + $sessionId = md5(1); + + $session = self::createSession(array(session_name() => $sessionId, 'nette-browser' => $B = '1lm7e5iqsk')); + $session->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($client = new RedisClient()))); + + // fake session + $client->set('Nette.Session:' . $sessionId, '__NF|' . serialize(array('Time' => $T = time() - 1000, 'B' => $B))); + + // open session + $counter = $session->getSection('counter'); + $counter->visits = 0; + + // explicitly close + $session->close(); + + Assert::same(array( + array('open' => array('', 'PHPSESSID')), + array('read' => array($sessionId)), + array('write' => array($sessionId, '__NF|a:3:{s:4:"Time";i:' . $_SESSION['__NF']['Time'] . ';s:1:"B";s:10:"' . $_SESSION['__NF']['B'] . '";s:4:"DATA";a:1:{s:7:"counter";a:1:{s:6:"visits";i:0;}}}')), + array('close' => array()), + ), $handler->methods); + + // only testing the behaviour of high concurency for one request, without regenerating the session id + // 30 processes will be started, but every one of them will work for at least 1 second + // a fuckload (~66%) of the processes should actually fail, because the timeout for lock acquire is 10 sec + $result = $this->threadStress(function () use ($sessionId, $B) { + $_COOKIE[session_name()] = $sessionId; + $_COOKIE['nette-browser'] = $B; + + $session = new Nette\Http\Session( + new Nette\Http\Request( + new Nette\Http\UrlScript('http://www.kdyby.org'), + NULL, array(), array(), array(session_name() => $sessionId, 'nette-browser' => $B), array(), 'GET' + ), + new Nette\Http\Response() + ); + + $session->setHandler($handler = new SessionHandlerDecorator(new RedisSessionHandler($client = new RedisClient()))); + $client->setupLockDuration(60, 10); + $handler->log = TRUE; + + $counter = $session->getSection('counter'); + $counter->visits += 1; + sleep(1); // hard work after session is opened ~ 1s + + $session->close(); // explicit close with unlock + }, 100, 30, FALSE); // silence, I kill you! + + self::assertRange(30, 40, $result[Tester\Runner\Runner::PASSED]); + + // hard unlock + $client->del('Nette.Session:' . $sessionId . ':lock'); + + // open session for visits verify, for second time + $counter = $session->getSection('counter'); + self::assertRange(30, 40, $counter->visits); + + $session->close(); // unlocking drops the key + + Assert::count(1, $this->client->keys('Nette.Session:*')); + } + + + + /** + * @param array $cookies + * @return Nette\Http\Session + */ + private static function createSession($cookies = array()) + { + foreach ($cookies as $key => $val) { + $_COOKIE[$key] = $val; + } + + return new Nette\Http\Session( + new Nette\Http\Request( + new Nette\Http\UrlScript('http://www.kdyby.org'), + NULL, + array(), + array(), + $cookies, + array(), + 'GET' + ), + new Nette\Http\Response() + ); + } + + + + protected static function assertRange($expectedLower, $expectedHigher, $actual) + { + Assert::$counter++; + if ($actual > $expectedHigher) { + Assert::fail('%1 should be lower than %2', $actual, $expectedHigher); + } + if ($actual < $expectedLower) { + Assert::fail('%1 should be higher than %2', $actual, $expectedLower); + } } } diff --git a/tests/KdybyTests/bootstrap.php b/tests/KdybyTests/bootstrap.php index 0910967..c7bf678 100755 --- a/tests/KdybyTests/bootstrap.php +++ b/tests/KdybyTests/bootstrap.php @@ -12,7 +12,6 @@ echo 'Install Nette Tester using `composer update --dev`'; exit(1); } -$loader->add('KdybyTests\\Redis\\', __DIR__ . '/..'); // configure environment Tester\Environment::setup(); diff --git a/tests/php.ini-unix b/tests/php.ini-unix index 02b6889..cdcc37b 100644 --- a/tests/php.ini-unix +++ b/tests/php.ini-unix @@ -1,2 +1,5 @@ +[php] +session.gc_probability=0 + [redis] extension=redis.so From 121ea1986ab7275c4442af4d15df92ff5241fb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Tue, 10 Feb 2015 15:30:07 +0100 Subject: [PATCH 3/5] Dump to output/*.actual instead of outputing the contents of failed threads --- .../Redis/AbstractRedisTestCase.php | 36 +++++++++++++++---- .../KdybyTests/Redis/RedisSessionHandler.phpt | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/tests/KdybyTests/Redis/AbstractRedisTestCase.php b/tests/KdybyTests/Redis/AbstractRedisTestCase.php index 9a0ae55..a5fea00 100644 --- a/tests/KdybyTests/Redis/AbstractRedisTestCase.php +++ b/tests/KdybyTests/Redis/AbstractRedisTestCase.php @@ -11,6 +11,7 @@ use Nette\Reflection\GlobalFunction; use Nette\Utils\AssertionException; use Nette\Utils\Callback; +use Nette\Utils\FileSystem; use Nette\Utils\Strings; use Tester; use Tracy; @@ -97,11 +98,11 @@ protected function tearDown() * @param bool $outputFailures * @return array */ - protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30, $outputFailures = TRUE) + protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30) { $runTest = Tracy\Helpers::findTrace(debug_backtrace(), 'Tester\TestCase::runTest') ?: array('args' => array(0 => 'test')); $scriptFile = TEMP_DIR . '/scripts/' . str_replace('%5C', '_', urlencode(get_class($this))) . '.' . urlencode($runTest['args'][0]) . '.php'; - Nette\Utils\FileSystem::createDir($dir = dirname($scriptFile)); + FileSystem::createDir($dir = dirname($scriptFile)); $extractor = new ClosureExtractor($closure); file_put_contents($scriptFile, $extractor->buildScript(ClassType::from($this), $repeat)); @@ -114,11 +115,8 @@ protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30, $runner->paths = array($scriptFile); $runner->run(); - if ($outputFailures) { - foreach ($messages->results as $process) { - echo 'FAILURE ' . $process[0] . "\n" . $process[1] . "\n"; - } - } + $testRefl = new \ReflectionClass($this); + $messages->dump(dirname($testRefl->getFileName()) . '/output', $runTest['args'][0]); return $runner->getResults(); } @@ -156,6 +154,30 @@ public function end() } + + + public function dump($dir, $testName) + { + if (is_dir($dir)) { + foreach (glob(sprintf('%s/%s.*.actual', $dir, urlencode($testName))) as $file) { + @unlink($file); + } + } + + if (!$this->results) { + return; + } + + FileSystem::createDir($dir); + + // write new + foreach ($this->results as $process) { + $args = !preg_match('~\\[(.+)\\]$~', trim($process[0]), $m) ? md5(basename($process[0])) : str_replace('=', '_', $m[1]); + $filename = urlencode($testName) . '.' . urlencode($args) . '.actual'; + file_put_contents($dir . '/' . $filename, $process[1]); + } + } + } diff --git a/tests/KdybyTests/Redis/RedisSessionHandler.phpt b/tests/KdybyTests/Redis/RedisSessionHandler.phpt index 3742422..7c9b856 100644 --- a/tests/KdybyTests/Redis/RedisSessionHandler.phpt +++ b/tests/KdybyTests/Redis/RedisSessionHandler.phpt @@ -261,7 +261,7 @@ class SessionHandlerTest extends AbstractRedisTestCase sleep(1); // hard work after session is opened ~ 1s $session->close(); // explicit close with unlock - }, 100, 30, FALSE); // silence, I kill you! + }, 100, 30); // silence, I kill you! self::assertRange(30, 40, $result[Tester\Runner\Runner::PASSED]); From 307e64a5021ba6160fe104b9e93b838b1994977a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Tue, 10 Feb 2015 15:37:25 +0100 Subject: [PATCH 4/5] Run consistency tests repeatedly --- tests/KdybyTests/Redis/RedisSessionHandler.phpt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/KdybyTests/Redis/RedisSessionHandler.phpt b/tests/KdybyTests/Redis/RedisSessionHandler.phpt index 7c9b856..2ae5f58 100644 --- a/tests/KdybyTests/Redis/RedisSessionHandler.phpt +++ b/tests/KdybyTests/Redis/RedisSessionHandler.phpt @@ -27,6 +27,7 @@ class SessionHandlerTest extends AbstractRedisTestCase { /** + * @dataProvider dataRepeatMe * @group concurrency */ public function testConsistency() @@ -211,6 +212,7 @@ class SessionHandlerTest extends AbstractRedisTestCase /** + * @dataProvider dataRepeatMe * @group concurrency */ public function testConsistency_Integration() @@ -279,6 +281,16 @@ class SessionHandlerTest extends AbstractRedisTestCase + /** + * @return array + */ + public function dataRepeatMe() + { + return array_fill(0, 10, array()); + } + + + /** * @param array $cookies * @return Nette\Http\Session From 14b064881e8bc60f30d898e095d7e95443faf854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Tue, 10 Feb 2015 15:46:54 +0100 Subject: [PATCH 5/5] Refactored ResultsCollector --- .../Redis/AbstractRedisTestCase.php | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/tests/KdybyTests/Redis/AbstractRedisTestCase.php b/tests/KdybyTests/Redis/AbstractRedisTestCase.php index a5fea00..0f68ce8 100644 --- a/tests/KdybyTests/Redis/AbstractRedisTestCase.php +++ b/tests/KdybyTests/Redis/AbstractRedisTestCase.php @@ -95,7 +95,6 @@ protected function tearDown() * @param callable $closure * @param int $repeat * @param int $threads - * @param bool $outputFailures * @return array */ protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30) @@ -108,16 +107,16 @@ protected function threadStress(\Closure $closure, $repeat = 100, $threads = 30) file_put_contents($scriptFile, $extractor->buildScript(ClassType::from($this), $repeat)); @chmod($scriptFile, 0755); + $testRefl = new \ReflectionClass($this); + $collector = new ResultsCollector(dirname($testRefl->getFileName()) . '/output', $runTest['args'][0]); + // todo: fix for hhvm $runner = new Tester\Runner\Runner(new Tester\Runner\ZendPhpInterpreter('php-cgi', ' -c ' . Tester\Helpers::escapeArg(__DIR__ . '/../../php.ini-unix'))); - $runner->outputHandlers[] = $messages = new ResultsCollector(); + $runner->outputHandlers[] = $collector; $runner->threadCount = $threads; $runner->paths = array($scriptFile); $runner->run(); - $testRefl = new \ReflectionClass($this); - $messages->dump(dirname($testRefl->getFileName()) . '/output', $runTest['args'][0]); - return $runner->getResults(); } @@ -129,11 +128,40 @@ class ResultsCollector implements Tester\Runner\OutputHandler public $results; + /** + * @var string + */ + private $dir; + + /** + * @var string + */ + private $testName; + + + + public function __construct($dir, $testName = NULL) + { + $this->dir = $dir; + + if (!$testName) { + $runTest = Tracy\Helpers::findTrace(debug_backtrace(), 'Tester\TestCase::runTest') ?: array('args' => array(0 => 'test')); + $testName = $runTest['args'][0]; + } + $this->testName = $testName; + } + public function begin() { $this->results = array(); + + if (is_dir($this->dir)) { + foreach (glob(sprintf('%s/%s.*.actual', $this->dir, urlencode($this->testName))) as $file) { + @unlink($file); + } + } } @@ -151,30 +179,17 @@ public function result($testName, $result, $message) public function end() { - - } - - - - public function dump($dir, $testName) - { - if (is_dir($dir)) { - foreach (glob(sprintf('%s/%s.*.actual', $dir, urlencode($testName))) as $file) { - @unlink($file); - } - } - if (!$this->results) { return; } - FileSystem::createDir($dir); + FileSystem::createDir($this->dir); // write new foreach ($this->results as $process) { $args = !preg_match('~\\[(.+)\\]$~', trim($process[0]), $m) ? md5(basename($process[0])) : str_replace('=', '_', $m[1]); - $filename = urlencode($testName) . '.' . urlencode($args) . '.actual'; - file_put_contents($dir . '/' . $filename, $process[1]); + $filename = urlencode($this->testName) . '.' . urlencode($args) . '.actual'; + file_put_contents($this->dir . '/' . $filename, $process[1]); } }