[TASK] Doctrine: Migrate cache Typo3DatabaseBackend 21/49121/8
authorChristian Kuhn <lolli@schwarzbu.ch>
Tue, 19 Jul 2016 22:43:30 +0000 (00:43 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Thu, 21 Jul 2016 12:17:40 +0000 (14:17 +0200)
Migration of Typo3 database cache backend to doctrine.
The unit tests that checked if specific db queries are
fired didn't make much sense, they are moved over to
a bunch of functional tests to verify final state in
database is as expected.

Change-Id: Idc1bc20b13eb30f4b83ef8e7d6083c669bc0ace7
Resolves: #77160
Releases: master
Reviewed-on: https://review.typo3.org/49121
Tested-by: Bamboo TYPO3com <info@typo3.com>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Manuel Selbach <manuel_selbach@yahoo.de>
Tested-by: Manuel Selbach <manuel_selbach@yahoo.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php
typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php
typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php

index ac96b4e..de24003 100644 (file)
@@ -13,14 +13,20 @@ namespace TYPO3\CMS\Core\Cache\Backend;
  *
  * The TYPO3 project - inspiring people to share!
  */
+
+use TYPO3\CMS\Core\Cache\Exception;
+use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * A caching backend which stores cache entries in database tables
  * @api
  */
-class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
+class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInterface
 {
     /**
      * @var int Timestamp of 2038-01-01)
@@ -47,69 +53,23 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     protected $compressionLevel = -1;
 
     /**
-     * @var string Name of the identifier field, 'table_name.identifier'
-     */
-    protected $identifierField;
-
-    /**
-     * @var string Name of the expire field, 'table_name.expires'
-     */
-    protected $expiresField;
-
-    /**
      * @var int Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
      */
     protected $maximumLifetime;
 
     /**
-     * @var string SQL where for a not expired entry
-     */
-    protected $notExpiredStatement;
-
-    /**
-     * @var string Opposite of notExpiredStatement
-     */
-    protected $expiredStatement;
-
-    /**
-     * @var string Data and tags table name comma separated
-     */
-    protected $tableList;
-
-    /**
-     * @var string Join condition for data and tags table
-     */
-    protected $tableJoin;
-
-    /**
      * Set cache frontend instance and calculate data and tags table name
      *
-     * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The frontend for this backend
+     * @param FrontendInterface $cache The frontend for this backend
      * @return void
      * @api
      */
-    public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
+    public function setCache(FrontendInterface $cache)
     {
         parent::setCache($cache);
         $this->cacheTable = 'cf_' . $this->cacheIdentifier;
         $this->tagsTable = 'cf_' . $this->cacheIdentifier . '_tags';
-        $this->initializeCommonReferences();
-    }
-
-    /**
-     * Initializes common references used in this backend.
-     *
-     * @return void
-     */
-    protected function initializeCommonReferences()
-    {
-        $this->identifierField = $this->cacheTable . '.identifier';
-        $this->expiresField = $this->cacheTable . '.expires';
         $this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
-        $this->tableList = $this->cacheTable . ', ' . $this->tagsTable;
-        $this->tableJoin = $this->identifierField . ' = ' . $this->tagsTable . '.identifier';
-        $this->expiredStatement = $this->expiresField . ' < ' . $GLOBALS['EXEC_TIME'];
-        $this->notExpiredStatement = $this->expiresField . ' >= ' . $GLOBALS['EXEC_TIME'];
     }
 
     /**
@@ -120,14 +80,14 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
      * @param array $tags Tags to associate with this cache entry
      * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
      * @return void
-     * @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set.
-     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if the data to be stored is not a string.
+     * @throws Exception if no cache frontend has been set.
+     * @throws InvalidDataException if the data to be stored is not a string.
      */
     public function set($entryIdentifier, $data, array $tags = array(), $lifetime = null)
     {
         $this->throwExceptionIfFrontendDoesNotExist();
         if (!is_string($data)) {
-            throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException(
+            throw new InvalidDataException(
                 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
                 1236518298
             );
@@ -143,18 +103,23 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
         if ($this->compression) {
             $data = gzcompress($data, $this->compressionLevel);
         }
-        $GLOBALS['TYPO3_DB']->exec_INSERTquery($this->cacheTable, array(
-            'identifier' => $entryIdentifier,
-            'expires' => $expires,
-            'content' => $data
-        ));
+        GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable($this->cacheTable)
+            ->insert(
+                $this->cacheTable,
+                [
+                    'identifier' => $entryIdentifier,
+                    'expires' => $expires,
+                    'content' => $data,
+                ]
+            );
         if (!empty($tags)) {
             $fields = array();
             $fields[] = 'identifier';
             $fields[] = 'tag';
             $tagRows = array();
             foreach ($tags as $tag) {
-                $tagRow = array();
+                $tagRow = [];
                 $tagRow[] = $entryIdentifier;
                 $tagRow[] = $tag;
                 $tagRows[] = $tagRow;
@@ -174,19 +139,24 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function get($entryIdentifier)
     {
         $this->throwExceptionIfFrontendDoesNotExist();
-
-        $cacheEntry = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
-            'content',
-            $this->cacheTable,
-            'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' AND ' . $this->notExpiredStatement
-        );
-        if (is_array($cacheEntry)) {
-            $cacheEntry = $cacheEntry['content'];
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable($this->cacheTable);
+        $cacheRow =  $queryBuilder->select('content')
+            ->from($this->cacheTable)
+            ->where(
+                $queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($entryIdentifier)),
+                $queryBuilder->expr()->gte('expires', (int)$GLOBALS['EXEC_TIME'])
+            )
+            ->execute()
+            ->fetch();
+        $content = '';
+        if (!empty($cacheRow)) {
+            $content = $cacheRow['content'];
         }
-        if ($this->compression && (string)$cacheEntry !== '') {
-            $cacheEntry = gzuncompress($cacheEntry);
+        if ($this->compression && (string)$content !== '') {
+            $content = gzuncompress($content);
         }
-        return $cacheEntry !== null ? $cacheEntry : false;
+        return !empty($cacheRow) ? $content : false;
     }
 
     /**
@@ -198,16 +168,17 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function has($entryIdentifier)
     {
         $this->throwExceptionIfFrontendDoesNotExist();
-        $hasEntry = false;
-        $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
-            '*',
-            $this->cacheTable,
-            'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' AND ' . $this->notExpiredStatement
-        );
-        if ($cacheEntries >= 1) {
-            $hasEntry = true;
-        }
-        return $hasEntry;
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable($this->cacheTable);
+        $count = $queryBuilder->count('*')
+            ->from($this->cacheTable)
+            ->where(
+                $queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($entryIdentifier)),
+                $queryBuilder->expr()->gte('expires', (int)$GLOBALS['EXEC_TIME'])
+            )
+            ->execute()
+            ->fetchColumn(0);
+        return (bool)$count;
     }
 
     /**
@@ -220,19 +191,19 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function remove($entryIdentifier)
     {
         $this->throwExceptionIfFrontendDoesNotExist();
-        $entryRemoved = false;
-        $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
-            $this->cacheTable,
-            'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
-        );
-        $GLOBALS['TYPO3_DB']->exec_DELETEquery(
-            $this->tagsTable,
-            'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
-        );
-        if ($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
-            $entryRemoved = true;
-        }
-        return $entryRemoved;
+        $numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable($this->cacheTable)
+            ->delete(
+                $this->cacheTable,
+                ['identifier' => $entryIdentifier]
+            );
+        GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable($this->tagsTable)
+            ->delete(
+                $this->tagsTable,
+                ['identifier' => $entryIdentifier]
+            );
+        return (bool)$numberOfRowsRemoved;
     }
 
     /**
@@ -244,15 +215,20 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function findIdentifiersByTag($tag)
     {
         $this->throwExceptionIfFrontendDoesNotExist();
-        $cacheEntryIdentifiers = array();
-        $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
-            $this->identifierField,
-            $this->tableList,
-            $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable) . ' AND ' . $this->tableJoin . ' AND ' . $this->notExpiredStatement,
-            $this->identifierField
-        );
-        foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
-            $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
+        $cacheEntryIdentifiers = [];
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable($this->tagsTable);
+        $result = $queryBuilder->select($this->cacheTable . '.identifier')
+            ->from($this->cacheTable)
+            ->from($this->tagsTable)
+            ->where(
+                $queryBuilder->expr()->eq($this->cacheTable . '.identifier',  $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
+                $queryBuilder->expr()->eq($this->tagsTable . '.tag', $queryBuilder->createNamedParameter($tag)),
+                $queryBuilder->expr()->gte($this->cacheTable . '.expires', (int)$GLOBALS['EXEC_TIME'])
+            )
+            ->execute();
+        while ($row = $result->fetch()) {
+            $cacheEntryIdentifiers[$row['identifier']] = $row['identifier'];
         }
         return $cacheEntryIdentifiers;
     }
@@ -283,36 +259,38 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
 
-        if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('dbal')) {
-            $this->flushByTagDbal($tag);
-        } else {
-            $GLOBALS['TYPO3_DB']->sql_query('
-                               DELETE ' . $this->cacheTable . ', ' . $this->tagsTable . '
-                               FROM ' . $this->cacheTable . ' JOIN ' . $this->tagsTable . ' ON ' . $this->cacheTable . '.identifier=' . $this->tagsTable . '.identifier
-                               WHERE ' . $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable)
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
+        if ($this->isConnectionMysql($connection)) {
+            // Use a optimized query on mysql ... don't use on your own
+            // * ansi sql does not know about multi table delete
+            // * doctrine query builder does not support join on delete()
+            $connection->executeQuery(
+                'DELETE tags2, cache1'
+                . ' FROM ' . $this->tagsTable . ' AS tags1'
+                . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
+                . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
+                . ' WHERE tags1.tag = ?',
+                [$tag]
             );
-        }
-    }
-
-    /**
-     * Removes all cache entries of this cache for DBAL databases which are tagged by the specified tag.
-     *
-     * @param string $tag The tag the entries must have
-     * @return void
-     */
-    protected function flushByTagDbal($tag)
-    {
-        $tagsTableWhereClause = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
-        $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->tagsTable, $tagsTableWhereClause);
-        $cacheEntryIdentifiers = array();
-        while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
-            $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->cacheTable);
-        }
-        $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
-        if (!empty($cacheEntryIdentifiers)) {
-            $deleteWhereClause = 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')';
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $deleteWhereClause);
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, $deleteWhereClause);
+        } else {
+            $queryBuilder = $connection->createQueryBuilder();
+            $result = $queryBuilder->select('identifier')
+                ->from($this->tagsTable)
+                ->where($queryBuilder->expr()->eq('tag', $queryBuilder->createNamedParameter($tag)))
+                // group by is like DISTINCT and used here to suppress possible duplicate identifiers
+                ->groupBy('identifier')
+                ->execute();
+            $cacheEntryIdentifiers = [];
+            while ($row = $result->fetch()) {
+                $cacheEntryIdentifiers[] = $row['identifier'];
+            }
+            $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
+            $queryBuilder->delete($this->cacheTable)
+                ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                ->execute();
+            $queryBuilder->delete($this->tagsTable)
+                ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                ->execute();
         }
     }
 
@@ -325,40 +303,78 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     {
         $this->throwExceptionIfFrontendDoesNotExist();
 
-        if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('dbal')) {
-            $this->collectGarbageDbal();
-        } else {
-            $GLOBALS['TYPO3_DB']->sql_query('
-                               DELETE ' . $this->cacheTable . ', ' . $this->tagsTable . '
-                               FROM ' . $this->cacheTable . ' JOIN ' . $this->tagsTable . ' ON ' . $this->cacheTable . '.identifier=' . $this->tagsTable . '.identifier
-                               WHERE ' . $this->expiredStatement
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
+        if ($this->isConnectionMysql($connection)) {
+            // Use a optimized query on mysql ... don't use on your own
+            // * ansi sql does not know about multi table delete
+            // * doctrine query builder does not support join on delete()
+            // First delete all expired rows from cache table and their connected tag rows
+            $connection->executeQuery(
+                'DELETE cache, tags'
+                . ' FROM ' . $this->cacheTable . ' AS cache'
+                . ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
+                . ' WHERE cache.expires < ?',
+                [(int)$GLOBALS['EXEC_TIME']]
             );
+            // Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
+            $connection->executeQuery(
+                'DELETE tags'
+                . ' FROM ' . $this->tagsTable . ' AS tags'
+                . ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
+                . ' WHERE cache.identifier IS NULL'
+            );
+        } else {
+            $queryBuilder = $connection->createQueryBuilder();
+            $result = $queryBuilder->select('identifier')
+                ->from($this->cacheTable)
+                ->where($queryBuilder->expr()->lt('expires', (int)$GLOBALS['EXEC_TIME']))
+                // group by is like DISTINCT and used here to suppress possible duplicate identifiers
+                ->groupBy('identifier')
+                ->execute();
+
+            // Get identifiers of expired cache entries
+            $cacheEntryIdentifiers = [];
+            while ($row = $result->fetch()) {
+                $cacheEntryIdentifiers[] = $row['identifier'];
+            }
+            if (!empty($cacheEntryIdentifiers)) {
+                // Delete tag rows connected to expired cache entries
+                $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
+                $queryBuilder->delete($this->tagsTable)
+                    ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                    ->execute();
+            }
+            $queryBuilder->delete($this->cacheTable)
+                ->where($queryBuilder->expr()->lt('expires', (int)$GLOBALS['EXEC_TIME']))
+                ->execute();
+
+            // Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
+            $queryBuilder = $connection->createQueryBuilder();
+            $result = $queryBuilder->select('tags.identifier')
+                ->from($this->tagsTable, 'tags')
+                ->leftJoin(
+                    'tags',
+                    $this->cacheTable,
+                    'cache',
+                    $queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
+                )
+                ->where($queryBuilder->expr()->isNull('cache.identifier'))
+                ->groupBy('tags.identifier')
+                ->execute();
+            $tagsEntryIdentifiers = [];
+            while ($row = $result->fetch()) {
+                $tagsEntryIdentifiers[] = $row['identifier'];
+            }
+            if (!empty($tagsEntryIdentifiers)) {
+                $quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
+                $queryBuilder->delete($this->tagsTable)
+                    ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
+                    ->execute();
+            }
         }
     }
 
     /**
-     * Does garbage collection for DBAL databases
-     *
-     * @return void
-     */
-    protected function collectGarbageDbal()
-    {
-        // Get identifiers of expired cache entries
-        $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->cacheTable, $this->expiredStatement);
-        $cacheEntryIdentifiers = array();
-        while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
-            $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->tagsTable);
-        }
-        $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
-        // Delete tag rows connected to expired cache entries
-        if (!empty($cacheEntryIdentifiers)) {
-            $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
-        }
-        // Delete expired cache rows
-        $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $this->expiredStatement);
-    }
-
-    /**
      * Returns the table where the cache entries are stored.
      *
      * @return string The cache table.
@@ -405,15 +421,28 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     }
 
     /**
+     * This database backend uses some optimized queries for mysql
+     * to get maximum performance.
+     *
+     * @param Connection $connection
+     * @return bool
+     */
+    protected function isConnectionMysql(Connection $connection): bool
+    {
+        $serverVersion = $connection->getServerVersion();
+        return (bool)(strpos($serverVersion, 'MySQL') === 0);
+    }
+
+    /**
      * Check if required frontend instance exists
      *
-     * @throws \TYPO3\CMS\Core\Cache\Exception If there is no frontend instance in $this->cache
+     * @throws Exception If there is no frontend instance in $this->cache
      * @return void
      */
     protected function throwExceptionIfFrontendDoesNotExist()
     {
-        if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set via setCache() yet.', 1236518288);
+        if (!$this->cache instanceof FrontendInterface) {
+            throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
         }
     }
 
