1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-23 09:02:41 +00:00

Merge branch 'master' into microsub

This commit is contained in:
J. King 2019-10-19 22:45:12 -04:00
commit 38a2776ae9
29 changed files with 415 additions and 123 deletions

View file

@ -264,6 +264,7 @@ class Conf {
$type |= Value::M_NULL; $type |= Value::M_NULL;
} }
} else { } else {
// catch-all for custom properties
$type = Value::T_MIXED; // @codeCoverageIgnore $type = Value::T_MIXED; // @codeCoverageIgnore
} }
$out[$p->name] = ['name' => $match[0], 'const' => $type]; $out[$p->name] = ['name' => $match[0], 'const' => $type];
@ -286,6 +287,7 @@ class Conf {
} }
switch (self::EXPECTED_TYPES[$key] ?? gettype($this->$key)) { switch (self::EXPECTED_TYPES[$key] ?? gettype($this->$key)) {
case "integer": case "integer":
// no properties are currently typed as integers
return Value::normalize($value, Value::T_INT | $mode); // @codeCoverageIgnore return Value::normalize($value, Value::T_INT | $mode); // @codeCoverageIgnore
case "double": case "double":
return Value::normalize($value, Value::T_FLOAT | $mode); return Value::normalize($value, Value::T_FLOAT | $mode);
@ -293,6 +295,7 @@ class Conf {
case "object": case "object":
return $value; return $value;
default: default:
// this should never occur
throw new Conf\Exception("ambiguousDefault", ['param' => $key]); // @codeCoverageIgnore throw new Conf\Exception("ambiguousDefault", ['param' => $key]); // @codeCoverageIgnore
} }
} }

View file

