2 namespace TYPO3\CMS\Core\Tests
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
18 * Utility class to set up and bootstrap TYPO3 CMS for functional tests
20 class FunctionalTestCaseBootstrapUtility
{
23 * @var string Identifier calculated from test case class
25 protected $identifier;
28 * @var string Absolute path to test instance document root
30 protected $instancePath;
33 * @var string Name of test database
35 protected $databaseName;
38 * @var string Name of original database
40 protected $originalDatabaseName;
43 * @var array These extensions are always loaded
45 protected $defaultActivatedCoreExtensions = array(
56 * @var array These folder are always created
58 protected $defaultFoldersToCreate = array(
68 * Set up creates a test instance and database.
70 * @param string $testCaseClassName Name of test case class
71 * @param array $coreExtensionsToLoad Array of core extensions to load
72 * @param array $testExtensionsToLoad Array of test extensions to load
73 * @param array $pathsToLinkInTestInstance Array of source => destination path pairs to be linked
74 * @param array $configurationToUse Array of TYPO3_CONF_VARS that need to be overridden
75 * @param array $additionalFoldersToCreate Array of folder paths to be created
76 * @return string Path to TYPO3 CMS test installation for this test case
78 public function setUp(
80 array $coreExtensionsToLoad,
81 array $testExtensionsToLoad,
82 array $pathsToLinkInTestInstance,
83 array $configurationToUse,
84 array $additionalFoldersToCreate
86 $this->setUpIdentifier($testCaseClassName);
87 $this->setUpInstancePath();
88 if ($this->recentTestInstanceExists()) {
89 $this->setUpBasicTypo3Bootstrap();
90 $this->initializeTestDatabase();
91 \TYPO3\CMS\Core\Core\Bootstrap
::getInstance()->loadExtensionTables(TRUE);
93 $this->removeOldInstanceIfExists();
94 $this->setUpInstanceDirectories($additionalFoldersToCreate);
95 $this->setUpInstanceCoreLinks();
96 $this->linkTestExtensionsToInstance($testExtensionsToLoad);
97 $this->linkPathsInTestInstance($pathsToLinkInTestInstance);
98 $this->setUpLocalConfiguration($configurationToUse);
99 $this->setUpPackageStates($coreExtensionsToLoad, $testExtensionsToLoad);
100 $this->setUpBasicTypo3Bootstrap();
101 $this->setUpTestDatabase();
102 \TYPO3\CMS\Core\Core\Bootstrap
::getInstance()->loadExtensionTables(TRUE);
103 $this->createDatabaseStructure();
106 return $this->instancePath
;
110 * Checks whether the current test instance exists and is younger than
115 protected function recentTestInstanceExists() {
116 if (@file_get_contents
($this->instancePath
. '/last_run.txt') <= (time() - 300)) {
119 // Test instance exists and is pretty young -> re-use
125 * Calculate a "unique" identifier for the test database and the
126 * instance patch based on the given test case class name.
128 * As a result, the database name will be identical between different
129 * test runs, but different between each test case.
131 protected function setUpIdentifier($testCaseClassName) {
132 // 7 characters of sha1 should be enough for a unique identification
133 $this->identifier
= substr(sha1($testCaseClassName), 0, 7);
137 * Calculates path to TYPO3 CMS test installation for this test case.
141 protected function setUpInstancePath() {
142 $this->instancePath
= ORIGINAL_ROOT
. 'typo3temp/functional-' . $this->identifier
;
146 * Remove test instance folder structure in setUp() if it exists.
147 * This may happen if a functional test before threw a fatal.
151 protected function removeOldInstanceIfExists() {
152 if (is_dir($this->instancePath
)) {
153 $this->removeInstance();
158 * Create folder structure of test instance.
160 * @param array $additionalFoldersToCreate Array of additional folders to be created
164 protected function setUpInstanceDirectories(array $additionalFoldersToCreate = array()) {
165 $foldersToCreate = array_merge($this->defaultFoldersToCreate
, $additionalFoldersToCreate);
166 foreach ($foldersToCreate as $folder) {
167 $success = mkdir($this->instancePath
. $folder);
170 'Creating directory failed: ' . $this->instancePath
. $folder,
176 // Store the time we created this directory
177 file_put_contents($this->instancePath
. '/last_run.txt', time());
181 * Link TYPO3 CMS core from "parent" instance.
186 protected function setUpInstanceCoreLinks() {
188 ORIGINAL_ROOT
. 'typo3' => $this->instancePath
. '/typo3',
189 ORIGINAL_ROOT
. 'index.php' => $this->instancePath
. '/index.php'
191 foreach ($linksToSet as $from => $to) {
192 $success = symlink($from, $to);
195 'Creating link failed: from ' . $from . ' to: ' . $to,
203 * Link test extensions to the typo3conf/ext folder of the instance.
205 * @param array $extensionPaths Contains paths to extensions relative to document root
209 protected function linkTestExtensionsToInstance(array $extensionPaths) {
210 foreach ($extensionPaths as $extensionPath) {
211 $absoluteExtensionPath = ORIGINAL_ROOT
. $extensionPath;
212 if (!is_dir($absoluteExtensionPath)) {
214 'Test extension path ' . $absoluteExtensionPath . ' not found',
218 $destinationPath = $this->instancePath
. '/typo3conf/ext/' . basename($absoluteExtensionPath);
219 $success = symlink($absoluteExtensionPath, $destinationPath);
222 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
230 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
231 * test instance fileadmin folder
233 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
234 * @throws \TYPO3\CMS\Core\Tests\Exception if a source path could not be found
235 * @throws \TYPO3\CMS\Core\Tests\Exception on failing creating the symlink
237 * @see \TYPO3\CMS\Core\Tests\FunctionalTestCase::$pathsToLinkInTestInstance
239 protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance) {
240 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
241 $sourcePath = $this->instancePath
. '/' . ltrim($sourcePathToLinkInTestInstance, '/');
242 if (!file_exists($sourcePath)) {
244 'Path ' . $sourcePath . ' not found',
248 $destinationPath = $this->instancePath
. '/' . ltrim($destinationPathToLinkInTestInstance, '/');
249 $success = symlink($sourcePath, $destinationPath);
252 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
260 * Create LocalConfiguration.php file in the test instance
262 * @param array $configurationToMerge
266 protected function setUpLocalConfiguration(array $configurationToMerge) {
267 $databaseName = trim(getenv('typo3DatabaseName'));
268 $databaseHost = trim(getenv('typo3DatabaseHost'));
269 $databaseUsername = trim(getenv('typo3DatabaseUsername'));
270 $databasePassword = trim(getenv('typo3DatabasePassword'));
271 $databasePort = trim(getenv('typo3DatabasePort'));
272 $databaseSocket = trim(getenv('typo3DatabaseSocket'));
273 if ($databaseName ||
$databaseHost ||
$databaseUsername ||
$databasePassword ||
$databasePort ||
$databaseSocket) {
274 // Try to get database credentials from environment variables first
275 $originalConfigurationArray = array(
279 $originalConfigurationArray['DB']['database'] = $databaseName;
282 $originalConfigurationArray['DB']['host'] = $databaseHost;
284 if ($databaseUsername) {
285 $originalConfigurationArray['DB']['username'] = $databaseUsername;
287 if ($databasePassword) {
288 $originalConfigurationArray['DB']['password'] = $databasePassword;
291 $originalConfigurationArray['DB']['port'] = $databasePort;
293 if ($databaseSocket) {
294 $originalConfigurationArray['DB']['socket'] = $databaseSocket;
296 } elseif (file_exists(ORIGINAL_ROOT
. 'typo3conf/LocalConfiguration.php')) {
297 // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
298 $originalConfigurationArray = require ORIGINAL_ROOT
. 'typo3conf/LocalConfiguration.php';
301 'Database credentials for functional tests are neither set through environment'
302 . ' variables, and can not be found in an existing LocalConfiguration file',
307 // Base of final LocalConfiguration is core factory configuration
308 $finalConfigurationArray = require ORIGINAL_ROOT
. 'typo3/sysext/core/Configuration/FactoryConfiguration.php';
310 $this->mergeRecursiveWithOverrule($finalConfigurationArray, require ORIGINAL_ROOT
. 'typo3/sysext/core/Build/Configuration/FunctionalTestsConfiguration.php');
311 $this->mergeRecursiveWithOverrule($finalConfigurationArray, $configurationToMerge);
312 $finalConfigurationArray['DB'] = $originalConfigurationArray['DB'];
313 // Calculate and set new database name
314 $this->originalDatabaseName
= $originalConfigurationArray['DB']['database'];
315 $this->databaseName
= $this->originalDatabaseName
. '_ft' . $this->identifier
;
317 // Maximum database name length for mysql is 64 characters
318 if (strlen($this->databaseName
) > 64) {
319 $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier
);
321 'The name of the database that is used for the functional test (' . $this->databaseName
. ')' .
322 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
323 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
328 $finalConfigurationArray['DB']['database'] = $this->databaseName
;
330 $result = $this->writeFile(
331 $this->instancePath
. '/typo3conf/LocalConfiguration.php',
335 $finalConfigurationArray
341 throw new Exception('Can not write local configuration', 1376657277);
346 * Compile typo3conf/PackageStates.php containing default packages like core,
347 * a functional test specific list of additional core extensions, and a list of
350 * @param array $coreExtensionsToLoad Additional core extensions to load
351 * @param array $testExtensionPaths Paths to extensions relative to document root
353 * @TODO Figure out what the intention of the upper arguments is
355 protected function setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths) {
356 $packageStates = array(
357 'packages' => array(),
361 // Register default list of extensions and set active
362 foreach ($this->defaultActivatedCoreExtensions
as $extensionName) {
363 $packageStates['packages'][$extensionName] = array(
365 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
366 'classesPath' => 'Classes/',
370 // Register additional core extensions and set active
371 foreach ($coreExtensionsToLoad as $extensionName) {
372 if (isset($packageSates['packages'][$extensionName])) {
374 $extensionName . ' is already registered as default core extension to load, no need to load it explicitly',
378 $packageStates['packages'][$extensionName] = array(
380 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
381 'classesPath' => 'Classes/',
385 // Activate test extensions that have been symlinked before
386 foreach ($testExtensionPaths as $extensionPath) {
387 $extensionName = basename($extensionPath);
388 if (isset($packageSates['packages'][$extensionName])) {
390 $extensionName . ' is already registered as extension to load, no need to load it explicitly',
394 $packageStates['packages'][$extensionName] = array(
396 'packagePath' => 'typo3conf/ext/' . $extensionName . '/',
397 'classesPath' => 'Classes/',
401 $result = $this->writeFile(
402 $this->instancePath
. '/typo3conf/PackageStates.php',
412 throw new Exception('Can not write PackageStates', 1381612729);
417 * Bootstrap basic TYPO3
421 protected function setUpBasicTypo3Bootstrap() {
422 $_SERVER['PWD'] = $this->instancePath
;
423 $_SERVER['argv'][0] = 'index.php';
425 define('TYPO3_MODE', 'BE');
426 define('TYPO3_cliMode', TRUE);
428 require_once $this->instancePath
. '/typo3/sysext/core/Classes/Core/CliBootstrap.php';
429 \TYPO3\CMS\Core\Core\CliBootstrap
::checkEnvironmentOrDie();
431 require_once $this->instancePath
. '/typo3/sysext/core/Classes/Core/Bootstrap.php';
432 \TYPO3\CMS\Core\Core\Bootstrap
::getInstance()
434 ->loadConfigurationAndInitialize(TRUE)
435 ->loadTypo3LoadedExtAndExtLocalconf(TRUE)
436 ->applyAdditionalConfigurationSettings();
440 * Populate $GLOBALS['TYPO3_DB'] and create test database
442 * @throws \TYPO3\CMS\Core\Tests\Exception
445 protected function setUpTestDatabase() {
446 \TYPO3\CMS\Core\Core\Bootstrap
::getInstance()->initializeTypo3DbGlobal();
447 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
448 $database = $GLOBALS['TYPO3_DB'];
449 if(!$database->sql_pconnect()) {
451 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
452 . ' connection to the database was attempted to be established!',
457 // Drop database in case a previous test had a fatal and did not clean up properly
458 $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName
. '`');
459 $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName
. '`');
460 if (!$createDatabaseResult) {
461 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
462 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
464 'Unable to create database with name ' . $this->databaseName
. '. This is probably a permission problem.'
465 . ' For this instance this could be fixed executing'
466 . ' "GRANT ALL ON `' . $this->originalDatabaseName
. '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
470 $database->setDatabaseName($this->databaseName
);
471 // On windows, this still works, but throws a warning, which we need to discard.
472 @$database->sql_select_db();
476 * Populate $GLOBALS['TYPO3_DB'] reusing an existing database with
477 * all tables truncated.
479 * @throws \TYPO3\CMS\Core\Tests\Exception
482 protected function initializeTestDatabase() {
483 \TYPO3\CMS\Core\Core\Bootstrap
::getInstance()->initializeTypo3DbGlobal();
484 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
485 $database = $GLOBALS['TYPO3_DB'];
486 if (!$database->sql_pconnect()) {
488 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
489 . ' connection to the database was attempted to be established!',
493 $this->databaseName
= $GLOBALS['TYPO3_CONF_VARS']['DB']['database'];
494 $database->setDatabaseName($this->databaseName
);
495 $database->sql_select_db();
496 foreach ($database->admin_get_tables() as $table) {
497 $database->admin_query('TRUNCATE ' . $table['Name'] . ';');
502 * Create tables and import static rows
506 protected function createDatabaseStructure() {
507 /** @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService */
508 $schemaMigrationService = \TYPO3\CMS\Core\Utility\GeneralUtility
::makeInstance(\TYPO3\CMS\Install\Service\SqlSchemaMigrationService
::class);
509 /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */
510 $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility
::makeInstance(\TYPO3\CMS\Extbase\
Object\ObjectManager
::class);
511 /** @var \TYPO3\CMS\Install\Service\SqlExpectedSchemaService $expectedSchemaService */
512 $expectedSchemaService = $objectManager->get(\TYPO3\CMS\Install\Service\SqlExpectedSchemaService
::class);
514 // Raw concatenated ext_tables.sql and friends string
515 $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(TRUE);
516 $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, TRUE);
517 list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, TRUE);
519 $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
520 $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
521 $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
522 $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
524 $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
525 $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
526 $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
528 foreach ($insertCount as $table => $count) {
529 $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
530 foreach ($insertStatements as $insertQuery) {
531 $insertQuery = rtrim($insertQuery, ';');
532 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
533 $database = $GLOBALS['TYPO3_DB'];
534 $database->admin_query($insertQuery);
540 * Drop test database.
542 * @throws \TYPO3\CMS\Core\Tests\Exception
545 protected function tearDownTestDatabase() {
546 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
547 $database = $GLOBALS['TYPO3_DB'];
548 $result = $database->admin_query('DROP DATABASE `' . $this->databaseName
. '`');
551 'Dropping test database ' . $this->databaseName
. ' failed',
558 * Removes instance directories and files
560 * @throws \TYPO3\CMS\Core\Tests\Exception
563 protected function removeInstance() {
564 $success = $this->rmdir($this->instancePath
, TRUE);
567 'Can not remove folder: ' . $this->instancePath
,
574 * COPIED FROM GeneralUtility
576 * Wrapper function for rmdir, allowing recursive deletion of folders and files
578 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
579 * @param bool $removeNonEmpty Allow deletion of non-empty directories
580 * @return bool TRUE if @rmdir went well!
582 protected function rmdir($path, $removeNonEmpty = FALSE) {
584 // Remove trailing slash
585 $path = preg_replace('|/$|', '', $path);
586 if (file_exists($path)) {
588 if (!is_link($path) && is_dir($path)) {
589 if ($removeNonEmpty == TRUE && ($handle = opendir($path))) {
590 while ($OK && FALSE !== ($file = readdir($handle))) {
591 if ($file == '.' ||
$file == '..') {
594 $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
602 // If $path is a symlink to a folder we need rmdir() on Windows systems
603 if (!stristr(PHP_OS
, 'darwin') && stristr(PHP_OS
, 'win') && is_link($path) && is_dir($path . '/')) {
610 } elseif (is_link($path)) {
618 * COPIED FROM GeneralUtility
620 * Writes $content to the file $file
622 * @param string $file Filepath to write to
623 * @param string $content Content to write
624 * @return bool TRUE if the file was successfully opened and written to.
626 protected function writeFile($file, $content) {
627 if ($fd = fopen($file, 'wb')) {
628 $res = fwrite($fd, $content);
630 if ($res === FALSE) {
639 * COPIED FROM ArrayUtility
641 * Exports an array as string.
642 * Similar to var_export(), but representation follows the TYPO3 core CGL.
644 * See unit tests for detailed examples
646 * @param array $array Array to export
647 * @param int $level Internal level used for recursion, do *not* set from outside!
648 * @return string String representation of array
649 * @throws \RuntimeException
651 protected function arrayExport(array $array = array(), $level = 0) {
652 $lines = 'array(' . chr(10);
654 $writeKeyIndex = FALSE;
655 $expectedKeyIndex = 0;
656 foreach ($array as $key => $value) {
657 if ($key === $expectedKeyIndex) {
660 // Found a non integer or non consecutive key, so we can break here
661 $writeKeyIndex = TRUE;
665 foreach ($array as $key => $value) {
667 $lines .= str_repeat(chr(9), $level);
668 if ($writeKeyIndex) {
669 // Numeric / string keys
670 $lines .= is_int($key) ?
$key . ' => ' : '\'' . $key . '\' => ';
672 if (is_array($value)) {
673 if (count($value) > 0) {
674 $lines .= $this->arrayExport($value, $level);
676 $lines .= 'array(),' . chr(10);
678 } elseif (is_int($value) ||
is_float($value)) {
679 $lines .= $value . ',' . chr(10);
680 } elseif (is_null($value)) {
681 $lines .= 'NULL' . ',' . chr(10);
682 } elseif (is_bool($value)) {
683 $lines .= $value ?
'TRUE' : 'FALSE';
684 $lines .= ',' . chr(10);
685 } elseif (is_string($value)) {
687 $stringContent = str_replace('\\', '\\\\', $value);
689 $stringContent = str_replace('\'', '\\\'', $stringContent);
690 $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
692 throw new \
RuntimeException('Objects are not supported', 1342294986);
695 $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ?
'' : ',' . chr(10));
700 * COPIED FROM ArrayUtility
702 * Merges two arrays recursively and "binary safe" (integer keys are
703 * overridden as well), overruling similar values in the original array
704 * with the values of the overrule array.
705 * In case of identical keys, ie. keeping the values of the overrule array.
707 * This method takes the original array by reference for speed optimization with large arrays
709 * The differences to the existing PHP function array_merge_recursive() are:
710 * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
711 * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
712 * * Elements or the original array get overwritten if the same key is present in the overrule array.
714 * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
715 * @param array $overrule Overrule array, overruling the original array
716 * @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.
717 * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
718 * @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.
721 protected function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = TRUE, $includeEmptyValues = TRUE, $enableUnsetFeature = TRUE) {
722 foreach ($overrule as $key => $_) {
723 if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
724 unset($original[$key]);
727 if (isset($original[$key]) && is_array($original[$key])) {
728 if (is_array($overrule[$key])) {
729 self
::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
732 ($addKeys ||
isset($original[$key])) &&
733 ($includeEmptyValues ||
$overrule[$key])
735 $original[$key] = $overrule[$key];
738 // This line is kept for backward compatibility reasons.