@@ -427,12 +456,12 @@ class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
     public function getTableDefinitions()
     {
         $cacheTableSql = file_get_contents(
-            \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
+            ExtensionManagementUtility::extPath('core') .
             'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
         );
         $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
         $tagsTableSql = file_get_contents(
-            \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
+            ExtensionManagementUtility::extPath('core') .
             'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
         );
         $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
diff --git a/typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php b/typo3/sysext/core/Tests/Functional/Cache/Backend/Typo3DatabaseBackendTest.php
new file mode 100644 (file)
index 0000000..e502cf5
--- /dev/null
@@ -0,0 +1,752 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Functional\Cache\Backend;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Tests\FunctionalTestCase;
+
+/**
+ * Test case
+ */
+class Typo3DatabaseBackendTest extends FunctionalTestCase
+{
+
+    /**
+     * @test
+     */
+    public function getReturnsPreviouslySetEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData');
+        $this->assertSame('myData', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsPreviouslySetEntryWithNewContentIfSetWasCalledMultipleTimes()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData');
+        $subject->set('myIdentifier', 'myNewData');
+        $this->assertSame('myNewData', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function setInsertsDataWithTagsIntoCacheTable()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myData', ['aTag', 'anotherTag']);
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'myIdentifier']));
+        $this->assertSame(1, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'myIdentifier', 'tag' => 'aTag']));
+        $this->assertSame(1, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'myIdentifier', 'tag' => 'anotherTag']));
+    }
+
+    /**
+     * @test
+     */
+    public function setStoresCompressedContent()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('myIdentifier', 'myCachedContent');
+
+        $row = (new ConnectionPool())
+            ->getConnectionForTable('cf_cache_pages')
+            ->select(
+                ['content'],
+                'cf_cache_pages',
+                ['identifier' => 'myIdentifier']
+            )
+            ->fetch();
+
+        // Content comes back uncompressed
+        $this->assertSame('myCachedContent', gzuncompress($row['content']));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsFalseIfNoCacheEntryExists()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsFalseForExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push an expired row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] - 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertSame('myCachedContent', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsUnzipsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a compressed row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => gzcompress('myCachedContent'),
+            ]
+        );
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Content comes back uncompressed
+        $this->assertSame('myCachedContent', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsEmptyStringUnzipped()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a compressed row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => gzcompress(''),
+            ]
+        );
+
+        // Have backend with compression enabled
+        $subject = new Typo3DatabaseBackend('Testing', ['compression' => true]);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Content comes back uncompressed
+        $this->assertSame('', $subject->get('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsFalseIfNoCacheEntryExists()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsFalseForExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push an expired row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] - 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function hasReturnsNotExpiredCacheEntry()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertTrue($subject->has('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeReturnsFalseIfNoEntryHasBeenRemoved()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertFalse($subject->remove('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeReturnsTrueIfAnEntryHasBeenRemoved()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Push a row into db
+        (new ConnectionPool())->getConnectionForTable('cf_cache_pages')->insert(
+            'cf_cache_pages',
+            [
+                'identifier' => 'myIdentifier',
+                'expires' => $GLOBALS['EXEC_TIME'] + 60,
+                'content' => 'myCachedContent',
+            ]
+        );
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $this->assertTrue($subject->remove('myIdentifier'));
+    }
+
+    /**
+     * @test
+     */
+    public function removeRemovesCorrectEntriesFromDatabase()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Add one cache row to remove and another one that shouldn't be removed
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $cacheTableConnection->bulkInsert(
+            'cf_cache_pages',
+            [
+                ['myIdentifier', $GLOBALS['EXEC_TIME'] + 60, 'myCachedContent'],
+                ['otherIdentifier', $GLOBALS['EXEC_TIME'] + 60, 'otherCachedContent'],
+            ],
+            ['identifier', 'expires', 'content']
+        );
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        // Add a couple of tags
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $tagsTableConnection->bulkInsert(
+            'cf_cache_pages_tags',
+            [
+                ['myIdentifier', 'aTag'],
+                ['myIdentifier', 'otherTag'],
+                ['otherIdentifier', 'aTag'],
+                ['otherIdentifier', 'otherTag'],
+            ],
+            ['identifier', 'tag']
+        );
+
+        $subject->remove('myIdentifier');
+
+        // cache row with removed identifier has been removed, other one exists
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'myIdentifier']));
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'otherIdentifier']));
+
+        // tags of myIdentifier should have been removed, others exist
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'myIdentifier']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'otherIdentifier']));
+    }
+
+    /**
+     * @test
+     */
+    public function findIdentifiersByTagReturnsIdentifierTaggedWithGivenTag()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+
+        $this->assertSame(['idA' => 'idA'], $subject->findIdentifiersByTag('tagA'));
+        $this->assertSame(['idA' => 'idA', 'idB' => 'idB'], $subject->findIdentifiersByTag('tagB'));
+        $this->assertSame(['idB' => 'idB'], $subject->findIdentifiersByTag('tagC'));
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagWorksWithEmptyCacheTablesWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->flushByTag('tagB');
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagRemovesCorrectRowsFromDatabaseWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+        $subject->set('idC', 'dataC', ['tagC', 'tagD']);
+        $subject->flushByTag('tagB');
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idA']));
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idB']));
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idC']));
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idC']));
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagWorksWithEmptyCacheTablesWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->flushByTag('tagB');
+    }
+
+    /**
+     * @test
+     */
+    public function flushByTagRemovesCorrectRowsFromDatabaseWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC']);
+        $subject->set('idC', 'dataC', ['tagC', 'tagD']);
+        $subject->flushByTag('tagB');
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idA']));
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idB']));
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idC']));
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idC']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageWorksWithEmptyTableWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->collectGarbage();
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesCacheEntryWithExpiredLifetimeWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // idA should be expired after EXEC_TIME manipulation, idB should stay
+        $subject->set('idA', 'dataA', [], 60);
+        $subject->set('idB', 'dataB', [], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idA']));
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idB']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesTagEntriesForCacheEntriesWithExpiredLifetimeWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesOrphanedTagEntriesFromTagsTableWithMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(true);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+
+        // Push two orphaned tag row into db - tags that have no related cache record anymore for whatever reason
+        $tagsTableConnection->insert(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagC'
+            ]
+        );
+        $tagsTableConnection->insert(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagD'
+            ]
+        );
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idC']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageWorksWithEmptyTableWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->collectGarbage();
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesCacheEntryWithExpiredLifetimeWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // idA should be expired after EXEC_TIME manipulation, idB should stay
+        $subject->set('idA', 'dataA', [], 60);
+        $subject->set('idB', 'dataB', [], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idA']));
+        $this->assertSame(1, $cacheTableConnection->count('*', 'cf_cache_pages', ['identifier' => 'idB']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesTagEntriesForCacheEntriesWithExpiredLifetimeWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+    }
+
+    /**
+     * @test
+     */
+    public function collectGarbageRemovesOrphanedTagEntriesFromTagsTableWithNonMysql()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        // Must be mocked here to test for "mysql" version implementation
+        $subject = $this->getMockBuilder(Typo3DatabaseBackend::class)
+            ->setMethods(['isConnectionMysql'])
+            ->setConstructorArgs(['Testing'])
+            ->getMock();
+        $subject->expects($this->once())->method('isConnectionMysql')->willReturn(false);
+        $subject->setCache($frontendProphecy->reveal());
+
+        // tag rows tagA and tagB should be removed by garbage collector after EXEC_TIME manipulation
+        $subject->set('idA', 'dataA', ['tagA', 'tagB'], 60);
+        $subject->set('idB', 'dataB', ['tagB', 'tagC'], 240);
+
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+
+        // Push two orphaned tag row into db - tags that have no related cache record anymore for whatever reason
+        $tagsTableConnection->insert(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagC'
+            ]
+        );
+        $tagsTableConnection->insert(
+            'cf_cache_pages_tags',
+            [
+                'identifier' => 'idC',
+                'tag' => 'tagD'
+            ]
+        );
+
+        $GLOBALS['EXEC_TIME'] = $GLOBALS['EXEC_TIME'] + 120;
+
+        $subject->collectGarbage();
+
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idA']));
+        $this->assertSame(2, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idB']));
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', ['identifier' => 'idC']));
+    }
+
+    /**
+     * @test
+     */
+    public function flushLeavesCacheAndTagsTableEmpty()
+    {
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_pages');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
+
+        $subject->set('idA', 'dataA', ['tagA', 'tagB']);
+
+        $subject->flush();
+
+        $cacheTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages');
+        $tagsTableConnection = (new ConnectionPool())->getConnectionForTable('cf_cache_pages_tags');
+        $this->assertSame(0, $cacheTableConnection->count('*', 'cf_cache_pages', []));
+        $this->assertSame(0, $tagsTableConnection->count('*', 'cf_cache_pages_tags', []));
+    }
+}
index f1db2c0..8ee5a10 100644 (file)
@@ -13,239 +13,76 @@ namespace TYPO3\CMS\Core\Tests\Unit\Cache\Backend;
  *
  * The TYPO3 project - inspiring people to share!
  */
