mirror of
https://framagit.org/hubzilla/core.git
synced 2026-06-21 00:52:33 -04:00
This patch introduced database transaction support to the Dba driver via
the DbaTransaction class.
The goal of this is to allow the driver control over the creation and
finalization of database transactions.
Until now code that has needed transaction support has done so directly
by issuing "BEGIN", "ROLLBACK" and "COMMIT" commands to the underlying
database directly.
This has several disadvantages:
- We do have no control or knowledge of whether any transactions being
active.
- Since transactions can not be nested, we run the risk of unrelated
code trying to create a transaction when one is already active.
- Code using transactions are not testable, as the test runner wraps
all tests within a transaction to begin with.
This patch should eliminate all these problems.
A transaction is started by instantiating the DbaTransaction class:
$my_transaction = new \DbaTransaction();
The transaction will automatically be _rolled back_ if it has not been
committed before the instance is destroyed. (When the variable holding
it goes out of scope, i.e when the containing function returns.)
A transaction is committed like this:
$my_transaction->commit();
This will immediately commit the changes in the transaction, and the
transaction will be marked as committed, so it will not be attempted to
be rolled back on destruction.
I have chosen to "ignore" the problem of nested transactions by having
the DbaTransaction class _not_ initiate a new transaction if one is
already active. This also makes the rollback and commit actions of the
DbaTransaction class into no-ops.
An alternative would be to simulate nested transactions by using save
points if a transaction is already active. However, I'm unsure about
wether there's any safe way to avoid all potential pitfalls when doing
that.
In any case, nested transactions should preferably be avoided, and
afaict we don't rely on that in any of the existing code. The reason we
need to support it in some way is that it's needed for testing where the
code under test is creating a transaction on it's own. (Since each test
is run within a db transaction to begin with.)
Also, I have taken the liberty to assume a PDO based db driver for this
stuff. I don't think that's going to be a problem, as that's the only
thing supported by the rest of the code in any case.
135 lines
4.0 KiB
PHP
135 lines
4.0 KiB
PHP
<?php
|
|
/* Copyright (c) 2016 Hubzilla
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
namespace Zotlabs\Tests\Unit;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use PHPUnit\Framework\TestResult;
|
|
|
|
/*
|
|
* Make sure global constants and the global App object is available to the
|
|
* tests.
|
|
*/
|
|
require_once __DIR__ . '/../../boot.php';
|
|
require_once 'include/dba/dba_driver.php' ;
|
|
|
|
/**
|
|
* @brief Base class for our Unit Tests.
|
|
*
|
|
* Empty class at the moment, but you should extend this class for unit test
|
|
* cases, so we could and for sure we will need to implement basic behaviour
|
|
* for all of our unit tests.
|
|
*
|
|
* @author Klaus Weidenbach
|
|
*/
|
|
class UnitTestCase extends TestCase {
|
|
protected array $fixtures = array();
|
|
|
|
/**
|
|
* Override the PHPUnit\Framework\TestCase::run method, so we can
|
|
* wrap it in a database transaction.
|
|
*
|
|
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
|
|
*/
|
|
public function run(TestResult $result = null): TestResult {
|
|
// $myclass = get_class($this);
|
|
// logger("[*] Running test: {$myclass}::{$this->getName(true)}", LOGGER_DEBUG);
|
|
|
|
if (! \DBA::$dba) {
|
|
//logger('[*] Connecting to test db...');
|
|
$this->connect_to_test_db();
|
|
}
|
|
|
|
// The $transactuion variable is needed to hold the transaction until the
|
|
// function returns.
|
|
$transaction = new \DbaTransaction(\DBA::$dba);
|
|
|
|
$this->loadFixtures();
|
|
|
|
// Make sure app config is reset and loaded from fixtures
|
|
\App::$config = array();
|
|
\Zotlabs\Lib\Config::Load('system');
|
|
|
|
$result = parent::run($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function connect_to_test_db() : void {
|
|
if ( !\DBA::$dba ) {
|
|
\DBA::dba_factory(
|
|
getenv('HZ_TEST_DB_HOST') ?: 'db',
|
|
|
|
// Use default port for db type if none specified
|
|
getenv('HZ_TEST_DB_PORT'),
|
|
getenv('HZ_TEST_DB_USER') ?: 'test_user',
|
|
getenv('HZ_TEST_DB_PASS') ?: 'hubzilla',
|
|
getenv('HZ_TEST_DB_DATABASE') ?: 'hubzilla_test_db',
|
|
Self::dbtype(getenv('HZ_TEST_DB_TYPE')),
|
|
getenv('HZ_TEST_DB_CHARSET') ?: 'UTF8',
|
|
false);
|
|
|
|
if ( !\DBA::$dba->connected ) {
|
|
$msg = "Unable to connect to db! ";
|
|
if(file_exists('dbfail.out')) {
|
|
$msg .= file_get_contents('dbfail.out');
|
|
}
|
|
|
|
throw new \Exception($msg);
|
|
}
|
|
|
|
\DBA::$dba->dbg(true);
|
|
}
|
|
}
|
|
|
|
private static function dbtype(string $type): int {
|
|
if (trim(strtolower($type)) === 'postgres') {
|
|
return DBTYPE_POSTGRES;
|
|
} else {
|
|
return DBTYPE_MYSQL;
|
|
}
|
|
}
|
|
|
|
private function loadFixtures() : void {
|
|
$files = glob(__DIR__ . '/includes/dba/_files/*.yml');
|
|
if ($files === false || empty($files)) {
|
|
error_log('[-] ' . __METHOD__ . ': No fixtures found! :(');
|
|
}
|
|
array_walk($files, fn($file) => $this->loadFixture($file));
|
|
}
|
|
|
|
private function loadFixture($file) : void {
|
|
$table_name = basename($file, '.yml');
|
|
$this->fixtures[$table_name] = yaml_parse_file($file)[$table_name];
|
|
|
|
foreach ($this->fixtures[$table_name] as $entry) {
|
|
$query = 'INSERT INTO ' . dbesc($table_name) . '('
|
|
. implode(',', array_keys($entry))
|
|
. ') VALUES('
|
|
. implode(',', array_map(fn($val) => "'{$val}'", array_values($entry)))
|
|
. ')';
|
|
|
|
q($query);
|
|
}
|
|
}
|
|
}
|