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
This commit is contained in:
Philipp Holzer 2019-07-27 14:37:24 +02:00
parent 07aaf292ec
commit 37d03bbeae
No known key found for this signature in database
GPG key ID: D8365C3D36B77D90
5 changed files with 246 additions and 50 deletions

View file

@ -21,29 +21,29 @@ use Psr\Log\LoggerInterface;
*/ */
class Database class Database
{ {
private $connected = false; protected $connected = false;
/** /**
* @var ConfigCache * @var ConfigCache
*/ */
private $configCache; protected $configCache;
/** /**
* @var Profiler * @var Profiler
*/ */
private $profiler; protected $profiler;
/** /**
* @var LoggerInterface * @var LoggerInterface
*/ */
private $logger; protected $logger;
private $server_info = ''; protected $server_info = '';
/** @var PDO|mysqli */ /** @var PDO|mysqli */
private $connection; protected $connection;
private $driver; protected $driver;
private $error = false; private $error = false;
private $errorno = 0; private $errorno = 0;
private $affected_rows = 0; private $affected_rows = 0;
private $in_transaction = false; protected $in_transaction = false;
private $in_retrial = false; protected $in_retrial = false;
private $relation = []; private $relation = [];
public function __construct(ConfigCache $configCache, Profiler $profiler, LoggerInterface $logger, array $server = []) public function __construct(ConfigCache $configCache, Profiler $profiler, LoggerInterface $logger, array $server = [])
@ -1070,7 +1070,7 @@ class Database
return true; return true;
} }
private function performCommit() protected function performCommit()
{ {
switch ($this->driver) { switch ($this->driver) {
case 'pdo': case 'pdo':

View file

@ -5,7 +5,7 @@
namespace Friendica\Test; namespace Friendica\Test;
use PDO; use Friendica\Test\Util\Database\StaticDatabase;
use PHPUnit\DbUnit\DataSet\YamlDataSet; use PHPUnit\DbUnit\DataSet\YamlDataSet;
use PHPUnit\DbUnit\TestCaseTrait; use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit_Extensions_Database_DB_IDatabaseConnection; use PHPUnit_Extensions_Database_DB_IDatabaseConnection;
@ -17,12 +17,6 @@ abstract class DatabaseTest extends MockedTest
{ {
use TestCaseTrait; 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. * Get database connection.
* *
@ -36,38 +30,7 @@ abstract class DatabaseTest extends MockedTest
*/ */
protected function getConnection() protected function getConnection()
{ {
$server = $_SERVER; return $this->createDefaultDBConnection(StaticDatabase::getGlobConnection(), getenv('MYSQL_DATABASE'));
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;
} }
/** /**

View file

@ -0,0 +1,97 @@
<?php
namespace Friendica\Test\Util\Database;
use PDO;
use PDOException;
/**
* This class extends native PDO one but allow nested transactions
* by using the SQL statements `SAVEPOINT', 'RELEASE SAVEPOINT' AND 'ROLLBACK SAVEPOINT'
*/
class ExtendedPDO extends PDO
{
/**
* @var array Database drivers that support SAVEPOINT * statements.
*/
protected static $_supportedDrivers = array("pgsql", "mysql");
/**
* @var int the current transaction depth
*/
protected $_transactionDepth = 0;
/**
* @return int
*/
public function getTransactionDepth()
{
return $this->_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}");
}
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Friendica\Test\Util\Database;
use Friendica\Database\Database;
use PDO;
use PDOException;
class StaticDatabase extends Database
{
/**
* @var ExtendedPDO
*/
private static $staticConnection;
/**
* Override the behaviour of connect, due there is just one, static connection at all
*
* @return bool|void
*/
public function connect()
{
if (!is_null($this->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();
}
}
}
}

View file

@ -12,7 +12,9 @@ use Friendica\Core\Config;
use Friendica\Core\PConfig; use Friendica\Core\PConfig;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Test\Util\Database\StaticDatabase;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
require_once __DIR__ . '/../../include/api.php'; require_once __DIR__ . '/../../include/api.php';
@ -47,13 +49,16 @@ class ApiTest extends DatabaseTest
*/ */
public function setUp() public function setUp()
{ {
parent::setUp(); StaticDatabase::statRollback();
$dice = new Dice(); $dice = new Dice();
$dice = $dice->addRules(include __DIR__ . '/../../static/dependencies.config.php'); $dice = $dice->addRules(include __DIR__ . '/../../static/dependencies.config.php');
$dice = $dice->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]);
BaseObject::setDependencyInjection($dice); BaseObject::setDependencyInjection($dice);
$this->app = BaseObject::getApp(); $this->app = BaseObject::getApp();
parent::setUp();
$this->app->argc = 1; $this->app->argc = 1;
$this->app->argv = ['home']; $this->app->argv = ['home'];
@ -99,6 +104,11 @@ class ApiTest extends DatabaseTest
Config::set('system', 'theme', 'system_theme'); Config::set('system', 'theme', 'system_theme');
} }
protected function tearDown()
{
StaticDatabase::statRollback();
}
/** /**
* Assert that an user array contains expected keys. * Assert that an user array contains expected keys.
* @param array $user User array * @param array $user User array