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