@ -78,21 +78,6 @@ class Database {
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; 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 */ /** 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 { public function driverSchemaVersion(): int {
return $this->db->schemaVersion(); return $this->db->schemaVersion();

View file

@ -46,23 +46,14 @@ abstract class AbstractStatement implements Statement {
return $query; return $query;
} }
public function retypeArray(array $bindings, bool $append = false): bool { public function retypeArray(array $bindings): bool {
if (!$append) {
$this->types = []; $this->types = [];
} foreach (ValueInfo::flatten($bindings) as $binding) { // recursively flatten any arrays, which may be provided for SET or IN() clauses
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; $bindId = self::TYPES[trim(strtolower($binding))] ?? 0;
assert($bindId, new Exception("paramTypeInvalid", $binding)); assert($bindId, new Exception("paramTypeInvalid", $binding));
$this->types[] = $bindId; $this->types[] = $bindId;
} }
}
if (!$append) {
$this->prepare(static::mungeQuery($this->query, $this->types)); $this->prepare(static::mungeQuery($this->query, $this->types));
}
return true; return true;
} }
@ -79,26 +70,22 @@ abstract class AbstractStatement implements Statement {
} }
} }
protected function bindValues(array $values, int $offset = null): int { protected function bindValues(array $values): bool {
$a = (int) $offset;
foreach ($values as $value) {
if (is_array($value)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses // recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a); $values = ValueInfo::flatten($values);
} elseif (array_key_exists($a, $this->types)) { foreach ($values as $a => $value) {
if (array_key_exists($a, $this->types)) {
$value = $this->cast($value, $this->types[$a]); $value = $this->cast($value, $this->types[$a]);
$this->bindValue($value, $this->types[$a] % self::T_NOT_NULL, ++$a); $this->bindValue($value, $this->types[$a] % self::T_NOT_NULL, ++$a);
} else { } else {
throw new Exception("paramTypeMissing", $a+1); 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 // SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error
if (is_null($offset)) { for ($a = sizeof($values); $a < sizeof($this->types); $a++) {
while ($a < sizeof($this->types)) { $this->bindValue(null, $this->types[$a] % self::T_NOT_NULL, $a + 1);
$this->bindValue(null, $this->types[$a] % self::T_NOT_NULL, ++$a); }
} return true;
}
return $a - $offset;
} }
} }

View file

@ -7,5 +7,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3; namespace JKingWeb\Arsse\Db\SQLite3;
abstract class AbstractPDODriver extends Driver { 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; use \JKingWeb\Arsse\Db\PDODriver;
} }

View file

@ -29,61 +29,63 @@ class Query {
$this->setBody($body, $types, $values); $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; $this->qBody = $body;
if (!is_null($types)) { if (!is_null($types)) {
$this->tBody[] = $types; $this->tBody[] = $types;
$this->vBody[] = $values; $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)"; $this->qCTE[] = "$tableSpec as ($body)";
if (!is_null($types)) { if (!is_null($types)) {
$this->tCTE[] = $types; $this->tCTE[] = $types;
$this->vCTE[] = $values; $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; $this->qWhere[] = $where;
if (!is_null($types)) { if (!is_null($types)) {
$this->tWhere[] = $types; $this->tWhere[] = $types;
$this->vWhere[] = $values; $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; $this->qWhereNot[] = $where;
if (!is_null($types)) { if (!is_null($types)) {
$this->tWhereNot[] = $types; $this->tWhereNot[] = $types;
$this->vWhereNot[] = $values; $this->vWhereNot[] = $values;
} }
return true; return $this;
} }
public function setGroup(string ...$column): bool { public function setGroup(string ...$column): self {
foreach ($column as $col) { foreach ($column as $col) {
$this->group[] = $col; $this->group[] = $col;
} }
return true; return $this;
} }
public function setOrder(string $order): bool { public function setOrder(string ...$order): self {
$this->order[] = $order; foreach ($order as $o) {
return true; $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->limit = $limit;
$this->offset = $offset; $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 // 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 // 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]); $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->order = [];
$this->group = []; $this->group = [];
$this->setLimit(0, 0); $this->setLimit(0, 0);
return true; return $this;
} }
public function __toString(): string { public function __toString(): string {
@ -117,11 +119,11 @@ class Query {
} }
public function getTypes(): array { 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 { 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 { protected function buildQueryBody(): string {
@ -144,9 +146,9 @@ class Query {
if (sizeof($this->order)) { if (sizeof($this->order)) {
$out .= " ORDER BY ".implode(", ", $this->order); $out .= " ORDER BY ".implode(", ", $this->order);
} }
// add LIMIT and OFFSET if the former is specified // add LIMIT and OFFSET if either is specified
if ($this->limit > 0) { if ($this->limit > 0 || $this->offset > 0) {
$out .= " LIMIT ".$this->limit; $out .= " LIMIT ".($this->limit < 1 ? -1 : $this->limit);
if ($this->offset > 0) { if ($this->offset > 0) {
$out .= " OFFSET ".$this->offset; $out .= " OFFSET ".$this->offset;
} }

View file

@ -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 { public static function int($value): int {
$out = 0; $out = 0;
if (is_null($value)) { if (is_null($value)) {

View file

@ -209,7 +209,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
// indexed arrays // indexed arrays
$p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1))); $p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1)));
} else { } 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 $p->appendChild($this->makeXMLAssoc($v, $d->createElement($k))); // @codeCoverageIgnore
} }
} }

View file

@ -12,30 +12,14 @@ class Service {
const DRIVER_NAMES = [ const DRIVER_NAMES = [
'serial' => \JKingWeb\Arsse\Service\Serial\Driver::class, 'serial' => \JKingWeb\Arsse\Service\Serial\Driver::class,
'subprocess' => \JKingWeb\Arsse\Service\Subprocess\Driver::class, 'subprocess' => \JKingWeb\Arsse\Service\Subprocess\Driver::class,
'curl' => \JKingWeb\Arsse\Service\Curl\Driver::class,
]; ];
/** @var Service\Driver */ /** @var Service\Driver */
protected $drv; 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() { public function __construct() {
$driver = Arsse::$conf->serviceDriver; $driver = Arsse::$conf->serviceDriver;
$this->drv = new $driver(); $this->drv = new $driver();
$this->interval = Arsse::$conf->serviceFrequency;
} }
public function watch(bool $loop = true): \DateTimeInterface { public function watch(bool $loop = true): \DateTimeInterface {
@ -46,17 +30,19 @@ class Service {
$list = Arsse::$db->feedListStale(); $list = Arsse::$db->feedListStale();
if ($list) { if ($list) {
$this->drv->queue(...$list); $this->drv->queue(...$list);
unset($list);
$this->drv->exec(); $this->drv->exec();
$this->drv->clean(); $this->drv->clean();
unset($list);
} }
static::cleanupPost(); static::cleanupPost();
$t->add($this->interval); $t->add(Arsse::$conf->serviceFrequency);
// @codeCoverageIgnoreStart
if ($loop) { if ($loop) {
do { do {
@time_sleep_until($t->getTimestamp()); @time_sleep_until($t->getTimestamp());
} while ($t->getTimestamp() > time()); } while ($t->getTimestamp() > time());
} }
// @codeCoverageIgnoreEnd
} while ($loop); } while ($loop);
return $t; return $t;
} }

