Browse Source

Use common cleanup code for all database-related tests

microsub
J. King 5 years ago
parent
commit
8a49202036
  1. 78
      tests/cases/Db/BaseDriver.php
  2. 36
      tests/cases/Db/BaseResult.php
  3. 38
      tests/cases/Db/BaseStatement.php
  4. 19
      tests/cases/Db/PostgreSQL/TestDatabase.php
  5. 9
      tests/cases/Db/PostgreSQL/TestDriver.php
  6. 13
      tests/cases/Db/PostgreSQL/TestStatement.php
  7. 91
      tests/cases/Db/SQLite3/TestDriver.php
  8. 21
      tests/cases/Db/SQLite3/TestResult.php
  9. 15
      tests/cases/Db/SQLite3/TestStatement.php
  10. 21
      tests/cases/Db/SQLite3PDO/TestDriver.php
  11. 13
      tests/cases/Db/SQLite3PDO/TestStatement.php
  12. 27
      tests/cases/Db/TestResultPDO.php
  13. 28
      tests/lib/DatabaseInformation.php

78
tests/cases/Db/BaseDriver.php

@ -11,51 +11,69 @@ use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Test\DatabaseInformation;
abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $dbInfo;
protected static $interface;
protected $drv;
protected $interface;
protected $create;
protected $lock;
protected $setVersion;
protected $conf = [
protected static $conf = [
'dbTimeoutExec' => 0.5,
'dbSQLite3Timeout' => 0,
//'dbSQLite3File' => "(temporary file)",
];
public static function setUpBeforeClass() {
// establish a clean baseline
static::clearData();
static::$dbInfo = new DatabaseInformation(static::$implementation);
static::setConf(static::$conf);
static::$interface = (static::$dbInfo->interfaceConstructor)();
}
public function setUp() {
self::clearData();
self::setConf($this->conf);
$info = new DatabaseInformation($this->implementation);
$this->interface = ($info->interfaceConstructor)();
if (!$this->interface) {
$this->markTestSkipped("$this->implementation database driver not available");
self::setConf(static::$conf);
if (!static::$interface) {
$this->markTestSkipped(static::$implementation." database driver not available");
}
$this->drv = new $info->driverClass;
$this->exec("DROP TABLE IF EXISTS arsse_test");
$this->exec("DROP TABLE IF EXISTS arsse_meta");
$this->exec("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)");
$this->exec("INSERT INTO arsse_meta(key,value) values('schema_version','0')");
// completely clear the database and ensure the schema version can easily be altered
(static::$dbInfo->razeFunction)(static::$interface, [
"CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)",
"INSERT INTO arsse_meta(key,value) values('schema_version','0')",
]);
// construct a fresh driver for each test
$this->drv = new static::$dbInfo->driverClass;
}
public function tearDown() {
self::clearData();
// deconstruct the driver
unset($this->drv);
try {
$this->exec("ROLLBACK");
} catch(\Throwable $e) {
if (static::$interface) {
// completely clear the database
(static::$dbInfo->razeFunction)(static::$interface);
}
$this->exec("DROP TABLE IF EXISTS arsse_meta");
$this->exec("DROP TABLE IF EXISTS arsse_test");
self::clearData();
}
protected function exec(string $q): bool {
public static function tearDownAfterClass() {
static::$implementation = null;
static::$dbInfo = null;
self::clearData();
}
protected function exec($q): bool {
// PDO implementation
$this->interface->exec($q);
$q = (!is_array($q)) ? [$q] : $q;
foreach ($q as $query) {
static::$interface->exec((string) $query);
}
return true;
}
protected function query(string $q) {
// PDO implementation
return $this->interface->query($q)->fetchColumn();
return static::$interface->query($q)->fetchColumn();
}
# TESTS
@ -87,7 +105,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$this->exec($this->create);
$this->exec($this->lock);
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->exec($this->lock);
$lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock;
$this->drv->exec($lock);
}
public function testExecConstraintViolation() {
@ -115,7 +134,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$this->exec($this->create);
$this->exec($this->lock);
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->exec($this->lock);
$lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock;
$this->drv->exec($lock);
}
public function testQueryConstraintViolation() {
@ -342,12 +362,20 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame(1, $this->drv->schemaVersion());
$this->drv->exec(str_replace("#", "2", $this->setVersion));
$this->assertSame(2, $this->drv->schemaVersion());
// SQLite is unaffected by the removal of the metadata table; other backends are
// in neither case should a query for the schema version produce an error, however
$this->exec("DROP TABLE IF EXISTS arsse_meta");
$exp = (static::$dbInfo->backend == "SQLite 3") ? 2 : 0;
$this->assertSame($exp, $this->drv->schemaVersion());
}
public function testLockTheDatabase() {
// PostgreSQL doesn't actually lock the whole database, only the metadata table
// normally the application will first query this table to ensure the schema version is correct,
// so the effect is usually the same
$this->drv->savepointCreate(true);
$this->assertException();
$this->exec($this->create);
$this->exec(str_replace("#", "3", $this->setVersion));
}
public function testUnlockTheDatabase() {
@ -355,6 +383,6 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
$this->drv->savepointRelease();
$this->drv->savepointCreate(true);
$this->drv->savepointUndo();
$this->assertTrue($this->exec($this->create));
$this->assertTrue($this->exec(str_replace("#", "3", $this->setVersion)));
}
}

36
tests/cases/Db/BaseResult.php

@ -10,29 +10,45 @@ use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Test\DatabaseInformation;
abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $dbInfo;
protected static $interface;
protected $resultClass;
protected $stringOutput;
protected $interface;
abstract protected function exec(string $q);
abstract protected function makeResult(string $q): array;
public static function setUpBeforeClass() {
// establish a clean baseline
static::clearData();
static::$dbInfo = new DatabaseInformation(static::$implementation);
static::setConf();
static::$interface = (static::$dbInfo->interfaceConstructor)();
}
public function setUp() {
self::clearData();
self::setConf();
$info = new DatabaseInformation($this->implementation);
$this->interface = ($info->interfaceConstructor)();
if (!$this->interface) {
$this->markTestSkipped("$this->implementation database driver not available");
if (!static::$interface) {
$this->markTestSkipped(static::$implementation." database driver not available");
}
$this->resultClass = $info->resultClass;
$this->stringOutput = $info->stringOutput;
$this->exec("DROP TABLE IF EXISTS arsse_meta");
// completely clear the database
(static::$dbInfo->razeFunction)(static::$interface);
$this->resultClass = static::$dbInfo->resultClass;
$this->stringOutput = static::$dbInfo->stringOutput;
}
public function tearDown() {
if (static::$interface) {
// completely clear the database
(static::$dbInfo->razeFunction)(static::$interface);
}
self::clearData();
}
public static function tearDownAfterClass() {
static::$implementation = null;
static::$dbInfo = null;
self::clearData();
$this->exec("DROP TABLE IF EXISTS arsse_meta");
}
public function testConstructResult() {

38
tests/cases/Db/BaseStatement.php

@ -10,29 +10,45 @@ use JKingWeb\Arsse\Db\Statement;
use JKingWeb\Arsse\Test\DatabaseInformation;
abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $dbInfo;
protected static $interface;
protected $statementClass;
protected $stringOutput;
protected $interface;
abstract protected function exec(string $q);
abstract protected function makeStatement(string $q, array $types = []): array;
abstract protected function decorateTypeSyntax(string $value, string $type): string;
public static function setUpBeforeClass() {
// establish a clean baseline
static::clearData();
static::$dbInfo = new DatabaseInformation(static::$implementation);
static::setConf();
static::$interface = (static::$dbInfo->interfaceConstructor)();
}
public function setUp() {
self::clearData();
self::setConf();
$info = new DatabaseInformation($this->implementation);
$this->interface = ($info->interfaceConstructor)();
if (!$this->interface) {
$this->markTestSkipped("$this->implementation database driver not available");
if (!static::$interface) {
$this->markTestSkipped(static::$implementation." database driver not available");
}
$this->statementClass = $info->statementClass;
$this->stringOutput = $info->stringOutput;
$this->exec("DROP TABLE IF EXISTS arsse_meta");
// completely clear the database
(static::$dbInfo->razeFunction)(static::$interface);
$this->statementClass = static::$dbInfo->statementClass;
$this->stringOutput = static::$dbInfo->stringOutput;
}
public function tearDown() {
$this->exec("DROP TABLE IF EXISTS arsse_meta");
if (static::$interface) {
// completely clear the database
(static::$dbInfo->razeFunction)(static::$interface);
}
self::clearData();
}
public static function tearDownAfterClass() {
static::$implementation = null;
static::$dbInfo = null;
self::clearData();
}
@ -56,7 +72,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideBinaryBindings */
public function testHandleBinaryData($value, string $type, string $exp) {
if (in_array($this->implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
$this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown");
}
if ($exp=="null") {

19
tests/cases/Db/PostgreSQL/TestDatabase.php

@ -0,0 +1,19 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Db\PosgreSQL;
/**
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
protected static $implementation = "PDO PostgreSQL";
protected function nextID(string $table): int {
return static::$drv->query("SELECT select cast(last_value as bigint) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue();
}
}

9
tests/cases/Db/PostgreSQL/TestDriver.php

@ -11,13 +11,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
* @covers \JKingWeb\Arsse\Db\PDODriver
* @covers \JKingWeb\Arsse\Db\PDOError */
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
protected $implementation = "PDO PostgreSQL";
protected static $implementation = "PDO PostgreSQL";
protected $create = "CREATE TABLE arsse_test(id bigserial primary key)";
protected $lock = "BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT";
protected $lock = ["BEGIN", "LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"];
protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'";
public function tearDown() {
parent::tearDown();
unset($this->interface);
}
}

13
tests/cases/Db/PostgreSQL/TestStatement.php

@ -10,19 +10,10 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
* @covers \JKingWeb\Arsse\Db\PDOStatement<extended>
* @covers \JKingWeb\Arsse\Db\PDOError */
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
protected $implementation = "PDO PostgreSQL";
public function tearDown() {
parent::tearDown();
unset($this->interface);
}
protected function exec(string $q) {
$this->interface->exec($q);
}
protected static $implementation = "PDO PostgreSQL";
protected function makeStatement(string $q, array $types = []): array {
return [$this->interface, $this->interface->prepare($q), $types];
return [static::$interface, static::$interface->prepare($q), $types];
}
protected function decorateTypeSyntax(string $value, string $type): string {

91
tests/cases/Db/SQLite3/TestDriver.php

@ -10,92 +10,39 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
protected $implementation = "SQLite 3";
protected static $implementation = "SQLite 3";
protected $create = "CREATE TABLE arsse_test(id integer primary key)";
protected $lock = "BEGIN EXCLUSIVE TRANSACTION";
protected $setVersion = "PRAGMA user_version=#";
protected static $file;
public static function setUpBeforeClass() {
self::$file = tempnam(sys_get_temp_dir(), 'ook');
// create a temporary database file rather than using a memory database
// some tests require one connection to block another, so a memory database is not suitable
static::$file = tempnam(sys_get_temp_dir(), 'ook');
static::$conf['dbSQLite3File'] = static::$file;
parent::setUpBeforeclass();
}
public static function tearDownAfterClass() {
@unlink(self::$file);
self::$file = null;
}
public function setUp() {
$this->conf['dbSQLite3File'] = self::$file;
parent::setUp();
$this->exec("PRAGMA user_version=0");
}
public function tearDown() {
parent::tearDown();
$this->exec("PRAGMA user_version=0");
$this->interface->close();
unset($this->interface);
if (static::$interface) {
static::$interface->close();
}
parent::tearDownAfterClass();
@unlink(static::$file);
static::$file = null;
}
protected function exec(string $q): bool {
$this->interface->exec($q);
protected function exec($q): bool {
// SQLite's implementation coincidentally matches PDO's, but we reproduce it here for correctness' sake
$q = (!is_array($q)) ? [$q] : $q;
foreach ($q as $query) {
static::$interface->exec((string) $query);
}
return true;
}
protected function query(string $q) {
return $this->interface->querySingle($q);
}
public function provideDrivers() {
self::clearData();
self::setConf([
'dbTimeoutExec' => 0.5,
'dbSQLite3Timeout' => 0,
'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'),
]);
$i = $this->provideDbInterfaces();
$d = $this->provideDbDrivers();
$pdoExec = function (string $q) {
$this->interface->exec($q);
return true;
};
$pdoQuery = function (string $q) {
return $this->interface->query($q)->fetchColumn();
};
return [
'SQLite 3' => [
$i['SQLite 3']['interface'],
$d['SQLite 3'],
"CREATE TABLE arsse_test(id integer primary key)",
"BEGIN EXCLUSIVE TRANSACTION",
"PRAGMA user_version=#",
function (string $q) {
$this->interface->exec($q);
return true;
},
function (string $q) {
return $this->interface->querySingle($q);
},
],
'PDO SQLite 3' => [
$i['PDO SQLite 3']['interface'],
$d['PDO SQLite 3'],
"CREATE TABLE arsse_test(id integer primary key)",
"BEGIN EXCLUSIVE TRANSACTION",
"PRAGMA user_version=#",
$pdoExec,
$pdoQuery,
],
'PDO PostgreSQL' => [
$i['PDO PostgreSQL']['interface'],
$d['PDO PostgreSQL'],
"CREATE TABLE arsse_test(id bigserial primary key)",
"BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT",
"UPDATE arsse_meta set value = '#' where key = 'schema_version'",
$pdoExec,
$pdoQuery,
],
];
return static::$interface->querySingle($q);
}
}

21
tests/cases/Db/SQLite3/TestResult.php

@ -12,22 +12,19 @@ use JKingWeb\Arsse\Test\DatabaseInformation;
* @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended>
*/
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
protected $implementation = "SQLite 3";
protected static $implementation = "SQLite 3";
public function tearDown() {
parent::tearDown();
$this->interface->close();
unset($this->interface);
}
protected function exec(string $q) {
$this->interface->exec($q);
public static function tearDownAfterClass() {
if (static::$interface) {
static::$interface->close();
}
parent::tearDownAfterClass();
}
protected function makeResult(string $q): array {
$set = $this->interface->query($q);
$rows = $this->interface->changes();
$id = $this->interface->lastInsertRowID();
$set = static::$interface->query($q);
$rows = static::$interface->changes();
$id = static::$interface->lastInsertRowID();
return [$set, [$rows, $id]];
}
}

15
tests/cases/Db/SQLite3/TestStatement.php

@ -10,20 +10,15 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
* @covers \JKingWeb\Arsse\Db\SQLite3\Statement<extended>
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
protected $implementation = "SQLite 3";
protected static $implementation = "SQLite 3";
public function tearDown() {
parent::tearDown();
$this->interface->close();
unset($this->interface);
}
protected function exec(string $q) {
$this->interface->exec($q);
public static function tearDownAfterClass() {
static::$interface->close();
parent::tearDownAfterClass();
}
protected function makeStatement(string $q, array $types = []): array {
return [$this->interface, $this->interface->prepare($q), $types];
return [static::$interface, static::$interface->prepare($q), $types];
}
protected function decorateTypeSyntax(string $value, string $type): string {

21
tests/cases/Db/SQLite3PDO/TestDriver.php

@ -11,30 +11,23 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
* @covers \JKingWeb\Arsse\Db\PDODriver
* @covers \JKingWeb\Arsse\Db\PDOError */
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
protected $implementation = "PDO SQLite 3";
protected static $implementation = "PDO SQLite 3";
protected $create = "CREATE TABLE arsse_test(id integer primary key)";
protected $lock = "BEGIN EXCLUSIVE TRANSACTION";
protected $setVersion = "PRAGMA user_version=#";
protected static $file;
public static function setUpBeforeClass() {
self::$file = tempnam(sys_get_temp_dir(), 'ook');
// create a temporary database file rather than using a memory database
// some tests require one connection to block another, so a memory database is not suitable
static::$file = tempnam(sys_get_temp_dir(), 'ook');
static::$conf['dbSQLite3File'] = static::$file;
parent::setUpBeforeclass();
}
public static function tearDownAfterClass() {
parent::tearDownAfterClass();
@unlink(self::$file);
self::$file = null;
}
public function setUp() {
$this->conf['dbSQLite3File'] = self::$file;
parent::setUp();
$this->exec("PRAGMA user_version=0");
}
public function tearDown() {
parent::tearDown();
$this->exec("PRAGMA user_version=0");
unset($this->interface);
}
}

13
tests/cases/Db/SQLite3PDO/TestStatement.php

@ -10,19 +10,10 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
* @covers \JKingWeb\Arsse\Db\PDOStatement<extended>
* @covers \JKingWeb\Arsse\Db\PDOError */
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
protected $implementation = "PDO SQLite 3";
public function tearDown() {
parent::tearDown();
unset($this->interface);
}
protected function exec(string $q) {
$this->interface->exec($q);
}
protected static $implementation = "PDO SQLite 3";
protected function makeStatement(string $q, array $types = []): array {
return [$this->interface, $this->interface->prepare($q), $types];
return [static::$interface, static::$interface->prepare($q), $types];
}
protected function decorateTypeSyntax(string $value, string $type): string {

27
tests/cases/Db/TestResultPDO.php

@ -12,41 +12,30 @@ use JKingWeb\Arsse\Test\DatabaseInformation;
* @covers \JKingWeb\Arsse\Db\PDOResult<extended>
*/
class TestResultPDO extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
protected static $firstAvailableDriver;
protected static $implementation;
public static function setUpBeforeClass() {
self::setConf();
// we only need to test one PDO implementation (they all use the same result class), so we find the first usable one
$drivers = DatabaseInformation::listPDO();
self::$firstAvailableDriver = $drivers[0];
self::$implementation = $drivers[0];
foreach ($drivers as $driver) {
$info = new DatabaseInformation($driver);
$interface = ($info->interfaceConstructor)();
if ($interface) {
self::$firstAvailableDriver = $driver;
self::$implementation = $driver;
break;
}
}
}
public function setUp() {
$this->implementation = self::$firstAvailableDriver;
parent::setUp();
}
public function tearDown() {
parent::tearDown();
unset($this->interface);
}
protected function exec(string $q) {
$this->interface->exec($q);
unset($interface);
unset($info);
parent::setUpBeforeClass();
}
protected function makeResult(string $q): array {
$set = $this->interface->query($q);
$set = static::$interface->query($q);
$rows = $set->rowCount();
$id = $this->interface->lastInsertID();
$id = static::$interface->lastInsertID();
return [$set, [$rows, $id]];
}
}

28
tests/lib/DatabaseInformation.php

@ -59,7 +59,12 @@ class DatabaseInformation {
$tables = $db->query($listTables)->getAll();
$tables = sizeof($tables) ? array_column($tables, "name") : [];
} elseif ($db instanceof \PDO) {
$tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC);
retry:
try {
$tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
goto retry;
}
$tables = sizeof($tables) ? array_column($tables, "name") : [];
} else {
$tables = [];
@ -72,6 +77,11 @@ class DatabaseInformation {
return $tables;
};
$sqlite3TruncateFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) {
// rollback any pending transaction
try {
$db->exec("ROLLBACK");
} catch(\Throwable $e) {
}
foreach ($sqlite3TableList($db) as $table) {
if ($table == "arsse_meta") {
$db->exec("DELETE FROM $table where key <> 'schema_version'");
@ -84,6 +94,11 @@ class DatabaseInformation {
}
};
$sqlite3RazeFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) {
// rollback any pending transaction
try {
$db->exec("ROLLBACK");
} catch(\Throwable $e) {
}
$db->exec("PRAGMA foreign_keys=0");
foreach ($sqlite3TableList($db) as $table) {
$db->exec("DROP TABLE IF EXISTS $table");
@ -163,7 +178,12 @@ class DatabaseInformation {
return $d;
},
'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) {
foreach ($objectList($db) as $obj) {
// rollback any pending transaction
try {
$db->exec("ROLLBACK");
} catch(\Throwable $e) {
}
foreach ($pgObjectList($db) as $obj) {
if ($obj['type'] != "TABLE") {
continue;
} elseif ($obj['name'] == "arsse_meta") {
@ -177,8 +197,8 @@ class DatabaseInformation {
}
},
'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) {
foreach ($objectList($db) as $obj) {
$db->exec("DROP {$obj['type']} {$obj['name']} IF EXISTS cascade");
foreach ($pgObjectList($db) as $obj) {
$db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade");
}
foreach ($afterStatements as $st) {

Loading…
Cancel
Save