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