View file

@ -11,5 +11,5 @@ interface Driver {
public static function requirementsMet(): bool; public static function requirementsMet(): bool;
public function queue(int ...$feeds): int; public function queue(int ...$feeds): int;
public function exec(): int; public function exec(): int;
public function clean(): bool; public function clean(): int;
} }

View file

@ -36,8 +36,9 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue); return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
} }
public function clean(): bool { public function clean(): int {
$out = sizeof($this->queue);
$this->queue = []; $this->queue = [];
return true; return $out;
} }
} }

View file

@ -33,7 +33,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
$id = (int) array_shift($this->queue); $id = (int) array_shift($this->queue);
$php = escapeshellarg(\PHP_BINARY); $php = escapeshellarg(\PHP_BINARY);
$arsse = escapeshellarg($_SERVER['argv'][0]); $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) { while ($pp) {
$p = array_pop($pp); $p = array_pop($pp);
@ -43,8 +43,14 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue); 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 = []; $this->queue = [];
return true; return $out;
} }
} }

View file

@ -20,18 +20,6 @@ class User {
*/ */
protected $u; 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) { public function __construct(\JKingWeb\Arsse\User\Driver $driver = null) {
$this->u = $driver ?? new Arsse::$conf->userDriver; $this->u = $driver ?? new Arsse::$conf->userDriver;
} }

View file

@ -19,13 +19,6 @@ trait SeriesMiscellany {
protected function tearDownSeriesMiscellany() { 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() { public function testInitializeDatabase() {
static::dbRaze(static::$drv); static::dbRaze(static::$drv);
$d = new Database(true); $d = new Database(true);

View file

@ -10,7 +10,6 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL;
* @group slow * @group slow
* @group coverageOptional * @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQL; use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQL;

View file

@ -11,7 +11,6 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO;
* @group optional * @group optional
* @group coverageOptional * @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQLPDO; use \JKingWeb\Arsse\Test\DatabaseDrivers\MySQLPDO;

View file

@ -10,7 +10,6 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
* @group slow * @group slow
* @group coverageOptional * @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQL; use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQL;

View file

@ -11,7 +11,6 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
* @group optional * @group optional
* @group coverageOptional * @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQLPDO; use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQLPDO;

View file

@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
/** /**
* @group optional * @group optional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3; use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3;

View file

@ -8,7 +8,6 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
/** /**
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest { class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\AbstractTest {
use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3PDO; use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3PDO;

View file

@ -0,0 +1,115 @@
<?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\Misc;
use JKingWeb\Arsse\Misc\Query;
use JKingWeb\Arsse\Misc\ValueInfo;
/** @covers \JKingWeb\Arsse\Misc\Query */
class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest {
public function testBasicQuery() {
$q = new Query("select * from table where a = ?", "int", 3);
$this->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());
}
}

View file

@ -92,4 +92,21 @@ class TestURL extends \JKingWeb\Arsse\Test\AbstractTest {
["/#ack", "", "/#ack"], ["/#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"],
];
}
} }

