[TASK] Shorten database names in 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 * 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 $database->sql_pconnect();
291 // Drop database in case a previous test had a fatal and did not clean up properly
292 $database->admin_query('DROP DATABASE IF EXISTS `' . $this->databaseName . '`');
293 $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $this->databaseName . '`');
294 if (!$createDatabaseResult) {
295 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
296 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
297 throw new Exception(
298 'Unable to create database with name ' . $this->databaseName . '. This is probably a permission problem.'
299 . ' For this instance this could be fixed executing'
300 . ' "GRANT ALL ON `' . $this->originalDatabaseName . '_ft%`.* TO `' . $user . '`@`' . $host . '`;"',
301 1376579070
302 );
303 }
304 $database->setDatabaseName($this->databaseName);
305 $database->sql_select_db($this->databaseName);
306 }
307
308 /**
309 * Create tables and import static rows
310 *
311 * @return void
312 */
313 protected function createDatabaseStructure() {
314 /** @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService */
315 $schemaMigrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlSchemaMigrationService');
316 /** @var \TYPO3\CMS\Install\Service\SqlExpectedSchemaService $expectedSchemaService */
317 $expectedSchemaService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Service\\SqlExpectedSchemaService');
318
319 // Raw concatenated ext_tables.sql and friends string
320 $expectedSchemaString = $expectedSchemaService->getTablesDefinitionString(TRUE);
321 $statements = $schemaMigrationService->getStatementArray($expectedSchemaString, TRUE);
322 list($_, $insertCount) = $schemaMigrationService->getCreateTables($statements, TRUE);
323
324 $fieldDefinitionsFile = $schemaMigrationService->getFieldDefinitions_fileContent($expectedSchemaString);
325 $fieldDefinitionsDatabase = $schemaMigrationService->getFieldDefinitions_database();
326 $difference = $schemaMigrationService->getDatabaseExtra($fieldDefinitionsFile, $fieldDefinitionsDatabase);
327 $updateStatements = $schemaMigrationService->getUpdateSuggestions($difference);
328
329 $schemaMigrationService->performUpdateQueries($updateStatements['add'], $updateStatements['add']);
330 $schemaMigrationService->performUpdateQueries($updateStatements['change'], $updateStatements['change']);
331 $schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $updateStatements['create_table']);
332
333 foreach ($insertCount as $table => $count) {
334 $insertStatements = $schemaMigrationService->getTableInsertStatements($statements, $table);
335 foreach ($insertStatements as $insertQuery) {
336 $insertQuery = rtrim($insertQuery, ';');
337 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
338 $database = $GLOBALS['TYPO3_DB'];
339 $database->admin_query($insertQuery);
340 }
341 }
342 }
343
344 /**
345 * Drop test database.
346 *
347 * @throws \TYPO3\CMS\Core\Tests\Exception
348 * @return void
349 */
350 protected function tearDownTestDatabase() {
351 /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
352 $database = $GLOBALS['TYPO3_DB'];
353 $result = $database->admin_query('DROP DATABASE `' . $this->databaseName . '`');
354 if (!$result) {
355 throw new Exception(
356 'Dropping test database ' . $this->databaseName . ' failed',
357 1376583188
358 );
359 }
360 }
361
362 /**
363 * Removes instance directories and files
364 *
365 * @throws \TYPO3\CMS\Core\Tests\Exception
366 * @return void
367 */
368 protected function removeInstance() {
369 $success = $this->rmdir($this->instancePath, TRUE);
370 if (!$success) {
371 throw new Exception(
372 'Can not remove folder: ' . $this->instancePath,
373 1376657210
374 );
375 }
376 }
377
378 /**
379 * COPIED FROM GeneralUtility
380 *
381 * Wrapper function for rmdir, allowing recursive deletion of folders and files
382 *
383 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
384 * @param boolean $removeNonEmpty Allow deletion of non-empty directories
385 * @return boolean TRUE if @rmdir went well!
386 */
387 protected function rmdir($path, $removeNonEmpty = FALSE) {
388 $OK = FALSE;
389 // Remove trailing slash
390 $path = preg_replace('|/$|', '', $path);
391 if (file_exists($path)) {
392 $OK = TRUE;
393 if (!is_link($path) && is_dir($path)) {
394 if ($removeNonEmpty == TRUE && ($handle = opendir($path))) {
395 while ($OK && FALSE !== ($file = readdir($handle))) {
396 if ($file == '.' || $file == '..') {
397 continue;
398 }
399 $OK = $this->rmdir($path . '/' . $file, $removeNonEmpty);
400 }
401 closedir($handle);
402 }
403 if ($OK) {
404 $OK = @rmdir($path);
405 }
406 } else {
407 // If $path is a file, simply remove it
408 $OK = unlink($path);
409 }
410 clearstatcache();
411 } elseif (is_link($path)) {
412 $OK = unlink($path);
413 clearstatcache();
414 }
415 return $OK;
416 }
417
418 /**
419 * COPIED FROM GeneralUtility
420 *
421 * Writes $content to the file $file
422 *
423 * @param string $file Filepath to write to
424 * @param string $content Content to write
425 * @return boolean TRUE if the file was successfully opened and written to.
426 */
427 protected function writeFile($file, $content) {
428 if ($fd = fopen($file, 'wb')) {
429 $res = fwrite($fd, $content);
430 fclose($fd);
431 if ($res === FALSE) {
432 return FALSE;
433 }
434 return TRUE;
435 }
436 return FALSE;
437 }
438
439 /**
440 * COPIED FROM ArrayUtility
441 *
442 * Exports an array as string.
443 * Similar to var_export(), but representation follows the TYPO3 core CGL.
444 *
445 * See unit tests for detailed examples
446 *
447 * @param array $array Array to export
448 * @param integer $level Internal level used for recursion, do *not* set from outside!
449 * @return string String representation of array
450 * @throws \RuntimeException
451 */
452 protected function arrayExport(array $array = array(), $level = 0) {
453 $lines = 'array(' . chr(10);
454 $level++;
455 $writeKeyIndex = FALSE;
456 $expectedKeyIndex = 0;
457 foreach ($array as $key => $value) {
458 if ($key === $expectedKeyIndex) {
459 $expectedKeyIndex++;
460 } else {
461 // Found a non integer or non consecutive key, so we can break here
462 $writeKeyIndex = TRUE;
463 break;
464 }
465 }
466 foreach ($array as $key => $value) {
467 // Indention
468 $lines .= str_repeat(chr(9), $level);
469 if ($writeKeyIndex) {
470 // Numeric / string keys
471 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
472 }
473 if (is_array($value)) {
474 if (count($value) > 0) {
475 $lines .= $this->arrayExport($value, $level);
476 } else {
477 $lines .= 'array(),' . chr(10);
478 }
479 } elseif (is_int($value) || is_float($value)) {
480 $lines .= $value . ',' . chr(10);
481 } elseif (is_null($value)) {
482 $lines .= 'NULL' . ',' . chr(10);
483 } elseif (is_bool($value)) {
484 $lines .= $value ? 'TRUE' : 'FALSE';
485 $lines .= ',' . chr(10);
486 } elseif (is_string($value)) {
487 // Quote \ to \\
488 $stringContent = str_replace('\\', '\\\\', $value);
489 // Quote ' to \'
490 $stringContent = str_replace('\'', '\\\'', $stringContent);
491 $lines .= '\'' . $stringContent . '\'' . ',' . chr(10);
492 } else {
493 throw new \RuntimeException('Objects are not supported', 1342294986);
494 }
495 }
496 $lines .= str_repeat(chr(9), ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . chr(10));
497 return $lines;
498 }
499 }
500 ?>