TYPO3 CMS  TYPO3_7-6
FunctionalTestCaseBootstrapUtility.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
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.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
21 {
25  protected $identifier;
26 
30  protected $instancePath;
31 
35  protected $databaseName;
36 
41 
46  'core',
47  'backend',
48  'frontend',
49  'lang',
50  'extbase',
51  'install',
52  ];
53 
58  '',
59  '/fileadmin',
60  '/typo3temp',
61  '/typo3conf',
62  '/typo3conf/ext',
63  '/uploads'
64  ];
65 
73  public static function getInstanceIdentifier($testCaseClassName)
74  {
75  // 7 characters of sha1 should be enough for a unique identification
76  return substr(sha1($testCaseClassName), 0, 7);
77  }
78 
85  public static function getInstancePath($testCaseClassName)
86  {
87  return ORIGINAL_ROOT . 'typo3temp/functional-' . static::getInstanceIdentifier($testCaseClassName);
88  }
89 
101  public function setUp(
102  $testCaseClassName,
103  array $coreExtensionsToLoad,
104  array $testExtensionsToLoad,
105  array $pathsToLinkInTestInstance,
106  array $configurationToUse,
107  array $additionalFoldersToCreate
108  ) {
109  $this->setUpIdentifier($testCaseClassName);
110  $this->setUpInstancePath($testCaseClassName);
111  if ($this->recentTestInstanceExists()) {
112  $this->setUpBasicTypo3Bootstrap();
113  $this->initializeTestDatabase();
114  \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(true);
115  } else {
116  $this->removeOldInstanceIfExists();
117  $this->setUpInstanceDirectories($additionalFoldersToCreate);
118  $this->setUpInstanceCoreLinks();
119  $this->linkTestExtensionsToInstance($testExtensionsToLoad);
120  $this->linkPathsInTestInstance($pathsToLinkInTestInstance);
121  $this->setUpLocalConfiguration($configurationToUse);
122  $this->setUpPackageStates($coreExtensionsToLoad, $testExtensionsToLoad);
123  $this->setUpBasicTypo3Bootstrap();
124  $this->setUpTestDatabase();
125  \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(true);
126  $this->createDatabaseStructure();
127  }
128 
129  return $this->instancePath;
130  }
131 
138  protected function recentTestInstanceExists()
139  {
140  if (@file_get_contents($this->instancePath . '/last_run.txt') <= (time() - 300)) {
141  return false;
142  } else {
143  // Test instance exists and is pretty young -> re-use
144  return true;
145  }
146  }
147 
158  protected function setUpIdentifier($testCaseClassName)
159  {
160  $this->identifier = static::getInstanceIdentifier($testCaseClassName);
161  }
162 
169  protected function setUpInstancePath($testCaseClassName)
170  {
171  $this->instancePath = static::getInstancePath($testCaseClassName);
172  }
173 
180  protected function removeOldInstanceIfExists()
181  {
182  if (is_dir($this->instancePath)) {
183  $this->removeInstance();
184  }
185  }
186 
194  protected function setUpInstanceDirectories(array $additionalFoldersToCreate = [])
195  {
196  $foldersToCreate = array_merge($this->defaultFoldersToCreate, $additionalFoldersToCreate);
197  foreach ($foldersToCreate as $folder) {
198  $success = mkdir($this->instancePath . $folder);
199  if (!$success) {
200  throw new Exception(
201  'Creating directory failed: ' . $this->instancePath . $folder,
202  1376657189
203  );
204  }
205  }
206 
207  // Store the time we created this directory
208  file_put_contents($this->instancePath . '/last_run.txt', time());
209  }
210 
217  protected function setUpInstanceCoreLinks()
218  {
219  $linksToSet = [
220  ORIGINAL_ROOT . 'typo3' => $this->instancePath . '/typo3',
221  ORIGINAL_ROOT . 'index.php' => $this->instancePath . '/index.php'
222  ];
223  foreach ($linksToSet as $from => $to) {
224  $success = symlink($from, $to);
225  if (!$success) {
226  throw new Exception(
227  'Creating link failed: from ' . $from . ' to: ' . $to,
228  1376657199
229  );
230  }
231  }
232  }
233 
241  protected function linkTestExtensionsToInstance(array $extensionPaths)
242  {
243  foreach ($extensionPaths as $extensionPath) {
244  $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
245  if (!is_dir($absoluteExtensionPath)) {
246  throw new Exception(
247  'Test extension path ' . $absoluteExtensionPath . ' not found',
248  1376745645
249  );
250  }
251  $destinationPath = $this->instancePath . '/typo3conf/ext/' . basename($absoluteExtensionPath);
252  $success = symlink($absoluteExtensionPath, $destinationPath);
253  if (!$success) {
254  throw new Exception(
255  'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
256  1376657142
257  );
258  }
259  }
260  }
261 
272  protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance)
273  {
274  foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
275  $sourcePath = $this->instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
276  if (!file_exists($sourcePath)) {
277  throw new Exception(
278  'Path ' . $sourcePath . ' not found',
279  1376745645
280  );
281  }
282  $destinationPath = $this->instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
283  $success = symlink($sourcePath, $destinationPath);
284  if (!$success) {
285  throw new Exception(
286  'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
287  1389969623
288  );
289  }
290  }
291  }
292 
300  protected function setUpLocalConfiguration(array $configurationToMerge)
301  {
302  $databaseName = trim(getenv('typo3DatabaseName'));
303  $databaseHost = trim(getenv('typo3DatabaseHost'));
304  $databaseUsername = trim(getenv('typo3DatabaseUsername'));
305  $databasePassword = trim(getenv('typo3DatabasePassword'));
306  $databasePort = trim(getenv('typo3DatabasePort'));
307  $databaseSocket = trim(getenv('typo3DatabaseSocket'));
308  if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) {
309  // Try to get database credentials from environment variables first
310  $originalConfigurationArray = [
311  'DB' => [],
312  ];
313  if ($databaseName) {
314  $originalConfigurationArray['DB']['database'] = $databaseName;
315  }
316  if ($databaseHost) {
317  $originalConfigurationArray['DB']['host'] = $databaseHost;
318  }
319  if ($databaseUsername) {
320  $originalConfigurationArray['DB']['username'] = $databaseUsername;
321  }
322  if ($databasePassword) {
323  $originalConfigurationArray['DB']['password'] = $databasePassword;
324  }
325  if ($databasePort) {
326  $originalConfigurationArray['DB']['port'] = $databasePort;
327  }
328  if ($databaseSocket) {
329  $originalConfigurationArray['DB']['socket'] = $databaseSocket;
330  }
331  } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) {
332  // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
333  $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
334  } else {
335  throw new Exception(
336  'Database credentials for functional tests are neither set through environment'
337  . ' variables, and can not be found in an existing LocalConfiguration file',
338  1397406356
339  );
340  }
341 
342  // Base of final LocalConfiguration is core factory configuration
343  $finalConfigurationArray = require ORIGINAL_ROOT . 'typo3/sysext/core/Configuration/FactoryConfiguration.php';
344 
345  $this->mergeRecursiveWithOverrule($finalConfigurationArray, require ORIGINAL_ROOT . 'typo3/sysext/core/Build/Configuration/FunctionalTestsConfiguration.php');
346  $this->mergeRecursiveWithOverrule($finalConfigurationArray, $configurationToMerge);
347  $finalConfigurationArray['DB'] = $originalConfigurationArray['DB'];
348  // Calculate and set new database name
349  $this->originalDatabaseName = $originalConfigurationArray['DB']['database'];
350  $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier;
351 
352  // Maximum database name length for mysql is 64 characters
353  if (strlen($this->databaseName) > 64) {
354  $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier);
355  throw new Exception(
356  'The name of the database that is used for the functional test (' . $this->databaseName . ')' .
357  ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
358  ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
359  1377600104
360  );
361  }
362 
363  $finalConfigurationArray['DB']['database'] = $this->databaseName;
364 
365  $result = $this->writeFile(
366  $this->instancePath . '/typo3conf/LocalConfiguration.php',
367  '<?php' . chr(10) .
368  'return ' .
369  $this->arrayExport(
370  $finalConfigurationArray
371  ) .
372  ';' . chr(10) .
373  '?>'
374  );
375  if (!$result) {
376  throw new Exception('Can not write local configuration', 1376657277);
377  }
378  }
379 
390  protected function setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths)
391  {
392  $packageStates = [
393  'packages' => [],
394  'version' => 4,
395  ];
396 
397  // Register default list of extensions and set active
398  foreach ($this->defaultActivatedCoreExtensions as $extensionName) {
399  $packageStates['packages'][$extensionName] = [
400  'state' => 'active',
401  'packagePath' => 'typo3/sysext/' . $extensionName . '/',
402  'classesPath' => 'Classes/',
403  ];
404  }
405 
406  // Register additional core extensions and set active
407  foreach ($coreExtensionsToLoad as $extensionName) {
408  if (isset($packageSates['packages'][$extensionName])) {
409  throw new Exception(
410  $extensionName . ' is already registered as default core extension to load, no need to load it explicitly',
411  1390913893
412  );
413  }
414  $packageStates['packages'][$extensionName] = [
415  'state' => 'active',
416  'packagePath' => 'typo3/sysext/' . $extensionName . '/',
417  'classesPath' => 'Classes/',
418  ];
419  }
420 
421  // Activate test extensions that have been symlinked before
422  foreach ($testExtensionPaths as $extensionPath) {
423  $extensionName = basename($extensionPath);
424  if (isset($packageSates['packages'][$extensionName])) {
425  throw new Exception(
426  $extensionName . ' is already registered as extension to load, no need to load it explicitly',
427  1390913894
428  );
429  }
430  $packageStates['packages'][$extensionName] = [
431  'state' => 'active',
432  'packagePath' => 'typo3conf/ext/' . $extensionName . '/',
433  'classesPath' => 'Classes/',
434  ];
435  }
436 
437  $result = $this->writeFile(
438  $this->instancePath . '/typo3conf/PackageStates.php',
439  '<?php' . chr(10) .
440  'return ' .
441  $this->arrayExport(
442  $packageStates
443  ) .
444  ';' . chr(10) .
445  '?>'
446  );
447  if (!$result) {
448  throw new Exception('Can not write PackageStates', 1381612729);
449  }
450  }
451 
457  protected function setUpBasicTypo3Bootstrap()
458  {
459  $_SERVER['PWD'] = $this->instancePath;
460  $_SERVER['argv'][0] = 'index.php';
461 
462  define('TYPO3_MODE', 'BE');
463  define('TYPO3_cliMode', true);
464 
465  $classLoader = require rtrim(realpath($this->instancePath . '/typo3'), '\\/') . '/../vendor/autoload.php';
467  ->initializeClassLoader($classLoader)
468  ->baseSetup('')
469  ->loadConfigurationAndInitialize(true)
470  ->loadTypo3LoadedExtAndExtLocalconf(true)
471  ->setFinalCachingFrameworkCacheConfiguration()
472  ->defineLoggingAndExceptionConstants()
473  ->unsetReservedGlobalVariables();
474  }
475 
482  protected function setUpTestDatabase()
483  {
484  \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeTypo3DbGlobal();
486  $database = $GLOBALS['TYPO3_DB'];
487  if (!$database->sql_pconnect()) {
488  throw new Exception(
489  'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
490  . ' connection to the database was attempted to be established!',
491  1377620117
492  );
493  }
494 
495  // Drop database in case a previous test had a fatal and did not clean up properly
496  $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`');
497  $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`');
498  if (!$createDatabaseResult) {
499  $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
500  $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
501  throw new Exception(
502  'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.'
503  . ' For this instance this could be fixed executing'
504  . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
505  1376579070
506  );
507  }
508  $database->setDatabaseName($this->databaseName);
509  // On windows, this still works, but throws a warning, which we need to discard.
510  @$database->sql_select_db();
511  }
512 
520  protected function initializeTestDatabase()
521  {
522  \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeTypo3DbGlobal();
524  $database = $GLOBALS['TYPO3_DB'];
525  if (!$database->sql_pconnect()) {
526  throw new Exception(
527  'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
528  . ' connection to the database was attempted to be established!',
529  1377620117
530  );
531  }
532  $this->databaseName = $GLOBALS['TYPO3_CONF_VARS']['DB']['database'];
533  $database->setDatabaseName($this->databaseName);
534  $database->sql_select_db();
535  foreach ($database->admin_get_tables() as $table) {
536  $database->admin_query('TRUNCATE ' . $table['Name'] . ';');
537  }
538  }
539 
545  protected function createDatabaseStructure()
546  {
548  $schemaMigrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\SqlSchemaMigrationService::class);
550  $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
552  $expectedSchemaService = $objectManager->get(\TYPO3\CMS\Install\Service\SqlExpectedSchemaService::class);
553 
554  // Raw concatenated ext_tables.sql and friends string
555  $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(true);
556  $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, true);
557  list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, true);
558 
559  $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
560  $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
561  $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
562  $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
563 
564  $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
565  $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
566  $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
567 
568  foreach ($insertCount as $table => $count) {
569  $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
570  foreach ($insertStatements as $insertQuery) {
571  $insertQuery = rtrim($insertQuery, ';');
573  $database = $GLOBALS['TYPO3_DB'];
574  $database->admin_query($insertQuery);
575  }
576  }
577  }
578 
585  protected function tearDownTestDatabase()
586  {
588  $database = $GLOBALS['TYPO3_DB'];
589  $result = $database->admin_query('DROP DATABASE `' . $this->databaseName . '`');
590  if (!$result) {
591  throw new Exception(
592  'Dropping test database ' . $this->databaseName . ' failed',
593  1376583188
594  );
595  }
596  }
597 
604  protected function removeInstance()
605  {
606  $success = $this->rmdir($this->instancePath, true);
607  if (!$success) {
608  throw new Exception(
609  'Can not remove folder: ' . $this->instancePath,
610  1376657210
611  );
612  }
613  }
614 
624  protected function rmdir($path, $removeNonEmpty = false)
625  {
626  $OK = false;
627  // Remove trailing slash
628  $path = preg_replace('|/$|', '', $path);
629  if (file_exists($path)) {
630  $OK = true;
631  if (!is_link($path) && is_dir($path)) {
632  if ($removeNonEmpty == true && ($handle = opendir($path))) {
633  while ($OK && false !== ($file = readdir($handle))) {
634  if ($file == '.' || $file == '..') {
635  continue;
636  }
637  $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
638  }
639  closedir($handle);
640  }
641  if ($OK) {
642  $OK = @rmdir($path);
643  }
644  } else {
645  // If $path is a symlink to a folder we need rmdir() on Windows systems
646  if (!stristr(PHP_OS, 'darwin') && stristr(PHP_OS, 'win') && is_link($path) && is_dir($path . '/')) {
647  $OK = rmdir($path);
648  } else {
649  $OK = unlink($path);
650  }
651  }
652  clearstatcache();
653  } elseif (is_link($path)) {
654  $OK = unlink($path);
655  clearstatcache();
656  }
657  return $OK;
658  }
659 
669  protected function writeFile($file, $content)
670  {
671  if ($fd = fopen($file, 'wb')) {
672  $res = fwrite($fd, $content);
673  fclose($fd);
674  if ($res === false) {
675  return false;
676  }
677  return true;
678  }
679  return false;
680  }
681 
695  protected function arrayExport(array $array = [], $level = 0)
696  {
697  $lines = 'array(' . chr(10);
698  $level++;
699  $writeKeyIndex = false;
700  $expectedKeyIndex = 0;
701  foreach ($array as $key => $value) {
702  if ($key === $expectedKeyIndex) {
703  $expectedKeyIndex++;
704  } else {
705  // Found a non integer or non consecutive key, so we can break here
706  $writeKeyIndex = true;
707  break;
708  }
709  }
710  foreach ($array as $key => $value) {
711  // Indention
712  $lines .= str_repeat(chr(9), $level);
713  if ($writeKeyIndex) {
714  // Numeric / string keys
715  $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
716  }
717  if (is_array($value)) {
718  if (!empty($value)) {
719  $lines .= $this->arrayExport($value, $level);
720  } else {
721  $lines .= 'array(),' . chr(10);
722  }
723  } elseif (is_int($value) || is_float($value)) {
724  $lines .= $value . ',' . chr(10);
725  } elseif (is_null($value)) {
726  $lines .= 'NULL' . ',' . chr(10);
727  } elseif (is_bool($value)) {
728  $lines .= $value ? 'TRUE' : 'FALSE';
729  $lines .= ',' . chr(10);
730  } elseif (is_string($value)) {
731  // Quote \ to \\
732  $stringContent = str_replace('\\', '\\\\', $value);
733  // Quote ' to \'
734  $stringContent = str_replace('\'', '\\\'', $stringContent);
735  $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
736  } else {
737  throw new \RuntimeException('Objects are not supported', 1342294986);
738  }
739  }
740  $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10));
741  return $lines;
742  }
743 
766  protected function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
767  {
768  foreach ($overrule as $key => $_) {
769  if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
770  unset($original[$key]);
771  continue;
772  }
773  if (isset($original[$key]) && is_array($original[$key])) {
774  if (is_array($overrule[$key])) {
775  self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
776  }
777  } elseif (
778  ($addKeys || isset($original[$key])) &&
779  ($includeEmptyValues || $overrule[$key])
780  ) {
781  $original[$key] = $overrule[$key];
782  }
783  }
784  // This line is kept for backward compatibility reasons.
785  reset($original);
786  }
787 }
$database
Definition: server.php:40
setUp( $testCaseClassName, array $coreExtensionsToLoad, array $testExtensionsToLoad, array $pathsToLinkInTestInstance, array $configurationToUse, array $additionalFoldersToCreate)
mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
$host
Definition: server.php:37
setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']