0af52240013beb92edad55460f694293c5e42deb
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / FunctionalTestCaseBootstrapUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Christian Kuhn <lolli@schwarzbu.ch>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 /**
28 * Utility class to set up and bootstrap TYPO3 CMS for functional tests
29 */
30 class FunctionalTestCaseBootstrapUtility {
31
32 /**
33 * @var string Identifier calculated from test case class
34 */
35 protected $identifier;
36
37 /**
38 * @var string Absolute path to test instance document root
39 */
40 protected $instancePath;
41
42 /**
43 * @var string Name of test database
44 */
45 protected $databaseName;
46
47 /**
48 * @var string Name of original database
49 */
50 protected $originalDatabaseName;
51
52 /**
53 * @var array These extensions are always loaded
54 */
55 protected $defaultActivatedCoreExtensions = array(
56 'core',
57 'backend',
58 'frontend',
59 'cms',
60 'lang',
61 'sv',
62 'extensionmanager',
63 'recordlist',
64 'extbase',
65 'fluid',
66 'cshmanual',
67 'install',
68 'saltedpasswords'
69 );
70
71 /**
72 * Set up creates a test instance and database.
73 *
74 * @param string $testCaseClassName Name of test case class
75 * @param array $coreExtensionsToLoad Array of core extensions to load
76 * @param array $testExtensionsToLoad Array of test extensions to load
77 * @param array $pathsToLinkInTestInstance Array of source => destination path pairs to be linked
78 * @return string Path to TYPO3 CMS test installation for this test case
79 */
80 public function setUp(
81 $testCaseClassName,
82 array $coreExtensionsToLoad,
83 array $testExtensionsToLoad,
84 array $pathsToLinkInTestInstance
85 ) {
86 $this->setUpIdentifier($testCaseClassName);
87 $this->setUpInstancePath();
88 $this->removeOldInstanceIfExists();
89 $this->setUpInstanceDirectories();
90 $this->setUpInstanceCoreLinks();
91 $this->linkTestExtensionsToInstance($testExtensionsToLoad);
92 $this->linkPathsInTestInstance($pathsToLinkInTestInstance);
93 $this->setUpLocalConfiguration();
94 $this->setUpPackageStates($coreExtensionsToLoad, $testExtensionsToLoad);
95 $this->setUpBasicTypo3Bootstrap();
96 $this->setUpTestDatabase();
97 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(TRUE);
98 $this->createDatabaseStructure();
99
100 return $this->instancePath;
101 }
102
103 /**
104 * Tear down destroys the instance and database.
105 *
106 * @throws Exception
107 * @return void
108 */
109 public function tearDown() {
110 $classLoader = \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->getEarlyInstance('TYPO3\\CMS\\Core\\Core\\ClassLoader');
111 spl_autoload_unregister(array($classLoader, 'loadClass'));
112 if (empty($this->identifier)) {
113 throw new Exception(
114 'Test identifier not set. Is parent::setUp() called in setUp()?',
115 1376739702
116 );
117 }
118 $this->tearDownTestDatabase();
119 $this->removeInstance();
120 }
121
122 /**
123 * Calculate a "unique" identifier for the test database and the
124 * instance patch based on the given test case class name.
125 *
126 * As a result, the database name will be identical between different
127 * test runs, but different between each test case.
128 */
129 protected function setUpIdentifier($testCaseClassName) {
130 // 7 characters of sha1 should be enough for a unique identification
131 $this->identifier = substr(sha1($testCaseClassName), 0, 7);
132 }
133
134 /**
135 * Calculates path to TYPO3 CMS test installation for this test case.
136 *
137 * @return void
138 */
139 protected function setUpInstancePath() {
140 $this->instancePath = ORIGINAL_ROOT . 'typo3temp/functional-' . $this->identifier;
141 }
142
143 /**
144 * Remove test instance folder structure in setUp() if it exists.
145 * This may happen if a functional test before threw a fatal.
146 *
147 * @return void
148 */
149 protected function removeOldInstanceIfExists() {
150 if (is_dir($this->instancePath)) {
151 $this->removeInstance();
152 }
153 }
154
155 /**
156 * Create folder structure of test instance.
157 *
158 * @throws Exception
159 * @return void
160 */
161 protected function setUpInstanceDirectories() {
162 $foldersToCreate = array(
163 '',
164 '/fileadmin',
165 '/typo3temp',
166 '/typo3conf',
167 '/typo3conf/ext',
168 '/uploads'
169 );
170 foreach ($foldersToCreate as $folder) {
171 $success = mkdir($this->instancePath . $folder);
172 if (!$success) {
173 throw new Exception(
174 'Creating directory failed: ' . $this->instancePath . $folder,
175 1376657189
176 );
177 }
178 }
179 }
180
181 /**
182 * Link TYPO3 CMS core from "parent" instance.
183 *
184 * @throws Exception
185 * @return void
186 */
187 protected function setUpInstanceCoreLinks() {
188 $linksToSet = array(
189 ORIGINAL_ROOT . 'typo3' => $this->instancePath . '/typo3',
190 ORIGINAL_ROOT . 'index.php' => $this->instancePath . '/index.php'
191 );
192 foreach ($linksToSet as $from => $to) {
193 $success = symlink($from, $to);
194 if (!$success) {
195 throw new Exception(
196 'Creating link failed: from ' . $from . ' to: ' . $to,
197 1376657199
198 );
199 }
200 }
201 }
202
203 /**
204 * Link test extensions to the typo3conf/ext folder of the instance.
205 *
206 * @param array $extensionPaths Contains paths to extensions relative to document root
207 * @throws Exception
208 * @return void
209 */
210 protected function linkTestExtensionsToInstance(array $extensionPaths) {
211 foreach ($extensionPaths as $extensionPath) {
212 $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
213 if (!is_dir($absoluteExtensionPath)) {
214 throw new Exception(
215 'Test extension path ' . $absoluteExtensionPath . ' not found',
216 1376745645
217 );
218 }
219 $destinationPath = $this->instancePath . '/typo3conf/ext/'. basename($absoluteExtensionPath);
220 $success = symlink($absoluteExtensionPath, $destinationPath);
221 if (!$success) {
222 throw new Exception(
223 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
224 1376657142
225 );
226 }
227 }
228 }
229
230 /**
231 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
232 * test instance fileadmin folder
233 *
234 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
235 * @throws \TYPO3\CMS\Core\Tests\Exception if a source path could not be found
236 * @throws \TYPO3\CMS\Core\Tests\Exception on failing creating the symlink
237 * @return void
238 * @see \TYPO3\CMS\Core\Tests\FunctionalTestCase::$pathsToLinkInTestInstance
239 */
240 protected function linkPathsInTestInstance(array $pathsToLinkInTestInstance) {
241 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
242 $sourcePath = $this->instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
243 if (!file_exists($sourcePath)) {
244 throw new Exception(
245 'Path ' . $sourcePath . ' not found',
246 1376745645
247 );
248 }
249 $destinationPath = $this->instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
250 $success = symlink($sourcePath, $destinationPath);
251 if (!$success) {
252 throw new Exception(
253 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
254 1389969623
255 );
256 }
257 }
258 }
259
260 /**
261 * Create LocalConfiguration.php file in the test instance
262 *
263 * @throws Exception
264 * @return void
265 */
266 protected function setUpLocalConfiguration() {
267 $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
268 // Base of final LocalConfiguration is core factory configuration
269 $finalConfigurationArray = require ORIGINAL_ROOT .'typo3/sysext/core/Configuration/FactoryConfiguration.php';
270
271 $finalConfigurationArray['DB'] = $originalConfigurationArray['DB'];
272 // Calculate and set new database name
273 $this->originalDatabaseName = $originalConfigurationArray['DB']['database'];
274 $this->databaseName = $this->originalDatabaseName . '_ft' . $this->identifier;
275
276 // Maximum database name length for mysql is 64 characters
277 if (strlen($this->databaseName) > 64) {
278 $maximumOriginalDatabaseName = 64 - strlen('_ft' . $this->identifier);
279 throw new Exception(
280 'The name of the database that is used for the functional test (' . $this->databaseName . ')' .
281 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
282 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
283 1377600104
284 );
285 }
286
287 $finalConfigurationArray['DB']['database'] = $this->databaseName;
288
289 $result = $this->writeFile(
290 $this->instancePath . '/typo3conf/LocalConfiguration.php',
291 '<?php' . chr(10) .
292 'return ' .
293 $this->arrayExport(
294 $finalConfigurationArray
295 ) .
296 ';' . chr(10) .
297 '?>'
298 );
299 if (!$result) {
300 throw new Exception('Can not write local configuration', 1376657277);
301 }
302 }
303
304 /**
305 * Compile typo3conf/PackageStates.php containing default packages like core,
306 * a functional test specific list of additional core extensions, and a list of
307 * test extensions.
308 *
309 * @param array $coreExtensionsToLoad Additional core extensions to load
310 * @param array $testExtensionPaths Paths to extensions relative to document root
311 * @throws Exception
312 * @TODO Figure out what the intention of the upper arguments is
313 */
314 protected function setUpPackageStates(array $coreExtensionsToLoad, array $testExtensionPaths) {
315 $packageStates = array(
316 'packages' => array(),
317 'version' => 4,
318 );
319
320 // Register default list of extensions and set active
321 foreach ($this->defaultActivatedCoreExtensions as $extensionName) {
322 $packageStates['packages'][$extensionName] = array(
323 'state' => 'active',
324 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
325 'classesPath' => 'Classes/',
326 );
327 }
328
329 // Register additional core extensions and set active
330 foreach ($coreExtensionsToLoad as $extensionName) {
331 if (isset($packageSates['packages'][$extensionName])) {
332 throw new Exception(
333 $extensionName . ' is already registered as default core extension to load, no need to load it explicitly',
334 1390913893
335 );
336 }
337 $packageStates['packages'][$extensionName] = array(
338 'state' => 'active',
339 'packagePath' => 'typo3/sysext/' . $extensionName . '/',
340 'classesPath' => 'Classes/',
341 );
342 }
343
344 // Activate test extensions that have been symlinked before
345 foreach ($testExtensionPaths as $extensionPath) {
346 if (isset($packageSates['packages'][$extensionName])) {
347 throw new Exception(
348 $extensionName . ' is already registered as extension to load, no need to load it explicitly',
349 1390913894
350 );
351 }
352 $extensionName = basename($extensionPath);
353 $packageStates['packages'][$extensionName] = array(
354 'state' => 'active',
355 'packagePath' => 'typo3conf/ext/' . $extensionName . '/',
356 'classesPath' => 'Classes/',
357 );
358 }
359
360 $result = $this->writeFile(
361 $this->instancePath . '/typo3conf/PackageStates.php',
362 '<?php' . chr(10) .
363 'return ' .
364 $this->arrayExport(
365 $packageStates
366 ) .
367 ';' . chr(10) .
368 '?>'
369 );
370 if (!$result) {
371 throw new Exception('Can not write PackageStates', 1381612729);
372 }
373 }
374
375 /**
376 * Bootstrap basic TYPO3
377 *
378 * @return void
379 */
380 protected function setUpBasicTypo3Bootstrap() {
381 $_SERVER['PWD'] = $this->instancePath;
382 $_SERVER['argv'][0] = 'index.php';
383
384 define('TYPO3_MODE', 'BE');
385 define('TYPO3_cliMode', TRUE);
386
387 require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/CliBootstrap.php';
388 \TYPO3\CMS\Core\Core\CliBootstrap::checkEnvironmentOrDie();
389
390 require_once $this->instancePath . '/typo3/sysext/core/Classes/Core/Bootstrap.php';
391 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()
392 ->baseSetup('')
393 ->loadConfigurationAndInitialize(FALSE)
394 ->loadTypo3LoadedExtAndExtLocalconf(FALSE)
395 ->applyAdditionalConfigurationSettings();
396 }
397
398 /**
399 * Populate $GLOBALS['TYPO3_DB'] and create test database
400 *
401 * @throws \TYPO3\CMS\Core\Tests\Exception
402 * @return void
403 */
404 protected function setUpTestDatabase() {
405 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeTypo3DbGlobal();
406 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
407 $database = $GLOBALS['TYPO3_DB'];
408 if(!$database->sql_pconnect()) {
409 throw new Exception(
410 'TYPO3 Fatal Error: The current username, password or host was not accepted when the'
411 . ' connection to the database was attempted to be established!',
412 1377620117
413 );
414 }
415
416 // Drop database in case a previous test had a fatal and did not clean up properly
417 $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`');
418 $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`');
419 if (!$createDatabaseResult) {
420 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
421 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
422 throw new Exception(
423 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.'
424 . ' For this instance this could be fixed executing'
425 . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
426 1376579070
427 );
428 }
429 $database->setDatabaseName($this->databaseName);
430 $database->sql_select_db($this->databaseName);
431 }
432
433 /**
434 * Create tables and import static rows
435 *
436 * @return void
437 */
438 protected function createDatabaseStructure() {
439 /** @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService */
440 $schemaMigrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlSchemaMigrationService');
441 /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */
442 $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager');
443 /** @var \TYPO3\CMS\Install\Service\SqlExpectedSchemaService $expectedSchemaService */
444 $expectedSchemaService = $objectManager->get('TYPO3\\CMS\\Install\\Service\\SqlExpectedSchemaService');
445
446 // Raw concatenated ext_tables.sql and friends string
447 $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(TRUE);
448 $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, TRUE);
449 list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, TRUE);
450
451 $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
452 $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
453 $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
454 $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
455
456 $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
457 $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
458 $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
459
460 foreach ($insertCount as $table => $count) {
461 $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
462 foreach ($insertStatements as $insertQuery) {
463 $insertQuery = rtrim($insertQuery, ';');
464 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
465 $database = $GLOBALS['TYPO3_DB'];
466 $database->admin_query($insertQuery);
467 }
468 }
469 }
470
471 /**
472 * Drop test database.
473 *
474 * @throws \TYPO3\CMS\Core\Tests\Exception
475 * @return void
476 */
477 protected function tearDownTestDatabase() {
478 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
479 $database = $GLOBALS['TYPO3_DB'];
480 $result = $database->admin_query('DROP DATABASE `' . $this->databaseName . '`');
481 if (!$result) {
482 throw new Exception(
483 'Dropping test database ' . $this->databaseName . ' failed',
484 1376583188
485 );
486 }
487 }
488
489 /**
490 * Removes instance directories and files
491 *
492 * @throws \TYPO3\CMS\Core\Tests\Exception
493 * @return void
494 */
495 protected function removeInstance() {
496 $success = $this->rmdir($this->instancePath, TRUE);
497 if (!$success) {
498 throw new Exception(
499 'Can not remove folder: ' . $this->instancePath,
500 1376657210
501 );
502 }
503 }
504
505 /**
506 * COPIED FROM GeneralUtility
507 *
508 * Wrapper function for rmdir, allowing recursive deletion of folders and files
509 *
510 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
511 * @param boolean $removeNonEmpty Allow deletion of non-empty directories
512 * @return boolean TRUE if @rmdir went well!
513 */
514 protected function rmdir($path, $removeNonEmpty = FALSE) {
515 $OK = FALSE;
516 // Remove trailing slash
517 $path = preg_replace('|/$|', '', $path);
518 if (file_exists($path)) {
519 $OK = TRUE;
520 if (!is_link($path) && is_dir($path)) {
521 if ($removeNonEmpty == TRUE && ($handle = opendir($path))) {
522 while ($OK && FALSE !== ($file = readdir($handle))) {
523 if ($file == '.' || $file == '..') {
524 continue;
525 }
526 $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
527 }
528 closedir($handle);
529 }
530 if ($OK) {
531 $OK = @rmdir($path);
532 }
533 } else {
534 // If $path is a file, simply remove it
535 $OK = unlink($path);
536 }
537 clearstatcache();
538 } elseif (is_link($path)) {
539 $OK = unlink($path);
540 clearstatcache();
541 }
542 return $OK;
543 }
544
545 /**
546 * COPIED FROM GeneralUtility
547 *
548 * Writes $content to the file $file
549 *
550 * @param string $file Filepath to write to
551 * @param string $content Content to write
552 * @return boolean TRUE if the file was successfully opened and written to.
553 */
554 protected function writeFile($file, $content) {
555 if ($fd = fopen($file, 'wb')) {
556 $res = fwrite($fd, $content);
557 fclose($fd);
558 if ($res === FALSE) {
559 return FALSE;
560 }
561 return TRUE;
562 }
563 return FALSE;
564 }
565
566 /**
567 * COPIED FROM ArrayUtility
568 *
569 * Exports an array as string.
570 * Similar to var_export(), but representation follows the TYPO3 core CGL.
571 *
572 * See unit tests for detailed examples
573 *
574 * @param array $array Array to export
575 * @param integer $level Internal level used for recursion, do *not* set from outside!
576 * @return string String representation of array
577 * @throws \RuntimeException
578 */
579 protected function arrayExport(array $array = array(), $level = 0) {
580 $lines = 'array(' . chr(10);
581 $level++;
582 $writeKeyIndex = FALSE;
583 $expectedKeyIndex = 0;
584 foreach ($array as $key => $value) {
585 if ($key === $expectedKeyIndex) {
586 $expectedKeyIndex++;
587 } else {
588 // Found a non integer or non consecutive key, so we can break here
589 $writeKeyIndex = TRUE;
590 break;
591 }
592 }
593 foreach ($array as $key => $value) {
594 // Indention
595 $lines .= str_repeat(chr(9), $level);
596 if ($writeKeyIndex) {
597 // Numeric / string keys
598 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
599 }
600 if (is_array($value)) {
601 if (count($value) > 0) {
602 $lines .= $this->arrayExport($value, $level);
603 } else {
604 $lines .= 'array(),' . chr(10);
605 }
606 } elseif (is_int($value) || is_float($value)) {
607 $lines .= $value . ',' . chr(10);
608 } elseif (is_null($value)) {
609 $lines .= 'NULL' . ',' . chr(10);
610 } elseif (is_bool($value)) {
611 $lines .= $value ? 'TRUE' : 'FALSE';
612 $lines .= ',' . chr(10);
613 } elseif (is_string($value)) {
614 // Quote \ to \\
615 $stringContent = str_replace('\\', '\\\\', $value);
616 // Quote ' to \'
617 $stringContent = str_replace('\'', '\\\'', $stringContent);
618 $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
619 } else {
620 throw new \RuntimeException('Objects are not supported', 1342294986);
621 }
622 }
623 $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10));
624 return $lines;
625 }
626 }