View file

@ -639,4 +639,10 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest {
$out->f = $msec; $out->f = $msec;
return $out; 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));
}
} }

View file

@ -0,0 +1,47 @@
<?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\Service;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service\Driver as DriverInterface;
use JKingWeb\Arsse\Service\Serial\Driver;
/** @covers \JKingWeb\Arsse\Service\Serial\Driver */
class TestSerial extends \JKingWeb\Arsse\Test\AbstractTest {
public function setUp() {
self::clearData();
self::setConf();
Arsse::$db = \Phake::mock(Database::class);
}
public function testConstruct() {
$this->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);
}
}

View file

@ -39,4 +39,45 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertTrue(Service::hasCheckedIn()); $this->assertTrue(Service::hasCheckedIn());
$this->assertFalse(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");
}
} }

View file

@ -0,0 +1,48 @@
<?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\Service;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service\Driver as DriverInterface;
use JKingWeb\Arsse\Service\Subprocess\Driver;
/** @covers \JKingWeb\Arsse\Service\Subprocess\Driver */
class TestSubprocess extends \JKingWeb\Arsse\Test\AbstractTest {
public function setUp() {
self::clearData();
self::setConf();
}
public function testConstruct() {
$this->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;
}
}

50
tests/cases/TestArsse.php Normal file
View file

@ -0,0 +1,50 @@
<?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;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Conf;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service;
/** @covers \JKingWeb\Arsse\Arsse */
class TestArsse extends \JKingWeb\Arsse\Test\AbstractTest {
public function setUp() {
self::clearData(false);
}
public function tearDown() {
self::clearData();
}
public function testLoadExistingData() {
$lang = Arsse::$lang = \Phake::mock(Lang::class);
$db = Arsse::$db = \Phake::mock(Database::class);
$user = Arsse::$user = \Phake::mock(User::class);
$conf1 = Arsse::$conf = \Phake::mock(Conf::class);
$conf2 = (new Conf)->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);
}
}

View file

@ -24,13 +24,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
$this->drv = \Phake::mock(Driver::class); $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() { public function testConstruct() {
$this->assertInstanceOf(User::class, new User($this->drv)); $this->assertInstanceOf(User::class, new User($this->drv));
$this->assertInstanceOf(User::class, new User); $this->assertInstanceOf(User::class, new User);

13
tests/lib/Service.php Normal file
View file

@ -0,0 +1,13 @@
<?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\Test;
class Service extends \JKingWeb\Arsse\Service {
public function __construct(\JKingWeb\Arsse\Service\Driver $drv) {
$this->drv = $drv;
}
}

View file

@ -45,6 +45,7 @@
<testsuite name="Sundry"> <testsuite name="Sundry">
<file>cases/Misc/TestValueInfo.php</file> <file>cases/Misc/TestValueInfo.php</file>
<file>cases/Misc/TestDate.php</file> <file>cases/Misc/TestDate.php</file>
<file>cases/Misc/TestQuery.php</file>
<file>cases/Misc/TestContext.php</file> <file>cases/Misc/TestContext.php</file>
<file>cases/Misc/TestURL.php</file> <file>cases/Misc/TestURL.php</file>
<file>cases/Misc/TestHTTP.php</file> <file>cases/Misc/TestHTTP.php</file>
@ -131,7 +132,10 @@
</testsuite> </testsuite>
<testsuite name="Admin tools"> <testsuite name="Admin tools">
<file>cases/Service/TestService.php</file> <file>cases/Service/TestService.php</file>
<file>cases/Service/TestSerial.php</file>
<file>cases/Service/TestSubprocess.php</file>
<file>cases/CLI/TestCLI.php</file> <file>cases/CLI/TestCLI.php</file>
<file>cases/TestArsse.php</file>
</testsuite> </testsuite>
<testsuite name="Import/Export"> <testsuite name="Import/Export">
<file>cases/ImportExport/TestFile.php</file> <file>cases/ImportExport/TestFile.php</file>