+
 use Prophecy\Argument;
+use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
+use TYPO3\CMS\Core\Cache\Exception;
 use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Test case
- *
  */
-class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
+class Typo3DatabaseBackendTest extends UnitTestCase
 {
     /**
-     * Helper method to inject a mock frontend to backend instance
-     *
-     * @param \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend $backend Current backend instance
-     * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface Mock frontend
-     */
-    protected function setUpMockFrontendOfBackend(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend $backend)
-    {
-        $mockCache = $this->createMock(\TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend::class);
-        $mockCache->expects($this->any())->method('getIdentifier')->will($this->returnValue('Testing'));
-        $backend->setCache($mockCache);
-        return $mockCache;
-    }
-
-    /**
      * @test
      */
     public function setCacheCalculatesCacheTableName()
     {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $this->assertEquals('cf_Testing', $backend->getCacheTable());
-    }
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
 
-    /**
-     * @test
-     */
-    public function setCacheCalculatesTagsTableName()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $this->assertEquals('cf_Testing_tags', $backend->getTagsTable());
-    }
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
 
-    /**
-     * @test
-     */
-    public function setThrowsExceptionIfFrontendWasNotSet()
-    {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
-        $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->set('identifier', 'data');
+        $this->assertEquals('cf_cache_test', $subject->getCacheTable());
     }
 
     /**
      * @test
      */
