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