The clean & modern RSS server that doesn't give you any crap. https://thearsse.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Base.php 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. <?php
  2. /** @license MIT
  3. * Copyright 2017 J. King, Dustin Wilson et al.
  4. * See LICENSE and AUTHORS files for details */
  5. declare(strict_types=1);
  6. namespace JKingWeb\Arsse\TestCase\Database;
  7. use JKingWeb\Arsse\Test\Database;
  8. use JKingWeb\Arsse\Arsse;
  9. use JKingWeb\Arsse\Conf;
  10. use JKingWeb\Arsse\User;
  11. use JKingWeb\Arsse\Misc\ValueInfo;
  12. use JKingWeb\Arsse\Db\Result;
  13. use JKingWeb\Arsse\Test\DatabaseInformation;
  14. use Phake;
  15. abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest {
  16. use SeriesMiscellany;
  17. use SeriesMeta;
  18. use SeriesUser;
  19. use SeriesSession;
  20. use SeriesFolder;
  21. use SeriesFeed;
  22. use SeriesSubscription;
  23. use SeriesArticle;
  24. use SeriesLabel;
  25. use SeriesCleanup;
  26. /** @var \JKingWeb\Arsse\Test\DatabaseInformation */
  27. protected static $dbInfo;
  28. /** @var \JKingWeb\Arsse\Db\Driver */
  29. protected static $drv;
  30. protected static $failureReason = "";
  31. protected $primed = false;
  32. abstract protected function nextID(string $table): int;
  33. protected function findTraitOfTest(string $test): string {
  34. $class = new \ReflectionClass(self::class);
  35. foreach ($class->getTraits() as $trait) {
  36. if ($trait->hasMethod($test)) {
  37. return $trait->getShortName();
  38. }
  39. }
  40. return $class->getShortName();
  41. }
  42. public static function setUpBeforeClass() {
  43. // establish a clean baseline
  44. static::clearData();
  45. // perform an initial connection to the database to reset its version to zero
  46. // in the case of SQLite this will always be the case (we use a memory database),
  47. // but other engines should clean up from potentially interrupted prior tests
  48. static::$dbInfo = new DatabaseInformation(static::$implementation);
  49. static::setConf();
  50. try {
  51. static::$drv = new static::$dbInfo->driverClass;
  52. } catch (\JKingWeb\Arsse\Db\Exception $e) {
  53. static::$failureReason = $e->getMessage();
  54. return;
  55. }
  56. // wipe the database absolutely clean
  57. (static::$dbInfo->razeFunction)(static::$drv);
  58. // create the database interface with the suitable driver and apply the latest schema
  59. Arsse::$db = new Database(static::$drv);
  60. Arsse::$db->driverSchemaUpdate();
  61. }
  62. public function setUp() {
  63. // get the name of the test's test series
  64. $this->series = $this->findTraitofTest($this->getName());
  65. static::clearData();
  66. static::setConf();
  67. if (strlen(static::$failureReason)) {
  68. $this->markTestSkipped(static::$failureReason);
  69. }
  70. Arsse::$db = new Database(static::$drv);
  71. Arsse::$db->driverSchemaUpdate();
  72. // create a mock user manager
  73. Arsse::$user = Phake::mock(User::class);
  74. Phake::when(Arsse::$user)->authorize->thenReturn(true);
  75. // call the series-specific setup method
  76. $setUp = "setUp".$this->series;
  77. $this->$setUp();
  78. // prime the database with series data if it hasn't already been done
  79. if (!$this->primed && isset($this->data)) {
  80. $this->primeDatabase($this->data);
  81. }
  82. }
  83. public function tearDown() {
  84. // call the series-specific teardown method
  85. $this->series = $this->findTraitofTest($this->getName());
  86. $tearDown = "tearDown".$this->series;
  87. $this->$tearDown();
  88. // clean up
  89. $this->primed = false;
  90. // call the database-specific table cleanup function
  91. (static::$dbInfo->truncateFunction)(static::$drv);
  92. // clear state
  93. static::clearData();
  94. }
  95. public static function tearDownAfterClass() {
  96. // wipe the database absolutely clean
  97. (static::$dbInfo->razeFunction)(static::$drv);
  98. // clean up
  99. static::$drv = null;
  100. static::$dbInfo = null;
  101. static::$failureReason = "";
  102. static::clearData();
  103. }
  104. public function primeDatabase(array $data): bool {
  105. $drv = static::$drv;
  106. $tr = $drv->begin();
  107. foreach ($data as $table => $info) {
  108. $cols = array_map(function($v) {
  109. return '"'.str_replace('"', '""', $v).'"';
  110. }, array_keys($info['columns']));
  111. $cols = implode(",", $cols);
  112. $bindings = array_values($info['columns']);
  113. $params = implode(",", array_fill(0, sizeof($info['columns']), "?"));
  114. $s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
  115. foreach ($info['rows'] as $row) {
  116. $s->runArray($row);
  117. }
  118. }
  119. $tr->commit();
  120. $this->primed = true;
  121. return true;
  122. }
  123. public function compareExpectations(array $expected): bool {
  124. foreach ($expected as $table => $info) {
  125. $cols = array_map(function($v) {
  126. return '"'.str_replace('"', '""', $v).'"';
  127. }, array_keys($info['columns']));
  128. $cols = implode(",", $cols);
  129. $types = $info['columns'];
  130. $data = static::$drv->prepare("SELECT $cols from $table")->run()->getAll();
  131. $cols = array_keys($info['columns']);
  132. foreach ($info['rows'] as $index => $row) {
  133. $this->assertCount(sizeof($cols), $row, "The number of values for array index $index does not match the number of fields");
  134. $row = array_combine($cols, $row);
  135. foreach ($data as $index => $test) {
  136. foreach ($test as $col => $value) {
  137. switch ($types[$col]) {
  138. case "datetime":
  139. $test[$col] = $this->approximateTime($row[$col], $value);
  140. break;
  141. case "int":
  142. $test[$col] = ValueInfo::normalize($value, ValueInfo::T_INT | ValueInfo::M_DROP | valueInfo::M_NULL);
  143. break;
  144. case "float":
  145. $test[$col] = ValueInfo::normalize($value, ValueInfo::T_FLOAT | ValueInfo::M_DROP | valueInfo::M_NULL);
  146. break;
  147. case "bool":
  148. $test[$col] = (int) ValueInfo::normalize($value, ValueInfo::T_BOOL | ValueInfo::M_DROP | valueInfo::M_NULL);
  149. break;
  150. }
  151. }
  152. if ($row===$test) {
  153. $data[$index] = $test;
  154. break;
  155. }
  156. }
  157. $this->assertContains($row, $data, "Table $table does not contain record at array index $index.");
  158. $found = array_search($row, $data, true);
  159. unset($data[$found]);
  160. }
  161. $this->assertSame([], $data);
  162. }
  163. return true;
  164. }
  165. public function primeExpectations(array $source, array $tableSpecs = null): array {
  166. $out = [];
  167. foreach ($tableSpecs as $table => $columns) {
  168. // make sure the source has the table we want
  169. $this->assertArrayHasKey($table, $source, "Source for expectations does not contain requested table $table.");
  170. $out[$table] = [
  171. 'columns' => [],
  172. 'rows' => array_fill(0, sizeof($source[$table]['rows']), []),
  173. ];
  174. // make sure the source has all the columns we want for the table
  175. $cols = array_flip($columns);
  176. $cols = array_intersect_key($cols, $source[$table]['columns']);
  177. $this->assertSame(array_keys($cols), $columns, "Source for table $table does not contain all requested columns");
  178. // get a map of source value offsets and keys
  179. $targets = array_flip(array_keys($source[$table]['columns']));
  180. foreach ($cols as $key => $order) {
  181. // fill the column-spec
  182. $out[$table]['columns'][$key] = $source[$table]['columns'][$key];
  183. foreach ($source[$table]['rows'] as $index => $row) {
  184. // fill each row column-wise with re-ordered values
  185. $out[$table]['rows'][$index][$order] = $row[$targets[$key]];
  186. }
  187. }
  188. }
  189. return $out;
  190. }
  191. public function assertResult(array $expected, Result $data) {
  192. $data = $data->getAll();
  193. $this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")");
  194. if (sizeof($expected)) {
  195. // make sure the expectations are consistent
  196. foreach ($expected as $exp) {
  197. if (!isset($keys)) {
  198. $keys = $exp;
  199. continue;
  200. }
  201. $this->assertSame(array_keys($keys), array_keys($exp), "Result set expectations are irregular");
  202. }
  203. // filter the result set to contain just the desired keys (we don't care if the result has extra keys)
  204. $rows = [];
  205. foreach ($data as $row) {
  206. $rows[] = array_intersect_key($row, $keys);
  207. }
  208. // compare the result set to the expectations
  209. foreach ($rows as $row) {
  210. $this->assertContains($row, $expected, "Result set contains unexpected record.");
  211. $found = array_search($row, $expected);
  212. unset($expected[$found]);
  213. }
  214. $this->assertArraySubset($expected, [], "Expectations not in result set.");
  215. }
  216. }
  217. }