-    public function setThrowsExceptionIfDataIsNotAString()
+    public function setCacheCalculatesTagsTableName()
     {
-        $this->expectException(InvalidDataException::class);
-        $this->expectExceptionCode(1236518298);
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
 
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $data = array('Some data');
-        $entryIdentifier = 'BackendDbTest';
-        $backend->set($entryIdentifier, $data);
-    }
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
 
-    /**
-     * @test
-     */
-    public function setInsertsEntryInTable()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_INSERTquery')
-            ->with('cf_Testing', $this->callback(function (array $data) {
-                if ($data['content'] !== 'someData') {
-                    return false;
-                }
-                if ($data['identifier'] !== 'anIdentifier') {
-                    return false;
-                }
-                return true;
-            }));
-        $backend->set('anIdentifier', 'someData');
+        $this->assertEquals('cf_cache_test_tags', $subject->getTagsTable());
     }
 
     /**
      * @test
      */
-    public function setRemovesAnAlreadyExistingCacheEntryForTheSameIdentifier()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('remove'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-
-        $backend->expects($this->once())->method('remove');
-        $data = $this->getUniqueId('someData');
-        $entryIdentifier = 'anIdentifier';
-        $backend->set($entryIdentifier, $data, array(), 500);
-    }
-
-    /**
-     * @test
-     */
-    public function setReallySavesSpecifiedTags()
+    public function setThrowsExceptionIfFrontendWasNotSet()
     {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $connectionPool = $this->createMock(ConnectionPool::class);
-        $connection = $this->createMock(Connection::class);
-        $connectionPool->expects($this->once())->method('getConnectionForTable')->willReturn($connection);
-        $connection->expects($this->once())->method('bulkInsert')->with(
-            'cf_Testing_tags',
-            $this->callback(function (array $data) {
-                if ($data[0][0] !== 'anIdentifier' || $data[0][1] !== 'UnitTestTag%tag1') {
-                    return false;
-                }
-                if ($data[1][0] !== 'anIdentifier' || $data[1][1] !== 'UnitTestTag%tag2') {
-                    return false;
-                }
-                return true;
-            }),
-            $this->callback(function (array $data) {
-                if ($data[0] === 'identifier' && $data[1] === 'tag') {
-                    return true;
-                }
-                return false;
-            })
-        );
-        GeneralUtility::addInstance(ConnectionPool::class, $connectionPool);
-        $backend->set('anIdentifier', 'someData', array('UnitTestTag%tag1', 'UnitTestTag%tag2'));
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
+        $this->expectExceptionCode(1236518288);
+        $subject->set('identifier', 'data');
     }
 
     /**
      * @test
      */
