From 37d03bbeae44993bb876bfaaaff3878125c44ef7 Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Sat, 27 Jul 2019 14:37:24 +0200 Subject: [PATCH] Introduce test optimization - Add static connection for whole tests - Introduce ExtendedPDO class to enable nested transactions - Add rollback logic for tests to ensure reliability and increase speed --- src/Database/Database.php | 20 ++-- tests/DatabaseTest.php | 41 +------- tests/Util/Database/ExtendedPDO.php | 97 +++++++++++++++++++ tests/Util/Database/StaticDatabase.php | 126 +++++++++++++++++++++++++ tests/include/ApiTest.php | 12 ++- 5 files changed, 246 insertions(+), 50 deletions(-) create mode 100644 tests/Util/Database/ExtendedPDO.php create mode 100644 tests/Util/Database/StaticDatabase.php diff --git a/src/Database/Database.php b/src/Database/Database.php index 1533b7674a..501d65fe76 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -21,29 +21,29 @@ use Psr\Log\LoggerInterface; */ class Database { - private $connected = false; + protected $connected = false; /** * @var ConfigCache */ - private $configCache; + protected $configCache; /** * @var Profiler */ - private $profiler; + protected $profiler; /** * @var LoggerInterface */ - private $logger; - private $server_info = ''; + protected $logger; + protected $server_info = ''; /** @var PDO|mysqli */ - private $connection; - private $driver; + protected $connection; + protected $driver; private $error = false; private $errorno = 0; private $affected_rows = 0; - private $in_transaction = false; - private $in_retrial = false; + protected $in_transaction = false; + protected $in_retrial = false; private $relation = []; public function __construct(ConfigCache $configCache, Profiler $profiler, LoggerInterface $logger, array $server = []) @@ -1070,7 +1070,7 @@ class Database return true; } - private function performCommit() + protected function performCommit() { switch ($this->driver) { case 'pdo': diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php index 98f79e351b..3c3f835eb6 100644 --- a/tests/DatabaseTest.php +++ b/tests/DatabaseTest.php @@ -5,7 +5,7 @@ namespace Friendica\Test; -use PDO; +use Friendica\Test\Util\Database\StaticDatabase; use PHPUnit\DbUnit\DataSet\YamlDataSet; use PHPUnit\DbUnit\TestCaseTrait; use PHPUnit_Extensions_Database_DB_IDatabaseConnection; @@ -17,12 +17,6 @@ abstract class DatabaseTest extends MockedTest { use TestCaseTrait; - // only instantiate pdo once for test clean-up/fixture load - static private $pdo = null; - - // only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test - private $conn = null; - /** * Get database connection. * @@ -36,38 +30,7 @@ abstract class DatabaseTest extends MockedTest */ protected function getConnection() { - $server = $_SERVER; - - if ($this->conn === null) { - if (self::$pdo == null) { - - if (!empty($server['MYSQL_HOST']) - && !empty($server['MYSQL_USERNAME'] || !empty($server['MYSQL_USER'])) - && $server['MYSQL_PASSWORD'] !== false - && !empty($server['MYSQL_DATABASE'])) { - - $connect = "mysql:host=" . $server['MYSQL_HOST'] . ";dbname=" . $server['MYSQL_DATABASE']; - - if (!empty($server['MYSQL_PORT'])) { - $connect .= ";port=" . $server['MYSQL_PORT']; - } - - if (!empty($server['MYSQL_USERNAME'])) { - $db_user = $server['MYSQL_USERNAME']; - } else { - $db_user = $server['MYSQL_USER']; - } - - $db_pass = (string)$server['MYSQL_PASSWORD']; - - self::$pdo = @new PDO($connect, $db_user, $db_pass); - self::$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); - } - } - $this->conn = $this->createDefaultDBConnection(self::$pdo, getenv('MYSQL_DATABASE')); - } - - return $this->conn; + return $this->createDefaultDBConnection(StaticDatabase::getGlobConnection(), getenv('MYSQL_DATABASE')); } /** diff --git a/tests/Util/Database/ExtendedPDO.php b/tests/Util/Database/ExtendedPDO.php new file mode 100644 index 0000000000..6bb0251c55 --- /dev/null +++ b/tests/Util/Database/ExtendedPDO.php @@ -0,0 +1,97 @@ +_transactionDepth; + } + + /** + * Test if database driver support savepoints + * + * @return bool + */ + protected function hasSavepoint() + { + return in_array($this->getAttribute(PDO::ATTR_DRIVER_NAME), + self::$_supportedDrivers); + } + + + /** + * Start transaction + * + * @return bool|void + */ + public function beginTransaction() + { + if($this->_transactionDepth == 0 || !$this->hasSavepoint()) { + parent::beginTransaction(); + } else { + $this->exec("SAVEPOINT LEVEL{$this->_transactionDepth}"); + } + + $this->_transactionDepth++; + } + + /** + * Commit current transaction + * + * @return bool|void + */ + public function commit() + { + $this->_transactionDepth--; + + if($this->_transactionDepth == 0 || !$this->hasSavepoint()) { + parent::commit(); + } else { + $this->exec("RELEASE SAVEPOINT LEVEL{$this->_transactionDepth}"); + } + } + + /** + * Rollback current transaction, + * + * @throws PDOException if there is no transaction started + * @return bool|void + */ + public function rollBack() + { + + if ($this->_transactionDepth == 0) { + throw new PDOException('Rollback error : There is no transaction started'); + } + + $this->_transactionDepth--; + + if($this->_transactionDepth == 0 || !$this->hasSavepoint()) { + parent::rollBack(); + } else { + $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->_transactionDepth}"); + } + } +} diff --git a/tests/Util/Database/StaticDatabase.php b/tests/Util/Database/StaticDatabase.php new file mode 100644 index 0000000000..6f9bdbe8b5 --- /dev/null +++ b/tests/Util/Database/StaticDatabase.php @@ -0,0 +1,126 @@ +connection) && $this->connected()) { + return true; + } + + if (!isset(self::$staticConnection)) { + + $port = 0; + $serveraddr = trim($this->configCache->get('database', 'hostname')); + $serverdata = explode(':', $serveraddr); + $server = $serverdata[0]; + if (count($serverdata) > 1) { + $port = trim($serverdata[1]); + } + $server = trim($server); + $user = trim($this->configCache->get('database', 'username')); + $pass = trim($this->configCache->get('database', 'password')); + $db = trim($this->configCache->get('database', 'database')); + $charset = trim($this->configCache->get('database', 'charset')); + + if (!(strlen($server) && strlen($user))) { + return false; + } + + $connect = "mysql:host=" . $server . ";dbname=" . $db; + + if ($port > 0) { + $connect .= ";port=" . $port; + } + + if ($charset) { + $connect .= ";charset=" . $charset; + } + + + try { + self::$staticConnection = @new ExtendedPDO($connect, $user, $pass); + self::$staticConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + } catch (PDOException $e) { + /// @TODO At least log exception, don't ignore it! + } + } + + $this->driver = 'pdo'; + $this->connection = self::$staticConnection; + $this->connected = true; + + return $this->connected; + } + + /** + * Override the transaction since there are now hierachical transactions possible + * + * @return bool + */ + public function transaction() + { + if (!$this->connection->inTransaction() && !$this->connection->beginTransaction()) { + return false; + } + + $this->in_transaction = true; + return true; + } + + /** + * @brief Does a commit + * + * @return boolean Was the command executed successfully? + */ + public function commit() + { + if (!$this->performCommit()) { + return false; + } + $this->in_transaction = false; + return true; + } + + /** + * @return ExtendedPDO + */ + public static function getGlobConnection() + { + return self::$staticConnection; + } + + public static function statCommit() + { + if (isset(self::$staticConnection)) { + while (self::$staticConnection->getTransactionDepth() > 0) { + self::$staticConnection->commit(); + } + } + } + + public static function statRollback() + { + if (isset(self::$staticConnection)) { + while (self::$staticConnection->getTransactionDepth() > 0) { + self::$staticConnection->rollBack(); + } + } + } +} diff --git a/tests/include/ApiTest.php b/tests/include/ApiTest.php index 351973d479..15da515277 100644 --- a/tests/include/ApiTest.php +++ b/tests/include/ApiTest.php @@ -12,7 +12,9 @@ use Friendica\Core\Config; use Friendica\Core\PConfig; use Friendica\Core\Protocol; use Friendica\Core\System; +use Friendica\Database\Database; use Friendica\Network\HTTPException; +use Friendica\Test\Util\Database\StaticDatabase; use Monolog\Handler\TestHandler; require_once __DIR__ . '/../../include/api.php'; @@ -47,13 +49,16 @@ class ApiTest extends DatabaseTest */ public function setUp() { - parent::setUp(); + StaticDatabase::statRollback(); $dice = new Dice(); $dice = $dice->addRules(include __DIR__ . '/../../static/dependencies.config.php'); + $dice = $dice->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]); BaseObject::setDependencyInjection($dice); $this->app = BaseObject::getApp(); + parent::setUp(); + $this->app->argc = 1; $this->app->argv = ['home']; @@ -99,6 +104,11 @@ class ApiTest extends DatabaseTest Config::set('system', 'theme', 'system_theme'); } + protected function tearDown() + { + StaticDatabase::statRollback(); + } + /** * Assert that an user array contains expected keys. * @param array $user User array