From 0c2b88773fa05732bb67a6835131750e6909a59f Mon Sep 17 00:00:00 2001 From: Nicole Cordes Date: Thu, 16 Feb 2017 14:59:01 +0100 Subject: [PATCH 1/4] [FEATURE] Add basic functional testing classes --- res/Configuration/FunctionalTests.xml | 15 ++ .../Bootstrap/FunctionalTestsBootstrap.php | 159 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 res/Configuration/FunctionalTests.xml create mode 100644 src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php diff --git a/res/Configuration/FunctionalTests.xml b/res/Configuration/FunctionalTests.xml new file mode 100644 index 0000000..ee72f5c --- /dev/null +++ b/res/Configuration/FunctionalTests.xml @@ -0,0 +1,15 @@ + diff --git a/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php b/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..a0306b2 --- /dev/null +++ b/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php @@ -0,0 +1,159 @@ +enableDisplayErrors() + ->loadClassFiles() + ->defineOriginalRootPath() + ->createNecessaryDirectoriesInDocumentRoot(); + } + + /** + * Makes sure error messages during the tests get displayed no matter what is set in php.ini. + * + * @return FunctionalTestsBootstrap fluent interface + */ + protected function enableDisplayErrors() + { + @ini_set('display_errors', 1); + + return $this; + } + + /** + * Requires classes the functional test classes extend from or use for further bootstrap. + * Only files required for "new TestCaseClass" are required here and a general exception + * that is thrown by setUp() code. + * + * @return FunctionalTestsBootstrap fluent interface + */ + protected function loadClassFiles() + { + if (!class_exists('PHPUnit_Framework_TestCase')) { + $this->exitWithMessage('PHPUnit wasn\'t found. Please check your settings and command.'); + } + if (!class_exists('Nimut\\TestingFramework\\TestCase\\BaseTestCase')) { + // PHPUnit is invoked globally, so we need to include the project autoload file + require_once __DIR__ . '/../../../../../autoload.php'; + } + + return $this; + } + + /** + * Defines the constant ORIGINAL_ROOT for the path to the original TYPO3 document root. + * + * If ORIGINAL_ROOT already is defined, this method is a no-op. + * + * @return FunctionalTestsBootstrap fluent interface + */ + protected function defineOriginalRootPath() + { + if (!defined('ORIGINAL_ROOT')) { + /** @var string */ + define('ORIGINAL_ROOT', $this->getWebRoot()); + } + + if (!file_exists(ORIGINAL_ROOT . 'typo3/cli_dispatch.phpsh')) { + $this->exitWithMessage('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.'); + } + + return $this; + } + + /** + * Creates the following directories in the TYPO3 core: + * - typo3temp + * + * @return FunctionalTestsBootstrap fluent interface + */ + protected function createNecessaryDirectoriesInDocumentRoot() + { + $this->createDirectory(ORIGINAL_ROOT . 'typo3temp'); + $this->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $this->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); + + return $this; + } + + /** + * Creates the directory $directory (recursively if required). + * + * If $directory already exists, this method is a no-op. + * + * @param string $directory absolute path of the directory to be created + * @throws \RuntimeException + * @return void + */ + protected function createDirectory($directory) + { + if (is_dir($directory)) { + return; + } + @mkdir($directory, 0777, true); + clearstatcache(); + if (!is_dir($directory)) { + throw new \RuntimeException('Directory "' . $directory . '" could not be created', 1404038665); + } + } + + /** + * Returns the absolute path the TYPO3 document root. + * + * @return string the TYPO3 document root using Unix path separators + */ + protected function getWebRoot() + { + if (getenv('TYPO3_PATH_WEB')) { + $webRoot = getenv('TYPO3_PATH_WEB'); + } else { + $webRoot = getcwd(); + } + + return rtrim(strtr($webRoot, '\\', '/'), '/') . '/'; + } + + /** + * Echo out a text message and exit with error code + * + * @param string $message + */ + protected function exitWithMessage($message) + { + echo $message . PHP_EOL; + exit(1); + } +} + +if (PHP_SAPI !== 'cli') { + die('This script supports command line usage only. Please check your command.'); +} +$bootstrap = new FunctionalTestsBootstrap(); +$bootstrap->bootstrapSystem(); +unset($bootstrap); From e0d1cbcd414ba5381be863dfeeff4fac1436816b Mon Sep 17 00:00:00 2001 From: Nicole Cordes Date: Thu, 16 Feb 2017 15:14:52 +0100 Subject: [PATCH 2/4] [FEATURE] Add functional test framework classes --- compat/Frontend/Response.php | 20 + compat/Frontend/ResponseContent.php | 20 + compat/Frontend/ResponseSection.php | 20 + compat/FunctionalTestCase.php | 20 + compat/FunctionalTestCaseBootstrapUtility.php | 20 + composer.json | 2 + res/Fixtures/Database/be_users.xml | 25 + .../FunctionalTestCaseBootstrapUtility.php | 820 ++++++++++++++++++ src/TestingFramework/Http/Response.php | 110 +++ src/TestingFramework/Http/ResponseContent.php | 75 ++ src/TestingFramework/Http/ResponseSection.php | 139 +++ .../TestCase/FunctionalTestCase.php | 414 +++++++++ 12 files changed, 1685 insertions(+) create mode 100644 compat/Frontend/Response.php create mode 100644 compat/Frontend/ResponseContent.php create mode 100644 compat/Frontend/ResponseSection.php create mode 100644 compat/FunctionalTestCase.php create mode 100644 compat/FunctionalTestCaseBootstrapUtility.php create mode 100644 res/Fixtures/Database/be_users.xml create mode 100644 src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php create mode 100644 src/TestingFramework/Http/Response.php create mode 100644 src/TestingFramework/Http/ResponseContent.php create mode 100644 src/TestingFramework/Http/ResponseSection.php create mode 100644 src/TestingFramework/TestCase/FunctionalTestCase.php diff --git a/compat/Frontend/Response.php b/compat/Frontend/Response.php new file mode 100644 index 0000000..b94c51a --- /dev/null +++ b/compat/Frontend/Response.php @@ -0,0 +1,20 @@ + + + + 1 + 0 + 1366642540 + admin + $1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1 + 1 + 0 + 0 + 0 + 0 + 1366642540 + 0 + 1 + 1 + 0 + NULL + 1371033743 + 0 + 0 + 1 + + \ No newline at end of file diff --git a/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php new file mode 100644 index 0000000..181689b --- /dev/null +++ b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php @@ -0,0 +1,820 @@ + destination path pairs to be linked + * @param array $configurationToUse Array of TYPO3_CONF_VARS that need to be overridden + * @param array $additionalFoldersToCreate Array of folder paths to be created + * @return string Path to TYPO3 CMS test installation for this test case + */ + public function setUp( + $testCaseClassName, + array $coreExtensionsToLoad, + array $testExtensionsToLoad, + array $pathsToLinkInTestInstance, + array $configurationToUse, + array $additionalFoldersToCreate + ) { + $this->setUpIdentifier($testCaseClassName); + $this->setUpInstancePath($testCaseClassName); + if ($this->recentTestInstanceExists()) { + $this->setUpBasicTypo3Bootstrap(); + $this->initializeTestDatabase(); + Bootstrap::getInstance()->loadExtensionTables(true); + } else { + $this->removeOldInstanceIfExists(); + $this->setUpInstanceDirectories($additionalFoldersToCreate); + $this->setUpInstanceCoreLinks(); + $this->linkTestExtensionsToInstance($testExtensionsToLoad); + $this->linkPathsInTestInstance($pathsToLinkInTestInstance); + $this->setUpLocalConfiguration($configurationToUse); + $this->setUpPackageStates($coreExtensionsToLoad, $testExtensionsToLoad); + $this->setUpBasicTypo3Bootstrap(); + $this->setUpTestDatabase(); + Bootstrap::getInstance()->loadExtensionTables(true); + $this->createDatabaseStructure(); + } + + return $this->instancePath; + } + + /** + * Checks whether the current test instance exists and is younger than + * some minutes. + * + * @return bool + */ + protected function recentTestInstanceExists() + { + if (@file_get_contents($this->instancePath . '/last_run.txt') <= (time() - 300)) { + return false; + } + + // Test instance exists and is pretty young -> re-use + return true; + } + + /** + * Calculate a "unique" identifier for the test database and the + * instance patch based on the given test case class name. + * + * As a result, the database name will be identical between different + * test runs, but different between each test case. + * + * @param string $testCaseClassName Name of test case class + * @return void + */ + protected function setUpIdentifier($testCaseClassName) + { + $this->identifier = static::getInstanceIdentifier($testCaseClassName); + } + + /** + * Calculates path to TYPO3 CMS test installation for this test case. + * + * @param string $testCaseClassName Name of test case class + * @return void + */ + protected function setUpInstancePath($testCaseClassName) + { + $this->instancePath = static::getInstancePath($testCaseClassName); + } + + /** + * Remove test instance folder structure in setUp() if it exists. + * This may happen if a functional test before threw a fatal. + * + * @return void + */ + protected function removeOldInstanceIfExists() + { + if (is_dir($this->instancePath)) { + $this->removeInstance(); + } + } + + /** + * Create folder structure of test instance. + * + * @param array $additionalFoldersToCreate Array of additional folders to be created + * @throws Exception + * @return void + */ + protected function setUpInstanceDirectories(array $additionalFoldersToCreate = []) + { + $foldersToCreate = array_merge($this->defaultFoldersToCreate, $additionalFoldersToCreate); + foreach ($foldersToCreate as $folder) { + $success = mkdir($this->instancePath . $folder); + if (!$success) { + throw new Exception( + 'Creating directory failed: ' . $this->instancePath . $folder, + 1376657189 + ); + } + } + + // Store the time we created this directory + file_put_contents($this->instancePath . '/last_run.txt', time()); + } + + /** + * Link TYPO3 CMS core from "parent" instance. + * + * @throws Exception + * @return void + */ + protected function setUpInstanceCoreLinks() + { + $linksToSet = [ + ORIGINAL_ROOT . 'typo3' => $this->instancePath . '/typo3', + ORIGINAL_ROOT . 'index.php' => $this->instancePath . '/index.php', + ]; + foreach ($linksToSet as $from => $to) { + $success = symlink($from, $to); + if (!$success) { + throw new Exception( + 'Creating link failed: from ' . $from . ' to: ' . $to, + 1376657199 + ); + } + } + } + + /** + * Link test extensions to the typo3conf/ext folder of the instance. + * + * @param array $extensionPaths Contains paths to extensions relative to document root + * @throws Exception + * @return void + */ + protected function linkTestExtensionsToInstance(array $extensionPaths) + { + foreach ($extensionPaths as $extensionPath) { + $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath; + if (!is_dir($absoluteExtensionPath)) { + throw new Exception( + 'Test extension path ' . $absoluteExtensionPath . ' not found', + 1376745645 + ); + } + $destinationPath = $this->instancePath . '/typo3conf/ext/' . basename($absoluteExtensionPath); + $success = symlink($absoluteExtensionPath, $destinationPath); + if (!$success) { + throw new Exception( + 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath, + 1376657142 + ); + } + } + } + + /** + * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the + * test instance fileadmin folder + * + * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root + * @throws Exception if a source path could not be found + * @throws Exception on failing creating the symlink + * @return void + * @see FunctionalTestCase::$pathsToLinkInTestInstance + */ + protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance) + { + foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) { + $sourcePath = $this->instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/'); + if (!file_exists($sourcePath)) { + throw new Exception( + 'Path ' . $sourcePath . ' not found', + 1376745645 + ); + } + $destinationPath = $this->instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/'); + $success = symlink($sourcePath, $destinationPath); + if (!$success) { + throw new Exception( + 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath, + 1389969623 + ); + } + } + } + + /** + * Create LocalConfiguration.php file in the test instance + * + * @param array $configurationToMerge + * @throws Exception + * @return void + */ + protected function setUpLocalConfiguration(array $configurationToMerge) + { + $databaseName = trim(getenv('typo3DatabaseName')); + $databaseHost = trim(getenv('typo3DatabaseHost')); + $databaseUsername = trim(getenv('typo3DatabaseUsername')); + $databasePassword = trim(getenv('typo3DatabasePassword')); + $databasePort = trim(getenv('typo3DatabasePort')); + $databaseSocket = trim(getenv('typo3DatabaseSocket')); + if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) { + // Try to get database credentials from environment variables first + $originalConfigurationArray = [ + 'DB' => [], + ]; + if ($databaseName) { + $originalConfigurationArray['DB']['database'] = $databaseName; + } + if ($databaseHost) { + $originalConfigurationArray['DB']['host'] = $databaseHost; + } + if ($databaseUsername) { + $originalConfigurationArray['DB']['username'] = $databaseUsername; + } + if ($databasePassword) { + $originalConfigurationArray['DB']['password'] = $databasePassword; + } + if ($databasePort) { + $originalConfigurationArray['DB']['port'] = $databasePort; + } + if ($databaseSocket) { + $originalConfigurationArray['DB']['socket'] = $databaseSocket; + } + } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) { + // See if a LocalConfiguration file exists in "parent" instance to get db credentials from + $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php'; + } else { + throw new Exception( + 'Database credentials for functional tests are neither set through environment' + . ' variables, and can not be found in an existing LocalConfiguration file', + 1397406356 + ); + } + + // Base of final LocalConfiguration is core factory configuration + $finalConfigurationArray = require ORIGINAL_ROOT . 'typo3/sysext/core/Configuration/FactoryConfiguration.php'; + + $configurationToMerge = array_replace_recursive( + [ + 'SYS' => [ + 'caching' => [ + 'cacheConfigurations' => [ + 'extbase_object' => [ + 'backend' => NullBackend::class, + ], + ], + ], + 'displayErrors' => '1', + 'debugExceptionHandler' => '', + 'isInitialDatabaseImportDone' => true, + 'isInitialInstallationInProgress' => false, + 'setDBinit' => 'SET SESSION sql_mode = \'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_VALUE_ON_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY\';', + 'trustedHostsPattern' => '.*', + ], + ], + $configurationToMerge + ); + $this->mergeRecursiveWithOverrule($finalConfigurationArray, $configurationToMerge); + $finalConfigurationArray['DB'] = $originalConfigurationArray['DB']; + // Calculate and set new database name + $this->originalDatabaseName = $originalConfigurationArray['DB']['database']; + $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier; + + // Maximum database name length for mysql is 64 characters + if (strlen($this->databaseName) > 64) { + $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier); + throw new Exception( + 'The name of the database that is used for the functional test (' . $this->databaseName . ')' . + ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' . + ' original database name to ' . $maximumOriginalDatabaseName . ' characters', + 1377600104 + ); + } + + $finalConfigurationArray['DB']['database'] = $this->databaseName; + + $result = $this->writeFile( + $this->instancePath . '/typo3conf/LocalConfiguration.php', + 'arrayExport( + $finalConfigurationArray + ) . + ';' . chr(10) . + '?>' + ); + if (!$result) { + throw new Exception('Can not write local configuration', 1376657277); + } + } + + /** + * Compile typo3conf/PackageStates.php containing default packages like core, + * a functional test specific list of additional core extensions, and a list of + * test extensions. + * + * @param array $coreExtensionsToLoad Additional core extensions to load + * @param array $testExtensionPaths Paths to extensions relative to document root + * @throws Exception + * @TODO Figure out what the intention of the upper arguments is + */ + protected function setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths) + { + $packageStates = [ + 'packages' => [], + 'version' => 4, + ]; + + // Register default list of extensions and set active + foreach ($this->defaultActivatedCoreExtensions as $extensionName) { + $packageStates['packages'][$extensionName] = [ + 'state' => 'active', + 'packagePath' => 'typo3/sysext/' . $extensionName . '/', + 'classesPath' => 'Classes/', + ]; + } + + // Register additional core extensions and set active + foreach ($coreExtensionsToLoad as $extensionName) { + if (isset($packageSates['packages'][$extensionName])) { + throw new Exception( + $extensionName . ' is already registered as default core extension to load, no need to load it explicitly', + 1390913893 + ); + } + $packageStates['packages'][$extensionName] = [ + 'state' => 'active', + 'packagePath' => 'typo3/sysext/' . $extensionName . '/', + 'classesPath' => 'Classes/', + ]; + } + + // Activate test extensions that have been symlinked before + foreach ($testExtensionPaths as $extensionPath) { + $extensionName = basename($extensionPath); + if (isset($packageSates['packages'][$extensionName])) { + throw new Exception( + $extensionName . ' is already registered as extension to load, no need to load it explicitly', + 1390913894 + ); + } + $packageStates['packages'][$extensionName] = [ + 'state' => 'active', + 'packagePath' => 'typo3conf/ext/' . $extensionName . '/', + 'classesPath' => 'Classes/', + ]; + } + + $result = $this->writeFile( + $this->instancePath . '/typo3conf/PackageStates.php', + 'arrayExport( + $packageStates + ) . + ';' . chr(10) . + '?>' + ); + if (!$result) { + throw new Exception('Can not write PackageStates', 1381612729); + } + } + + /** + * Bootstrap basic TYPO3 + * + * @return void + */ + protected function setUpBasicTypo3Bootstrap() + { + $_SERVER['PWD'] = $this->instancePath; + $_SERVER['argv'][0] = 'index.php'; + + define('TYPO3_MODE', 'BE'); + define('TYPO3_cliMode', true); + + $classLoader = require rtrim(realpath($this->instancePath . '/typo3/'), '\\/') . '/../vendor/autoload.php'; + \TYPO3\CMS\Core\Core\Bootstrap::getInstance() + ->initializeClassLoader($classLoader) + ->baseSetup('') + ->loadConfigurationAndInitialize(true) + ->loadTypo3LoadedExtAndExtLocalconf(true) + ->setFinalCachingFrameworkCacheConfiguration() + ->defineLoggingAndExceptionConstants() + ->unsetReservedGlobalVariables(); + } + + /** + * Populate $GLOBALS['TYPO3_DB'] and create test database + * + * @throws Exception + * @return void + */ + protected function setUpTestDatabase() + { + Bootstrap::getInstance()->initializeTypo3DbGlobal(); + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + if (!$database->sql_pconnect()) { + throw new Exception( + 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' + . ' connection to the database was attempted to be established!', + 1377620117 + ); + } + + // Drop database in case a previous test had a fatal and did not clean up properly + $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`'); + $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`'); + if (!$createDatabaseResult) { + $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username']; + $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host']; + throw new Exception( + 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.' + . ' For this instance this could be fixed executing' + . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"', + 1376579070 + ); + } + $database->setDatabaseName($this->databaseName); + // On windows, this still works, but throws a warning, which we need to discard. + @$database->sql_select_db(); + } + + /** + * Populate $GLOBALS['TYPO3_DB'] reusing an existing database with + * all tables truncated. + * + * @throws Exception + * @return void + */ + protected function initializeTestDatabase() + { + Bootstrap::getInstance()->initializeTypo3DbGlobal(); + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + if (!$database->sql_pconnect()) { + throw new Exception( + 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' + . ' connection to the database was attempted to be established!', + 1377620117 + ); + } + $this->databaseName = $GLOBALS['TYPO3_CONF_VARS']['DB']['database']; + $database->setDatabaseName($this->databaseName); + $database->sql_select_db(); + foreach ($database->admin_get_tables() as $table) { + $database->admin_query('TRUNCATE ' . $table['Name'] . ';'); + } + } + + /** + * Create tables and import static rows + * + * @return void + */ + protected function createDatabaseStructure() + { + /** @var SqlSchemaMigrationService $schemaMigrationService */ + $schemaMigrationService = GeneralUtility::makeInstance(SqlSchemaMigrationService::class); + /** @var ObjectManager $objectManager */ + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + /** @var SqlExpectedSchemaService $expectedSchemaService */ + $expectedSchemaService = $objectManager->get(SqlExpectedSchemaService::class); + + // Raw concatenated ext_tables.sql and friends string + $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(true); + $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, true); + list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, true); + + $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString); + $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database(); + $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase); + $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference); + + $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']); + $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']); + $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']); + + foreach ($insertCount as $table => $count) { + $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table); + foreach ($insertStatements as $insertQuery) { + $insertQuery = rtrim($insertQuery, ';'); + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + $database->admin_query($insertQuery); + } + } + } + + /** + * Drop test database. + * + * @throws Exception + * @return void + */ + protected function tearDownTestDatabase() + { + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + $result = $database->admin_query('DROP DATABASE `' . $this->databaseName . '`'); + if (!$result) { + throw new Exception( + 'Dropping test database ' . $this->databaseName . ' failed', + 1376583188 + ); + } + } + + /** + * Removes instance directories and files + * + * @throws Exception + * @return void + */ + protected function removeInstance() + { + $success = $this->rmdir($this->instancePath, true); + if (!$success) { + throw new Exception( + 'Can not remove folder: ' . $this->instancePath, + 1376657210 + ); + } + } + + /** + * COPIED FROM GeneralUtility + * + * Wrapper function for rmdir, allowing recursive deletion of folders and files + * + * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally. + * @param bool $removeNonEmpty Allow deletion of non-empty directories + * @return bool TRUE if @rmdir went well! + */ + protected function rmdir($path, $removeNonEmpty = false) + { + $OK = false; + // Remove trailing slash + $path = preg_replace('|/$|', '', $path); + if (file_exists($path)) { + $OK = true; + if (!is_link($path) && is_dir($path)) { + if ($removeNonEmpty == true && ($handle = opendir($path))) { + while ($OK && false !== ($file = readdir($handle))) { + if ($file == '.' || $file == '..') { + continue; + } + $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty); + } + closedir($handle); + } + if ($OK) { + $OK = @rmdir($path); + } + } else { + // If $path is a symlink to a folder we need rmdir() on Windows systems + if (!stristr(PHP_OS, 'darwin') && stristr(PHP_OS, 'win') && is_link($path) && is_dir($path . '/')) { + $OK = rmdir($path); + } else { + $OK = unlink($path); + } + } + clearstatcache(); + } elseif (is_link($path)) { + $OK = unlink($path); + clearstatcache(); + } + + return $OK; + } + + /** + * COPIED FROM GeneralUtility + * + * Writes $content to the file $file + * + * @param string $file Filepath to write to + * @param string $content Content to write + * @return bool TRUE if the file was successfully opened and written to. + */ + protected function writeFile($file, $content) + { + if ($fd = fopen($file, 'wb')) { + $res = fwrite($fd, $content); + fclose($fd); + if ($res === false) { + return false; + } + + return true; + } + + return false; + } + + /** + * COPIED FROM ArrayUtility + * + * Exports an array as string. + * Similar to var_export(), but representation follows the TYPO3 core CGL. + * + * See unit tests for detailed examples + * + * @param array $array Array to export + * @param int $level Internal level used for recursion, do *not* set from outside! + * @throws \RuntimeException + * @return string String representation of array + */ + protected function arrayExport(array $array = [], $level = 0) + { + $lines = 'array(' . chr(10); + $level++; + $writeKeyIndex = false; + $expectedKeyIndex = 0; + foreach ($array as $key => $value) { + if ($key === $expectedKeyIndex) { + $expectedKeyIndex++; + } else { + // Found a non integer or non consecutive key, so we can break here + $writeKeyIndex = true; + break; + } + } + foreach ($array as $key => $value) { + // Indention + $lines .= str_repeat(chr(9), $level); + if ($writeKeyIndex) { + // Numeric / string keys + $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => '; + } + if (is_array($value)) { + if (!empty($value)) { + $lines .= $this->arrayExport($value, $level); + } else { + $lines .= 'array(),' . chr(10); + } + } elseif (is_int($value) || is_float($value)) { + $lines .= $value . ',' . chr(10); + } elseif (is_null($value)) { + $lines .= 'NULL' . ',' . chr(10); + } elseif (is_bool($value)) { + $lines .= $value ? 'TRUE' : 'FALSE'; + $lines .= ',' . chr(10); + } elseif (is_string($value)) { + // Quote \ to \\ + $stringContent = str_replace('\\', '\\\\', $value); + // Quote ' to \' + $stringContent = str_replace('\'', '\\\'', $stringContent); + $lines .= '\'' . $stringContent . '\'' . ',' . chr(10); + } else { + throw new \RuntimeException('Objects are not supported', 1342294986); + } + } + $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10)); + + return $lines; + } + + /** + * COPIED FROM ArrayUtility + * + * Merges two arrays recursively and "binary safe" (integer keys are + * overridden as well), overruling similar values in the original array + * with the values of the overrule array. + * In case of identical keys, ie. keeping the values of the overrule array. + * + * This method takes the original array by reference for speed optimization with large arrays + * + * The differences to the existing PHP function array_merge_recursive() are: + * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature) + * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues) + * * Elements or the original array get overwritten if the same key is present in the overrule array. + * + * @param array $original Original array. It will be *modified* by this method and contains the result afterwards! + * @param array $overrule Overrule array, overruling the original array + * @param bool $addKeys If set to FALSE, keys that are NOT found in $original will not be set. Thus only existing value can/will be overruled from overrule array. + * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero. + * @param bool $enableUnsetFeature If set, special values "__UNSET" can be used in the overrule array in order to unset array keys in the original array. + * @return void + */ + protected function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true) + { + foreach ($overrule as $key => $_) { + if ($enableUnsetFeature && $overrule[$key] === '__UNSET') { + unset($original[$key]); + continue; + } + if (isset($original[$key]) && is_array($original[$key])) { + if (is_array($overrule[$key])) { + self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature); + } + } elseif ( + ($addKeys || isset($original[$key])) && + ($includeEmptyValues || $overrule[$key]) + ) { + $original[$key] = $overrule[$key]; + } + } + // This line is kept for backward compatibility reasons. + reset($original); + } +} diff --git a/src/TestingFramework/Http/Response.php b/src/TestingFramework/Http/Response.php new file mode 100644 index 0000000..842226c --- /dev/null +++ b/src/TestingFramework/Http/Response.php @@ -0,0 +1,110 @@ +status = $status; + $this->content = $content; + $this->error = $error; + } + + /** + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * @return array|null|string + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function getError() + { + return $this->error; + } + + /** + * @return ResponseContent + */ + public function getResponseContent() + { + if (!isset($this->responseContent)) { + $this->responseContent = new ResponseContent($this); + } + return $this->responseContent; + } + + /** + * @return null|array|ResponseSection[] + */ + public function getResponseSections() + { + $sectionIdentifiers = func_get_args(); + + if (empty($sectionIdentifiers)) { + $sectionIdentifiers = ['Default']; + } + + $sections = []; + foreach ($sectionIdentifiers as $sectionIdentifier) { + $sections[] = $this->getResponseContent()->getSection($sectionIdentifier); + } + + return $sections; + } +} diff --git a/src/TestingFramework/Http/ResponseContent.php b/src/TestingFramework/Http/ResponseContent.php new file mode 100644 index 0000000..89884d8 --- /dev/null +++ b/src/TestingFramework/Http/ResponseContent.php @@ -0,0 +1,75 @@ +getContent(), true); + + if ($content !== null && is_array($content)) { + foreach ($content as $sectionIdentifier => $sectionData) { + $section = new ResponseSection($sectionIdentifier, $sectionData); + $this->sections[$sectionIdentifier] = $section; + } + } + } + + /** + * @param string $sectionIdentifier + * @throws \RuntimeException + * @return null|ResponseSection + */ + public function getSection($sectionIdentifier) + { + if (isset($this->sections[$sectionIdentifier])) { + return $this->sections[$sectionIdentifier]; + } + + throw new \RuntimeException('ResponseSection "' . $sectionIdentifier . '" does not exist'); + } +} diff --git a/src/TestingFramework/Http/ResponseSection.php b/src/TestingFramework/Http/ResponseSection.php new file mode 100644 index 0000000..fab4ffd --- /dev/null +++ b/src/TestingFramework/Http/ResponseSection.php @@ -0,0 +1,139 @@ +identifier = (string)$identifier; + $this->structure = $data['structure']; + $this->structurePaths = $data['structurePaths']; + $this->records = $data['records']; + + if (!empty($data['queries'])) { + $this->queries = $data['queries']; + } + } + + /** + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @return array + */ + public function getStructure() + { + return $this->structure; + } + + /** + * @return array + */ + public function getStructurePaths() + { + return $this->structurePaths; + } + + /** + * @return array + */ + public function getRecords() + { + return $this->records; + } + + /** + * @return array + */ + public function getQueries() + { + return $this->queries; + } + + /** + * @param string $recordIdentifier + * @param string $fieldName + * @return array + */ + public function findStructures($recordIdentifier, $fieldName = '') + { + $structures = []; + + if (empty($this->structurePaths[$recordIdentifier])) { + return $structures; + } + + foreach ((array)$this->structurePaths[$recordIdentifier] as $steps) { + $structure = $this->structure; + $steps[] = $recordIdentifier; + + if (!empty($fieldName)) { + $steps[] = $fieldName; + } + + foreach ((array)$steps as $step) { + if (!isset($structure[$step])) { + $structure = null; + break; + } + $structure = $structure[$step]; + } + + if (!empty($structure)) { + $structures[implode('/', $steps)] = $structure; + } + } + + return $structures; + } +} diff --git a/src/TestingFramework/TestCase/FunctionalTestCase.php b/src/TestingFramework/TestCase/FunctionalTestCase.php new file mode 100644 index 0000000..161be3e --- /dev/null +++ b/src/TestingFramework/TestCase/FunctionalTestCase.php @@ -0,0 +1,414 @@ + 'link-destination' + * ); + * + * Given paths are expected to be relative to the test instance root. + * The array keys are the source paths and the array values are the destination + * paths, example: + * + * array( + * 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' => + * 'fileadmin/user_upload', + * 'typo3conf/ext/my_own_ext/Tests/Functional/Fixtures/Folders/uploads/tx_myownext' => + * 'uploads/tx_myownext' + * ); + * + * To be able to link from my_own_ext the extension path needs also to be registered in + * property $testExtensionsToLoad + * + * @var array + */ + protected $pathsToLinkInTestInstance = []; + + /** + * This configuration array is merged with TYPO3_CONF_VARS + * that are set in default configuration and factory configuration + * + * @var array + */ + protected $configurationToUseInTestInstance = []; + + /** + * Array of folders that should be created inside the test instance document root. + * + * This property will stay empty in this abstract, so it is possible + * to just overwrite it in extending classes. Path noted here will + * be linked for every test of a test case and it is not possible to change + * the list of folders between single tests of a test case. + * + * Per default the following folder are created + * /fileadmin + * /typo3temp + * /typo3conf + * /typo3conf/ext + * /uploads + * + * To create additional folders add the paths to this array. Given paths are expected to be + * relative to the test instance root and have to begin with a slash. Example: + * + * array( + * 'fileadmin/user_upload' + * ); + * + * @var array + */ + protected $additionalFoldersToCreate = []; + + /** + * The fixture which is used when initializing a backend user + * + * @var string + */ + protected $backendUserFixture = '/../../../res/Fixtures/Database/be_users.xml'; + + /** + * Private utility class used in setUp() and tearDown(). Do NOT use in test cases! + * + * @var FunctionalTestCaseBootstrapUtility + */ + private $bootstrapUtility = null; + + /** + * Calculate a "unique" identifier for the test database and the + * instance patch based on the given test case class name. + * + * @return string + */ + protected function getInstanceIdentifier() + { + return FunctionalTestCaseBootstrapUtility::getInstanceIdentifier(get_class($this)); + } + + /** + * Calculates path to TYPO3 CMS test installation for this test case. + * + * @return string + */ + protected function getInstancePath() + { + return FunctionalTestCaseBootstrapUtility::getInstancePath(get_class($this)); + } + + /** + * Set up creates a test instance and database. + * + * This method should be called with parent::setUp() in your test cases! + * + * @return void + */ + protected function setUp() + { + if (!defined('ORIGINAL_ROOT')) { + $this->markTestSkipped('Functional tests must be called through phpunit on CLI'); + } + $this->bootstrapUtility = new FunctionalTestCaseBootstrapUtility(); + $this->bootstrapUtility->setUp( + get_class($this), + $this->coreExtensionsToLoad, + $this->testExtensionsToLoad, + $this->pathsToLinkInTestInstance, + $this->configurationToUseInTestInstance, + $this->additionalFoldersToCreate + ); + } + + /** + * Get DatabaseConnection instance - $GLOBALS['TYPO3_DB'] + * + * This method should be used instead of direct access to + * $GLOBALS['TYPO3_DB'] for easy IDE auto completion. + * + * @return \TYPO3\CMS\Core\Database\DatabaseConnection + */ + protected function getDatabaseConnection() + { + return $GLOBALS['TYPO3_DB']; + } + + /** + * Initialize backend user + * + * @param int $userUid uid of the user we want to initialize. This user must exist in the fixture file + * @throws Exception + * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication + */ + protected function setUpBackendUserFromFixture($userUid) + { + $this->importDataSet(__DIR__ . $this->backendUserFixture); + $database = $this->getDatabaseConnection(); + $userRow = $database->exec_SELECTgetSingleRow('*', 'be_users', 'uid = ' . (int)$userUid); + + /** @var $backendUser \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */ + $backendUser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class); + $sessionId = $backendUser->createSessionId(); + $_COOKIE['be_typo_user'] = $sessionId; + $backendUser->id = $sessionId; + $backendUser->sendNoCacheHeaders = false; + $backendUser->dontSetCookie = true; + $backendUser->createUserSession($userRow); + + $GLOBALS['BE_USER'] = $backendUser; + $GLOBALS['BE_USER']->start(); + if (!is_array($GLOBALS['BE_USER']->user) || !$GLOBALS['BE_USER']->user['uid']) { + throw new Exception( + 'Can not initialize backend user', + 1377095807 + ); + } + $GLOBALS['BE_USER']->backendCheckLogin(); + + return $backendUser; + } + + /** + * Imports a data set represented as XML into the test database, + * + * @param string $path Absolute path to the XML file containing the data set to load + * @throws Exception + * @return void + */ + protected function importDataSet($path) + { + if (!is_file($path)) { + throw new Exception( + 'Fixture file ' . $path . ' not found', + 1376746261 + ); + } + + $database = $this->getDatabaseConnection(); + + $fileContent = file_get_contents($path); + // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept + $previousValueOfEntityLoader = libxml_disable_entity_loader(true); + $xml = simplexml_load_string($fileContent); + libxml_disable_entity_loader($previousValueOfEntityLoader); + $foreignKeys = []; + + /** @var $table \SimpleXMLElement */ + foreach ($xml->children() as $table) { + $insertArray = []; + + /** @var $column \SimpleXMLElement */ + foreach ($table->children() as $column) { + $columnName = $column->getName(); + $columnValue = null; + + if (isset($column['ref'])) { + list($tableName, $elementId) = explode('#', $column['ref']); + $columnValue = $foreignKeys[$tableName][$elementId]; + } elseif (isset($column['is-NULL']) && ($column['is-NULL'] === 'yes')) { + $columnValue = null; + } else { + $columnValue = (string)$table->$columnName; + } + + $insertArray[$columnName] = $columnValue; + } + + $tableName = $table->getName(); + $result = $database->exec_INSERTquery($tableName, $insertArray); + if ($result === false) { + throw new Exception( + 'Error when processing fixture file: ' . $path . ' Can not insert data to table ' . $tableName . ': ' . $database->sql_error(), + 1376746262 + ); + } + if (isset($table['id'])) { + $elementId = (string)$table['id']; + $foreignKeys[$tableName][$elementId] = $database->sql_insert_id(); + } + } + } + + /** + * @param int $pageId + * @param array $typoScriptFiles + */ + protected function setUpFrontendRootPage($pageId, array $typoScriptFiles = []) + { + $pageId = (int)$pageId; + $page = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'pages', 'uid=' . $pageId); + + if (empty($page)) { + $this->fail('Cannot set up frontend root page "' . $pageId . '"'); + } + + $pagesFields = [ + 'is_siteroot' => 1, + ]; + + $this->getDatabaseConnection()->exec_UPDATEquery('pages', 'uid=' . $pageId, $pagesFields); + + $templateFields = [ + 'pid' => $pageId, + 'title' => '', + 'config' => '', + 'clear' => 3, + 'root' => 1, + ]; + + foreach ($typoScriptFiles as $typoScriptFile) { + $templateFields['config'] .= '' . LF; + } + + $this->getDatabaseConnection()->exec_DELETEquery('sys_template', 'pid = ' . $pageId); + $this->getDatabaseConnection()->exec_INSERTquery('sys_template', $templateFields); + } + + /** + * @param int $pageId + * @param int $languageId + * @param int $backendUserId + * @param int $workspaceId + * @param bool $failOnFailure + * @param int $frontendUserId + * @return Response + */ + protected function getFrontendResponse($pageId, $languageId = 0, $backendUserId = 0, $workspaceId = 0, $failOnFailure = true, $frontendUserId = 0) + { + $pageId = (int)$pageId; + $languageId = (int)$languageId; + + $additionalParameter = ''; + + if (!empty($frontendUserId)) { + $additionalParameter .= '&frontendUserId=' . (int)$frontendUserId; + } + if (!empty($backendUserId)) { + $additionalParameter .= '&backendUserId=' . (int)$backendUserId; + } + if (!empty($workspaceId)) { + $additionalParameter .= '&workspaceId=' . (int)$workspaceId; + } + + $arguments = [ + 'documentRoot' => $this->getInstancePath(), + 'requestUrl' => 'http://localhost/?id=' . $pageId . '&L=' . $languageId . $additionalParameter, + ]; + + $template = new \Text_Template(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/request.tpl'); + $template->setVar( + [ + 'arguments' => var_export($arguments, true), + 'originalRoot' => ORIGINAL_ROOT, + ] + ); + + $php = \PHPUnit_Util_PHP::factory(); + $response = $php->runJob($template->render()); + $result = json_decode($response['stdout'], true); + + if ($result === null) { + $this->fail('Frontend Response is empty'); + } + + if ($failOnFailure && $result['status'] === Response::STATUS_Failure) { + $this->fail('Frontend Response has failure:' . LF . $result['error']); + } + + $response = new Response($result['status'], $result['content'], $result['error']); + return $response; + } +} From 22fd37a75b3928a208ba3e43c0a3bebb36a19d56 Mon Sep 17 00:00:00 2001 From: Nicole Cordes Date: Thu, 16 Feb 2017 17:43:33 +0100 Subject: [PATCH 3/4] [FEATURE] Add support for TYPO3 CMS 6.2 --- .../FunctionalTestCaseBootstrapUtility.php | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php index 181689b..649309c 100644 --- a/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php +++ b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php @@ -59,6 +59,7 @@ class FunctionalTestCaseBootstrapUtility 'lang', 'extbase', 'install', + 'cms', ]; /** @@ -425,11 +426,13 @@ protected function setUpPackageStates(array $coreExtensionsToLoad, array $testEx // Register default list of extensions and set active foreach ($this->defaultActivatedCoreExtensions as $extensionName) { - $packageStates['packages'][$extensionName] = [ - 'state' => 'active', - 'packagePath' => 'typo3/sysext/' . $extensionName . '/', - 'classesPath' => 'Classes/', - ]; + if (is_dir($this->instancePath . '/typo3/sysext/' . $extensionName)) { + $packageStates['packages'][$extensionName] = [ + 'state' => 'active', + 'packagePath' => 'typo3/sysext/' . $extensionName . '/', + 'classesPath' => 'Classes/', + ]; + } } // Register additional core extensions and set active @@ -491,15 +494,28 @@ protected function setUpBasicTypo3Bootstrap() define('TYPO3_MODE', 'BE'); define('TYPO3_cliMode', true); - $classLoader = require rtrim(realpath($this->instancePath . '/typo3/'), '\\/') . '/../vendor/autoload.php'; - \TYPO3\CMS\Core\Core\Bootstrap::getInstance() - ->initializeClassLoader($classLoader) - ->baseSetup('') - ->loadConfigurationAndInitialize(true) - ->loadTypo3LoadedExtAndExtLocalconf(true) - ->setFinalCachingFrameworkCacheConfiguration() - ->defineLoggingAndExceptionConstants() - ->unsetReservedGlobalVariables(); + $autoloadFilepath = rtrim(realpath($this->instancePath . '/typo3/'), '\\/') . '/../vendor/autoload.php'; + if (file_exists($autoloadFilepath)) { + $classLoader = require $autoloadFilepath; + Bootstrap::getInstance() + ->initializeClassLoader($classLoader) + ->baseSetup('') + ->loadConfigurationAndInitialize(true) + ->loadTypo3LoadedExtAndExtLocalconf(true) + ->setFinalCachingFrameworkCacheConfiguration() + ->defineLoggingAndExceptionConstants() + ->unsetReservedGlobalVariables(); + } else { + require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/CliBootstrap.php'; + \TYPO3\CMS\Core\Core\CliBootstrap::checkEnvironmentOrDie(); + + require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/Bootstrap.php'; + Bootstrap::getInstance() + ->baseSetup('') + ->loadConfigurationAndInitialize(true) + ->loadTypo3LoadedExtAndExtLocalconf(true) + ->applyAdditionalConfigurationSettings(); + } } /** From 31a0b37a9c49ff81e1fe8f667a2b4e7181a78cf6 Mon Sep 17 00:00:00 2001 From: Nicole Cordes Date: Thu, 16 Feb 2017 18:16:01 +0100 Subject: [PATCH 4/4] [FEATURE] Add missing compatibility classes for TYPO3 CMS 8.6 --- compat/Packages/class-alias/ClassAliasMap.php | 4 + .../FunctionalTestCaseBootstrapUtility.php | 340 +++++++++++++----- .../Bootstrap/FunctionalTestsBootstrap.php | 28 +- 3 files changed, 275 insertions(+), 97 deletions(-) diff --git a/compat/Packages/class-alias/ClassAliasMap.php b/compat/Packages/class-alias/ClassAliasMap.php index dd8ed54..47fdecf 100644 --- a/compat/Packages/class-alias/ClassAliasMap.php +++ b/compat/Packages/class-alias/ClassAliasMap.php @@ -3,6 +3,10 @@ return [ 'TYPO3\\Components\\TestingFramework\\Core\\Exception' => \Nimut\TestingFramework\Exception\Exception::class, 'TYPO3\\Components\\TestingFramework\\Core\\FileStreamWrapper' => \Nimut\TestingFramework\File\FileStreamWrapper::class, + 'TYPO3\\Components\\TestingFramework\\Core\\Functional\\Framework\\Frontend\\Response' => \Nimut\TestingFramework\Http\Response::class, + 'TYPO3\\Components\\TestingFramework\\Core\\Functional\\Framework\\Frontend\\ResponseContent' => \Nimut\TestingFramework\Http\ResponseContent::class, + 'TYPO3\\Components\\TestingFramework\\Core\\Functional\\Framework\\Frontend\\ResponseSection' => \Nimut\TestingFramework\Http\ResponseSection::class, + 'TYPO3\\Components\\TestingFramework\\Core\\Functional\\FunctionalTestCase' => \Nimut\TestingFramework\TestCase\FunctionalTestCase::class, 'TYPO3\\Components\\TestingFramework\\Core\\Unit\\UnitTestCase' => \Nimut\TestingFramework\TestCase\UnitTestCase::class, 'TYPO3\\Components\\TestingFramework\\Fluid\\Unit\\ViewHelpers\\ViewHelperBaseTestcase' => \Nimut\TestingFramework\TestCase\ViewHelperBaseTestcase::class, ]; diff --git a/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php index 649309c..18c9b8b 100644 --- a/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php +++ b/src/TestingFramework/Bootstrap/FunctionalTestCaseBootstrapUtility.php @@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Cache\Backend\NullBackend; use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Database\DatabaseConnection; +use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Install\Service\SqlExpectedSchemaService; @@ -68,9 +69,12 @@ class FunctionalTestCaseBootstrapUtility protected $defaultFoldersToCreate = [ '', '/fileadmin', - '/typo3temp', '/typo3conf', '/typo3conf/ext', + '/typo3temp', + '/typo3temp/var', + '/typo3temp/var/tests', + '/typo3temp/var/transient', '/uploads', ]; @@ -95,7 +99,7 @@ public static function getInstanceIdentifier($testCaseClassName) */ public static function getInstancePath($testCaseClassName) { - return ORIGINAL_ROOT . 'typo3temp/functional-' . static::getInstanceIdentifier($testCaseClassName); + return ORIGINAL_ROOT . 'typo3temp/var/tests/functional-' . static::getInstanceIdentifier($testCaseClassName); } /** @@ -310,44 +314,23 @@ protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance) */ protected function setUpLocalConfiguration(array $configurationToMerge) { - $databaseName = trim(getenv('typo3DatabaseName')); - $databaseHost = trim(getenv('typo3DatabaseHost')); - $databaseUsername = trim(getenv('typo3DatabaseUsername')); - $databasePassword = trim(getenv('typo3DatabasePassword')); - $databasePort = trim(getenv('typo3DatabasePort')); - $databaseSocket = trim(getenv('typo3DatabaseSocket')); - if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) { - // Try to get database credentials from environment variables first - $originalConfigurationArray = [ - 'DB' => [], - ]; - if ($databaseName) { - $originalConfigurationArray['DB']['database'] = $databaseName; - } - if ($databaseHost) { - $originalConfigurationArray['DB']['host'] = $databaseHost; - } - if ($databaseUsername) { - $originalConfigurationArray['DB']['username'] = $databaseUsername; - } - if ($databasePassword) { - $originalConfigurationArray['DB']['password'] = $databasePassword; - } - if ($databasePort) { - $originalConfigurationArray['DB']['port'] = $databasePort; - } - if ($databaseSocket) { - $originalConfigurationArray['DB']['socket'] = $databaseSocket; - } - } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) { - // See if a LocalConfiguration file exists in "parent" instance to get db credentials from - $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php'; + $isDoctrineAvailable = class_exists('Doctrine\\DBAL\\DriverManager'); + if ($isDoctrineAvailable) { + $originalConfigurationArray = $this->getDoctrineDatabaseSettings(); } else { - throw new Exception( - 'Database credentials for functional tests are neither set through environment' - . ' variables, and can not be found in an existing LocalConfiguration file', - 1397406356 - ); + $originalConfigurationArray = $this->getDatabaseConnectionSettings(); + } + if (empty($originalConfigurationArray)) { + if (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) { + // See if a LocalConfiguration file exists in "parent" instance to get db credentials from + $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php'; + } else { + throw new Exception( + 'Database credentials for functional tests are neither set through environment' + . ' variables, and can not be found in an existing LocalConfiguration file', + 1397406356 + ); + } } // Base of final LocalConfiguration is core factory configuration @@ -375,10 +358,16 @@ protected function setUpLocalConfiguration(array $configurationToMerge) ); $this->mergeRecursiveWithOverrule($finalConfigurationArray, $configurationToMerge); $finalConfigurationArray['DB'] = $originalConfigurationArray['DB']; + // Calculate and set new database name - $this->originalDatabaseName = $originalConfigurationArray['DB']['database']; - $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier; + if ($isDoctrineAvailable) { + $originalDatabaseName = &$finalConfigurationArray['DB']['Connections']['Default']['dbname']; + } else { + $originalDatabaseName = &$finalConfigurationArray['DB']['database']; + } + $this->originalDatabaseName = $originalDatabaseName; + $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier; // Maximum database name length for mysql is 64 characters if (strlen($this->databaseName) > 64) { $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier); @@ -389,8 +378,7 @@ protected function setUpLocalConfiguration(array $configurationToMerge) 1377600104 ); } - - $finalConfigurationArray['DB']['database'] = $this->databaseName; + $originalDatabaseName = $this->databaseName; $result = $this->writeFile( $this->instancePath . '/typo3conf/LocalConfiguration.php', @@ -407,6 +395,99 @@ protected function setUpLocalConfiguration(array $configurationToMerge) } } + /** + * @return array + */ + protected function getDoctrineDatabaseSettings() + { + $originalConfigurationArray = []; + + $databaseName = trim(getenv('typo3DatabaseName')); + $databaseHost = trim(getenv('typo3DatabaseHost')); + $databaseUsername = trim(getenv('typo3DatabaseUsername')); + $databasePassword = getenv('typo3DatabasePassword'); + $databasePasswordTrimmed = trim($databasePassword); + $databasePort = trim(getenv('typo3DatabasePort')); + $databaseSocket = trim(getenv('typo3DatabaseSocket')); + $databaseDriver = trim(getenv('typo3DatabaseDriver')); + if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) { + // Try to get database credentials from environment variables first + $originalConfigurationArray = [ + 'DB' => [ + 'Connections' => [ + 'Default' => [ + 'driver' => 'mysqli', + ], + ], + ], + ]; + if ($databaseName) { + $originalConfigurationArray['DB']['Connections']['Default']['dbname'] = $databaseName; + } + if ($databaseHost) { + $originalConfigurationArray['DB']['Connections']['Default']['host'] = $databaseHost; + } + if ($databaseUsername) { + $originalConfigurationArray['DB']['Connections']['Default']['user'] = $databaseUsername; + } + if ($databasePassword !== false) { + $originalConfigurationArray['DB']['Connections']['Default']['password'] = $databasePasswordTrimmed; + } + if ($databasePort) { + $originalConfigurationArray['DB']['Connections']['Default']['port'] = $databasePort; + } + if ($databaseSocket) { + $originalConfigurationArray['DB']['Connections']['Default']['unix_socket'] = $databaseSocket; + } + if ($databaseDriver) { + $originalConfigurationArray['DB']['Connections']['Default']['driver'] = $databaseDriver; + } + } + + return $originalConfigurationArray; + } + + /** + * @return array + */ + protected function getDatabaseConnectionSettings() + { + $originalConfigurationArray = []; + + $databaseName = trim(getenv('typo3DatabaseName')); + $databaseHost = trim(getenv('typo3DatabaseHost')); + $databaseUsername = trim(getenv('typo3DatabaseUsername')); + $databasePassword = trim(getenv('typo3DatabasePassword')); + $databasePort = trim(getenv('typo3DatabasePort')); + $databaseSocket = trim(getenv('typo3DatabaseSocket')); + if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) { + // Try to get database credentials from environment variables first + $originalConfigurationArray = [ + 'DB' => [], + ]; + if ($databaseName) { + $originalConfigurationArray['DB']['database'] = $databaseName; + } + if ($databaseHost) { + $originalConfigurationArray['DB']['host'] = $databaseHost; + } + if ($databaseUsername) { + $originalConfigurationArray['DB']['username'] = $databaseUsername; + } + if ($databasePassword) { + $originalConfigurationArray['DB']['password'] = $databasePassword; + } + if ($databasePort) { + $originalConfigurationArray['DB']['port'] = $databasePort; + } + if ($databaseSocket) { + $originalConfigurationArray['DB']['socket'] = $databaseSocket; + } + } + + return $originalConfigurationArray; + } + /** * Compile typo3conf/PackageStates.php containing default packages like core, * a functional test specific list of additional core extensions, and a list of @@ -421,7 +502,7 @@ protected function setUpPackageStates(array $coreExtensionsToLoad, array $testEx { $packageStates = [ 'packages' => [], - 'version' => 4, + 'version' => $this->getPackageStatesVersion(), ]; // Register default list of extensions and set active @@ -481,6 +562,30 @@ protected function setUpPackageStates(array $coreExtensionsToLoad, array $testEx } } + /** + * Parse PackageManager class for correct version number + * + * @return int + */ + protected function getPackageStatesVersion() + { + $reflection = new \ReflectionClass(PackageManager::class); + $packageManagerClassFile = $reflection->getFileName(); + + if ($packageManagerClassFile === false) { + return 4; + } + + $fileContent = file_get_contents($packageManagerClassFile); + $matches = []; + preg_match('/\$this->packageStatesConfiguration\[\'version\'\] = (\d+);/', $fileContent, $matches); + if (empty($matches[1])) { + return 4; + } + + return (int)$matches[1]; + } + /** * Bootstrap basic TYPO3 * @@ -494,27 +599,35 @@ protected function setUpBasicTypo3Bootstrap() define('TYPO3_MODE', 'BE'); define('TYPO3_cliMode', true); + putenv('TYPO3_CONTEXT=Testing'); + + $classLoader = null; $autoloadFilepath = rtrim(realpath($this->instancePath . '/typo3/'), '\\/') . '/../vendor/autoload.php'; if (file_exists($autoloadFilepath)) { $classLoader = require $autoloadFilepath; - Bootstrap::getInstance() - ->initializeClassLoader($classLoader) - ->baseSetup('') - ->loadConfigurationAndInitialize(true) - ->loadTypo3LoadedExtAndExtLocalconf(true) - ->setFinalCachingFrameworkCacheConfiguration() - ->defineLoggingAndExceptionConstants() - ->unsetReservedGlobalVariables(); } else { require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/CliBootstrap.php'; \TYPO3\CMS\Core\Core\CliBootstrap::checkEnvironmentOrDie(); + } - require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/Bootstrap.php'; - Bootstrap::getInstance() - ->baseSetup('') - ->loadConfigurationAndInitialize(true) - ->loadTypo3LoadedExtAndExtLocalconf(true) - ->applyAdditionalConfigurationSettings(); + $bootstrap = Bootstrap::getInstance(); + $reflection = new \ReflectionMethod($bootstrap, 'initializeClassLoader'); + if (empty($reflection->getNumberOfParameters())) { + $bootstrap->baseSetup()->initializeClassLoader(); + } else { + if (is_callable([$bootstrap, 'setRequestType'])) { + $bootstrap->setRequestType(TYPO3_REQUESTTYPE_BE | TYPO3_REQUESTTYPE_CLI); + } + $bootstrap->initializeClassLoader($classLoader)->baseSetup(); + } + $bootstrap->loadConfigurationAndInitialize(true) + ->loadTypo3LoadedExtAndExtLocalconf(true); + if (is_callable([$bootstrap, 'setFinalCachingFrameworkCacheConfiguration'])) { + $bootstrap->setFinalCachingFrameworkCacheConfiguration() + ->defineLoggingAndExceptionConstants() + ->unsetReservedGlobalVariables(); + } else { + $bootstrap->applyAdditionalConfigurationSettings(); } } @@ -527,32 +640,57 @@ protected function setUpBasicTypo3Bootstrap() protected function setUpTestDatabase() { Bootstrap::getInstance()->initializeTypo3DbGlobal(); - /** @var DatabaseConnection $database */ - $database = $GLOBALS['TYPO3_DB']; - if (!$database->sql_pconnect()) { - throw new Exception( - 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' - . ' connection to the database was attempted to be established!', - 1377620117 - ); - } - // Drop database in case a previous test had a fatal and did not clean up properly - $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`'); - $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`'); - if (!$createDatabaseResult) { - $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username']; - $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host']; - throw new Exception( - 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.' - . ' For this instance this could be fixed executing' - . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"', - 1376579070 - ); + if (class_exists('Doctrine\\DBAL\\DriverManager')) { + $connectionParameters = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']; + unset($connectionParameters['dbname']); + $schemaManager = \Doctrine\DBAL\DriverManager::getConnection($connectionParameters)->getSchemaManager(); + + if (in_array($this->databaseName, $schemaManager->listDatabases(), true)) { + $schemaManager->dropDatabase($this->databaseName); + } + + try { + $schemaManager->createDatabase($this->databaseName); + } catch (\Doctrine\DBAL\DBALException $e) { + $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']; + $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']; + throw new Exception( + 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.' + . ' For this instance this could be fixed executing:' + . ' GRANT ALL ON `' . $this->originalDatabaseName . '_%`.* TO `' . $user . '`@`' . $host . '`;' + . ' Original message thrown by database layer: ' . $e->getMessage(), + 1376579070 + ); + } + } else { + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + if (!$database->sql_pconnect()) { + throw new Exception( + 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' + . ' connection to the database was attempted to be established!', + 1377620117 + ); + } + + // Drop database in case a previous test had a fatal and did not clean up properly + $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`'); + $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`'); + if (!$createDatabaseResult) { + $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username']; + $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host']; + throw new Exception( + 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.' + . ' For this instance this could be fixed executing' + . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"', + 1376579070 + ); + } + $database->setDatabaseName($this->databaseName); + // On windows, this still works, but throws a warning, which we need to discard. + @$database->sql_select_db(); } - $database->setDatabaseName($this->databaseName); - // On windows, this still works, but throws a warning, which we need to discard. - @$database->sql_select_db(); } /** @@ -565,20 +703,30 @@ protected function setUpTestDatabase() protected function initializeTestDatabase() { Bootstrap::getInstance()->initializeTypo3DbGlobal(); - /** @var DatabaseConnection $database */ - $database = $GLOBALS['TYPO3_DB']; - if (!$database->sql_pconnect()) { - throw new Exception( - 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' - . ' connection to the database was attempted to be established!', - 1377620117 - ); - } - $this->databaseName = $GLOBALS['TYPO3_CONF_VARS']['DB']['database']; - $database->setDatabaseName($this->databaseName); - $database->sql_select_db(); - foreach ($database->admin_get_tables() as $table) { - $database->admin_query('TRUNCATE ' . $table['Name'] . ';'); + + if (class_exists('TYPO3\\CMS\\Core\\Database\\ConnectionPool')) { + $connection = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getConnectionByName(\TYPO3\CMS\Core\Database\ConnectionPool::DEFAULT_CONNECTION_NAME); + $schemaManager = $connection->getSchemaManager(); + foreach ($schemaManager->listTables() as $table) { + $connection->truncate($table->getName()); + } + } else { + /** @var DatabaseConnection $database */ + $database = $GLOBALS['TYPO3_DB']; + if (!$database->sql_pconnect()) { + throw new Exception( + 'TYPO3 Fatal Error: The current username, password or host was not accepted when the' + . ' connection to the database was attempted to be established!', + 1377620117 + ); + } + $this->databaseName = $GLOBALS['TYPO3_CONF_VARS']['DB']['database']; + $database->setDatabaseName($this->databaseName); + $database->sql_select_db(); + foreach ($database->admin_get_tables() as $table) { + $database->admin_query('TRUNCATE ' . $table['Name'] . ';'); + } } } diff --git a/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php b/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php index a0306b2..08d7631 100644 --- a/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php +++ b/src/TestingFramework/Bootstrap/FunctionalTestsBootstrap.php @@ -31,7 +31,8 @@ public function bootstrapSystem() $this->enableDisplayErrors() ->loadClassFiles() ->defineOriginalRootPath() - ->createNecessaryDirectoriesInDocumentRoot(); + ->createNecessaryDirectoriesInDocumentRoot() + ->addCompatibilityPrerequests(); } /** @@ -102,6 +103,31 @@ protected function createNecessaryDirectoriesInDocumentRoot() return $this; } + protected function addCompatibilityPrerequests() + { + if (class_exists('TYPO3\\CMS\\Core\\Tests\\Testbase')) { + // A null, a tabulator, a linefeed, a carriage return, a substitution, a CR-LF combination + defined('NUL') ?: define('NUL', chr(0)); + defined('TAB') ?: define('TAB', chr(9)); + defined('LF') ?: define('LF', chr(10)); + defined('CR') ?: define('CR', chr(13)); + defined('SUB') ?: define('SUB', chr(26)); + defined('CRLF') ?: define('CRLF', CR . LF); + + if (!defined('TYPO3_OS')) { + // Operating system identifier + // Either "WIN" or empty string + $typoOs = ''; + if (!stristr(PHP_OS, 'darwin') && !stristr(PHP_OS, 'cygwin') && stristr(PHP_OS, 'win')) { + $typoOs = 'WIN'; + } + define('TYPO3_OS', $typoOs); + } + } + + return $this; + } + /** * Creates the directory $directory (recursively if required). *