From 8c20411359e2ac4d5c96fac2db9b8b3f2a1fea10 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 23:18:20 -0500 Subject: [PATCH] Align statement tests with other database driver tests --- tests/cases/Db/BaseDriver.php | 2 + tests/cases/Db/BaseResult.php | 2 + .../{TestStatement.php => BaseStatement.php} | 168 ++++++------------ tests/cases/Db/PostgreSQL/TestDriver.php | 23 +++ tests/cases/Db/PostgreSQL/TestStatement.php | 42 +++++ tests/cases/Db/SQLite3/TestStatement.php | 32 ++++ tests/cases/Db/SQLite3PDO/TestStatement.php | 35 ++++ tests/lib/AbstractTest.php | 4 +- 8 files changed, 194 insertions(+), 114 deletions(-) rename tests/cases/Db/{TestStatement.php => BaseStatement.php} (74%) create mode 100644 tests/cases/Db/PostgreSQL/TestDriver.php create mode 100644 tests/cases/Db/PostgreSQL/TestStatement.php create mode 100644 tests/cases/Db/SQLite3/TestStatement.php create mode 100644 tests/cases/Db/SQLite3PDO/TestStatement.php diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index a6e7f37..653da32 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -22,6 +22,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { ]; public function setUp() { + $this->clearData(); self::setConf($this->conf); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -36,6 +37,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { + $this->clearData(); unset($this->drv); try { $this->exec("ROLLBACK"); diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index a4708dd..8a79d74 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -18,6 +18,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { abstract protected function makeResult(string $q): array; public function setUp() { + $this->clearData(); self::setConf(); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -30,6 +31,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { + $this->clearData(); $this->exec("DROP TABLE IF EXISTS arsse_meta"); } diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/BaseStatement.php similarity index 74% rename from tests/cases/Db/TestStatement.php rename to tests/cases/Db/BaseStatement.php index d6a3cb8..8670544 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -6,53 +6,49 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db; -use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\Statement; -use JKingWeb\Arsse\Db\PDOStatement; +use JKingWeb\Arsse\Test\DatabaseInformation; -/** - * @covers \JKingWeb\Arsse\Db\SQLite3\Statement - * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder - * @covers \JKingWeb\Arsse\Db\PDOStatement - * @covers \JKingWeb\Arsse\Db\PDOError */ -class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { - public function provideStatements() { - $interfaces = $this->provideDbInterfaces(); - $constructors = [ - 'SQLite 3' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['SQLite 3']['interface']->prepare($query); - return [$interfaces['SQLite 3']['interface'], $s, $types]; - }, - 'PDO SQLite 3' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['PDO SQLite 3']['interface']->prepare($query); - return [$interfaces['PDO SQLite 3']['interface'], $s, $types]; - }, - 'PDO PostgreSQL' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['PDO PostgreSQL']['interface']->prepare($query); - return [$interfaces['PDO PostgreSQL']['interface'], $s, $types]; - }, - ]; - foreach ($constructors as $drv => $func) { - yield $drv => [isset($interfaces[$drv]['interface']), $interfaces[$drv]['stringOutput'], $interfaces[$drv]['statement'], $func]; +abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { + 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 function setUp() { + $this->clearData(); + self::setConf(); + $info = new DatabaseInformation($this->implementation); + $this->interface = ($info->interfaceConstructor)(); + if (!$this->interface) { + $this->markTestSkipped("$this->implementation database driver not available"); } + $this->statementClass = $info->statementClass; + $this->stringOutput = $info->stringOutput; + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + } + + public function tearDown() { + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + $this->clearData(); } - /** @dataProvider provideStatements */ public function testConstructStatement() { - $class = $this->statementClass; - $this->assertInstanceOf(Statement::class, new $class(...$func("SELECT ? as value"))); + $this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value"))); } /** @dataProvider provideBindings */ - public function testBindATypedValue(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { - $class = $this->statementClass; + public function testBindATypedValue($value, string $type, string $exp) { if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } $typeStr = "'".str_replace("'", "''", $type)."'"; - $s = new $class(...$func($query)); + $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); $this->assertTrue((bool) $act); @@ -60,77 +56,67 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideBinaryBindings */ public function testHandleBinaryData($value, string $type, string $exp) { - $class = $this->statementClass; + if (in_array($this->implementation, ["PostgreSQL", "PDO PostgreSQL"])) { + $this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown"); + } if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } $typeStr = "'".str_replace("'", "''", $type)."'"; - $s = new $class(...$func($query)); + $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); $this->assertTrue((bool) $act); } - /** @dataProvider provideStatements */ public function testBindMissingValue() { - $class = $this->statementClass; - $s = new $class(...$func("SELECT ? as value", ["int"])); + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"])); $val = $s->runArray()->getRow()['value']; $this->assertSame(null, $val); } - /** @dataProvider provideStatements */ public function testBindMultipleValues() { - $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, ]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $s = new $class(...$func("SELECT ? as one, ? as two", ["int", "int"])); + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two", ["int", "int"])); $val = $s->runArray([1,2])->getRow(); $this->assertSame($exp, $val); } - /** @dataProvider provideStatements */ public function testBindRecursively() { - $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, ]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $s = new $class(...$func("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"])); + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"])); $val = $s->runArray([1, [2, 3], 4])->getRow(); $this->assertSame($exp, $val); } - /** @dataProvider provideStatements */ public function testBindWithoutType() { - $class = $this->statementClass; $this->assertException("paramTypeMissing", "Db"); - $s = new $class(...$func("SELECT ? as value", [])); + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", [])); $s->runArray([1]); } - /** @dataProvider provideStatements */ public function testViolateConstraint() { - $class = $this->statementClass; - (new $class(...$func("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run(); - $s = new $class(...$func("INSERT INTO arsse_meta(key) values(?)", ["str"])); + (new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run(); + $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(key) values(?)", ["str"])); $this->assertException("constraintViolation", "Db", "ExceptionInput"); $s->runArray([null]); } - /** @dataProvider provideStatements */ public function testMismatchTypes() { - $class = $this->statementClass; - (new $class(...$func("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); - $s = new $class(...$func("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); + (new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); + $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); $this->assertException("typeViolation", "Db", "ExceptionInput"); $s->runArray(['ook', 'eek']); } @@ -250,35 +236,32 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], - 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], + 'DateTime as integer' => [$dateMutable, "integer", (string) $dateUTC->getTimestamp()], 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as boolean' => [$dateMutable, "boolean", "1"], - 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTime as strict integer' => [$dateMutable, "strict integer", (string) $dateUTC->getTimestamp()], 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], - 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as integer' => [$dateImmutable, "integer", (string) $dateUTC->getTimestamp()], 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], - 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", (string) $dateUTC->getTimestamp()], 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], ]; - $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { - $conv = $decorators[$drvName] ?? $conv = $decorators['']; - foreach ($tests as $index => list($value, $type, $exp)) { - $t = preg_replace("<^strict >", "", $type); - $exp = ($exp=="null") ? $exp : $conv($exp, $t); - yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; - } + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + if (gettype($exp) != "string") var_export($index); + $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); + yield $index => [$value, $type, $exp]; } } @@ -326,50 +309,11 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], ]; - $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { - $conv = $decorators[$drvName] ?? $conv = $decorators['']; - if ($drvName=="PDO PostgreSQL") { - // skip PostgreSQL for these tests - $drv = false; - } - foreach ($tests as $index => list($value, $type, $exp)) { - $t = preg_replace("<^strict >", "", $type); - $exp = ($exp=="null") ? $exp : $conv($exp, $t); - yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; - } + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + if (gettype($exp) != "string") var_export($index); + $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); + yield $index => [$value, $type, $exp]; } } - - function provideSyntaxDecorators() { - return [ - 'PDO PostgreSQL' => (function($v, $t) { - switch ($t) { - case "float": - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - case "string": - if (preg_match("<^char\((\d+)\)$>", $v, $match)) { - return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; - } else { - return $v; - } - default: - return $v; - } - }), - 'PDO SQLite 3' => (function($v, $t) { - if ($t=="float") { - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - } else { - return $v; - } - }), - 'SQLite 3' => (function($v, $t) { - return $v; - }), - '' => (function($v, $t) { - return $v; - }), - ]; - } } diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php new file mode 100644 index 0000000..497488f --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -0,0 +1,23 @@ + + * @covers \JKingWeb\Arsse\Db\PDODriver + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { + protected $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 $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'"; + + public function tearDown() { + parent::tearDown(); + unset($this->interface); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php new file mode 100644 index 0000000..2c8586f --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -0,0 +1,42 @@ + + * @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 function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->interface->prepare($q), $types]; + } + + protected function decorateTypeSyntax(string $value, string $type): string { + switch ($type) { + case "float": + return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'"; + case "string": + if (preg_match("<^char\((\d+)\)$>", $value, $match)) { + return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; + } else { + return $value; + } + default: + return $value; + } + } +} diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php new file mode 100644 index 0000000..fc06dbd --- /dev/null +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -0,0 +1,32 @@ + + * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ +class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { + protected $implementation = "SQLite 3"; + + public function tearDown() { + parent::tearDown(); + $this->interface->close(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->interface->prepare($q), $types]; + } + + protected function decorateTypeSyntax(string $value, string $type): string { + return $value; + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestStatement.php b/tests/cases/Db/SQLite3PDO/TestStatement.php new file mode 100644 index 0000000..74f05f1 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestStatement.php @@ -0,0 +1,35 @@ + + * @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 function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->interface->prepare($q), $types]; + } + + protected function decorateTypeSyntax(string $value, string $type): string { + if ($type=="float") { + return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'"; + } else { + return $value; + } + } +} diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 1b244db..a15d674 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -40,7 +40,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } } - public static function setConf(array $conf = []) { + public static function setConf(array $conf = [], bool $force = true) { $defaults = [ 'dbSQLite3File' => ":memory:", 'dbSQLite3Timeout' => 0, @@ -48,7 +48,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { 'dbPostgreSQLPass' => "arsse_test", 'dbPostgreSQLDb' => "arsse_test", ]; - Arsse::$conf = Arsse::$conf ?? (new Conf)->import($defaults)->import($conf); + Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf); } public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") {