-    public function setSavesCompressedDataWithEnabledCompression()
+    public function setThrowsExceptionIfDataIsNotAString()
     {
-        $backendOptions = array(
-            'compression' => true
-        );
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing', $backendOptions))
-            ->getMock();
-
-        $this->setUpMockFrontendOfBackend($backend);
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
 
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_INSERTquery')
-            ->with(
-                'cf_Testing',
-                $this->callback(function (array $data) {
-                    if (@gzuncompress($data['content']) === 'someData') {
-                        return true;
-                    }
-                    return false;
-                }
-                ));
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
 
-        $backend->set('anIdentifier', 'someData');
-    }
-
-    /**
-     * @test
-     */
-    public function setWithUnlimitedLifetimeWritesCorrectEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_INSERTquery')
-            ->with(
-                'cf_Testing',
-                $this->callback(function (array $data) {
-                    $lifetime = $data['expires'];
-                    if ($lifetime > 2000000000) {
-                        return true;
-                    }
-                    return false;
-                }
-                ));
+        $this->expectException(InvalidDataException::class);
+        $this->expectExceptionCode(1236518298);
 
-        $backend->set('aIdentifier', 'someData', array(), 0);
+        $subject->set('identifier', array('iAmAnArray'));
     }
 
     /**
@@ -253,63 +90,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->get('identifier');
-    }
-
-    /**
-     * @test
-     */
-    public function getReturnsContentOfTheCorrectCacheEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_SELECTgetSingleRow')
-            ->with('content', 'cf_Testing', $this->anything())
-            ->will($this->returnValue(array('content' => 'someData')));
-
-        $loadedData = $backend->get('aIdentifier');
-        $this->assertEquals('someData', $loadedData);
-    }
-
-    /**
-     * @test
-     */
-    public function getSetsExceededLifetimeQueryPart()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_SELECTgetSingleRow')
-            ->with(
-                'content',
-                'cf_Testing',
-                $this->stringContains('identifier =  AND cf_Testing.expires >=')
-            );
-
-        $backend->get('aIdentifier');
+        $subject->get('identifier');
     }
 
     /**
@@ -317,62 +101,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function hasThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->has('identifier');
-    }
-
-    /**
-     * @test
-     */
-    public function hasReturnsTrueForExistingEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_SELECTcountRows')
-            ->with('*', 'cf_Testing', $this->anything())
-            ->will($this->returnValue(1));
-
-        $this->assertTrue($backend->has('aIdentifier'));
-    }
-
-    /**
-     * @test
-     */
-    public function hasSetsExceededLifetimeQueryPart()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->once())
-            ->method('exec_SELECTcountRows')
-            ->with(
-                '*',
-                'cf_Testing',
-                $this->stringContains('identifier =  AND cf_Testing.expires >='))
-            ->will($this->returnValue(1));
-
-        $this->assertTrue($backend->has('aIdentifier'));
+        $subject->has('identifier');
     }
 
     /**
@@ -380,48 +112,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function removeThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->remove('identifier');
-    }
-
-    /**
-     * @test
-     */
-    public function removeReallyRemovesACacheEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(0))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('aIdentifier'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing', 'identifier = aIdentifier');
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(2))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('aIdentifier'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing_tags', 'identifier = aIdentifier');
-
-        $backend->remove('aIdentifier');
+        $subject->remove('identifier');
     }
 
     /**
@@ -429,73 +123,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function collectGarbageThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->collectGarbage();
-    }
-
-    /**
-     * @test
-     */
-    public function collectGarbageDeletesTagsFromExpiredEntries()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(array('identifier' => 'aIdentifier')));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(2))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('aIdentifier'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(false));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(5))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing_tags', 'identifier IN (aIdentifier)');
-
-        $backend->collectGarbage();
-    }
-
-    /**
-     * @test
-     */
-    public function collectGarbageDeletesExpiredEntry()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(false));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing', $this->stringContains('cf_Testing.expires < '));
-
-        $backend->collectGarbage();
+        $subject->collectGarbage();
     }
 
     /**
@@ -503,45 +134,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function findIdentifiersByTagThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->findIdentifiersByTag('identifier');
-    }
-
-    /**
-     * @test
-     */
-    public function findIdentifiersByTagFindsCacheEntriesWithSpecifiedTag()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(0))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('cf_Testing_tags'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('exec_SELECTgetRows')
-            ->with(
-                'cf_Testing.identifier',
-                'cf_Testing, cf_Testing_tags',
-                $this->stringContains('cf_Testing_tags.tag = cf_Testing_tags AND cf_Testing.identifier = cf_Testing_tags.identifier AND cf_Testing.expires >= '),
-                'cf_Testing.identifier'
-            )
-            ->will($this->returnValue(array(array('identifier' => 'aIdentifier'))));
-        $this->assertSame(array('aIdentifier' => 'aIdentifier'), $backend->findIdentifiersByTag('aTag'));
+        $subject->findIdentifiersByTag('identifier');
     }
 
     /**
@@ -549,15 +145,10 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function flushThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->flush();
+        $subject->flush();
     }
 
     /**
@@ -565,16 +156,15 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function flushRemovesAllCacheEntries()
     {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
+        $frontendProphecy = $this->prophesize(FrontendInterface::class);
+        $frontendProphecy->getIdentifier()->willReturn('cache_test');
+
+        $subject = new Typo3DatabaseBackend('Testing');
+        $subject->setCache($frontendProphecy->reveal());
 
         $connectionProphet = $this->prophesize(Connection::class);
-        $connectionProphet->truncate('cf_Testing')->shouldBeCalled()->willReturn(0);
-        $connectionProphet->truncate('cf_Testing_tags')->shouldBeCalled()->willReturn(0);
+        $connectionProphet->truncate('cf_cache_test')->shouldBeCalled()->willReturn(0);
+        $connectionProphet->truncate('cf_cache_test_tags')->shouldBeCalled()->willReturn(0);
 
         $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
         $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal());
@@ -583,7 +173,7 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
         GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
 
-        $backend->flush();
+        $subject->flush();
     }
 
     /**
@@ -591,77 +181,9 @@ class Typo3DatabaseBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function flushByTagThrowsExceptionIfFrontendWasNotSet()
     {
-        $this->expectException(\TYPO3\CMS\Core\Cache\Exception::class);
+        $subject = new Typo3DatabaseBackend('Testing');
+        $this->expectException(Exception::class);
         $this->expectExceptionCode(1236518288);
-
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $backend->flushByTag(array());
-    }
-
-    /**
-     * @test
-     */
-    public function flushByTagRemovesCacheEntriesWithSpecifiedTag()
-    {
-        /** @var \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend|\PHPUnit_Framework_MockObject_MockObject $backend */
-        $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class)
-            ->setMethods(array('dummy'))
-            ->setConstructorArgs(array('Testing'))
-            ->getMock();
-        $this->setUpMockFrontendOfBackend($backend);
-
-        $GLOBALS['TYPO3_DB'] = $this->createMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(0))
-            ->method('fullQuoteStr')
-            ->will($this->returnValue('UnitTestTag%special'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(1))
-            ->method('exec_SELECTquery')
-            ->with(
-                'DISTINCT identifier',
-                'cf_Testing_tags',
-                'cf_Testing_tags.tag = UnitTestTag%special'
-            );
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(2))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(array('identifier' => 'BackendDbTest1')));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(3))
-            ->method('fullQuoteStr')
-            ->with('BackendDbTest1', 'cf_Testing')
-            ->will($this->returnValue('BackendDbTest1'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(4))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(array('identifier' => 'BackendDbTest2')));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(5))
-            ->method('fullQuoteStr')
-            ->with('BackendDbTest2', 'cf_Testing')
-            ->will($this->returnValue('BackendDbTest2'));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(6))
-            ->method('sql_fetch_assoc')
-            ->will($this->returnValue(false));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(7))
-            ->method('sql_free_result')
-            ->will($this->returnValue(true));
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(8))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing', 'identifier IN (BackendDbTest1, BackendDbTest2)');
-        $GLOBALS['TYPO3_DB']
-            ->expects($this->at(9))
-            ->method('exec_DELETEquery')
-            ->with('cf_Testing_tags', 'identifier IN (BackendDbTest1, BackendDbTest2)');
-
-        $backend->flushByTag('UnitTestTag%special');
+        $subject->flushByTag(array());
     }
 }
