From 64c3ec3571584d9bc17cdd0e2cf219508c475e61 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 17 Oct 2019 13:00:56 -0400 Subject: [PATCH 01/15] Coverage fixes and OPML bugfix --- CHANGELOG | 1 + RoboFile.php | 6 ++++-- lib/CLI.php | 4 ++-- lib/ImportExport/AbstractImportExport.php | 8 ++++---- lib/REST/Fever/API.php | 3 ++- tests/cases/ImportExport/TestImportExport.php | 2 ++ .../Lang/{testComplex.php => TestComplex.php} | 0 tests/lib/Lang/Setup.php | 6 +++++- tests/lib/Lang/TestLang.php | 15 --------------- 9 files changed, 20 insertions(+), 25 deletions(-) rename tests/cases/Lang/{testComplex.php => TestComplex.php} (100%) delete mode 100644 tests/lib/Lang/TestLang.php diff --git a/CHANGELOG b/CHANGELOG index 3e48ece..5448364 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Version 0.8.1 (2019-??-??) Bug fixes: - Don't crash updating feeds cached solely via ETag +- Don't fail importing new folders from OPML files - Don't fail adding a feed which collides with another via redirection - Don't fail on very long text-search queries containing question marks when using PostgreSQL or MySQL diff --git a/RoboFile.php b/RoboFile.php index 23cd78a..f382090 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -5,6 +5,7 @@ use Robo\Result; const BASE = __DIR__.\DIRECTORY_SEPARATOR; const BASE_TEST = BASE."tests".\DIRECTORY_SEPARATOR; define("IS_WIN", defined("PHP_WINDOWS_VERSION_MAJOR")); +define("IS_MAC", php_uname("s") === "Darwin"); function norm(string $path): string { $out = realpath($path); @@ -92,12 +93,13 @@ class RoboFile extends \Robo\Tasks { $dbg = dirname(\PHP_BINARY)."\\phpdbg.exe"; $dbg = file_exists($dbg) ? $dbg : ""; } else { - $dbg = trim(`which phpdbg`); + $dbg = trim(`which phpdbg 2>/dev/null`); } if ($dbg) { return escapeshellarg($dbg)." -qrr"; } else { - return escapeshellarg(\PHP_BINARY); + $ext = IS_WIN ? "dll" : (IS_MAC ? "dylib" : "so"); + return escapeshellarg(\PHP_BINARY)." -d zend_extension=xdebug.$ext"; } } diff --git a/lib/CLI.php b/lib/CLI.php index bbd9930..702b9f5 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -206,7 +206,7 @@ USAGE_TEXT; $this->logError($e->getMessage()); return $e->getCode(); } - } + } // @codeCoverageIgnore /** @codeCoverageIgnore */ protected function logError(string $msg) { @@ -248,7 +248,7 @@ USAGE_TEXT; case "": return $this->userList(); } - } + } // @codeCoverageIgnore protected function userAddOrSetPassword(string $method, string $user, string $password = null, string $oldpass = null): int { $passwd = Arsse::$user->$method(...array_slice(func_get_args(), 1)); diff --git a/lib/ImportExport/AbstractImportExport.php b/lib/ImportExport/AbstractImportExport.php index f882ea1..19fa5fc 100644 --- a/lib/ImportExport/AbstractImportExport.php +++ b/lib/ImportExport/AbstractImportExport.php @@ -57,7 +57,7 @@ abstract class AbstractImportExport { } if (!isset($folderMap[$id])) { // if no existing folder exists, add one - $folderMap[$id] = Arsse::$db->folderAdd($user, ['name' => $f['name'], 'parent' -> $parent]); + $folderMap[$id] = Arsse::$db->folderAdd($user, ['name' => $f['name'], 'parent' => $parent]); } } // process newsfeed subscriptions @@ -118,21 +118,21 @@ abstract class AbstractImportExport { foreach (array_diff(array_column($feedsDb, "id"), $feedMap) as $id) { try { Arsse::$db->subscriptionRemove($user, $id); - } catch (InputException $e) { + } catch (InputException $e) { // @codeCoverageIgnore // ignore errors } } foreach (array_diff(array_column($foldersDb, "id"), $folderMap) as $id) { try { Arsse::$db->folderRemove($user, $id); - } catch (InputException $e) { + } catch (InputException $e) { // @codeCoverageIgnore // ignore errors } } foreach (array_diff(array_column($tagsDb, "name"), array_keys($tagMap)) as $id) { try { Arsse::$db->tagRemove($user, $id, true); - } catch (InputException $e) { + } catch (InputException $e) { // @codeCoverageIgnore // ignore errors } } diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 1401d63..ac85aa8 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -207,7 +207,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // indexed arrays $p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1))); } else { - $p->appendChild($this->makeXMLAssoc($v, $d->createElement($k))); + // this case does not actually occur in a proper Fever response + $p->appendChild($this->makeXMLAssoc($v, $d->createElement($k))); // @codeCoverageIgnore } } return $p; diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index c8af2c1..6c7de1f 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -212,11 +212,13 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 4, 'name' => "Politics", 'parent' => 0], ['id' => 5, 'name' => "Local", 'parent' => 4], ['id' => 6, 'name' => "National", 'parent' => 4], + ['id' => 7, 'name' => "Nature", 'parent' => 0], // new folder ]]; \Phake::when($this->proc)->parse->thenReturn($in); $this->proc->import("john.doe@example.com", "", false, true); $exp = $this->primeExpectations($this->data, $this->checkTables); $exp['arsse_subscriptions']['rows'][3] = [4, "john.doe@example.com", null, 4, "CBC"]; + $exp['arsse_folders']['rows'][] = [7, "john.doe@example.com", null, "Nature"]; $this->compareExpectations($this->drv, $exp); } diff --git a/tests/cases/Lang/testComplex.php b/tests/cases/Lang/TestComplex.php similarity index 100% rename from tests/cases/Lang/testComplex.php rename to tests/cases/Lang/TestComplex.php diff --git a/tests/lib/Lang/Setup.php b/tests/lib/Lang/Setup.php index 381806f..e70ccb0 100644 --- a/tests/lib/Lang/Setup.php +++ b/tests/lib/Lang/Setup.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\Test\Lang; use JKingWeb\Arsse\Lang; use JKingWeb\Arsse\Arsse; use org\bovigo\vfs\vfsStream; +use Webmozart\Glob\Glob; trait Setup { public function setUp() { @@ -36,7 +37,10 @@ trait Setup { // set up a file without read access chmod($this->path."ru.php", 0000); // make the test Lang class use the vfs files - $this->l = new TestLang($this->path); + $this->l = \Phake::partialMock(Lang::class, $this->path); + \Phake::when($this->l)->globFiles->thenReturnCallback(function(string $path): array { + return Glob::glob($this->path."*.php"); + }); // create a mock Lang object so as not to create a dependency loop self::clearData(false); Arsse::$lang = \Phake::mock(Lang::class); diff --git a/tests/lib/Lang/TestLang.php b/tests/lib/Lang/TestLang.php deleted file mode 100644 index 3022535..0000000 --- a/tests/lib/Lang/TestLang.php +++ /dev/null @@ -1,15 +0,0 @@ -path."*.php"); - } -} From c3643fba10c350e83f69843eed3f1263c45a1cbf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 17 Oct 2019 16:23:41 -0400 Subject: [PATCH 02/15] Tests for URL::absolute() --- tests/cases/Misc/TestURL.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/cases/Misc/TestURL.php b/tests/cases/Misc/TestURL.php index 8260c0b..e045d30 100644 --- a/tests/cases/Misc/TestURL.php +++ b/tests/cases/Misc/TestURL.php @@ -91,4 +91,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"], + ]; + } } From c706a76057f36aaac22c30ed3f4657e593f8ae80 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 18 Oct 2019 13:10:03 -0400 Subject: [PATCH 03/15] Simplify array flattening --- lib/Db/AbstractStatement.php | 45 +++++++++++------------------- lib/Misc/ValueInfo.php | 11 ++++++++ tests/cases/Misc/TestValueInfo.php | 6 ++++ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index cc8ce42..8471986 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/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 752704d..1b3d260 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/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 9bf12ba..83053c5 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)); + } } From b6dd8ab20d86a57abb1af84f34a1de72315f39cf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 18 Oct 2019 13:11:03 -0400 Subject: [PATCH 04/15] Improvements to and proper tests for query builder --- lib/Misc/Query.php | 46 +++---- tests/cases/Db/MySQL/TestDatabase.php | 1 - tests/cases/Db/MySQLPDO/TestDatabase.php | 1 - tests/cases/Db/PostgreSQL/TestDatabase.php | 1 - tests/cases/Db/PostgreSQLPDO/TestDatabase.php | 1 - tests/cases/Db/SQLite3/TestDatabase.php | 1 - tests/cases/Db/SQLite3PDO/TestDatabase.php | 1 - tests/cases/Misc/TestQuery.php | 115 ++++++++++++++++++ tests/phpunit.dist.xml | 1 + 9 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 tests/cases/Misc/TestQuery.php diff --git a/lib/Misc/Query.php b/lib/Misc/Query.php index 55d2cac..95b5c2c 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/tests/cases/Db/MySQL/TestDatabase.php b/tests/cases/Db/MySQL/TestDatabase.php index 4364170..9ad47ed 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 6a7550e..e734555 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 9fda4d9..e72c2a2 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 6f8ef2a..22afa5d 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 eab0970..ea30221 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 079bcc1..751647a 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 0000000..a1588f1 --- /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/phpunit.dist.xml b/tests/phpunit.dist.xml index 5489f94..e24e0d0 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 From 3ef1177f063489024be8563ab7ffca3e02f026f0 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 18 Oct 2019 13:20:28 -0400 Subject: [PATCH 05/15] Remove driver lists --- lib/Database.php | 15 --------------- lib/Service.php | 12 ------------ lib/User.php | 12 ------------ tests/cases/Database/SeriesMiscellany.php | 7 ------- tests/cases/User/TestUser.php | 7 ------- 5 files changed, 53 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index b50f040..4036f91 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/Service.php b/lib/Service.php index 93d4e9b..aed35c7 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -20,18 +20,6 @@ class Service { /** @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(); diff --git a/lib/User.php b/lib/User.php index 691d6fa..713f17c 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 a7591bb..e91b83b 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/User/TestUser.php b/tests/cases/User/TestUser.php index 1398b17..49fc472 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); From 7ac4fb47150fbdb5bb3e084cfaae7d743b02fc24 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 18 Oct 2019 16:09:01 -0400 Subject: [PATCH 06/15] Clarify PDO workaround for SQLite --- lib/Db/SQLite3/AbstractPDODriver.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Db/SQLite3/AbstractPDODriver.php b/lib/Db/SQLite3/AbstractPDODriver.php index a743a25..bc7ea61 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; } From bad86cedb38c35dea6b7b744da127795d6f7a14a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 19 Oct 2019 12:13:42 -0400 Subject: [PATCH 07/15] Tests for bootstrapper --- tests/cases/TestArsse.php | 47 +++++++++++++++++++++++++++++++++++++++ tests/phpunit.dist.xml | 1 + 2 files changed, 48 insertions(+) create mode 100644 tests/cases/TestArsse.php diff --git a/tests/cases/TestArsse.php b/tests/cases/TestArsse.php new file mode 100644 index 0000000..179f399 --- /dev/null +++ b/tests/cases/TestArsse.php @@ -0,0 +1,47 @@ +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() { + $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/phpunit.dist.xml b/tests/phpunit.dist.xml index e24e0d0..dd2ba50 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -129,6 +129,7 @@ cases/Service/TestService.php cases/CLI/TestCLI.php + cases/TestArsse.php cases/ImportExport/TestFile.php From 728eecfbb5696a94c4b463e71a331299c9add175 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 19 Oct 2019 12:14:13 -0400 Subject: [PATCH 08/15] Additional service tests --- lib/Conf.php | 3 +++ lib/REST/Fever/API.php | 2 +- lib/Service.php | 2 ++ tests/cases/Service/TestService.php | 20 ++++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/Conf.php b/lib/Conf.php index 15a8345..13635c8 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/REST/Fever/API.php b/lib/REST/Fever/API.php index ac85aa8..16bd889 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -207,7 +207,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 aed35c7..bd02fc9 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -40,11 +40,13 @@ class Service { } static::cleanupPost(); $t->add($this->interval); + // @codeCoverageIgnoreStart if ($loop) { do { @time_sleep_until($t->getTimestamp()); } while ($t->getTimestamp() > time()); } + // @codeCoverageIgnoreEnd } while ($loop); return $t; } diff --git a/tests/cases/Service/TestService.php b/tests/cases/Service/TestService.php index 3f6ea6b..a1db862 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -39,4 +39,24 @@ 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(); + } } From 71c7cd8fb17f2eb114ad6490a2a15dc403835f40 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 19 Oct 2019 18:51:01 -0400 Subject: [PATCH 09/15] Full coverage! Fixes #66 --- lib/Service.php | 8 ++--- lib/Service/Driver.php | 2 +- lib/Service/Serial/Driver.php | 5 +-- lib/Service/Subprocess/Driver.php | 12 +++++-- tests/cases/Service/TestSerial.php | 47 +++++++++++++++++++++++++ tests/cases/Service/TestService.php | 21 +++++++++++ tests/cases/Service/TestSubprocess.php | 48 ++++++++++++++++++++++++++ tests/cases/TestArsse.php | 3 ++ tests/lib/Service.php | 13 +++++++ tests/phpunit.dist.xml | 2 ++ 10 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/cases/Service/TestSerial.php create mode 100644 tests/cases/Service/TestSubprocess.php create mode 100644 tests/lib/Service.php diff --git a/lib/Service.php b/lib/Service.php index bd02fc9..ed234d6 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -12,18 +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 function __construct() { $driver = Arsse::$conf->serviceDriver; $this->drv = new $driver(); - $this->interval = Arsse::$conf->serviceFrequency; } public function watch(bool $loop = true): \DateTimeInterface { @@ -34,12 +30,12 @@ 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 { diff --git a/lib/Service/Driver.php b/lib/Service/Driver.php index 0f63383..e2dcf92 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 df3580c..dc2c74f 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 5e79ed0..0986f2c 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/tests/cases/Service/TestSerial.php b/tests/cases/Service/TestSerial.php new file mode 100644 index 0000000..5a96078 --- /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 a1db862..102a9ce 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -59,4 +59,25 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { \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 0000000..c02c4b2 --- /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 index 179f399..f28e7ee 100644 --- a/tests/cases/TestArsse.php +++ b/tests/cases/TestArsse.php @@ -37,6 +37,9 @@ class TestArsse extends \JKingWeb\Arsse\Test\AbstractTest { } 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); diff --git a/tests/lib/Service.php b/tests/lib/Service.php new file mode 100644 index 0000000..cfd11f4 --- /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 dd2ba50..997c6a7 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -128,6 +128,8 @@ cases/Service/TestService.php + cases/Service/TestSerial.php + cases/Service/TestSubprocess.php cases/CLI/TestCLI.php cases/TestArsse.php From c59cdfef76771c50226210461efb7ea3db4c0c97 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 25 Oct 2019 15:16:35 -0400 Subject: [PATCH 10/15] Whitespace fixes --- lib/Db/AbstractStatement.php | 2 +- lib/Misc/URL.php | 2 +- tests/cases/Misc/TestValueInfo.php | 2 +- tests/lib/AbstractTest.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index 8471986..45b6801 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -71,7 +71,7 @@ abstract class AbstractStatement implements Statement { } protected function bindValues(array $values): bool { - // 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 $values = ValueInfo::flatten($values); foreach ($values as $a => $value) { if (array_key_exists($a, $this->types)) { diff --git a/lib/Misc/URL.php b/lib/Misc/URL.php index da49eff..4a4459c 100644 --- a/lib/Misc/URL.php +++ b/lib/Misc/URL.php @@ -145,7 +145,7 @@ class URL { } /** Appends data to a URL's query component - * + * * @param string $url The input URL * @param string $data The data to append. This should already be escaped where necessary and not start with any delimiter * @param string $glue The query subcomponent delimiter, usually "&". If the URL has no query, "?" will be prepended instead diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 83053c5..2d5ffc4 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -642,7 +642,7 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { public function testFlattenArray() { $arr = [1, [2, 3, [4, 5]], 6, [[7, 8], 9, 10]]; - $exp = range(1,10); + $exp = range(1, 10); $this->assertSame($exp, I::flatten($arr)); } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index a2e66a3..f3a1ffb 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -74,7 +74,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { if (isset($params)) { if (is_array($params)) { $params = implode("&", array_map(function($v, $k) { - return rawurlencode($k).(isset($v) ? "=".rawurlencode($v) : ""); + return rawurlencode($k).(isset($v) ? "=".rawurlencode($v) : ""); }, $params, array_keys($params))); } $url = URL::queryAppend($url, (string) $params); @@ -90,7 +90,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) { parse_str($body, $parsedBody); } elseif (!is_string($body) && in_array(strtolower($type), ["application/json", "text/json"])) { - $body = json_encode($body, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + $body = json_encode($body, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); } elseif (!is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) { $parsedBody = $body; $body = http_build_query($body, "a", "&"); From 0e5b242c67e073b43a59f9acdce29a1285657922 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Oct 2019 11:07:04 -0400 Subject: [PATCH 11/15] Version bump --- CHANGELOG | 2 +- lib/Arsse.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5448364..a381c82 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -Version 0.8.1 (2019-??-??) +Version 0.8.1 (2019-10-28) ========================== Bug fixes: diff --git a/lib/Arsse.php b/lib/Arsse.php index 82c4332..6808831 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - const VERSION = "0.8.0"; + const VERSION = "0.8.1"; /** @var Lang */ public static $lang; From 733f0d7fd5bbda710e669e711b7d768b4603db70 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Oct 2019 12:43:47 -0400 Subject: [PATCH 12/15] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e8e51..e6ed075 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Information on how to install and use the software can be found in [the manual]( # Installing from source -The main repository for The Arsse can be found at [code.mensbeam.com](https://code.mensbeam.com/MensBeam/arsse/), with a mirror also available [at GitHub](https://github.com/meansbeam/arsse/). The main repository is preferred, as the GitHub mirror can sometimes be out of date. +The main repository for The Arsse can be found at [code.mensbeam.com](https://code.mensbeam.com/MensBeam/arsse/), with a mirror also available [at GitHub](https://github.com/mensbeam/arsse/). The main repository is preferred, as the GitHub mirror can sometimes be out of date. [Composer](https://getcomposer.org/) is required to manage PHP dependencies. After cloning the repository or downloading a source code tarball, running `composer install` will download all the required dependencies, and will advise if any PHP extensions need to be installed. If not installing as a programming environment, running `composer install --no-dev` is recommended. From bbc96e4f372424a4b0b14c3b29fc845dd4a8a9e1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Oct 2019 13:09:10 -0400 Subject: [PATCH 13/15] Remove reference to obsolete CSS-only manual task --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e6ed075..0a4f0af 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,6 @@ The Arsse's user manual, made using [Daux](https://daux.io/), can be compiled by The manual employs a custom theme derived from the standard Daux theme. If the standard Daux theme receives improvements, the custom theme can be rebuilt by running `./robo manual:theme`. This requires that [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com/) be installed, but JavaScript tools are not required to modify The Arsse itself, nor the content of the manual. -The Robo task `manual:css` will recompile the theme's stylesheet without rebuilding the entire theme. - ## Packaging a release Producing a release package is done by running `./robo package`. This performs the following operations: From 5ede4cbdb25950188b5a38a0d8682c4d48b3ba9d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Oct 2019 13:14:31 -0400 Subject: [PATCH 14/15] Clarify coverage requirements --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a4f0af..0658d78 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ There is also a `test:quick` Robo task which excludes slower tests, and a `test: ### Test coverage -Computing the coverage of tests can be done by running `./robo coverage`. Either [phpdbg](https://php.net/manual/en/book.phpdbg.php) or [Xdebug](https://xdebug.org) is required for this. An HTML-format coverage report will be written to `/tests/coverage/`. +Computing the coverage of tests can be done by running `./robo coverage`, after which an HTML-format coverage report will be written to `/tests/coverage/`. Either [Xdebug](https://xdebug.org) or [phpdbg](https://php.net/manual/en/book.phpdbg.php) is required for this. Xdebug is generally recommended as it is better maintained, though phpdbg is significantly faster. If using Xdebug, the extension need not be enabled globally; PHPUnit will enable it when needed. ## Enforcing coding style From c6b79d49ba36ed0eabf586263a9e8613b6edbdfa Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Oct 2019 19:48:10 -0400 Subject: [PATCH 15/15] Documentation corrections --- .../020_Getting_Started/040_Database_Setup/010_PostgreSQL.md | 2 +- docs/en/020_Getting_Started/040_Database_Setup/020_MySQL.md | 4 ++-- docs/en/020_Getting_Started/050_Configuration.md | 2 +- docs/en/020_Getting_Started/index.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md b/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md index 7dc1af2..5127242 100644 --- a/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md +++ b/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md @@ -22,7 +22,7 @@ sudo -u postgres psql -c "CREATE USER arsseuser WITH PASSWORD 'super secret pass sudo -u postgres psql -c "CREATE DATABASE arssedb WITH OWNER arsseuser" ``` -Tha Arsse must then be configured to use the created database. A suitable [configuration file](/en/Getting_Started/Configuration) might look like this: +The Arsse must then be configured to use the created database. A suitable [configuration file](/en/Getting_Started/Configuration) might look like this: ```php