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