index bc9b96d..065385b 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Resource;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Doctrine\DBAL\Driver\Statement;
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -271,16 +272,27 @@ class ResourceStorageTest extends BaseTestCase
      */
     public function isWithinFileMountBoundariesRespectsReadOnlyFileMounts($fileIdentifier, $fileMountFolderIdentifier, $isFileMountReadOnly, $checkWriteAccess, $expectedResult)
     {
+        // @todo mess ahead - rewrite those tests!
         $connectionProphet = $this->prophesize(Connection::class);
         $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+        $connectionProphet->delete(Argument::cetera())->willReturn(0);
+        $connectionProphet->insert(Argument::cetera())->willReturn(0);
 
         $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
         $queryBuilderProphet->expr()->willReturn(
             GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
         );
+        $queryBuilderProphet->select(Argument::cetera())->willReturn($queryBuilderProphet->reveal());
+        $queryBuilderProphet->from(Argument::cetera())->willReturn($queryBuilderProphet->reveal());
+        $queryBuilderProphet->createNamedParameter(Argument::cetera())->willReturn($queryBuilderProphet->reveal());
+        $queryBuilderProphet->__toString()->willReturn('');
+        $queryBuilderProphet->where(Argument::cetera())->willReturn($queryBuilderProphet->reveal());
+        $statementProphecy = $this->prophesize(Statement::class);
+        $queryBuilderProphet->execute()->willReturn($statementProphecy->reveal());
 
         $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
         $connectionPoolProphet->getQueryBuilderForTable(Argument::cetera())->willReturn($queryBuilderProphet->reveal());
+        $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal());
         GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
 
         /** @var AbstractDriver|\PHPUnit_Framework_MockObject_MockObject $driverMock */