diff --git a/lib/Conf.php b/lib/Conf.php index 15a8345d..13635c89 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -264,6 +264,7 @@ class Conf { $type |= Value::M_NULL; } } else { + // catch-all for custom properties $type = Value::T_MIXED; // @codeCoverageIgnore } $out[$p->name] = ['name' => $match[0], 'const' => $type]; @@ -286,6 +287,7 @@ class Conf { } switch (self::EXPECTED_TYPES[$key] ?? gettype($this->$key)) { case "integer": + // no properties are currently typed as integers return Value::normalize($value, Value::T_INT | $mode); // @codeCoverageIgnore case "double": return Value::normalize($value, Value::T_FLOAT | $mode); @@ -293,6 +295,7 @@ class Conf { case "object": return $value; default: + // this should never occur throw new Conf\Exception("ambiguousDefault", ['param' => $key]); // @codeCoverageIgnore } } diff --git a/lib/Database.php b/lib/Database.php index 16925890..d3faadb3 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -78,21 +78,6 @@ class Database { return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; } - /** Lists the available database drivers, as an associative array with - * fully-qualified class names as keys, and human-readable descriptions as values - */ - public static function driverList(): array { - $sep = \DIRECTORY_SEPARATOR; - $path = __DIR__.$sep."Db".$sep; - $classes = []; - foreach (glob($path."*".$sep."Driver.php") as $file) { - $name = basename(dirname($file)); - $class = NS_BASE."Db\\$name\\Driver"; - $classes[$class] = $class::driverName(); - } - return $classes; - } - /** Returns the current (actual) schema version of the database; compared against self::SCHEMA_VERSION to know when an upgrade is required */ public function driverSchemaVersion(): int { return $this->db->schemaVersion(); diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index cc8ce422..84719860 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -46,23 +46,14 @@ abstract class AbstractStatement implements Statement { return $query; } - public function retypeArray(array $bindings, bool $append = false): bool { - if (!$append) { - $this->types = []; - } - foreach ($bindings as $binding) { - if (is_array($binding)) { - // recursively flatten any arrays, which may be provided for SET or IN() clauses - $this->retypeArray($binding, true); - } else { - $bindId = self::TYPES[trim(strtolower($binding))] ?? 0; - assert($bindId, new Exception("paramTypeInvalid", $binding)); - $this->types[] = $bindId; - } - } - if (!$append) { - $this->prepare(static::mungeQuery($this->query, $this->types)); + public function retypeArray(array $bindings): bool { + $this->types = []; + foreach (ValueInfo::flatten($bindings) as $binding) { // recursively flatten any arrays, which may be provided for SET or IN() clauses + $bindId = self::TYPES[trim(strtolower($binding))] ?? 0; + assert($bindId, new Exception("paramTypeInvalid", $binding)); + $this->types[] = $bindId; } + $this->prepare(static::mungeQuery($this->query, $this->types)); return true; } @@ -79,26 +70,22 @@ abstract class AbstractStatement implements Statement { } } - protected function bindValues(array $values, int $offset = null): int { - $a = (int) $offset; - foreach ($values as $value) { - if (is_array($value)) { - // recursively flatten any arrays, which may be provided for SET or IN() clauses - $a += $this->bindValues($value, $a); - } elseif (array_key_exists($a, $this->types)) { + protected function bindValues(array $values): bool { + // recursively flatten any arrays, which may be provided for SET or IN() clauses + $values = ValueInfo::flatten($values); + foreach ($values as $a => $value) { + if (array_key_exists($a, $this->types)) { $value = $this->cast($value, $this->types[$a]); $this->bindValue($value, $this->types[$a] % self::T_NOT_NULL, ++$a); } else { throw new Exception("paramTypeMissing", $a+1); } } - // once the last value is bound, check that all parameters have been supplied values and bind null for any missing ones + // once all values are bound, check that all parameters have been supplied values and bind null for any missing ones // SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error - if (is_null($offset)) { - while ($a < sizeof($this->types)) { - $this->bindValue(null, $this->types[$a] % self::T_NOT_NULL, ++$a); - } + for ($a = sizeof($values); $a < sizeof($this->types); $a++) { + $this->bindValue(null, $this->types[$a] % self::T_NOT_NULL, $a + 1); } - return $a - $offset; + return true; } } diff --git a/lib/Db/SQLite3/AbstractPDODriver.php b/lib/Db/SQLite3/AbstractPDODriver.php index a743a254..bc7ea614 100644 --- a/lib/Db/SQLite3/AbstractPDODriver.php +++ b/lib/Db/SQLite3/AbstractPDODriver.php @@ -7,5 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\SQLite3; abstract class AbstractPDODriver extends Driver { + // this class exists solely so SQLite's PDO driver can call methods of the generic PDO driver via parent::method() + // if there's a better way to do this, please FIXME ;) use \JKingWeb\Arsse\Db\PDODriver; } diff --git a/lib/Misc/Query.php b/lib/Misc/Query.php index 55d2cac8..95b5c2c0 100644 --- a/lib/Misc/Query.php +++ b/lib/Misc/Query.php @@ -29,61 +29,63 @@ class Query { $this->setBody($body, $types, $values); } - public function setBody(string $body = "", $types = null, $values = null): bool { + public function setBody(string $body = "", $types = null, $values = null): self { $this->qBody = $body; if (!is_null($types)) { $this->tBody[] = $types; $this->vBody[] = $values; } - return true; + return $this; } - public function setCTE(string $tableSpec, string $body, $types = null, $values = null): bool { + public function setCTE(string $tableSpec, string $body, $types = null, $values = null): self { $this->qCTE[] = "$tableSpec as ($body)"; if (!is_null($types)) { $this->tCTE[] = $types; $this->vCTE[] = $values; } - return true; + return $this; } - public function setWhere(string $where, $types = null, $values = null): bool { + public function setWhere(string $where, $types = null, $values = null): self { $this->qWhere[] = $where; if (!is_null($types)) { $this->tWhere[] = $types; $this->vWhere[] = $values; } - return true; + return $this; } - public function setWhereNot(string $where, $types = null, $values = null): bool { + public function setWhereNot(string $where, $types = null, $values = null): self { $this->qWhereNot[] = $where; if (!is_null($types)) { $this->tWhereNot[] = $types; $this->vWhereNot[] = $values; } - return true; + return $this; } - public function setGroup(string ...$column): bool { + public function setGroup(string ...$column): self { foreach ($column as $col) { $this->group[] = $col; } - return true; + return $this; } - public function setOrder(string $order): bool { - $this->order[] = $order; - return true; + public function setOrder(string ...$order): self { + foreach ($order as $o) { + $this->order[] = $o; + } + return $this; } - public function setLimit(int $limit, int $offset = 0): bool { + public function setLimit(int $limit, int $offset = 0): self { $this->limit = $limit; $this->offset = $offset; - return true; + return $this; } - public function pushCTE(string $tableSpec): bool { + public function pushCTE(string $tableSpec): self { // this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack // all WHERE, ORDER BY, and LIMIT parts belong to the new CTE and are removed from the main query $this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere, $this->tWhereNot], [$this->vBody, $this->vWhere, $this->vWhereNot]); @@ -98,7 +100,7 @@ class Query { $this->order = []; $this->group = []; $this->setLimit(0, 0); - return true; + return $this; } public function __toString(): string { @@ -117,11 +119,11 @@ class Query { } public function getTypes(): array { - return [$this->tCTE, $this->tBody, $this->tWhere, $this->tWhereNot]; + return ValueInfo::flatten([$this->tCTE, $this->tBody, $this->tWhere, $this->tWhereNot]); } public function getValues(): array { - return [$this->vCTE, $this->vBody, $this->vWhere, $this->vWhereNot]; + return ValueInfo::flatten([$this->vCTE, $this->vBody, $this->vWhere, $this->vWhereNot]); } protected function buildQueryBody(): string { @@ -144,9 +146,9 @@ class Query { if (sizeof($this->order)) { $out .= " ORDER BY ".implode(", ", $this->order); } - // add LIMIT and OFFSET if the former is specified - if ($this->limit > 0) { - $out .= " LIMIT ".$this->limit; + // add LIMIT and OFFSET if either is specified + if ($this->limit > 0 || $this->offset > 0) { + $out .= " LIMIT ".($this->limit < 1 ? -1 : $this->limit); if ($this->offset > 0) { $out .= " OFFSET ".$this->offset; } diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 752704d0..1b3d2608 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -397,6 +397,17 @@ class ValueInfo { } } + public static function flatten(array $arr): array { + $arr = array_values($arr); + for ($a = 0; $a < sizeof($arr); $a++) { + if (is_array($arr[$a])) { + array_splice($arr, $a, 1, $arr[$a]); + $a--; + } + } + return $arr; + } + public static function int($value): int { $out = 0; if (is_null($value)) { diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 7301b016..1569c928 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -209,7 +209,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // indexed arrays $p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1))); } else { - // this case does not actually occur in a proper Fever response + // this case is never encountered with Fever's output $p->appendChild($this->makeXMLAssoc($v, $d->createElement($k))); // @codeCoverageIgnore } } diff --git a/lib/Service.php b/lib/Service.php index 93d4e9ba..ed234d68 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -12,30 +12,14 @@ class Service { const DRIVER_NAMES = [ 'serial' => \JKingWeb\Arsse\Service\Serial\Driver::class, 'subprocess' => \JKingWeb\Arsse\Service\Subprocess\Driver::class, - 'curl' => \JKingWeb\Arsse\Service\Curl\Driver::class, ]; /** @var Service\Driver */ protected $drv; - /** @var \DateInterval */ - protected $interval; - - public static function driverList(): array { - $sep = \DIRECTORY_SEPARATOR; - $path = __DIR__.$sep."Service".$sep; - $classes = []; - foreach (glob($path."*".$sep."Driver.php") as $file) { - $name = basename(dirname($file)); - $class = NS_BASE."User\\$name\\Driver"; - $classes[$class] = $class::driverName(); - } - return $classes; - } public function __construct() { $driver = Arsse::$conf->serviceDriver; $this->drv = new $driver(); - $this->interval = Arsse::$conf->serviceFrequency; } public function watch(bool $loop = true): \DateTimeInterface { @@ -46,17 +30,19 @@ class Service { $list = Arsse::$db->feedListStale(); if ($list) { $this->drv->queue(...$list); + unset($list); $this->drv->exec(); $this->drv->clean(); - unset($list); } static::cleanupPost(); - $t->add($this->interval); + $t->add(Arsse::$conf->serviceFrequency); + // @codeCoverageIgnoreStart if ($loop) { do { @time_sleep_until($t->getTimestamp()); } while ($t->getTimestamp() > time()); } + // @codeCoverageIgnoreEnd } while ($loop); return $t; } diff --git a/lib/Service/Driver.php b/lib/Service/Driver.php index 0f633838..e2dcf929 100644 --- a/lib/Service/Driver.php +++ b/lib/Service/Driver.php @@ -11,5 +11,5 @@ interface Driver { public static function requirementsMet(): bool; public function queue(int ...$feeds): int; public function exec(): int; - public function clean(): bool; + public function clean(): int; } diff --git a/lib/Service/Serial/Driver.php b/lib/Service/Serial/Driver.php index df3580c8..dc2c74f0 100644 --- a/lib/Service/Serial/Driver.php +++ b/lib/Service/Serial/Driver.php @@ -36,8 +36,9 @@ class Driver implements \JKingWeb\Arsse\Service\Driver { return Arsse::$conf->serviceQueueWidth - sizeof($this->queue); } - public function clean(): bool { + public function clean(): int { + $out = sizeof($this->queue); $this->queue = []; - return true; + return $out; } } diff --git a/lib/Service/Subprocess/Driver.php b/lib/Service/Subprocess/Driver.php index 5e79ed09..0986f2c0 100644 --- a/lib/Service/Subprocess/Driver.php +++ b/lib/Service/Subprocess/Driver.php @@ -33,7 +33,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver { $id = (int) array_shift($this->queue); $php = escapeshellarg(\PHP_BINARY); $arsse = escapeshellarg($_SERVER['argv'][0]); - array_push($pp, popen("$php $arsse feed refresh $id", "r")); + array_push($pp, $this->execCmd("$php $arsse feed refresh $id")); } while ($pp) { $p = array_pop($pp); @@ -43,8 +43,14 @@ class Driver implements \JKingWeb\Arsse\Service\Driver { return Arsse::$conf->serviceQueueWidth - sizeof($this->queue); } - public function clean(): bool { + /** @codeCoverageIgnore */ + protected function execCmd(string $cmd) { + return popen($cmd, "r"); + } + + public function clean(): int { + $out = sizeof($this->queue); $this->queue = []; - return true; + return $out; } } diff --git a/lib/User.php b/lib/User.php index 691d6faf..713f17c3 100644 --- a/lib/User.php +++ b/lib/User.php @@ -20,18 +20,6 @@ class User { */ protected $u; - public static function driverList(): array { - $sep = \DIRECTORY_SEPARATOR; - $path = __DIR__.$sep."User".$sep; - $classes = []; - foreach (glob($path."*".$sep."Driver.php") as $file) { - $name = basename(dirname($file)); - $class = NS_BASE."User\\$name\\Driver"; - $classes[$class] = $class::driverName(); - } - return $classes; - } - public function __construct(\JKingWeb\Arsse\User\Driver $driver = null) { $this->u = $driver ?? new Arsse::$conf->userDriver; } diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index a7591bbe..e91b83b3 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -19,13 +19,6 @@ trait SeriesMiscellany { protected function tearDownSeriesMiscellany() { } - public function testListDrivers() { - $exp = [ - 'JKingWeb\\Arsse\\Db\\SQLite3\\Driver' => Arsse::$lang->msg("Driver.Db.SQLite3.Name"), - ]; - $this->assertArraySubset($exp, Database::driverList()); - } - public function testInitializeDatabase() { static::dbRaze(static::$drv); $d = new Database(true); diff --git a/tests/cases/Db/MySQL/TestDatabase.php b/tests/cases/Db/MySQL/TestDatabase.php index 4364170d..9ad47edb 100644 --- a/tests/cases/Db/MySQL/TestDatabase.php +++ b/tests/cases/Db/MySQL/TestDatabase.php @@ -10,7 +10,6 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL; * @group slow * @group coverageOptional * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQL; diff --git a/tests/cases/Db/MySQLPDO/TestDatabase.php b/tests/cases/Db/MySQLPDO/TestDatabase.php index 6a7550ea..e7345550 100644 --- a/tests/cases/Db/MySQLPDO/TestDatabase.php +++ b/tests/cases/Db/MySQLPDO/TestDatabase.php @@ -11,7 +11,6 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO; * @group optional * @group coverageOptional * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQLPDO; diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 9fda4d97..e72c2a28 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -10,7 +10,6 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; * @group slow * @group coverageOptional * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQL; diff --git a/tests/cases/Db/PostgreSQLPDO/TestDatabase.php b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php index 6f8ef2a1..22afa5d9 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestDatabase.php +++ b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php @@ -11,7 +11,6 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; * @group optional * @group coverageOptional * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQLPDO; diff --git a/tests/cases/Db/SQLite3/TestDatabase.php b/tests/cases/Db/SQLite3/TestDatabase.php index eab0970e..ea302218 100644 --- a/tests/cases/Db/SQLite3/TestDatabase.php +++ b/tests/cases/Db/SQLite3/TestDatabase.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3; /** * @group optional * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3; diff --git a/tests/cases/Db/SQLite3PDO/TestDatabase.php b/tests/cases/Db/SQLite3PDO/TestDatabase.php index 079bcc14..751647ae 100644 --- a/tests/cases/Db/SQLite3PDO/TestDatabase.php +++ b/tests/cases/Db/SQLite3PDO/TestDatabase.php @@ -8,7 +8,6 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; /** * @covers \JKingWeb\Arsse\Database - * @covers \JKingWeb\Arsse\Misc\Query */ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3PDO; diff --git a/tests/cases/Misc/TestQuery.php b/tests/cases/Misc/TestQuery.php new file mode 100644 index 00000000..a1588f1b --- /dev/null +++ b/tests/cases/Misc/TestQuery.php @@ -0,0 +1,115 @@ +assertSame("select * from table where a = ?", $q->getQuery()); + $this->assertSame(["int"], $q->getTypes()); + $this->assertSame([3], $q->getValues()); + } + + public function testWhereQuery() { + // simple where clause + $q = (new Query("select * from table"))->setWhere("a = ?", "int", 3); + $this->assertSame("select * from table WHERE a = ?", $q->getQuery()); + $this->assertSame(["int"], $q->getTypes()); + $this->assertSame([3], $q->getValues()); + // compound where clause + $q = (new Query("select * from table"))->setWhere("a = ?", "int", 3)->setWhere("b = ?", "str", 4); + $this->assertSame("select * from table WHERE a = ? AND b = ?", $q->getQuery()); + $this->assertSame(["int", "str"], $q->getTypes()); + $this->assertSame([3, 4], $q->getValues()); + // negative where clause + $q = (new Query("select * from table"))->setWhereNot("a = ?", "int", 3); + $this->assertSame("select * from table WHERE NOT (a = ?)", $q->getQuery()); + $this->assertSame(["int"], $q->getTypes()); + $this->assertSame([3], $q->getValues()); + // compound negative where clause + $q = (new Query("select * from table"))->setWhereNot("a = ?", "int", 3)->setWhereNot("b = ?", "str", 4); + $this->assertSame("select * from table WHERE NOT (a = ? OR b = ?)", $q->getQuery()); + $this->assertSame(["int", "str"], $q->getTypes()); + $this->assertSame([3, 4], $q->getValues()); + // mixed where clause + $q = (new Query("select * from table"))->setWhereNot("a = ?", "int", 1)->setWhere("b = ?", "str", 2)->setWhereNot("c = ?", "int", 3)->setWhere("d = ?", "str", 4); + $this->assertSame("select * from table WHERE b = ? AND d = ? AND NOT (a = ? OR c = ?)", $q->getQuery()); + $this->assertSame(["str", "str", "int", "int"], $q->getTypes()); + $this->assertSame([2, 4, 1, 3], $q->getValues()); + } + + public function testGroupedQuery() { + $q = (new Query("select col1, col2, count(*) as count from table"))->setGroup("col1", "col2"); + $this->assertSame("select col1, col2, count(*) as count from table GROUP BY col1, col2", $q->getQuery()); + $this->assertSame([], $q->getTypes()); + $this->assertSame([], $q->getValues()); + } + + public function testOrderedQuery() { + $q = (new Query("select col1, col2, col3 from table"))->setOrder("col1 desc", "col2")->setOrder("col3 asc"); + $this->assertSame("select col1, col2, col3 from table ORDER BY col1 desc, col2, col3 asc", $q->getQuery()); + $this->assertSame([], $q->getTypes()); + $this->assertSame([], $q->getValues()); + } + + public function testLimitedQuery() { + // no offset + $q = (new Query("select * from table"))->setLimit(5); + $this->assertSame("select * from table LIMIT 5", $q->getQuery()); + $this->assertSame([], $q->getTypes()); + $this->assertSame([], $q->getValues()); + // with offset + $q = (new Query("select * from table"))->setLimit(5, 10); + $this->assertSame("select * from table LIMIT 5 OFFSET 10", $q->getQuery()); + $this->assertSame([], $q->getTypes()); + $this->assertSame([], $q->getValues()); + // no limit with offset + $q = (new Query("select * from table"))->setLimit(0, 10); + $this->assertSame("select * from table LIMIT -1 OFFSET 10", $q->getQuery()); + $this->assertSame([], $q->getTypes()); + $this->assertSame([], $q->getValues()); + } + + public function testQueryWithCommonTableExpression() { + $q = (new Query("select * from table where a in (select * from cte where a = ?)", "int", 1))->setCTE("cte", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3]); + $this->assertSame("WITH RECURSIVE cte as (select * from other_table where a = ? and b = ?) select * from table where a in (select * from cte where a = ?)", $q->getQuery()); + $this->assertSame(["str", "str", "int"], $q->getTypes()); + $this->assertSame([2, 3, 1], $q->getValues()); + // multiple CTEs + $q = (new Query("select * from table where a in (select * from cte1 join cte2 using (a) where a = ?)", "int", 1))->setCTE("cte1", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3])->setCTE("cte2", "select * from other_table where c between ? and ?", ["datetime", "datetime"], [4, 5]); + $this->assertSame("WITH RECURSIVE cte1 as (select * from other_table where a = ? and b = ?), cte2 as (select * from other_table where c between ? and ?) select * from table where a in (select * from cte1 join cte2 using (a) where a = ?)", $q->getQuery()); + $this->assertSame(["str", "str", "datetime", "datetime", "int"], $q->getTypes()); + $this->assertSame([2, 3, 4, 5, 1], $q->getValues()); + } + + public function testQueryWithPushedCommonTableExpression() { + $q = (new Query("select * from table1"))->setWhere("a between ? and ?", ["datetime", "datetime"], [1, 2]) + ->setCTE("cte1", "select * from table2 where a = ? and b = ?", ["str", "str"], [3, 4]) + ->pushCTE("cte2") + ->setBody("select * from table3 join cte1 using (a) join cte2 using (a) where a = ?", "int", 5); + $this->assertSame("WITH RECURSIVE cte1 as (select * from table2 where a = ? and b = ?), cte2 as (select * from table1 WHERE a between ? and ?) select * from table3 join cte1 using (a) join cte2 using (a) where a = ?", $q->getQuery()); + $this->assertSame(["str", "str", "datetime", "datetime", "int"], $q->getTypes()); + $this->assertSame([3, 4, 1, 2, 5], $q->getValues()); + } + + public function testComplexQuery() { + $q = (new query("select *, ? as const from table", "datetime", 1)) + ->setWhereNot("b = ?", "bool", 2) + ->setGroup("col1", "col2") + ->setWhere("a = ?", "str", 3) + ->setLimit(4, 5) + ->setOrder("col3") + ->setCTE("cte", "select ? as const", "int", 6); + $this->assertSame("WITH RECURSIVE cte as (select ? as const) select *, ? as const from table WHERE a = ? AND NOT (b = ?) GROUP BY col1, col2 ORDER BY col3 LIMIT 4 OFFSET 5", $q->getQuery()); + $this->assertSame(["int", "datetime", "str", "bool"], $q->getTypes()); + $this->assertSame([6, 1, 3, 2], $q->getValues()); + } +} diff --git a/tests/cases/Misc/TestURL.php b/tests/cases/Misc/TestURL.php index 18bf3bc6..3ad208b1 100644 --- a/tests/cases/Misc/TestURL.php +++ b/tests/cases/Misc/TestURL.php @@ -92,4 +92,21 @@ class TestURL extends \JKingWeb\Arsse\Test\AbstractTest { ["/#ack", "", "/#ack"], ]; } + + /** @dataProvider provideAbsolutes */ + public function testDetermineAbsoluteness(bool $exp, string $url) { + $this->assertSame($exp, URL::absolute($url)); + } + + public function provideAbsolutes() { + return [ + [true, "http://example.com/"], + [true, "HTTP://example.com/"], + [false, "//example.com/"], + [false, "/example"], + [false, "example.com/"], + [false, "example.com"], + [false, "http:///example"], + ]; + } } diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 9bf12bad..83053c5b 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -639,4 +639,10 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { $out->f = $msec; return $out; } + + public function testFlattenArray() { + $arr = [1, [2, 3, [4, 5]], 6, [[7, 8], 9, 10]]; + $exp = range(1,10); + $this->assertSame($exp, I::flatten($arr)); + } } diff --git a/tests/cases/Service/TestSerial.php b/tests/cases/Service/TestSerial.php new file mode 100644 index 00000000..5a960788 --- /dev/null +++ b/tests/cases/Service/TestSerial.php @@ -0,0 +1,47 @@ +assertTrue(Driver::requirementsMet()); + $this->assertInstanceOf(DriverInterface::class, new Driver); + } + + public function testFetchDriverName() { + $this->assertTrue(strlen(Driver::driverName()) > 0); + } + + public function testEnqueueFeeds() { + $d = new Driver; + $this->assertSame(3, $d->queue(1, 2, 3)); + $this->assertSame(5, $d->queue(4, 5)); + $this->assertSame(5, $d->clean()); + $this->assertSame(1, $d->queue(5)); + } + + public function testRefreshFeeds() { + $d = new Driver; + $d->queue(1, 4, 3); + $this->assertSame(Arsse::$conf->serviceQueueWidth, $d->exec()); + \Phake::verify(Arsse::$db)->feedUpdate(1); + \Phake::verify(Arsse::$db)->feedUpdate(4); + \Phake::verify(Arsse::$db)->feedUpdate(3); + } +} diff --git a/tests/cases/Service/TestService.php b/tests/cases/Service/TestService.php index 3f6ea6b3..102a9ce5 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -39,4 +39,45 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue(Service::hasCheckedIn()); $this->assertFalse(Service::hasCheckedIn()); } + + public function testPerformPreCleanup() { + $this->assertTrue(Service::cleanupPre()); + \Phake::verify(Arsse::$db)->feedCleanup(); + \Phake::verify(Arsse::$db)->sessionCleanup(); + } + + public function testPerformShortPostCleanup() { + \Phake::when(Arsse::$db)->articleCleanup()->thenReturn(0); + $this->assertTrue(Service::cleanupPost()); + \Phake::verify(Arsse::$db)->articleCleanup(); + \Phake::verify(Arsse::$db, \Phake::times(0))->driverMaintenance(); + } + + public function testPerformFullPostCleanup() { + \Phake::when(Arsse::$db)->articleCleanup()->thenReturn(1); + $this->assertTrue(Service::cleanupPost()); + \Phake::verify(Arsse::$db)->articleCleanup(); + \Phake::verify(Arsse::$db)->driverMaintenance(); + } + + public function testRefreshFeeds() { + // set up mock database actions + \Phake::when(Arsse::$db)->metaSet->thenReturn(true); + \Phake::when(Arsse::$db)->feedCleanup->thenReturn(true); + \Phake::when(Arsse::$db)->sessionCleanup->thenReturn(true); + \Phake::when(Arsse::$db)->articleCleanup->thenReturn(0); + \Phake::when(Arsse::$db)->feedListStale->thenReturn([1,2,3]); + // perform the test + $d = \Phake::mock(\JKingWeb\Arsse\Service\Driver::class); + $s = new \JKingWeb\Arsse\Test\Service($d); + $this->assertInstanceOf(\DateTimeInterface::class, $s->watch(false)); + // verify invocations + \Phake::verify($d)->queue(1, 2, 3); + \Phake::verify($d)->exec(); + \Phake::verify($d)->clean(); + \Phake::verify(Arsse::$db)->feedCleanup(); + \Phake::verify(Arsse::$db)->sessionCleanup(); + \Phake::verify(Arsse::$db)->articleCleanup(); + \Phake::verify(Arsse::$db)->metaSet("service_last_checkin", $this->anything(), "datetime"); + } } diff --git a/tests/cases/Service/TestSubprocess.php b/tests/cases/Service/TestSubprocess.php new file mode 100644 index 00000000..c02c4b26 --- /dev/null +++ b/tests/cases/Service/TestSubprocess.php @@ -0,0 +1,48 @@ +assertTrue(Driver::requirementsMet()); + $this->assertInstanceOf(DriverInterface::class, new Driver); + } + + public function testFetchDriverName() { + $this->assertTrue(strlen(Driver::driverName()) > 0); + } + + public function testEnqueueFeeds() { + $d = new Driver; + $this->assertSame(3, $d->queue(1, 2, 3)); + $this->assertSame(5, $d->queue(4, 5)); + $this->assertSame(5, $d->clean()); + $this->assertSame(1, $d->queue(5)); + } + + public function testRefreshFeeds() { + $d = \Phake::partialMock(Driver::class); + \Phake::when($d)->execCmd->thenReturnCallback(function(string $cmd) { + // FIXME: Does this work in Windows? + return popen("echo ".escapeshellarg($cmd), "r"); + }); + $this->assertSame(3, $d->queue(1, 4, 3)); + $this->assertSame(Arsse::$conf->serviceQueueWidth, $d->exec()); + \Phake::verify($d, \Phake::times(3))->execCmd; + } +} diff --git a/tests/cases/TestArsse.php b/tests/cases/TestArsse.php new file mode 100644 index 00000000..f28e7ee8 --- /dev/null +++ b/tests/cases/TestArsse.php @@ -0,0 +1,50 @@ +import(['lang' => "test"]); + Arsse::load($conf2); + $this->assertSame($conf2, Arsse::$conf); + $this->assertSame($lang, Arsse::$lang); + $this->assertSame($db, Arsse::$db); + $this->assertSame($user, Arsse::$user); + \Phake::verify($lang)->set("test"); + } + + public function testLoadNewData() { + if (!\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet() && !\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { + $this->markTestSkipped("A functional SQLite interface is required for this test"); + } + $conf = (new Conf)->import(['dbSQLite3File' => ":memory:"]); + Arsse::load($conf); + $this->assertInstanceOf(Conf::class, Arsse::$conf); + $this->assertInstanceOf(Lang::class, Arsse::$lang); + $this->assertInstanceOf(Database::class, Arsse::$db); + $this->assertInstanceOf(User::class, Arsse::$user); + } +} diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 1398b177..49fc4723 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -24,13 +24,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $this->drv = \Phake::mock(Driver::class); } - public function testListDrivers() { - $exp = [ - 'JKingWeb\\Arsse\\User\\Internal\\Driver' => Arsse::$lang->msg("Driver.User.Internal.Name"), - ]; - $this->assertArraySubset($exp, User::driverList()); - } - public function testConstruct() { $this->assertInstanceOf(User::class, new User($this->drv)); $this->assertInstanceOf(User::class, new User); diff --git a/tests/lib/Service.php b/tests/lib/Service.php new file mode 100644 index 00000000..cfd11f4d --- /dev/null +++ b/tests/lib/Service.php @@ -0,0 +1,13 @@ +drv = $drv; + } +} diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 62e3238e..705e6599 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -45,6 +45,7 @@ cases/Misc/TestValueInfo.php cases/Misc/TestDate.php + cases/Misc/TestQuery.php cases/Misc/TestContext.php cases/Misc/TestURL.php cases/Misc/TestHTTP.php @@ -131,7 +132,10 @@ cases/Service/TestService.php + cases/Service/TestSerial.php + cases/Service/TestSubprocess.php cases/CLI/TestCLI.php + cases/TestArsse.php cases/ImportExport/TestFile.php